返回 DeepSeek-Reasonix
runtime-state.mjs
根目录 / desktop / frontend / bench / runtime-state.mjs
1 #!/usr/bin/env node
2 // Real Chromium UI with controlled runtime frames; backend ownership is covered
3 // separately by the controller/Serve/remote HTTP regression tests.
4 import path from "node:path";
5 import fs from "node:fs/promises";
6 import { fileURLToPath } from "node:url";
7 import { createServer } from "vite";
8 import { selectSession } from "./app-page-actions.mjs";
9 const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10 process.env.PLAYWRIGHT_BROWSERS_PATH = path.join(root, ".pw-browsers");
11 const { chromium } = await import("playwright");
12 const server = await createServer({ root, logLevel: "error", server: { host: "127.0.0.1", port: 4668, strictPort: true } });
13 await server.listen();
14 const browser = await chromium.launch({ headless: true });
15 const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
16 const errors = [];
17 page.on("pageerror", error => errors.push(error.message));
18 const check = (yes, message) => { if (!yes) throw new Error(message); console.log("PASS " + message); };
19 try {
20 await page.goto("http://127.0.0.1:4668/?mock=bench&bench=1");
21 const input = page.locator("textarea.composer__input:not([aria-hidden=true])");
22 await input.waitFor();
23 await selectSession(page, "bench:small-6t");
24 await page.waitForFunction(() => document.querySelector(".transcript")?.textContent?.includes("ASYNC LAYOUT EXPANSION COMPLETE"));
25 await page.evaluate(async () => {
26 const { app, onRemoteTabOpened, onRemoteTabUpdated } = await import("/src/lib/bridge.ts");
27 const { runtimeStateStore } = await import("/src/lib/runtimeStateStore.ts");
28 const { acceptRuntimeState } = await import("/src/lib/runtimeStateReducer.ts");
29 const { sessionIdentityRoute } = await import("/src/lib/sessionIdentity.ts");
30 const { installDesktopHostStub } = await import("/src/__tests__/desktopHostStub.ts");
31 const { DESKTOP_COMMANDS } = await import("/src/generated/desktopContract.generated.ts");
32 const fallback = Object.fromEntries(DESKTOP_COMMANDS.map(key => [key, app[key]]));
33 const tabs = await app.ListTabs();
34 const tree = { topics: [] };
35 const selected = tabs.find(tab => tab.sessionPath?.includes("small")) ?? tabs[0];
36 window.__runtimeFixture = { tab: selected, revision: 0, calls: [], queries: [], fail: true, accept: (...args) => acceptRuntimeState(runtimeStateStore, ...args), topics: tree.topics };
37 onRemoteTabOpened(tab => { window.__runtimeFixture.tab = tab; });
38 onRemoteTabUpdated(tab => { window.__runtimeFixture.tab = tab; });
39 const host = installDesktopHostStub(new Proxy(fallback, { get(_target, key) {
40 // Periodic/focus reads must observe the same producer as pushed frames.
41 if (key === "SyncRuntimeState" || key === "GetRuntimeStateSnapshot") return async () => runtimeStateStore.getSnapshot();
42 if (key === "CaptureInboxTarget") return async (tabId, sessionPath) => {
43 const expected = sessionIdentityRoute(window.__runtimeFixture.tab);
44 if (!sessionPath || sessionPath !== expected) throw new Error("Composer did not bind its selected session identity: " + JSON.stringify({ tabId, sessionPath, expected }));
45 const remote = window.__runtimeFixture.tab.remote;
46 return { tabId, sessionPath, generation: 1, selection: 0, remote: Boolean(remote), hostId: remote?.hostId, workspace: remote?.workspace };
47 };
48 if (key === "LookupInboxFollowupForTarget") return async (...args) => {
49 window.__runtimeFixture.queries.push(args);
50 if (window.__runtimeFixture.fail) throw new Error("receipt unavailable");
51 return { itemId: "runtime-queued", disposition: "idempotent_hit", position: 0, paused: false };
52 };
53 if (key === "EnqueueInboxFollowupForTarget") return async (...args) => {
54 window.__runtimeFixture.calls.push(args);
55 if (window.__runtimeFixture.fail) throw new Error("fixture enqueue unavailable");
56 return { itemId: "runtime-queued", disposition: "queued", position: 1, paused: false };
57 };
58 const value = fallback[key];
59 if (key === "OpenRemoteProjectTab") return async (...args) => { const tab = await value(...args); window.__runtimeFixture.tab = tab; return tab; };
60 return value;
61 } }));
62 window.__runtimeFixture.emit = (tabId, channel, payload) => host.emit(`remote-tab:${tabId}:${channel}`, payload);
63 });
64 const publish = async (phase, extra = {}, remote = false) => page.evaluate(({ phase, extra, remote }) => {
65 const f = window.__runtimeFixture;
66 const tab = f.tab;
67 const state = { schemaVersion: 1, runtimeEpoch: "fixture-controller", revision: ++f.revision, phase,
68 running: phase === "executing" || phase === "finishing", turnId: "fixture-turn", turnStatus: phase === "executing" ? "in_progress" : "completed",
69 turnEventSeq: 1, pendingPrompt: false, cancelRequested: false, cancellable: phase === "executing", backgroundJobs: 0, activity: phase === "executing" ? "thinking" : "", ...extra };
70 return f.accept({ epoch: "fixture-app", revision: f.revision, topics: f.topics, sessions: [{
71 tabId: tab.id, scope: tab.scope ?? "project", workspaceRoot: tab.workspaceRoot, topicId: tab.topicId ?? "",
72 sessionId: tab.session?.sessionId, sessionPath: tab.sessionPath ?? "", sessionGeneration: tab.sessionGeneration ?? 1, open: true, remote,
73 hostId: tab.remote?.hostId, freshness: extra.freshness ?? "synced", state,
74 }] }, true);
75 }, { phase, extra, remote });
76 await publish("finishing");
77 await page.locator(".composer-run-strip").filter({ hasText: /Finishing|正在收尾/ }).waitFor();
78 check(await page.locator(".composer__btn--stop").count() === 0, "finishing hides Stop");
79 check(await page.locator(".composer-card--running,.composer-run-strip__dot").count() === 0, "finishing has no animated run marker");
80 await input.fill("durable next turn");
81 await input.press("Enter");
82 await page.waitForFunction(() => window.__runtimeFixture.calls.length === 1);
83 check(await input.inputValue() === "durable next turn", "failed enqueue preserves draft");
84 await fs.mkdir("/tmp/reasonix-runtime-evidence", { recursive: true });
85 await page.screenshot({ path: "/tmp/reasonix-runtime-evidence/pending-followup.png" });
86 await publish("idle");
87 check(await page.locator(".composer__btn--send").getAttribute("aria-label") === "Check send result", "phase transition keeps receipt confirmation action");
88 await page.evaluate(() => { window.__runtimeFixture.fail = false; });
89 await input.press("Enter");
90 await page.waitForFunction(() => document.querySelector("textarea.composer__input:not([aria-hidden=true])")?.value === "");
91 const calls = await page.evaluate(() => window.__runtimeFixture.calls);
92 const queries = await page.evaluate(() => window.__runtimeFixture.queries);
93 check(calls.length === 1 && queries.length === 1 && calls[0].at(-1) === queries[0].at(-1), "retry only queries the original durable idempotency key");
94 await publish("idle", { backgroundJobs: 2 });
95 await page.evaluate(() => window.dispatchEvent(new Event("focus")));
96 await page.locator(".composer-run-strip").filter({ hasText: /2/ }).waitFor();
97 await page.locator(".runtime-activity-indicator:not(.runtime-activity-indicator--static)").first().waitFor();
98 check(await page.locator(".runtime-activity-indicator:not(.runtime-activity-indicator--static)").count() > 0, "background jobs keep project activity visible");
99 await publish("idle");
100 await page.locator(".composer-run-strip").waitFor({ state: "hidden" });
101 // The invariant is that no activity indicator stays visible once the last
102 // job completes. Sidebar surfaces settle asynchronously, so wait for the
103 // settled state and name whatever stayed lit if it never arrives.
104 const lingering = await page.evaluate(async () => {
105 const visible = () => [...document.querySelectorAll(".runtime-activity-indicator")].filter(el => el.getClientRects().length > 0);
106 const deadline = Date.now() + 15000;
107 while (visible().length && Date.now() < deadline) await new Promise(resolve => setTimeout(resolve, 50));
108 return visible().map(el => `${el.closest("[class]")?.className ?? "?"}: ${el.getAttribute("aria-label") ?? el.className}`);
109 });
110 check(lingering.length === 0, `last job completion clears project activity (lingering: ${lingering.join(" | ") || "none"})`);
111 await page.locator('.project-tree__folder-main:has(svg.lucide-cloud)').click();
112 await page.locator('.project-tree__topic-main:has-text("Remote demo session")').click();
113 await page.locator(".remote-surface--ready").waitFor();
114 await publish("finishing", {}, true);
115 await page.locator(".composer-run-strip").filter({ hasText: /Finishing|正在收尾/ }).waitFor();
116 await input.fill("remote durable next turn");
117 await input.press("Enter");
118 await page.waitForFunction(() => window.__runtimeFixture.calls.length === 2);
119 check(await input.inputValue() === "", "remote finishing queues the next input and clears it after receipt");
120 await publish("executing", { freshness: "unknown" }, true);
121 await page.locator(".composer-run-strip").filter({ hasText: /sync|同步/i }).waitFor();
122 check(await input.isDisabled(), "remote disconnect blocks send while preserving unknown state");
123 check(await page.locator(".composer__btn--stop").count() === 0, "unknown remote state hides Stop");
124 await fs.mkdir("/tmp/reasonix-runtime-evidence", { recursive: true });
125 await page.screenshot({ path: "/tmp/reasonix-runtime-evidence/remote-unknown.png" });
126 await publish("executing", {}, true);
127 await page.locator(".composer__btn--stop").waitFor();
128 check(!(await input.isDisabled()), "remote reconnect restores authoritative execution controls");
129 await page.evaluate(async () => {
130 const __emitMockRemoteTab = window.__runtimeFixture.emit;
131 const tabId = window.__runtimeFixture.tab.id;
132 __emitMockRemoteTab(tabId, "event", { kind: "turn_started", turnId: "fixture-turn" });
133 __emitMockRemoteTab(tabId, "event", { kind: "text", text: "runtime missing completion fixture" });
134 });
135 await page.locator(".remote-surface").getByText("runtime missing completion fixture", { exact: true }).waitFor();
136 await publish("idle", {}, true);
137 await page.locator(".composer-run-strip").waitFor({ state: "hidden" });
138 check(await page.locator(".composer__btn--stop").count() === 0, "remote completion removes the run control");
139 check(await page.locator(".remote-surface").getByText("runtime missing completion fixture", { exact: true }).count() === 1,
140 "ancillary idle cannot erase output before transcript v2 confirms completion");
141 await selectSession(page, "bench:geometry");
142 await page.waitForFunction(() => document.querySelector(".transcript")?.textContent?.includes("Geometry contract fixture complete."));
143 check(await page.locator(".remote-surface").count() === 0, "local switch retains ownership after remote runtime frames");
144 check(errors.length === 0, "runtime scenarios produce no browser errors: " + errors.join("; "));
145 } catch (error) {
146 console.error("Runtime fixture toasts:", await page.locator(".toast__text").allTextContents());
147 console.error("Runtime fixture state:", await page.evaluate(async () => ({ tab: window.__runtimeFixture?.tab, tabs: await (await import("/src/lib/bridge.ts")).app.ListTabs(), runtime: (await import("/src/lib/runtimeStateStore.ts")).runtimeStateStore.getSnapshot() })));
148 throw error;
149 } finally { await browser.close(); await server.close(); }
150
150 lines Plain Text