What
WorkerResilienceTests.ProposalHousekeepingWorker_WhenDbThrows_LogsErrorAndContinuesPolling asserts a background worker has polled at least once after a fixed 300 ms wall-clock delay. On a contended Windows runner the worker has not reached its first iteration inside that window, and the test fails.
Observed on PR #2522, run 33922229492, job 101182893700, API Integration / API Integration (windows-latest):
[xUnit.net 00:11:12.36] Taskdeck.Api.Tests.Resilience.WorkerResilienceTests
.ProposalHousekeepingWorker_WhenDbThrows_LogsErrorAndContinuesPolling [FAIL]
Failed ... [894 ms]
Error Message:
Expected callCount to be greater than 0, but found 0.
at FluentAssertions.Numeric.NumericAssertions`2.BeGreaterThan(...)
Failed! - Failed: 1, Passed: 2829, Skipped: 4, Total: 2834, Duration: 20 m 33 s
One failure in 2 834. The suite took 20 m 33 s on that leg, which is the corroborating detail: the runner was heavily contended, which is exactly when a fixed 300 ms budget stops being enough.
Mechanism
backend/tests/Taskdeck.Api.Tests/Resilience/WorkerResilienceTests.cs:75-103:
var runTask = worker.StartAsync(cts.Token);
await Task.Delay(300); // <- fixed wall-clock window
cts.Cancel();
...
callCount.Should().BeGreaterThan(0);
callCount is incremented from inside the scope factory the worker calls on its first iteration. The test grants exactly 300 ms for thread-pool scheduling, host startup and that first iteration to all happen. Nothing waits for the condition; the delay is the synchronisation. When the runner is slow the worker simply has not run yet, and the assertion reports 0 — a scheduling artifact, not a resilience defect.
Note the failure was reported at 894 ms, i.e. the test itself took longer than its own 300 ms budget to get through the delay-and-cancel sequence, which is consistent with the runner being the bottleneck rather than the worker being broken.
Suggested fix
Replace the fixed delay with a bounded wait on the condition — poll callCount > 0 (and the heartbeat) up to a generous ceiling, e.g. 5 s, returning as soon as it is satisfied. That keeps the test fast on a healthy runner, removes the wall-clock assumption, and preserves exactly what it means to assert. The same pattern is worth auditing across WorkerResilienceTests — any other await Task.Delay(...) followed by a "did it happen yet" assertion has the identical shape.
Why this is filed separately
It shares a theme with the Windows-runner-speed cohort but is a different suite, language and mechanism, and folding it in would let a fix for one be miscredited as closing the other:
Not caused by the PR it was seen on
PR #2522 changes only scripts/ci/dev-up.test.mjs, scripts/dev-up.ps1 and scripts/dev-up.sh. It touches no C#, no worker, and nothing this test exercises — git diff origin/main...HEAD | grep -ci 'ProposalHousekeeping\|WorkerResilience' returns 0.
Refs #2378, #2161, #2561
What
WorkerResilienceTests.ProposalHousekeepingWorker_WhenDbThrows_LogsErrorAndContinuesPollingasserts a background worker has polled at least once after a fixed 300 ms wall-clock delay. On a contended Windows runner the worker has not reached its first iteration inside that window, and the test fails.Observed on PR #2522, run 33922229492, job 101182893700,
API Integration / API Integration (windows-latest):One failure in 2 834. The suite took 20 m 33 s on that leg, which is the corroborating detail: the runner was heavily contended, which is exactly when a fixed 300 ms budget stops being enough.
Mechanism
backend/tests/Taskdeck.Api.Tests/Resilience/WorkerResilienceTests.cs:75-103:callCountis incremented from inside the scope factory the worker calls on its first iteration. The test grants exactly 300 ms for thread-pool scheduling, host startup and that first iteration to all happen. Nothing waits for the condition; the delay is the synchronisation. When the runner is slow the worker simply has not run yet, and the assertion reports0— a scheduling artifact, not a resilience defect.Note the failure was reported at 894 ms, i.e. the test itself took longer than its own 300 ms budget to get through the delay-and-cancel sequence, which is consistent with the runner being the bottleneck rather than the worker being broken.
Suggested fix
Replace the fixed delay with a bounded wait on the condition — poll
callCount > 0(and the heartbeat) up to a generous ceiling, e.g. 5 s, returning as soon as it is satisfied. That keeps the test fast on a healthy runner, removes the wall-clock assumption, and preserves exactly what it means to assert. The same pattern is worth auditing acrossWorkerResilienceTests— any otherawait Task.Delay(...)followed by a "did it happen yet" assertion has the identical shape.Why this is filed separately
It shares a theme with the Windows-runner-speed cohort but is a different suite, language and mechanism, and folding it in would let a fix for one be miscredited as closing the other:
scripts/ci/dev-up.test.mjs, Node/PowerShell,ETIMEDOUTand job/parent-test timeout shapes.dev-up.ps1fail-closed state retention, an assertion failure with a specific identity-probe mechanism.Taskdeck.Api.Testswith a fixedTask.Delaysynchronisation.Not caused by the PR it was seen on
PR #2522 changes only
scripts/ci/dev-up.test.mjs,scripts/dev-up.ps1andscripts/dev-up.sh. It touches no C#, no worker, and nothing this test exercises —git diff origin/main...HEAD | grep -ci 'ProposalHousekeeping\|WorkerResilience'returns 0.Refs #2378, #2161, #2561