test(muse): drive the failed-repair retry test on the fake clock

The test polled the fake's request log and then slept 50 ms before advancing
the clock again, so a slow host could advance time before the plugin had
handled the failed page and re-armed its watchdog. The fake now never answers
the first page, so the plugin's own request timeout fails the repair on the
fake clock, and the test steps through the timeout and the re-armed watchdog
one timer at a time. Waiting for the first request moves into a harness
helper.
This commit is contained in:
kb
2026-11-03 12:03:17 +00:00
committed by paseo-bot[bot]
parent 8a349c6045
commit 2e81768969
2 changed files with 26 additions and 17 deletions
+6 -6
View File
@@ -30,7 +30,7 @@ const catalogRows = readFixture("catalog-controls");
const replacements = new Map();
const counts = new Map();
const receipts = new Set();
let pageFailed = false;
let pageHung = false;
const lines = createInterface({ input: process.stdin });
lines.on("line", receive);
function receive(line) {
@@ -143,9 +143,9 @@ function scriptedResponse(frame) {
respond(frame, { status: "accepted", commandId: frame.params.commandId });
return true;
}
return frame.method === "view/page" && rejectedPage(frame);
return frame.method === "view/page" && failedPage(frame);
}
function rejectedPage(frame) {
function failedPage(frame) {
const anchor = frame.params.cursor;
// Real Muse rejects an anchor from another session's view the same way (live probe, Muse 1.4.7).
if (
@@ -164,9 +164,9 @@ function rejectedPage(frame) {
});
return true;
}
if (process.env.MUSE_TEST_PAGE_FAIL_ONCE && !pageFailed) {
pageFailed = true;
rpcError(frame, -32603, "internal", "view range read failed");
// Never answering lets the plugin's own request timeout fail the repair on the test's fake clock.
if (process.env.MUSE_TEST_PAGE_HANG_ONCE && !pageHung) {
pageHung = true;
return true;
}
return false;
+20 -11
View File
@@ -138,6 +138,15 @@ async function harness(
.filter(Boolean)
.map((line) => JSON.parse(line));
}
async function waitForRequests(method: string, count: number) {
const deadline = Date.now() + 5000;
while (Date.now() < deadline) {
const sent = (await recorded()).filter((frame) => frame.method === method);
if (sent.length >= count) return sent;
await delay(10);
}
throw new Error(`Muse never received ${count} ${method} request(s)`);
}
return {
provider,
launch,
@@ -148,6 +157,7 @@ async function harness(
open,
prompt,
recorded,
waitForRequests,
root,
usageSource,
};
@@ -1318,25 +1328,24 @@ test("a subagent approval does not move the parent's silence repair onto the chi
test("a failed silence repair keeps the session and retries at the next silence", async () => {
const h = await harness("text-reasoning", {
MUSE_TEST_SILENT: "1",
MUSE_TEST_PAGE_FAIL_ONCE: "1",
MUSE_TEST_PAGE_HANG_ONCE: "1",
});
await h.open();
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
await h.prompt();
await h.wait((e) => e.type === "session.prompt_result");
// The first repair's page is never answered. The silence, that page's request timeout and the
// re-armed watchdog all run on the fake clock, one timer per step, whatever the timeout length.
await vi.advanceTimersByTimeAsync(120000);
const deadline = Date.now() + 5000;
while ((await h.recorded()).filter((f) => f.method === "view/page").length === 0) {
if (Date.now() > deadline) throw new Error("The first repair never paged");
await delay(10);
}
await delay(50);
await h.waitForRequests("view/page", 1);
await vi.advanceTimersToNextTimerAsync();
await vi.advanceTimersToNextTimerAsync();
await h.wait(
(e) =>
(e.type === "session.turn" && e.state === "completed") || e.type === "session.runtime_failed",
);
expect(h.events.filter((e) => e.type === "session.runtime_failed")).toEqual([]);
expect(h.events.filter((e) => e.type === "session.turn" && e.state === "completed")).toEqual([]);
await vi.advanceTimersByTimeAsync(120000);
await h.wait((e) => e.type === "session.turn" && e.state === "completed");
expect((await h.recorded()).filter((f) => f.method === "view/page")).toHaveLength(2);
expect(h.events.filter((e) => e.type === "session.runtime_failed")).toEqual([]);
expect(h.events.filter((e) => e.type === "session.turn" && e.state === "completed")).toHaveLength(
1,
);