Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 168 additions & 1 deletion src/node/services/workspaceService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -842,6 +843,172 @@ 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 acknowledgeMonitorWake = mock(() => undefined);
Object.assign(backgroundProcessManager, { acknowledgeMonitorWake });
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<void>();
const idleWaitStarted = createDeferred<void>();
const secondAccepted = createDeferred<void>();
let idleWaitObserved = false;
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: () => {
if (!idleWaitObserved) {
idleWaitObserved = true;
idleWaitStarted.resolve();
}
return idleGate.promise;
},
onChatEvent: () => () => undefined,
} as unknown as AgentSession;

interface AcceptedCallbacks {
onAccepted?: () => Promise<void>;
}
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;
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);
});
}
);

const internal = service as unknown as {
sessions: Map<string, AgentSession>;
aiService: { isStreaming(workspaceId: string): boolean };
getDelegatedTurnContinuationSendOptions(workspaceId: string): Promise<object>;
sendMessage: typeof sendMessage;
bashMonitorWakeReconciler: { reconcile(workspaceId: string): Promise<void> };
};
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;
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);
expect(queuedModes).toEqual(["tool-end"]);
expect(secondStartedAfterIdle).toBe(false);

preparing = false;
streaming = true;
await idleWaitStarted.promise;
expect(sendCount).toBe(1);

busy = false;
streaming = false;
idleReleased = true;
idleGate.resolve();
await secondAccepted.promise;
Comment thread
coadler marked this conversation as resolved.
await internal.bashMonitorWakeReconciler.reconcile(workspaceId);
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();
}
});
});

async function setWorkspaceGoalOk(
Expand Down
Loading