From 98452efbb6180eb44b831cc6442950c9ad6537f6 Mon Sep 17 00:00:00 2001 From: Arul Sharma <31745423+arul28@users.noreply.github.com> Date: Fri, 17 Jul 2026 12:57:57 -0400 Subject: [PATCH] test(chat): make durable-cron scheduled-wake tests wait robustly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two hasActiveWorkloads cron tests polled for an async scheduled-wake user_message by advancing fake timers only 100×1ms. The durable- scheduling pipeline needs more timer+microtask cycles than that to emit the event, so the tests passed only on fast/unloaded runners and failed deterministically elsewhere (surfaced when an unrelated merge reshuffled the 8-way shard split). Replace the fragile fixed-iteration loops with a waitForFakeTimerCondition helper that advances fake time up to 10s while flushing microtasks until the predicate holds. Test-only; no assertions weakened, no product code changed. Co-Authored-By: Claude Opus 4.8 --- .../services/chat/agentChatService.test.ts | 113 ++++++++++++------ 1 file changed, 76 insertions(+), 37 deletions(-) diff --git a/apps/desktop/src/main/services/chat/agentChatService.test.ts b/apps/desktop/src/main/services/chat/agentChatService.test.ts index e160bdb27..c9cf9afa2 100644 --- a/apps/desktop/src/main/services/chat/agentChatService.test.ts +++ b/apps/desktop/src/main/services/chat/agentChatService.test.ts @@ -1727,6 +1727,31 @@ async function waitForEvent( throw new Error("Timed out waiting for agent chat event."); } +async function waitForFakeTimerCondition( + predicate: () => boolean, + description: string, +): Promise { + const timeoutMs = 10_000; + for (let elapsedMs = 0; elapsedMs < timeoutMs; elapsedMs += 1) { + if (predicate()) return; + await vi.advanceTimersByTimeAsync(1); + await Promise.resolve(); + } + if (predicate()) return; + throw new Error(`Timed out after ${timeoutMs}ms waiting for ${description}.`); +} + +async function waitForFakeTimerPromise( + promise: Promise, + description: string, +): Promise { + let settled = false; + const trackedPromise = promise.finally(() => { settled = true; }); + void trackedPromise.catch(() => undefined); + await waitForFakeTimerCondition(() => settled, description); + return trackedPromise; +} + async function createClaudeStreamFixture(args: { sdkSessionId: string; messages: Array>; @@ -13069,9 +13094,10 @@ describe("createAgentChatService", () => { sessionId: session.id, text: "Keep working through the backstop deadline.", }); - for (let index = 0; index < 100 && !send.mock.calls.length; index += 1) { - await vi.advanceTimersByTimeAsync(1); - } + await waitForFakeTimerCondition( + () => send.mock.calls.length > 0, + "the foreground Claude send to start", + ); expect(send).toHaveBeenCalled(); expect(service.hasActiveWorkloads()).toBe(true); const options = vi.mocked(claudeSdkCreateSessionCompat).mock.calls.at(-1)?.[0] as { @@ -15127,6 +15153,8 @@ describe("createAgentChatService", () => { }); it("does not create task-id scheduled rows for ambiguous parentless cron runs", async () => { + vi.useFakeTimers(); + vi.setSystemTime(SCHEDULE_TEST_START); const scheduledWork = createScheduledWorkDb(); const events: AgentChatEventEnvelope[] = []; const setPermissionMode = vi.fn().mockResolvedValue(undefined); @@ -15204,13 +15232,14 @@ describe("createAgentChatService", () => { } | undefined; const stopHook = opts?.hooks?.Stop?.[0]?.hooks[0]; - await service.runSessionTurn({ - sessionId: session.id, - text: "Schedule two recurring crons.", - }); + await waitForFakeTimerPromise( + service.runSessionTurn({ + sessionId: session.id, + text: "Schedule two recurring crons.", + }), + "the cron setup turn to settle", + ); - vi.useFakeTimers(); - vi.setSystemTime(SCHEDULE_TEST_START); const postToolUseHook = opts?.hooks?.PostToolUse?.[0]?.hooks[0]; await postToolUseHook?.({ hook_event_name: "PostToolUse", @@ -15246,15 +15275,20 @@ describe("createAgentChatService", () => { ], }); - await vi.advanceTimersByTimeAsync(15 * 60_000); + vi.setSystemTime(SCHEDULE_TEST_START + 15 * 60_000); startCronRun(); - for (let index = 0; index < 100 && !events.some((event) => - event.sessionId === session.id - && event.event.type === "subagent_result" - && event.event.taskId === "cron-run-task-ambiguous" - ); index += 1) { - await vi.advanceTimersByTimeAsync(1); - } + await waitForFakeTimerCondition( + () => events.some((event) => + event.sessionId === session.id + && event.event.type === "subagent_result" + && event.event.taskId === "cron-run-task-ambiguous" + ) && events.some((event) => + event.sessionId === session.id + && event.event.type === "user_message" + && event.event.metadata?.scheduledWake != null + ), + "the ambiguous cron result and scheduled-wake message", + ); expect(events.some((event) => event.sessionId === session.id && event.event.type === "subagent_result" @@ -15297,6 +15331,8 @@ describe("createAgentChatService", () => { }); it("claims a native cron when unrelated idle output already opened the turn", async () => { + vi.useFakeTimers(); + vi.setSystemTime(SCHEDULE_TEST_START); const scheduledWork = createScheduledWorkDb(); const events: AgentChatEventEnvelope[] = []; let streamCall = 0; @@ -15378,12 +15414,13 @@ describe("createAgentChatService", () => { } | undefined; const postToolUseHook = options?.hooks?.PostToolUse?.[0]?.hooks[0]; - await service.runSessionTurn({ - sessionId: session.id, - text: "Keep watching CI in the background.", - }); - vi.useFakeTimers(); - vi.setSystemTime(SCHEDULE_TEST_START); + await waitForFakeTimerPromise( + service.runSessionTurn({ + sessionId: session.id, + text: "Keep watching CI in the background.", + }), + "the idle-output setup turn to settle", + ); await postToolUseHook?.({ hook_event_name: "PostToolUse", session_id: "sdk-cron-existing-idle-turn", @@ -15402,13 +15439,14 @@ describe("createAgentChatService", () => { }], }); releaseIdle(); - for (let index = 0; index < 100 && !events.some((event) => - event.sessionId === session.id - && event.event.type === "activity" - && event.event.detail === "Tool 'BackgroundTask' running (1s)" - ); index += 1) { - await vi.advanceTimersByTimeAsync(1); - } + await waitForFakeTimerCondition( + () => events.some((event) => + event.sessionId === session.id + && event.event.type === "activity" + && event.event.detail === "Tool 'BackgroundTask' running (1s)" + ), + "the unrelated idle activity", + ); const idleActivity = events.find((event) => event.sessionId === session.id && event.event.type === "activity" @@ -15418,13 +15456,14 @@ describe("createAgentChatService", () => { await vi.advanceTimersByTimeAsync(15 * 60_000); releaseCron(); - for (let index = 0; index < 100 && !events.some((event) => - event.sessionId === session.id - && event.event.type === "user_message" - && event.event.metadata?.scheduledWake?.scheduleId === "cron-provider-existing-idle-turn" - ); index += 1) { - await vi.advanceTimersByTimeAsync(1); - } + await waitForFakeTimerCondition( + () => events.some((event) => + event.sessionId === session.id + && event.event.type === "user_message" + && event.event.metadata?.scheduledWake?.scheduleId === "cron-provider-existing-idle-turn" + ), + "the native cron scheduled-wake message", + ); const scheduledWake = events.find((event) => event.sessionId === session.id