From f46cd6770c8dd05e22ead3f082acf4e214bfd8b4 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sun, 15 Nov 2026 18:33:52 +0800 Subject: [PATCH] test: advance the Rizin and build computer fixed waits instead of sleeping through them (#11026) --- .../desktop/DesktopBuildComputers.ts | 5 ++++ src/desktop/AgentEnvComputer.ts | 2 +- src/server/registerDesktopBuildTools.ts | 28 +++++++++++-------- tests/boundary/mcp/buildComputerMcp.test.ts | 10 +++++-- .../RizinDebugSessionCoverage.part1.test.ts | 21 ++++++++++++-- 5 files changed, 49 insertions(+), 17 deletions(-) diff --git a/src/application/desktop/DesktopBuildComputers.ts b/src/application/desktop/DesktopBuildComputers.ts index 46111ed665..019ef6322c 100644 --- a/src/application/desktop/DesktopBuildComputers.ts +++ b/src/application/desktop/DesktopBuildComputers.ts @@ -421,6 +421,11 @@ export class DesktopBuildComputers { this.#shared = { settle, secrets: this.#secrets }; } + /** How often a computer's start, and a tool call waiting on one, reports progress. */ + get progressMs(): number { + return this.startTiming.progressMs; + } + /** How results show the secrets typed on this server's computers. */ get redaction(): Pick { return this.#secrets; diff --git a/src/desktop/AgentEnvComputer.ts b/src/desktop/AgentEnvComputer.ts index 599fc801ab..4ab7c8adcb 100644 --- a/src/desktop/AgentEnvComputer.ts +++ b/src/desktop/AgentEnvComputer.ts @@ -26,7 +26,7 @@ import { awaitHttpReply, type HttpReply } from "./HttpReply.js"; const READY_TIMEOUT_MS = 180_000; const REQUEST_TIMEOUT_MS = 120_000; -/** How long a computer's first screenshot waits for the desktop to draw, and how often its start reports progress. */ +/** How long a computer's first screenshot waits for the desktop to draw, and how often its start and the tool calls waiting on it report progress. */ export interface ComputerStartTiming { readonly drawnWaitMs: number; readonly progressMs: number; diff --git a/src/server/registerDesktopBuildTools.ts b/src/server/registerDesktopBuildTools.ts index 4163bb56ce..2dcbf44848 100644 --- a/src/server/registerDesktopBuildTools.ts +++ b/src/server/registerDesktopBuildTools.ts @@ -37,16 +37,15 @@ const admit = ( ); }; -const HEARTBEAT_MS = 5_000; - /** - * Report progress every few seconds while a long build step works, so a + * Report progress every `intervalMs` while a long build step works, so a * client's request timeout can reset; the step reports on the same * reporter, whose notifications keep one order. */ const withHeartbeat = async ( context: ServerContext, phase: string, + intervalMs: number, run: (progress: ProgressReporter) => Promise, ): Promise => { const progress = mcpProgressReporter(context); @@ -60,7 +59,7 @@ const withHeartbeat = async ( message: `working (${String(Math.round((Date.now() - startedAt) / 1000))} s)`, }) .catch(() => undefined); - }, HEARTBEAT_MS); + }, intervalMs); try { return await run(progress); } finally { @@ -173,10 +172,15 @@ export const registerDesktopBuildTools = ( server: EvidenceMcpServer, computers: Pick< DesktopBuildComputers, - "start" | "act" | "close" | "steps" | "measure" | "redaction" + "start" | "act" | "close" | "steps" | "measure" | "redaction" | "progressMs" >, logger: Logger, ): void => { + const heartbeat = ( + context: ServerContext, + phase: string, + run: (progress: ProgressReporter) => Promise, + ) => withHeartbeat(context, phase, computers.progressMs, run); const tool = [0]>( name: Name, ) => ({ @@ -191,38 +195,38 @@ export const registerDesktopBuildTools = ( ); // An action can wait for the call before it on its computer, and a reset for the relaunched app, which can take minutes. registerForwarded(tool("act_on_build_computer"), (input, context) => - withHeartbeat(context, "act_on_build_computer", () => + heartbeat(context, "act_on_build_computer", () => computers.act(input, context.mcpReq.signal), ), ); registerForwarded(tool("start_build_capture"), (input, context) => - withHeartbeat(context, "start_build_capture", () => + heartbeat(context, "start_build_capture", () => steps.startCapture(input, context.mcpReq.signal), ), ); registerPlain(tool("mark_build_checkpoint"), (input, context) => - withHeartbeat(context, "mark_build_checkpoint", () => + heartbeat(context, "mark_build_checkpoint", () => steps.markCheckpoint(input, context.mcpReq.signal), ), ); registerPlain(tool("stop_build_capture"), (input, context) => - withHeartbeat(context, "stop_build_capture", () => + heartbeat(context, "stop_build_capture", () => steps.stopCapture(input, context.mcpReq.signal), ), ); registerPlain(tool("export_build_environment"), (input, context) => - withHeartbeat(context, "export_build_environment", () => + heartbeat(context, "export_build_environment", () => steps.exportEnvironment(input, context.mcpReq.signal), ), ); registerPlain(tool("check_build_task"), (input, context) => - withHeartbeat(context, "check_build_task", (progress) => + heartbeat(context, "check_build_task", (progress) => steps.checkTask(input, context.mcpReq.signal, progress), ), ); // Solves run one at a time for minutes each, so progress keeps the client waiting. registerPlain(tool("measure_build_task"), (input, context) => - withHeartbeat(context, "measure_build_task", (progress) => + heartbeat(context, "measure_build_task", (progress) => computers.measure(input, context.mcpReq.signal, progress), ), ); diff --git a/tests/boundary/mcp/buildComputerMcp.test.ts b/tests/boundary/mcp/buildComputerMcp.test.ts index fd03fb8374..d0d2a729cd 100644 --- a/tests/boundary/mcp/buildComputerMcp.test.ts +++ b/tests/boundary/mcp/buildComputerMcp.test.ts @@ -23,6 +23,7 @@ import { expectNoToken, startBuildComputer, } from "../../fixtures/buildComputerMcp.js"; +import { COMPUTER_START_TIMING } from "../../../src/desktop/AgentEnvComputer.js"; import { FAKE_FRAME_PNG, type FakeApp, @@ -695,9 +696,14 @@ it("runs one computer's calls in the order they arrive and another computer's be }); it("sends progress while an action waits for the call before it on its computer", async () => { - const app = slowApp(14); + const app = slowApp(2); const { runner } = await fakes({ app }); - const client = await connect(runner.environment); + // A short heartbeat lets the queued action report progress while the reset + // before it is still polling the relaunched app. + const client = await connect(runner.environment, undefined, undefined, { + ...COMPUTER_START_TIMING, + progressMs: 100, + }); const id = await startBuildComputer(client, { kind: "bundle", path: await bundleFolder(), diff --git a/tests/boundary/providers/RizinDebugSessionCoverage.part1.test.ts b/tests/boundary/providers/RizinDebugSessionCoverage.part1.test.ts index 5986238bf4..1027f20ffa 100644 --- a/tests/boundary/providers/RizinDebugSessionCoverage.part1.test.ts +++ b/tests/boundary/providers/RizinDebugSessionCoverage.part1.test.ts @@ -287,9 +287,16 @@ it return launched; }); try { + // The real stop's detach command gets no reply from this fixture and + // returns incomplete after its 1 s detach deadline without signalling + // the child. Report the same uncertainty at once; the child stays alive + // for the harness to reap either way. const stopProcess: typeof incompleteStop = stopMode === "is uncertain" - ? incompleteStop + ? async () => ({ + status: "incomplete", + reason: "injected stop uncertainty", + }) : async () => ({ status: "stopped" }); const manager = createManager( { REA_RIZIN_COMMAND: provider }, @@ -475,7 +482,17 @@ it.skipIf(process.platform === "win32")( const sessionId = started.value.session_id; injected.control.exitLeader(); - const firstClose = await manager.close(sessionId); + // The output-settlement grace is a fixed timer; advance it instead of + // waiting it out. The open output keeps the close incomplete regardless. + vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] }); + let firstClose: Awaited>; + try { + const closing = manager.close(sessionId); + await vi.advanceTimersByTimeAsync(10_000); + firstClose = await closing; + } finally { + vi.useRealTimers(); + } expect(firstClose.ok).toBe(false); if (!firstClose.ok) { expect(firstClose.error.cleanupIncomplete).toBe(true);