返回 DeepSeek-Reasonix
fork-targets-app.mjs
根目录 / desktop / frontend / bench / fork-targets-app.mjs
1 // Real-page gate for the turn-fork entry: it loads the shipped application
2 // against the browser dev mock and drives the entry a user clicks, so the
3 // fixture data, the app wiring, and the notice channel are all exercised
4 // together. Browsers resolve from the default Playwright cache.
5 import assert from "node:assert/strict";
6 import path from "node:path";
7 import { fileURLToPath } from "node:url";
8 import { startPreviewServer } from "./vite-preview-server.mjs";
9
10 const frontendDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
11 const { chromium } = await import("playwright");
12 const port = Number(process.env.REASONIX_FORK_APP_PORT ?? 4659);
13 const server = await startPreviewServer(frontendDir, port);
14 const browser = await chromium.launch({ headless: true });
15 const base = `http://127.0.0.1:${port}/`;
16 const report = { browser: browser.version(), platform: process.platform, arch: process.arch };
17
18 const ENTRY = ".chat-actions button.chat-action-icon:not(.copybtn)";
19 const settle = (page) => page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))));
20 const entry = (page) => page.locator(ENTRY).last();
21 const labels = (page) => page.locator(".workbench-dock__tab-label").allTextContents();
22
23 async function openApp(page, query) {
24 await page.goto(`${base}${query}`);
25 await page.locator("textarea.composer__input:not([aria-hidden=true])").waitFor();
26 await page.locator(".chat-column .chat-turn-tail").last().waitFor();
27 await settle(page);
28 }
29
30 try {
31 const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
32 const errors = [];
33 page.on("pageerror", (error) => errors.push(error.message));
34
35 // 1. The dev fixture offers a forkable turn on the page a developer opens.
36 await openApp(page, "?mock=1");
37 await entry(page).waitFor();
38 // The entry exists before the target read lands; wait for it to be enabled and
39 // labelled rather than sampling a loading state.
40 await page.waitForFunction((selector) => {
41 const nodes = document.querySelectorAll(selector);
42 const button = nodes[nodes.length - 1];
43 return Boolean(button) && button.getAttribute("aria-disabled") === null
44 && button.getAttribute("aria-label") === "Branch into a new conversation";
45 }, ENTRY, { timeout: 10_000 });
46 const label = await entry(page).getAttribute("aria-label");
47 assert.equal(await entry(page).getAttribute("aria-disabled"), null, "the running dev app renders an enabled fork entry");
48 assert.equal(await entry(page).getAttribute("data-unavailable"), null, "the enabled dev entry is not marked unavailable");
49 await entry(page).focus();
50 await page.waitForFunction(() => document.querySelector('[role="tooltip"]')?.textContent === "Branch into a new conversation", null, { timeout: 10_000 });
51 assert.equal(await page.locator('[role="tooltip"]').textContent(), "Branch into a new conversation", "the dev entry explains itself with the branch label");
52 report.devEntry = { label, tooltip: "Branch into a new conversation" };
53
54 // 2. Clicking it creates the child through the shipped controller and adopts its tab.
55 const childTitle = () => page.evaluate(() => document.body.innerText.match(/\S[^\n]*\(1\)[^\n]*/)?.[0]?.trim() ?? "");
56 assert.equal(await childTitle(), "", "no forked child exists before the click");
57 await entry(page).click();
58 await page.waitForFunction(() => /\(1\)/.test(document.body.innerText));
59 await settle(page);
60 const adopted = await childTitle();
61 assert.match(adopted, /\(1\)$/, `the created child opens with the mock's fork title (${adopted})`);
62 report.adoptedTab = adopted;
63
64 // 3. A child whose tab cannot open surfaces the localized recovery notice, and
65 // a second click on the same turn reuses that child instead of creating another.
66 await openApp(page, "?mock=1&fork-attach-failure=1");
67 await entry(page).click();
68 const warnNotices = () => page.locator('.chat-notice[data-level="warn"]');
69 await page.waitForFunction(() => document.querySelectorAll('.chat-notice[data-level="warn"]').length === 1, null, { timeout: 10_000 });
70 const first = await warnNotices().last().textContent();
71 assert.match(first ?? "", /The branched session mock-fork-/, "the recovery notice names the created child");
72 await entry(page).click();
73 // The repeated fork reports the child again; wait for the rendered notice
74 // instead of assuming one frame is enough.
75 await page.waitForFunction(() => document.querySelectorAll('.chat-notice[data-level="warn"]').length === 2, null, { timeout: 10_000 });
76 const second = await warnNotices().last().textContent();
77 assert.equal(await warnNotices().count(), 2, "a repeated fork reports the child again");
78 assert.ok(second?.includes(first?.match(/mock-fork-\S+/)?.[0] ?? "child"), "the repeated fork names the same child, never a second one");
79 report.recovery = { first, second };
80
81 await page.screenshot({ path: path.join(process.env.REASONIX_FORK_EVIDENCE ?? "/tmp", "fork-targets-app.png") });
82 assert.deepEqual(errors, []);
83 console.log(JSON.stringify(report, null, 2));
84 } finally {
85 await browser.close();
86 await server.httpServer.close();
87 }
88
88 lines Plain Text