返回 DeepSeek-Reasonix
independent-sessions.mjs
根目录 / desktop / frontend / bench / independent-sessions.mjs
1 import assert from "node:assert/strict";
2 import path from "node:path";
3 import fs from "node:fs/promises";
4 import { tmpdir } from "node:os";
5 import { fileURLToPath } from "node:url";
6 import { createServer } from "vite";
7 const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
8 process.env.PLAYWRIGHT_BROWSERS_PATH ??= path.join(root, ".pw-browsers");
9 const { chromium } = await import("playwright");
10 const port = Number(process.env.REASONIX_INDEPENDENT_BROWSER_PORT ?? 4687);
11 const server = await createServer({ root, logLevel: "error", server: { host: "127.0.0.1", port, strictPort: true, hmr: false, watch: { ignored: ["**"] } } });
12 await server.listen();
13 const browser = await chromium.launch({ headless: true });
14 const page = await browser.newPage({ viewport: { width: 1440, height: 1000 }, locale: "en-US" });
15 const errors = [];
16 page.on("pageerror", error => errors.push(error.message));
17 const evidence = process.env.REASONIX_INDEPENDENT_EVIDENCE ?? path.join(tmpdir(), "reasonix-independent-browser");
18 const row = title => page.locator(".project-tree__topic-main").filter({ hasText: title });
19 async function dragRow(sourceTitle, target, position = "before") {
20 const source = row(sourceTitle).locator("..");
21 const transfer = await page.evaluateHandle(() => new DataTransfer());
22 await source.dispatchEvent("dragstart", { dataTransfer: transfer });
23 const box = await target.boundingBox();
24 const point = { clientX: box.x + box.width / 2, clientY: position === "before" ? box.y + 2 : box.y + box.height - 2, dataTransfer: transfer };
25 await target.dispatchEvent("dragover", point);
26 await target.dispatchEvent("drop", point);
27 await source.dispatchEvent("dragend", { dataTransfer: transfer });
28 await transfer.dispose();
29 }
30 try {
31 await fs.mkdir(evidence, { recursive: true });
32 await page.goto(`http://127.0.0.1:${port}/bench/independent-sessions.html`);
33 await row("Session A").waitFor();
34 await row("Session B").waitFor();
35 assert.equal(await page.locator(".project-tree__topic-main").count(), 2);
36 const x = await page.locator(".project-tree__topic-main").evaluateAll(nodes => nodes.map(node => node.getBoundingClientRect().x));
37 assert.equal(x[0], x[1], "ordinary forks occupy sibling rows");
38 await page.waitForFunction(() => JSON.parse(localStorage.getItem("projectTree:readActivity:v3") || "{}").records?.["ref\x00local\x00b"]?.value === 10);
39 await page.getByRole("button", { name: "Advance B activity observation", exact: true }).click();
40 await page.locator(".project-tree__topic--unread").filter({ hasText: "Session B" }).waitFor();
41 assert.equal(await page.locator(".project-tree__topic--unread").count(), 1, "B result 20 is unread independently of A result 100");
42 await row("Session A").focus();
43 await row("Session A").press("Enter");
44 assert.equal(await page.locator(".project-tree__topic--unread").filter({ hasText: "Session B" }).count(), 1, "reading A leaves B unread");
45 await row("Session B").focus();
46 await row("Session B").press("Enter");
47 await page.waitForFunction(() => document.querySelector(".topicbar h1")?.textContent === "Session B");
48 assert.equal(await page.locator(".project-tree__topic--active").count(), 1, "keyboard opening selects B alone");
49 await page.waitForFunction(() => JSON.parse(localStorage.getItem("projectTree:readActivity:v3") || "{}").records?.["ref\x00local\x00b"]?.value === 20);
50 assert.equal(await page.locator(".project-tree__topic--unread").count(), 0, "opening B clears only B unread");
51 await row("Session A").click();
52 await page.waitForFunction(() => document.querySelector(".topicbar h1")?.textContent === "Session A");
53 for (const sequence of [10, 20]) {
54 await page.getByRole("button", { name: "Advance B activity observation", exact: true }).click();
55 await page.getByRole("button", { name: "Remount sidebar", exact: true }).click();
56 await row("Session B").waitFor();
57 assert.equal(await page.evaluate(() => JSON.parse(localStorage.getItem("projectTree:readActivity:v3")).records["ref\x00local\x00b"].value), 20, `ready result ${sequence} cannot lower B baseline`);
58 assert.equal(await page.locator(".project-tree__topic--unread").count(), 0, `ready result ${sequence} cannot create false unread`);
59 }
60 await row("Session B").click();
61 await page.waitForFunction(() => document.querySelector(".topicbar h1")?.textContent === "Session B");
62 await dragRow("Session B", row("Session A").locator(".."));
63 await page.waitForFunction(() => window.__independentEvidence.organizationCalls.some(call => call.kind === "move"));
64 await page.waitForFunction(() => document.querySelector(".project-tree__topic-main")?.textContent?.includes("Session B"));
65 assert.deepEqual(await page.locator(".project-tree__topic-main").evaluateAll(nodes => nodes.map(node => node.textContent?.match(/Session [AB]/)?.[0])), ["Session B", "Session A"]);
66 // Group creation belongs to the expanded creation sidebar; compact workbench
67 // deliberately exposes a smaller project menu.
68 await page.getByRole("button", { name: "Toggle creation", exact: true }).click();
69 await page.locator(".project-tree__folder-main").filter({ hasText: "Independent sessions" }).click({ button: "right" });
70 await page.getByRole("menuitem", { name: "New group", exact: true }).click();
71 const group = page.locator(".project-tree__group-main").filter({ hasText: "New group" });
72 await group.waitFor();
73 await page.keyboard.press("Escape");
74 await dragRow("Session B", group);
75 await page.waitForFunction(() => window.__independentEvidence.organizationCalls.some(call => call.kind === "set-group" && call.groupId));
76 assert.equal(await page.locator(".project-tree__group").filter({ hasText: "New group" }).locator(".project-tree__topic-main").count(), 1);
77 assert.equal(await page.locator(".project-tree__group").filter({ hasText: "New group" }).getByText("Session B", { exact: true }).count(), 1);
78 await row("Session B").click({ button: "right" });
79 await page.getByRole("menuitem", { name: "Remove from group", exact: true }).click();
80 await page.waitForFunction(() => window.__independentEvidence.organizationCalls.some(call => call.kind === "set-group" && call.groupId === ""));
81 await page.getByRole("button", { name: "Remount sidebar", exact: true }).click();
82 await row("Session B").waitFor();
83 assert.equal(await page.locator(".project-tree__group").locator(".project-tree__topic-main").count(), 0, "explicitly ungrouped B remains ungrouped after remount");
84 await page.getByRole("button", { name: "Toggle creation", exact: true }).click();
85 await page.getByRole("button", { name: "Show selected runtime approval", exact: true }).click();
86 await page.getByRole("option", { name: /Allow once/i }).click();
87 await page.getByRole("button", { name: "Confirm", exact: true }).click();
88 await page.waitForFunction(() => window.__independentEvidence.runtimeCalls.some(call => call.method === "approve"));
89 await page.getByRole("button", { name: "Show selected runtime approval", exact: true }).click();
90 await page.getByRole("button", { name: "Stop task", exact: true }).click();
91 await page.waitForFunction(() => window.__independentEvidence.runtimeCalls.some(call => call.method === "stop"));
92 assert.deepEqual(await page.evaluate(() => window.__independentEvidence.runtimeCalls), [
93 { method: "approve", tabId: "tab-b", promptId: "approval-b", turnId: "turn-tab-b", epoch: "epoch-tab-b" },
94 { method: "stop", tabId: "tab-b" },
95 ]);
96 await row("Session B").click();
97 await page.waitForFunction(() => document.querySelector(".topicbar h1")?.textContent === "Session B");
98 assert.equal(await page.locator(".project-tree__topic--active").count(), 1);
99 await page.getByRole("button", { name: "Rename selected session", exact: true }).click();
100 await page.locator(".topicbar__title-input").fill("Only B from top");
101 await page.locator(".topicbar__title-input").press("Enter");
102 await row("Only B from top").waitFor();
103 assert.equal(await row("Session A").count(), 1);
104 await row("Only B from top").click({ button: "right" });
105 await page.getByRole("menuitem", { name: "Rename session", exact: true }).click();
106 await page.locator(".project-tree__topic-input").waitFor();
107 await page.locator(".project-tree__topic-input").fill("Only B from sidebar");
108 await page.locator(".project-tree__topic-input").press("Enter");
109 await row("Only B from sidebar").waitFor();
110 assert.equal(await row("Session A").count(), 1, "sidebar rename must preserve same-topic A");
111 await page.getByRole("button", { name: "Open fixture history", exact: true }).click();
112 const historyB = page.locator(".hist-item").filter({ hasText: "Only B from sidebar" });
113 await historyB.locator(".hist-item__main").click();
114 await page.locator(".history-preview").getByRole("button", { name: "Rename", exact: true }).click();
115 await page.locator(".hist-item__rename").fill("Only B from history");
116 await page.locator(".hist-item__rename").press("Enter");
117 await page.waitForFunction(() => window.__independentEvidence.calls.some(call => call.title === "Only B from history"));
118 await page.locator(".history-modal").getByRole("button", { name: "Close", exact: true }).click();
119 await page.locator(".history-modal").waitFor({ state: "hidden" });
120 await page.getByRole("button", { name: "Remount sidebar", exact: true }).click();
121 await row("Only B from history").waitFor();
122 for (const theme of ["Light", "Dark"]) {
123 await page.getByRole("button", { name: `${theme} theme`, exact: true }).click();
124 assert.equal(await page.locator("html").getAttribute("data-theme"), theme.toLowerCase());
125 await page.screenshot({ path: path.join(evidence, `workbench-${theme.toLowerCase()}.png`) });
126 }
127 await page.getByRole("button", { name: "Toggle creation", exact: true }).click();
128 await page.locator(".app--creation").waitFor();
129 assert.equal(await page.locator(".project-tree__topic-main").count(), 2);
130 await row("Session A").click();
131 await page.waitForFunction(() => document.querySelector(".topicbar h1")?.textContent === "Session A");
132 await page.screenshot({ path: path.join(evidence, "creation-dark.png") });
133 await page.getByRole("button", { name: "Light theme", exact: true }).click();
134 await page.screenshot({ path: path.join(evidence, "creation-light.png") });
135 await page.getByRole("button", { name: "Toggle creation", exact: true }).click();
136 await row("Only B from history").click({ button: "right" });
137 await page.getByRole("menuitem", { name: /Move to trash/i }).click();
138 await page.getByRole("menuitem", { name: /Confirm/i }).click();
139 await row("Only B from history").waitFor({ state: "hidden" });
140 await page.getByRole("button", { name: "Inject late snapshots", exact: true }).click();
141 await page.getByRole("button", { name: "Remount sidebar", exact: true }).click();
142 await row("Session A").waitFor();
143 await page.waitForTimeout(300);
144 assert.equal(await page.locator(".project-tree__topic-main").count(), 1, "archived B remains fenced across stale snapshots and remount");
145 const calls = await page.evaluate(() => window.__independentEvidence.calls);
146 assert.deepEqual(calls, [
147 { method: "rename", id: "b", title: "Only B from top" },
148 { method: "rename", id: "b", title: "Only B from sidebar" },
149 { method: "rename", id: "b", title: "Only B from history" },
150 { method: "archive", id: "b" },
151 ], "each user action writes exactly once to B and never to same-topic A");
152 assert.deepEqual(errors, []);
153 const organizationCalls = await page.evaluate(() => window.__independentEvidence.organizationCalls);
154 assert.equal(organizationCalls.length, 4, "one move, one group creation and two exact B membership changes");
155 assert.ok(organizationCalls.filter(call => call.target).every(call => call.target.ref.sessionId === "b"));
156 const runtimeCalls = await page.evaluate(() => window.__independentEvidence.runtimeCalls);
157 // A fresh mounted real tree initially shows five old-snapshot rows. Its old
158 // continuation fails, so the UI must discard that window and read page one.
159 await page.goto(`http://127.0.0.1:${port}/bench/independent-sessions.html?pagination=1`);
160 await row("Session A").waitFor();
161 assert.equal(await page.locator(".project-tree__topic-main").count(), 5);
162 await page.getByRole("button", { name: /Show more/i }).click();
163 await row("Session C").waitFor();
164 await row("Session G").waitFor();
165 assert.deepEqual(await page.locator(".project-tree__topic-label").allTextContents(), ["Session C", "Session A", "Session B", "Session D", "Session E", "Session F", "Session G"], "stale cursor recovery replaces the old window without omissions or duplicate rows");
166 const pageRequests = await page.evaluate(() => window.__independentEvidence.pageRequests);
167 const rejection = pageRequests.findIndex(request => request.rejected);
168 assert.ok(rejection >= 0);
169 assert.equal(pageRequests[rejection].cursor, "old:5");
170 assert.equal(pageRequests[rejection + 1].cursor, "", "stale cursor recovery must restart at page one");
171 await page.screenshot({ path: path.join(evidence, "pagination-recovered.png") });
172 assert.deepEqual(errors, []);
173 await fs.writeFile(path.join(evidence, "result.json"), JSON.stringify({ passed: true, calls, organizationCalls, runtimeCalls, pageRequests, unreadChecks: ["B 10→20 unread while A=100", "reading A preserves B unread", "reading B clears its unread", "stale ready 10 then20 preserves baseline20 across remount"], errors, scope: "Chromium product components + actual command owners, controlled Desktop RPC; approval/stop use real prompt card (sidebar has no such action)" }, null, 2));
174 console.log("PASS independent-session browser: sibling rows, keyboard open, exact group/drag order, approval/stop target, top/history rename, light/dark, creation, archive stale runtime/remount");
175 } catch (error) {
176 console.error("Browser errors:", errors);
177 console.error("Boundary evidence:", await page.evaluate(() => window.__independentEvidence));
178 console.error("Visible text:", (await page.locator("body").innerText()).slice(0, 4000));
179 throw error;
180 } finally { await browser.close(); await server.close(); }
181
181 lines Plain Text