返回 DeepSeek-Reasonix
submission-handoff.mjs
根目录 / desktop / frontend / bench / submission-handoff.mjs
1 import assert from "node:assert/strict";
2 import { mkdtemp, copyFile, writeFile, mkdir, rm } from "node:fs/promises";
3 import { writeFileSync } from "node:fs";
4 import { createRequire } from "node:module";
5 import { tmpdir } from "node:os";
6 import path from "node:path";
7 import { fileURLToPath } from "node:url";
8 import { createServer } from "vite";
9 import { verifyNativeReaderInput } from "./submission-native-input.mjs";
10
11 const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
12 process.env.PLAYWRIGHT_BROWSERS_PATH = path.join(root, ".pw-browsers");
13 const { chromium, _electron } = await import("playwright");
14 const electron = process.argv.includes("--electron");
15 const nativeInput = process.argv.includes("--native-input");
16 assert.ok(!nativeInput || electron, "native input requires Electron");
17 const evidence = process.env.REASONIX_HANDOFF_EVIDENCE ?? path.join(tmpdir(), "reasonix-handoff-evidence");
18 await mkdir(evidence, { recursive: true });
19 const server = await createServer({ root, server: { host: "127.0.0.1", port: 0 } });
20 await server.listen();
21 const url = `http://127.0.0.1:${server.httpServer.address().port}/bench/submission-handoff.html`;
22 console.log(url);
23 const host = await mkdtemp(path.join(tmpdir(), "reasonix-handoff-host-"));
24 let browser, app;
25 const report = { host: electron ? "electron" : "chromium", actions: [], rounds: [], errors: [], nativeInput: "not_run", complete: false };
26 try {
27 let page;
28 if (electron) {
29 await copyFile(path.join(root, nativeInput ? "bench/submission-native-electron.cjs" : "bench/transcript-layout-electron.cjs"), path.join(host, "main.cjs"));
30 app = await _electron.launch({ executablePath: createRequire(path.join(root, "../electron/package.json"))("electron"), args: [path.join(host, "main.cjs")], env: { ...process.env, REASONIX_LAYOUT_URL: url } });
31 page = await app.firstWindow();
32 } else {
33 browser = await chromium.launch({ headless: true });
34 page = await browser.newPage({ viewport: { width: 1280, height: 900 } });
35 }
36 page.on("pageerror", error => report.errors.push(error.message));
37 page.on("console", message => { if (message.type() === "error" && /same key|unique.*key|Maximum update|Uncaught/i.test(message.text())) report.errors.push(message.text()); });
38 await page.addInitScript(() => { window.handoffWrites = []; window.__REASONIX_TRANSCRIPT_SCROLL_WRITE__ = write => window.handoffWrites.push(write); });
39 await page.goto(url);
40 await page.waitForFunction(() => Boolean(window.handoff));
41 const frame = () => page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))));
42 await page.locator('[data-chat-kind="user"]').first().waitFor();
43 await frame();
44 const input = page.locator("textarea.composer__input:not(.composer__input--measure)");
45 await input.fill("handoff question"); await input.press("Enter");
46 report.actions.push("composer-submit", "live-output", "rpc-accepted", "output-completed", "expand-process", "batched-identity-and-message-only-record");
47 await page.waitForFunction(() => window.handoff.inspect().locals === 1);
48 await page.evaluate(() => window.handoff.output()); await frame();
49 await page.evaluate(() => window.handoff.finish()); await frame();
50 const disclosure = page.locator('[data-chat-kind="process"]').last().locator('button[aria-expanded]');
51 await disclosure.click();
52 await frame();
53 assert.equal(await disclosure.getAttribute("aria-expanded"), "true");
54 await page.evaluate(() => {
55 window.beforeHandoff = Object.fromEntries(["user", "process", "tail"].map(kind => [kind, [...document.querySelectorAll(`[data-chat-kind="${kind}"]`)].at(-1)]));
56 const text = window.beforeHandoff.user.querySelector(".msg__body");
57 if (text?.firstChild) { const range = document.createRange(); range.selectNodeContents(text); getSelection().removeAllRanges(); getSelection().addRange(range); }
58 window.selectionBefore = getSelection().toString();
59 window.focusBefore = document.activeElement;
60 window.writeCountBefore = window.handoffWrites.length;
61 window.handoff.batched();
62 });
63 await page.waitForFunction(() => window.handoff.inspect().locals === 0); await frame();
64 report.handoff = await page.evaluate(() => ({
65 sameNodes: Object.fromEntries(Object.entries(window.beforeHandoff).map(([kind, node]) => [kind, Boolean(node && node === [...document.querySelectorAll(`[data-chat-kind="${kind}"]`)].at(-1))])),
66 selectionPreserved: getSelection().toString() === window.selectionBefore,
67 focusPreserved: document.activeElement === window.focusBefore,
68 processExpanded: [...document.querySelectorAll('[data-chat-kind="process"]')].at(-1)?.querySelector('button')?.getAttribute("aria-expanded"),
69 newWrites: window.handoffWrites.slice(window.writeCountBefore), ...window.handoff.inspect(),
70 }));
71 assert.ok(Object.values(report.handoff.sameNodes).every(Boolean), JSON.stringify(report.handoff.sameNodes));
72 assert.ok(report.handoff.selectionPreserved);
73 assert.ok(report.handoff.focusPreserved);
74 assert.equal(report.handoff.processExpanded, "true");
75 assert.equal(report.handoff.ids.filter(id => id === "m:sent-0").length, 1);
76 await page.evaluate(() => window.handoff.finish()); await frame();
77
78 for (let round = 0; round < 20; round++) {
79 report.actions.push({ round, direction: "older", pages: 4 });
80 await page.evaluate(async () => { for (let step = 0; step < 4; step++) await window.handoff.page("older"); }); await frame();
81 let sample = await page.evaluate(() => window.handoff.inspect());
82 assert.ok(sample.stats.residentWindowEntries <= 96);
83 assert.ok(sample.handoffs <= sample.users);
84 assert.ok(sample.presentation.messages <= sample.users);
85 assert.equal(sample.locals, 0);
86 assert.equal(sample.ids.includes("m:sent-0"), false);
87 if (round === 0) {
88 report.actions.push("reader-wheel", "offscreen-submit", "offscreen-record");
89 if (nativeInput) {
90 report.nativeInput = {};
91 const saveProgress = () => writeFileSync(path.join(evidence, "native-input-progress.json"), JSON.stringify(report.nativeInput, null, 2));
92 const progress = setInterval(saveProgress, 2000);
93 const deadline = setTimeout(() => {
94 report.nativeInput.timeout = true;
95 saveProgress();
96 app.process().kill("SIGKILL");
97 }, 90_000);
98 try { await verifyNativeReaderInput(page, report.nativeInput); }
99 catch (error) {
100 await page.screenshot({ path: path.join(evidence, "native-input-failure.png"), timeout: 5000 }).catch(() => {});
101 throw error;
102 } finally {
103 clearInterval(progress); clearTimeout(deadline); saveProgress();
104 }
105 }
106 else {
107 await page.locator(".chat-flow-scroll").hover();
108 await page.mouse.wheel(0, -300);
109 }
110 await page.waitForFunction(() => document.querySelector(".chat-flow-scroll")?.getAttribute("data-scroll-mode") === "reader");
111 await frame();
112 await page.evaluate(() => { window.handoff.send("offscreen question"); window.readerIds = window.handoff.inspect().ids; }); await frame();
113 await page.evaluate(() => {
114 const row = document.querySelector('[data-chat-kind="user"]');
115 window.readerNode = row; window.readerTop = row.getBoundingClientRect().top;
116 window.writeCountBefore = window.handoffWrites.length;
117 window.handoff.record();
118 });
119 await page.waitForFunction(() => window.handoff.inspect().locals === 0); await frame();
120 report.offscreen = await page.evaluate(() => ({ sameIds: JSON.stringify(window.readerIds) === JSON.stringify(window.handoff.inspect().ids),
121 sameNode: window.readerNode.isConnected, drift: Math.abs(window.readerNode.getBoundingClientRect().top - window.readerTop),
122 newWrites: window.handoffWrites.slice(window.writeCountBefore) }));
123 assert.ok(report.offscreen.sameIds && report.offscreen.sameNode);
124 assert.ok(report.offscreen.drift <= 1);
125 assert.ok(report.offscreen.newWrites.every(write => write.owner === "restore" && write.outcome === "no-op"), "confirmation must not scroll the reader or request tail-follow");
126 await page.evaluate(() => window.handoff.finish());
127 }
128 report.actions.push({ round, direction: "newer", pages: 4 });
129 await page.evaluate(async () => { for (let step = 0; step < 4; step++) await window.handoff.page("newer"); }); await frame();
130 sample = await page.evaluate(() => window.handoff.inspect());
131 assert.ok(sample.stats.residentWindowEntries <= 96);
132 assert.ok(sample.handoffs <= sample.users);
133 assert.ok(sample.presentation.messages <= sample.users);
134 assert.ok(sample.presentation.submissions <= sample.users + sample.locals);
135 assert.equal(new Set(sample.ids).size, sample.ids.length);
136 assert.equal(sample.ids.filter(id => id === "m:sent-0").length, 1, "reclaimed message must reload exactly once");
137 assert.equal(await page.locator('[data-chat-kind="user"]').filter({ hasText: "handoff question" }).count(), 1);
138 assert.equal(await page.locator('[data-chat-kind="user"]').filter({ hasText: "offscreen question" }).count(), 1);
139 assert.equal(sample.locals, 0);
140 report.rounds.push(sample);
141 }
142 assert.ok(report.rounds.at(-1).stats.reclaimedPages > 0);
143 assert.deepEqual(report.errors, []);
144 report.complete = true;
145 console.log(JSON.stringify({ host: report.host, complete: true, rounds: report.rounds.length, handoff: report.handoff.sameNodes, offscreen: report.offscreen, stats: report.rounds.at(-1).stats }));
146 } finally {
147 await writeFile(path.join(evidence, `${report.host}.json`), JSON.stringify(report, null, 2));
148 await app?.close(); await browser?.close(); await server.close(); await rm(host, { recursive: true, force: true });
149 }
150
150 lines Plain Text