From c6d1fff8b1119dd605e5e5d34e309352ca04f6c2 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Fri, 7 Aug 2026 10:19:54 +0200 Subject: [PATCH] fix(worker-tests): close TOCTOU race in slot-failure broadcast poll QueueServiceSlotFailureTests's throwing-slot test broke its poll loop the instant the DB read observed Status==Failed, but TaskStateService.FailAsync commits the status flip before calling the broadcaster's TaskUpdated, so the assertion could race ahead of the broadcast landing in hub.Proxy.Calls (~1-in-5 failures in isolation). Wait for both signals before breaking. --- .../Services/QueueServiceSlotFailureTests.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/ClaudeDo.Worker.Tests/Services/QueueServiceSlotFailureTests.cs b/tests/ClaudeDo.Worker.Tests/Services/QueueServiceSlotFailureTests.cs index 82558c39..bf604e78 100644 --- a/tests/ClaudeDo.Worker.Tests/Services/QueueServiceSlotFailureTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Services/QueueServiceSlotFailureTests.cs @@ -132,13 +132,20 @@ public sealed class QueueServiceSlotFailureTests : IDisposable await service.StartAsync(cts.Token); waker.Wake(); + // FailAsync (TaskStateService.cs:236-249) commits the DB status flip via + // ExecuteUpdateAsync *before* it calls the broadcaster's TaskUpdated — so a poll that + // breaks the instant it observes Status==Failed can race ahead of the broadcast still + // landing in hub.Proxy.Calls. Wait for both signals together so the assertions below + // never sample a genuinely-not-yet-broadcast window as a failure. TaskEntity? reloaded = null; var deadline = DateTime.UtcNow.AddSeconds(10); while (DateTime.UtcNow < deadline) { using var verify = _db.CreateContext(); reloaded = await new TaskRepository(verify).GetByIdAsync(taskId); - if (reloaded!.Status == TaskStatus.Failed) break; + var broadcastSeen = hub.Proxy.Calls.Any( + c => c.Method == "TaskUpdated" && (string)c.Args[0]! == taskId); + if (reloaded!.Status == TaskStatus.Failed && broadcastSeen) break; await Task.Delay(25); } cts.Cancel();