From ad389851a55aff5697c525ad3a96f6e4bb82b4c0 Mon Sep 17 00:00:00 2001 From: Mux Date: Tue, 1 Sep 2026 12:21:05 -0500 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=A4=96=20tests:=20cover=20concurrent?= =?UTF-8?q?=20bash=20monitor=20wake=20admission?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/node/services/workspaceService.test.ts | 133 ++++++++++++++++++++- 1 file changed, 132 insertions(+), 1 deletion(-) diff --git a/src/node/services/workspaceService.test.ts b/src/node/services/workspaceService.test.ts index bf67f2099e..b42ca1d7a6 100644 --- a/src/node/services/workspaceService.test.ts +++ b/src/node/services/workspaceService.test.ts @@ -49,6 +49,7 @@ import type { } from "@/common/types/workspace"; import { makeAgentTaskIntegrationFake } from "./taskWorkspaceSeam.testUtils"; import type { BackgroundProcessManager } from "./backgroundProcessManager"; +import type { BashMonitorProcessSnapshot } from "./bashMonitorWakeReconciler"; import type { TerminalService } from "@/node/services/terminalService"; import type { DesktopSessionManager } from "@/node/services/desktop/DesktopSessionManager"; import type { WorktreeArchiveSnapshot } from "@/common/schemas/project"; @@ -248,7 +249,7 @@ describe("WorkspaceService bash monitor wake reconciler wiring", () => { ), backgroundProcessManager, }); - return { config, service, events, cleanup }; + return { config, historyService, service, events, backgroundProcessManager, cleanup }; } test("monitor lifecycle and shown-output events poke the reconciler", async () => { @@ -842,6 +843,136 @@ describe("WorkspaceService bash monitor wake reconciler wiring", () => { await cleanup(); } }); + + test("defers a second wake while the accepted first wake waits to start", async () => { + const { config, service, backgroundProcessManager, cleanup } = await createWakeWiringService(); + const workspaceId = "concurrent-wake-owner"; + await config.addWorkspace("/tmp/concurrent-wake-project", { + id: workspaceId, + name: workspaceId, + projectName: "concurrent-wake-project", + projectPath: "/tmp/concurrent-wake-project", + runtimeConfig: { type: "local" }, + }); + + const firstWake: BashMonitorProcessSnapshot = { + processId: "first-proc", + taskId: "bash:first-proc", + ownerWorkspaceId: workspaceId, + displayName: "first monitor", + filter: "FIRST", + filterExclude: false, + script: "run-first", + createdAt: "2026-09-01T00:00:00.000Z", + match: { throughOffset: 5, lines: ["FIRST"], totalMatches: 1 }, + retired: false, + }; + const secondWake: BashMonitorProcessSnapshot = { + ...firstWake, + processId: "second-proc", + taskId: "bash:second-proc", + displayName: "second monitor", + filter: "SECOND", + script: "run-second", + match: { throughOffset: 6, lines: ["SECOND"], totalMatches: 1 }, + }; + let liveWakes = [firstWake]; + backgroundProcessManager.pullMonitorWakeSignals = mock(() => liveWakes); + backgroundProcessManager.getMonitorWakeDeliveryState = mock(() => + Promise.resolve({ + status: "settled" as const, + shownThroughOffset: 0, + terminalStatusShown: false, + }) + ); + + const idleGate = createDeferred(); + let queuedEntryCount = 0; + let preparing = false; + let busy = true; + let streaming = true; + let idleReleased = false; + const fakeSession = { + hasQueuedMessages: () => queuedEntryCount > 0, + isPreparingTurn: () => preparing, + hasPendingAutoRetry: () => false, + isBusy: () => busy, + waitForIdle: () => idleGate.promise, + onChatEvent: () => () => undefined, + } as unknown as AgentSession; + + interface AcceptedCallbacks { + onAccepted?: () => Promise; + } + let firstCallbacks: AcceptedCallbacks | undefined; + const queuedModes: string[] = []; + let secondStartedAfterIdle = false; + let sendCount = 0; + const sendMessage = mock( + ( + _workspaceId: string, + _prompt: string, + options: { queueDispatchMode?: string }, + callbacks?: AcceptedCallbacks + ) => { + sendCount++; + queuedModes.push(options.queueDispatchMode ?? ""); + if (sendCount === 1) { + queuedEntryCount++; + firstCallbacks = callbacks; + return Promise.resolve(Ok(undefined)); + } + secondStartedAfterIdle = idleReleased; + return ( + callbacks?.onAccepted?.().then(() => Ok(undefined)) ?? Promise.resolve(Ok(undefined)) + ); + } + ); + + const internal = service as unknown as { + sessions: Map; + aiService: { isStreaming(workspaceId: string): boolean }; + getDelegatedTurnContinuationSendOptions(workspaceId: string): Promise; + sendMessage: typeof sendMessage; + bashMonitorWakeReconciler: { reconcile(workspaceId: string): Promise }; + }; + try { + internal.sessions.set(workspaceId, fakeSession); + internal.aiService = { isStreaming: () => streaming }; + internal.getDelegatedTurnContinuationSendOptions = () => + Promise.resolve({ model: "anthropic:claude-sonnet-4-5", agentId: "exec" }); + internal.sendMessage = sendMessage; + + await internal.bashMonitorWakeReconciler.reconcile(workspaceId); + expect(queuedEntryCount).toBe(1); + expect(queuedModes).toEqual(["tool-end"]); + + liveWakes = [firstWake, secondWake]; + queuedEntryCount--; + preparing = true; + streaming = false; + await firstCallbacks?.onAccepted?.(); + await internal.bashMonitorWakeReconciler.reconcile(workspaceId); + + expect(sendCount).toBe(1); + expect(queuedModes).toEqual(["tool-end"]); + expect(secondStartedAfterIdle).toBe(false); + + preparing = false; + streaming = true; + await Promise.resolve(); + expect(sendCount).toBe(1); + + busy = false; + streaming = false; + idleReleased = true; + idleGate.resolve(); + await waitForCondition(() => sendCount === 2); + expect(secondStartedAfterIdle).toBe(true); + } finally { + await cleanup(); + } + }); }); async function setWorkspaceGoalOk( From 67c73d667095c7fe1de7bf9da9f95e64c0208833 Mon Sep 17 00:00:00 2001 From: Mux Date: Tue, 1 Sep 2026 15:54:12 -0500 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=A4=96=20tests:=20await=20monitor=20w?= =?UTF-8?q?ake=20acceptance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/node/services/workspaceService.test.ts | 49 ++++++++++++++++++---- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/src/node/services/workspaceService.test.ts b/src/node/services/workspaceService.test.ts index b42ca1d7a6..9b4699b17f 100644 --- a/src/node/services/workspaceService.test.ts +++ b/src/node/services/workspaceService.test.ts @@ -846,6 +846,8 @@ describe("WorkspaceService bash monitor wake reconciler wiring", () => { test("defers a second wake while the accepted first wake waits to start", async () => { const { config, service, backgroundProcessManager, cleanup } = await createWakeWiringService(); + const acknowledgeMonitorWake = mock(() => undefined); + Object.assign(backgroundProcessManager, { acknowledgeMonitorWake }); const workspaceId = "concurrent-wake-owner"; await config.addWorkspace("/tmp/concurrent-wake-project", { id: workspaceId, @@ -887,6 +889,9 @@ describe("WorkspaceService bash monitor wake reconciler wiring", () => { ); const idleGate = createDeferred(); + const idleWaitStarted = createDeferred(); + const secondAccepted = createDeferred(); + let idleWaitObserved = false; let queuedEntryCount = 0; let preparing = false; let busy = true; @@ -897,7 +902,13 @@ describe("WorkspaceService bash monitor wake reconciler wiring", () => { isPreparingTurn: () => preparing, hasPendingAutoRetry: () => false, isBusy: () => busy, - waitForIdle: () => idleGate.promise, + waitForIdle: () => { + if (!idleWaitObserved) { + idleWaitObserved = true; + idleWaitStarted.resolve(); + } + return idleGate.promise; + }, onChatEvent: () => () => undefined, } as unknown as AgentSession; @@ -923,9 +934,14 @@ describe("WorkspaceService bash monitor wake reconciler wiring", () => { return Promise.resolve(Ok(undefined)); } secondStartedAfterIdle = idleReleased; - return ( - callbacks?.onAccepted?.().then(() => Ok(undefined)) ?? Promise.resolve(Ok(undefined)) - ); + const onAccepted = callbacks?.onAccepted; + if (onAccepted == null) { + throw new Error("Expected the second wake to provide an acceptance callback"); + } + return onAccepted().then(() => { + secondAccepted.resolve(); + return Ok(undefined); + }); } ); @@ -951,7 +967,18 @@ describe("WorkspaceService bash monitor wake reconciler wiring", () => { queuedEntryCount--; preparing = true; streaming = false; - await firstCallbacks?.onAccepted?.(); + const firstOnAccepted = firstCallbacks?.onAccepted; + expect(firstOnAccepted).toBeDefined(); + if (firstOnAccepted == null) { + throw new Error("Expected the first wake to provide an acceptance callback"); + } + await firstOnAccepted(); + expect(acknowledgeMonitorWake).toHaveBeenCalledWith( + "first-proc", + Date.parse(firstWake.createdAt), + firstWake.match?.throughOffset, + undefined + ); await internal.bashMonitorWakeReconciler.reconcile(workspaceId); expect(sendCount).toBe(1); @@ -960,15 +987,23 @@ describe("WorkspaceService bash monitor wake reconciler wiring", () => { preparing = false; streaming = true; - await Promise.resolve(); + await idleWaitStarted.promise; expect(sendCount).toBe(1); busy = false; streaming = false; idleReleased = true; idleGate.resolve(); - await waitForCondition(() => sendCount === 2); + await secondAccepted.promise; + expect(sendCount).toBe(2); expect(secondStartedAfterIdle).toBe(true); + expect(acknowledgeMonitorWake).toHaveBeenCalledTimes(2); + expect(acknowledgeMonitorWake).toHaveBeenLastCalledWith( + "second-proc", + Date.parse(secondWake.createdAt), + secondWake.match?.throughOffset, + undefined + ); } finally { await cleanup(); } From 03e9b2e09461815b27d1ba3b0acba7d8930c2c23 Mon Sep 17 00:00:00 2001 From: Mux Date: Tue, 1 Sep 2026 16:00:59 -0500 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=A4=96=20tests:=20await=20final=20wak?= =?UTF-8?q?e=20reconciliation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/node/services/workspaceService.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/node/services/workspaceService.test.ts b/src/node/services/workspaceService.test.ts index 9b4699b17f..f8c4a1c4db 100644 --- a/src/node/services/workspaceService.test.ts +++ b/src/node/services/workspaceService.test.ts @@ -995,6 +995,7 @@ describe("WorkspaceService bash monitor wake reconciler wiring", () => { idleReleased = true; idleGate.resolve(); await secondAccepted.promise; + await internal.bashMonitorWakeReconciler.reconcile(workspaceId); expect(sendCount).toBe(2); expect(secondStartedAfterIdle).toBe(true); expect(acknowledgeMonitorWake).toHaveBeenCalledTimes(2);