test: advance the Rizin and build computer fixed waits instead of sleeping through them (#11026)

This commit is contained in:
morluto authored and GitHub committed 2026-11-15 18:33:52 +08:00
1 parent 5cfaac0834
commit f46cd6770c
5 files changed
+49 -17

No files matched your search

@@ -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<DesktopBuildSecrets, "redact" | "redactJson"> {
return this.#secrets;
+1 -1
View File
@@ -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;
+16 -12
View File
@@ -37,16 +37,15 @@ const admit = <Schema extends z.ZodType>(
);
};
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 <Value>(
context: ServerContext,
phase: string,
intervalMs: number,
run: (progress: ProgressReporter) => Promise<Value>,
): Promise<Value> => {
const progress = mcpProgressReporter(context);
@@ -60,7 +59,7 @@ const withHeartbeat = async <Value>(
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 = <Value>(
context: ServerContext,
phase: string,
run: (progress: ProgressReporter) => Promise<Value>,
) => withHeartbeat(context, phase, computers.progressMs, run);
const tool = <Name extends Parameters<typeof toolContract>[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),
),
);
+8 -2
View File
@@ -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(),
@@ -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<ReturnType<typeof manager.close>>;
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);