From 2f9788aeae21bf1c86bbdb890e98b57186155ae0 Mon Sep 17 00:00:00 2001 From: Lihua <1017343802@qq.com> Date: Sat, 7 Nov 2026 20:08:57 -0800 Subject: [PATCH 1/3] fix(smoke): pause the conversation clock at a future instant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The conversation-activity scenario sampled the page's mocked clock with page.evaluate(() => Date.now()) and passed that instant to page.clock.pauseAt. Between the evaluate round trip and the pause command the running clock keeps advancing, so on a loaded runner the clock can already be past the sampled instant, and pausing into the past throws 'Cannot fast-forward to the past' — the failure that has been flipping the required dashboard-browser shard 1 red for open PRs (#8256, #8290) and main's browser lanes. goal-showcase and navigation-sorting already freeze at a fixed future instant for exactly this reason ('a sampled now can already be in the past when the next browser command arrives'); conversation-activity was the one remaining site that still sampled. Pause at the sampled instant plus a five-second headroom, which keeps the scenario's property — one frozen instant compared across views — while guaranteeing the target stays in the clock's future regardless of command latency. Signed-off-by: Lihua <1017343802@qq.com> --- .../personal-workspace-browser/conversation-activity.mjs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/personal-workspace-browser/conversation-activity.mjs b/examples/personal-workspace-browser/conversation-activity.mjs index 3410ffd3c5..9bd5e116ce 100644 --- a/examples/personal-workspace-browser/conversation-activity.mjs +++ b/examples/personal-workspace-browser/conversation-activity.mjs @@ -50,7 +50,11 @@ export const conversationActivityScenario = { await page.clock.fastForward(25000); // Compare the same instant across views; clock.install alone keeps // advancing and can cross a second boundary while navigation completes. - await page.clock.pauseAt(await page.evaluate(() => Date.now())); + // The pause target must sit in the future of the sampled instant: by + // the time the pause command arrives the running clock can already be + // past a sampled "now", and pausing into the past throws. + const sampledNow = await page.evaluate(() => Date.now()); + await page.clock.pauseAt(sampledNow + 5_000); await pending.locator(".personal-message-quiet").waitFor(); const elapsedBefore = await pending.locator(".personal-message-elapsed").textContent(); await page.getByRole("navigation", { name: "管家视图" }).getByRole("button", { name: "总览", exact: true }).click(); From c0f701f88a0759546c3f2e47a408f0b0f801528a Mon Sep 17 00:00:00 2001 From: Lihua <1017343802@qq.com> Date: Sat, 7 Nov 2026 23:00:08 -0800 Subject: [PATCH 2/3] fix(smoke): freeze the conversation clock from a controlled origin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-up: five seconds of headroom only narrowed the race — a controlled 5.25 s command delay still reproduces 'Cannot fast-forward to the past', because the sampled instant stays subject to command scheduling. Install the fake clock at a fixed origin and pause at a deterministic future instant from that origin, the pattern goal-showcase and navigation-sorting already use; only the equality of the frozen instant across views is asserted, which the fixed origin preserves. Verified in a real browser: the scenario passes (6,784 ms). Signed-off-by: Lihua <1017343802@qq.com> --- .../conversation-activity.mjs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/examples/personal-workspace-browser/conversation-activity.mjs b/examples/personal-workspace-browser/conversation-activity.mjs index 9bd5e116ce..59cbb5bcc2 100644 --- a/examples/personal-workspace-browser/conversation-activity.mjs +++ b/examples/personal-workspace-browser/conversation-activity.mjs @@ -46,15 +46,16 @@ export const conversationActivityScenario = { const turn = await send("请检查当前任务状态,并说明下一步。"); await page.getByRole("status", {name: "正在回复", exact: true}).waitFor(); const pending = page.locator(".personal-message").filter({ has: page.getByRole("button", { name: "中断本轮", exact: true }) }); - await page.clock.install(); + // Freeze at a controlled future instant, the pattern goal-showcase and + // navigation-sorting already use: a sampled "now" can be in the past by + // the time the pause command arrives, and any headroom only narrows the + // race instead of removing it. A fixed origin makes the pause target + // deterministic regardless of command latency; only the equality of the + // frozen instant across views is asserted afterwards. + const clockStart = Date.UTC(2030, 0, 1); + await page.clock.install({time: clockStart}); await page.clock.fastForward(25000); - // Compare the same instant across views; clock.install alone keeps - // advancing and can cross a second boundary while navigation completes. - // The pause target must sit in the future of the sampled instant: by - // the time the pause command arrives the running clock can already be - // past a sampled "now", and pausing into the past throws. - const sampledNow = await page.evaluate(() => Date.now()); - await page.clock.pauseAt(sampledNow + 5_000); + await page.clock.pauseAt(clockStart + 60_000); await pending.locator(".personal-message-quiet").waitFor(); const elapsedBefore = await pending.locator(".personal-message-elapsed").textContent(); await page.getByRole("navigation", { name: "管家视图" }).getByRole("button", { name: "总览", exact: true }).click(); From 985e6c4540f0dcd48a88c15e65dbcd43da8a8778 Mon Sep 17 00:00:00 2001 From: Lihua <1017343802@qq.com> Date: Sun, 8 Nov 2026 00:02:54 -0800 Subject: [PATCH 3/3] fix(smoke): pin the conversation Date before advancing its timers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second review round: a finite future pause target still leaves a scheduling race — install({time}) only sets the starting value and the clock keeps running between commands, so a sufficiently delayed pause (35.25 s in the review's probe) again aims into the past. Follow the review's validated control sequence: setFixedTime pins Date to the controlled instant, fastForward advances the timers without a running wall clock, and the pause lands on the already-pinned instant — no command latency can move the target. After the navigation comparison the scenario restores the system time and resumes, so later steering assertions run on a live clock again. Verified in a real browser: the scenario passes (7,701 ms). Signed-off-by: Lihua <1017343802@qq.com> --- .../conversation-activity.mjs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/examples/personal-workspace-browser/conversation-activity.mjs b/examples/personal-workspace-browser/conversation-activity.mjs index 59cbb5bcc2..bf5b85bc7e 100644 --- a/examples/personal-workspace-browser/conversation-activity.mjs +++ b/examples/personal-workspace-browser/conversation-activity.mjs @@ -46,16 +46,17 @@ export const conversationActivityScenario = { const turn = await send("请检查当前任务状态,并说明下一步。"); await page.getByRole("status", {name: "正在回复", exact: true}).waitFor(); const pending = page.locator(".personal-message").filter({ has: page.getByRole("button", { name: "中断本轮", exact: true }) }); - // Freeze at a controlled future instant, the pattern goal-showcase and - // navigation-sorting already use: a sampled "now" can be in the past by - // the time the pause command arrives, and any headroom only narrows the - // race instead of removing it. A fixed origin makes the pause target - // deterministic regardless of command latency; only the equality of the - // frozen instant across views is asserted afterwards. + // Pin Date to a fixed instant first, then advance timers without a + // running wall clock, and pause. Any finite future pause target leaves a + // scheduling race — the clock keeps running between commands, so a + // delayed pause can again aim into the past. With the Date pinned the + // comparisons use one deterministic instant regardless of command + // latency; the system time is restored before the scenario continues. const clockStart = Date.UTC(2030, 0, 1); await page.clock.install({time: clockStart}); + await page.clock.setFixedTime(clockStart); await page.clock.fastForward(25000); - await page.clock.pauseAt(clockStart + 60_000); + await page.clock.pauseAt(clockStart); await pending.locator(".personal-message-quiet").waitFor(); const elapsedBefore = await pending.locator(".personal-message-elapsed").textContent(); await page.getByRole("navigation", { name: "管家视图" }).getByRole("button", { name: "总览", exact: true }).click(); @@ -65,6 +66,7 @@ export const conversationActivityScenario = { assert.equal(await page.getByRole("status", {name: "正在回复", exact: true}).count(), 1, "Activity stays on the Chat entry"); await page.getByRole("navigation", { name: "管家视图" }).getByRole("button", { name: "对话", exact: true }).click(); assert.equal(await pending.locator(".personal-message-elapsed").textContent(), elapsedBefore); + await page.clock.setSystemTime(Date.now()); await page.clock.resume(); const adjustments = []; let legacyMigrationAttempts = 0;