返回 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 { installDesktopHostStub } = await import("/src/__tests__/desktopHostStub.ts");
30 const { DESKTOP_COMMANDS } = await import("/src/generated/desktopContract.generated.ts");
31 const fallback = Object.fromEntries(DESKTOP_COMMANDS.map(key => [key, app[key]]));
32 const tabs = await app.ListTabs();
33 const tree = { topics: [] };
34 const selected = tabs.find(tab => tab.sessionPath?.includes("small")) ?? tabs[0];
35 window.__runtimeFixture = { tab: selected, revision: 0, calls: [], queries: [], fail: true, accept: (...args) => acceptRuntimeState(runtimeStateStore, ...args), topics: tree.topics };
36 onRemoteTabOpened(tab => { window.__runtimeFixture.tab = tab; });
37 onRemoteTabUpdated(tab => { window.__runtimeFixture.tab = tab; });
38 const host = installDesktopHostStub(new Proxy(fallback, { get(_target, key) {
39 if (key === "CaptureInboxTarget") return async (tabId, sessionPath) => {
40 if (!sessionPath || sessionPath !== window.__runtimeFixture.tab.sessionPath) throw new Error("Composer did not bind its selected session path: " + JSON.stringify({ tabId, sessionPath, expected: window.__runtimeFixture.tab.sessionPath }));
41 const remote = window.__runtimeFixture.tab.remote;
42 return { tabId, sessionPath, generation: 1, selection: 0, remote: Boolean(remote), hostId: remote?.hostId, workspace: remote?.workspace };
43 };
44 if (key === "LookupInboxFollowupForTarget") return async (...args) => {
45 window.__runtimeFixture.queries.push(args);
46 if (window.__runtimeFixture.fail) throw new Error("receipt unavailable");
47 return { itemId: "runtime-queued", disposition: "idempotent_hit", position: 0, paused: false };
48 };
49 if (key === "EnqueueInboxFollowupForTarget") return async (...args) => {
50 window.__runtimeFixture.calls.push(args);
51 if (window.__runtimeFixture.fail) throw new Error("fixture enqueue unavailable");
52 return { itemId: "runtime-queued", disposition: "queued", position: 1, paused: false };
53 };
54 const value = fallback[key];
55 if (key === "OpenRemoteProjectTab") return async (...args) => { const tab = await value(...args); window.__runtimeFixture.tab = tab; return tab; };
56 return value;
57 } }));
58 window.__runtimeFixture.emit = (tabId, channel, payload) => host.emit(`remote-tab:${tabId}:${channel}`, payload);
59 });
60 const publish = async (phase, extra = {}, remote = false) => page.evaluate(({ phase, extra, remote }) => {
61 const f = window.__runtimeFixture;
62 const tab = f.tab;
63 const state = { schemaVersion: 1, runtimeEpoch: "fixture-controller", revision: ++f.revision, phase,
64 running: phase === "executing" || phase === "finishing", turnId: "fixture-turn", turnStatus: phase === "executing" ? "in_progress" : "completed",
65 turnEventSeq: 1, pendingPrompt: false, cancelRequested: false, cancellable: phase === "executing", backgroundJobs: 0, activity: phase === "executing" ? "thinking" : "", ...extra };
66 return f.accept({ epoch: "fixture-app", revision: f.revision, topics: f.topics, sessions: [{
67 tabId: tab.id, scope: tab.scope ?? "project", workspaceRoot: tab.workspaceRoot, topicId: tab.topicId ?? "",
68 sessionPath: tab.sessionPath ?? "", sessionGeneration: 1, open: true, remote,
69 hostId: tab.remote?.hostId, freshness: extra.freshness ?? "synced", state,
70 }] }, true);
71 }, { phase, extra, remote });
72 await publish("finishing");
73 await page.locator(".composer-run-strip").filter({ hasText: /Finishing|正在收尾/ }).waitFor();
74 check(await page.locator(".composer__btn--stop").count() === 0, "finishing hides Stop");
75 check(await page.locator(".composer-card--running,.composer-run-strip__dot").count() === 0, "finishing has no animated run marker");
76 await input.fill("durable next turn");
77 await input.press("Enter");
78 await page.waitForFunction(() => window.__runtimeFixture.calls.length === 1);
79 check(await input.inputValue() === "durable next turn", "failed enqueue preserves draft");
80 await fs.mkdir("/tmp/reasonix-runtime-evidence", { recursive: true });
81 await page.screenshot({ path: "/tmp/reasonix-runtime-evidence/pending-followup.png" });
82 await publish("idle");
83 check(await page.locator(".composer__btn--send").getAttribute("aria-label") === "Check send result", "phase transition keeps receipt confirmation action");
84 await page.evaluate(() => { window.__runtimeFixture.fail = false; });
85 await input.press("Enter");
86 await page.waitForFunction(() => document.querySelector("textarea.composer__input:not([aria-hidden=true])")?.value === "");
87 const calls = await page.evaluate(() => window.__runtimeFixture.calls);
88 const queries = await page.evaluate(() => window.__runtimeFixture.queries);
89 check(calls.length === 1 && queries.length === 1 && calls[0].at(-1) === queries[0].at(-1), "retry only queries the original durable idempotency key");
90 await publish("idle", { backgroundJobs: 2 });
91 await page.locator(".composer-run-strip").filter({ hasText: /2/ }).waitFor();
92 await page.locator(".runtime-activity-indicator:not(.runtime-activity-indicator--static)").first().waitFor();
93 check(await page.locator(".runtime-activity-indicator:not(.runtime-activity-indicator--static)").count() > 0, "background jobs keep project activity visible");
94 await publish("idle");
95 await page.locator(".composer-run-strip").waitFor({ state: "hidden" });
96 // The invariant is that no activity indicator stays visible once the last
97 // job completes. Sidebar surfaces settle asynchronously, so wait for the
98 // settled state and name whatever stayed lit if it never arrives.
99 const lingering = await page.evaluate(async () => {
100 const visible = () => [...document.querySelectorAll(".runtime-activity-indicator")].filter(el => el.getClientRects().length > 0);
101 const deadline = Date.now() + 15000;
102 while (visible().length && Date.now() < deadline) await new Promise(resolve => setTimeout(resolve, 50));
103 return visible().map(el => `${el.closest("[class]")?.className ?? "?"}: ${el.getAttribute("aria-label") ?? el.className}`);
104 });
105 check(lingering.length === 0, `last job completion clears project activity (lingering: ${lingering.join(" | ") || "none"})`);
106 await page.locator('.project-tree__folder-main:has(svg.lucide-cloud)').click();
107 await page.locator('.project-tree__topic-main:has-text("Remote demo session")').click();
108 await page.locator(".remote-surface--ready").waitFor();
109 await publish("finishing", {}, true);
110 await page.locator(".composer-run-strip").filter({ hasText: /Finishing|正在收尾/ }).waitFor();
111 await input.fill("remote durable next turn");
112 await input.press("Enter");
113 await page.waitForFunction(() => window.__runtimeFixture.calls.length === 2);
114 check(await input.inputValue() === "", "remote finishing queues the next input and clears it after receipt");
115 await publish("executing", { freshness: "unknown" }, true);
116 await page.locator(".composer-run-strip").filter({ hasText: /sync|同步/i }).waitFor();
117 check(await input.isDisabled(), "remote disconnect blocks send while preserving unknown state");
118 check(await page.locator(".composer__btn--stop").count() === 0, "unknown remote state hides Stop");
119 await fs.mkdir("/tmp/reasonix-runtime-evidence", { recursive: true });
120 await page.screenshot({ path: "/tmp/reasonix-runtime-evidence/remote-unknown.png" });
121 await publish("executing", {}, true);
122 await page.locator(".composer__btn--stop").waitFor();
123 check(!(await input.isDisabled()), "remote reconnect restores authoritative execution controls");
124 await page.evaluate(async () => {
125 const __emitMockRemoteTab = window.__runtimeFixture.emit;
126 const tabId = window.__runtimeFixture.tab.id;
127 __emitMockRemoteTab(tabId, "event", { kind: "turn_started", turnId: "fixture-turn" });
128 __emitMockRemoteTab(tabId, "event", { kind: "text", text: "runtime missing completion fixture" });
129 });
130 await page.locator(".remote-surface").getByText("runtime missing completion fixture", { exact: true }).waitFor();
131 await publish("idle", {}, true);
132 await page.locator(".composer-run-strip").waitFor({ state: "hidden" });
133 check(await page.locator(".composer__btn--stop").count() === 0, "remote completion removes the run control");
134 check(await page.locator(".remote-surface").getByText("runtime missing completion fixture", { exact: true }).count() === 1,
135 "ancillary idle cannot erase output before transcript v2 confirms completion");
136 await selectSession(page, "bench:geometry");
137 await page.waitForFunction(() => document.querySelector(".transcript")?.textContent?.includes("Geometry contract fixture complete."));
138 check(await page.locator(".remote-surface").count() === 0, "local switch retains ownership after remote runtime frames");
139 check(errors.length === 0, "runtime scenarios produce no browser errors: " + errors.join("; "));
140 } catch (error) {
141 console.error("Runtime fixture toasts:", await page.locator(".toast__text").allTextContents());
142 throw error;
143 } finally { await browser.close(); await server.close(); }
144
144 lines Plain Text