Observed while adding Phase 4 of the Lessons integration coverage (spikersoft-backend#176), but reproduces on master today with only the Phase 1-3 Lessons test files (spikersoft-backend#174, #175) — not a regression introduced by that work.
Symptom: running dotnet test with a filter that selects 3 or more of the SpikerSoft.Tests.Integration.Lessons.* test classes together (all sharing [Collection("TestContainers")]) reports every individual test as Passed, but the overall run prints Test Run Failed. and the process exits with code 1. Running any 2 of those classes together, or any single class alone, exits 0 with Test Run Successful.
Repro (all pass individually, but the run fails):
dotnet test SpikerSoft.Tests.Integration -c Debug --filter "FullyQualifiedName~SpikerSoft.Tests.Integration.Lessons.LessonCatalogAndAttemptIntegrationTests|FullyQualifiedName~SpikerSoft.Tests.Integration.Lessons.AnonymousLessonJourneyIntegrationTests|FullyQualifiedName~SpikerSoft.Tests.Integration.Lessons.LessonGradingIntegrationTests|FullyQualifiedName~SpikerSoft.Tests.Integration.Lessons.CodeExecutionSignalRIntegrationTests"
# => Total tests: 30 / Passed: 30 / "Test Run Failed." / exit code 1
Correlated with heavy HTTP request failed after ~2700ms / SocketError noise in the logs right around the point multiple WebApplicationFactory instances (and their in-process code-execution/regrade worker IHosts from TestEnvironmentFixture) are being disposed close together — looks like a background connection (Rabbit/Redis/Mongo client, or a lingering SignalR/TestServer socket) throwing after its owning test has already reported success, which the console runner or test host then surfaces as a failed run despite zero failed assertions.
Impact: CI green/red status for this project can't be trusted at the dotnet test process-exit-code level once enough Lessons tests exist in one collection; someone has to actually read the Passed: N / Total: N summary rather than trust the exit code. Low urgency since no test outcome is wrong, but worth root-causing before this collection grows further (Phase 5 will add more classes to it).
Suggested next step: reproduce with --diag / a memory dump or DOTNET_gcServer=0 + verbose logging to catch the actual unobserved exception; likely candidates are TestEnvironmentFixture's worker IHost.StopAsync/DisposeAsync ordering, or WebApplicationFactory teardown racing a still-open RabbitMQ channel from CodeExecutionWorkerHostedService / LessonRegradeWorkerHostedService.
Observed while adding Phase 4 of the Lessons integration coverage (spikersoft-backend#176), but reproduces on `master` today with only the Phase 1-3 Lessons test files (spikersoft-backend#174, #175) — not a regression introduced by that work.
**Symptom**: running `dotnet test` with a filter that selects 3 or more of the `SpikerSoft.Tests.Integration.Lessons.*` test classes together (all sharing `[Collection("TestContainers")]`) reports every individual test as `Passed`, but the overall run prints `Test Run Failed.` and the process exits with code 1. Running any 2 of those classes together, or any single class alone, exits 0 with `Test Run Successful.`
Repro (all pass individually, but the run fails):
```
dotnet test SpikerSoft.Tests.Integration -c Debug --filter "FullyQualifiedName~SpikerSoft.Tests.Integration.Lessons.LessonCatalogAndAttemptIntegrationTests|FullyQualifiedName~SpikerSoft.Tests.Integration.Lessons.AnonymousLessonJourneyIntegrationTests|FullyQualifiedName~SpikerSoft.Tests.Integration.Lessons.LessonGradingIntegrationTests|FullyQualifiedName~SpikerSoft.Tests.Integration.Lessons.CodeExecutionSignalRIntegrationTests"
# => Total tests: 30 / Passed: 30 / "Test Run Failed." / exit code 1
```
Correlated with heavy `HTTP request failed after ~2700ms` / `SocketError` noise in the logs right around the point multiple `WebApplicationFactory` instances (and their in-process code-execution/regrade worker `IHost`s from `TestEnvironmentFixture`) are being disposed close together — looks like a background connection (Rabbit/Redis/Mongo client, or a lingering SignalR/TestServer socket) throwing after its owning test has already reported success, which the console runner or test host then surfaces as a failed run despite zero failed assertions.
**Impact**: CI green/red status for this project can't be trusted at the `dotnet test` process-exit-code level once enough Lessons tests exist in one collection; someone has to actually read the `Passed: N / Total: N` summary rather than trust the exit code. Low urgency since no test outcome is wrong, but worth root-causing before this collection grows further (Phase 5 will add more classes to it).
**Suggested next step**: reproduce with `--diag` / a memory dump or `DOTNET_gcServer=0` + verbose logging to catch the actual unobserved exception; likely candidates are `TestEnvironmentFixture`'s worker `IHost.StopAsync`/`DisposeAsync` ordering, or `WebApplicationFactory` teardown racing a still-open RabbitMQ channel from `CodeExecutionWorkerHostedService` / `LessonRegradeWorkerHostedService`.
Root-caused and fixed in spikersoft-backend#179 (merged to master).
Root cause — not the fixture's worker IHost ordering or a lingering worker RabbitMQ channel. The unobserved exception is an xUnit class cleanup failure on CodeExecutionSignalRIntegrationTests (the only Lessons class that registers SignalRNotificationConsumerService in its WebApplicationFactory):
Under minimal hosting, WebApplicationFactory.Dispose() stops the host twice from two threads (dotnet/aspnetcore#40271) — once from the factory's dispose thread and once from the app.Run() thread unblocked via ApplicationStopping. Diag logs show "SignalR Notification Consumer is stopping" twice at the same timestamp.
SignalRNotificationConsumerService.StopAsync wasn't idempotent, so the two concurrent calls raced CloseAsync()/Dispose() on the same RabbitMQ AutorecoveringChannel, and the loser threw ObjectDisposedException from inside AutorecoveringChannel.CloseAsync's cleanup (DeleteRecordedChannelAsync → get_ConsumerTags).
That lands during class-fixture teardown after all tests already reported Passed, so xUnit records a cleanup failure and VSTest exits 1 with Passed: N / Total: N. It's a timing race — hence needing 3+ classes' teardowns close together to reproduce reliably.
Fix — StopAsync now guards the channel/connection close with Interlocked.Exchange so only the first caller closes; every caller still awaits base.StopAsync. Nothing is suppressed; the real failure mode (double host stop) simply no longer corrupts the close sequence.
Verified on the fix branch (exit 0 for all): the 4-class repro filter from this ticket (30/30), the full SpikerSoft.Tests.Integration.Lessons namespace (42/42, previously exit 1), and the full integration suite minus the Redis-cluster classes (128 passed / 3 skipped) — those three Redis-cluster classes fail on this machine only because an unrelated compose stack is squatting on their fixed host ports 7014–7016 (pre-existing, same on unpatched master, unrelated to this fix).
Root-caused and fixed in spikersoft-backend#179 (merged to master).
**Root cause** — not the fixture's worker `IHost` ordering or a lingering worker RabbitMQ channel. The unobserved exception is an xUnit **class cleanup failure** on `CodeExecutionSignalRIntegrationTests` (the only Lessons class that registers `SignalRNotificationConsumerService` in its `WebApplicationFactory`):
- Under minimal hosting, `WebApplicationFactory.Dispose()` stops the host **twice from two threads** (dotnet/aspnetcore#40271) — once from the factory's dispose thread and once from the `app.Run()` thread unblocked via `ApplicationStopping`. Diag logs show "SignalR Notification Consumer is stopping" twice at the same timestamp.
- `SignalRNotificationConsumerService.StopAsync` wasn't idempotent, so the two concurrent calls raced `CloseAsync()`/`Dispose()` on the same RabbitMQ `AutorecoveringChannel`, and the loser threw `ObjectDisposedException` from inside `AutorecoveringChannel.CloseAsync`'s cleanup (`DeleteRecordedChannelAsync` → `get_ConsumerTags`).
- That lands during class-fixture teardown after all tests already reported Passed, so xUnit records a cleanup failure and VSTest exits 1 with `Passed: N / Total: N`. It's a timing race — hence needing 3+ classes' teardowns close together to reproduce reliably.
**Fix** — `StopAsync` now guards the channel/connection close with `Interlocked.Exchange` so only the first caller closes; every caller still awaits `base.StopAsync`. Nothing is suppressed; the real failure mode (double host stop) simply no longer corrupts the close sequence.
**Verified on the fix branch (exit 0 for all):** the 4-class repro filter from this ticket (30/30), the full `SpikerSoft.Tests.Integration.Lessons` namespace (42/42, previously exit 1), and the full integration suite minus the Redis-cluster classes (128 passed / 3 skipped) — those three Redis-cluster classes fail on this machine only because an unrelated compose stack is squatting on their fixed host ports 7014–7016 (pre-existing, same on unpatched master, unrelated to this fix).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Observed while adding Phase 4 of the Lessons integration coverage (spikersoft-backend#176), but reproduces on
mastertoday with only the Phase 1-3 Lessons test files (spikersoft-backend#174, #175) — not a regression introduced by that work.Symptom: running
dotnet testwith a filter that selects 3 or more of theSpikerSoft.Tests.Integration.Lessons.*test classes together (all sharing[Collection("TestContainers")]) reports every individual test asPassed, but the overall run printsTest Run Failed.and the process exits with code 1. Running any 2 of those classes together, or any single class alone, exits 0 withTest Run Successful.Repro (all pass individually, but the run fails):
Correlated with heavy
HTTP request failed after ~2700ms/SocketErrornoise in the logs right around the point multipleWebApplicationFactoryinstances (and their in-process code-execution/regrade workerIHosts fromTestEnvironmentFixture) are being disposed close together — looks like a background connection (Rabbit/Redis/Mongo client, or a lingering SignalR/TestServer socket) throwing after its owning test has already reported success, which the console runner or test host then surfaces as a failed run despite zero failed assertions.Impact: CI green/red status for this project can't be trusted at the
dotnet testprocess-exit-code level once enough Lessons tests exist in one collection; someone has to actually read thePassed: N / Total: Nsummary rather than trust the exit code. Low urgency since no test outcome is wrong, but worth root-causing before this collection grows further (Phase 5 will add more classes to it).Suggested next step: reproduce with
--diag/ a memory dump orDOTNET_gcServer=0+ verbose logging to catch the actual unobserved exception; likely candidates areTestEnvironmentFixture's workerIHost.StopAsync/DisposeAsyncordering, orWebApplicationFactoryteardown racing a still-open RabbitMQ channel fromCodeExecutionWorkerHostedService/LessonRegradeWorkerHostedService.Root-caused and fixed in spikersoft-backend#179 (merged to master).
Root cause — not the fixture's worker
IHostordering or a lingering worker RabbitMQ channel. The unobserved exception is an xUnit class cleanup failure onCodeExecutionSignalRIntegrationTests(the only Lessons class that registersSignalRNotificationConsumerServicein itsWebApplicationFactory):WebApplicationFactory.Dispose()stops the host twice from two threads (dotnet/aspnetcore#40271) — once from the factory's dispose thread and once from theapp.Run()thread unblocked viaApplicationStopping. Diag logs show "SignalR Notification Consumer is stopping" twice at the same timestamp.SignalRNotificationConsumerService.StopAsyncwasn't idempotent, so the two concurrent calls racedCloseAsync()/Dispose()on the same RabbitMQAutorecoveringChannel, and the loser threwObjectDisposedExceptionfrom insideAutorecoveringChannel.CloseAsync's cleanup (DeleteRecordedChannelAsync→get_ConsumerTags).Passed: N / Total: N. It's a timing race — hence needing 3+ classes' teardowns close together to reproduce reliably.Fix —
StopAsyncnow guards the channel/connection close withInterlocked.Exchangeso only the first caller closes; every caller still awaitsbase.StopAsync. Nothing is suppressed; the real failure mode (double host stop) simply no longer corrupts the close sequence.Verified on the fix branch (exit 0 for all): the 4-class repro filter from this ticket (30/30), the full
SpikerSoft.Tests.Integration.Lessonsnamespace (42/42, previously exit 1), and the full integration suite minus the Redis-cluster classes (128 passed / 3 skipped) — those three Redis-cluster classes fail on this machine only because an unrelated compose stack is squatting on their fixed host ports 7014–7016 (pre-existing, same on unpatched master, unrelated to this fix).