返回 DeepSeek-Reasonix
session-navigation-smoke.mjs
根目录 / desktop / packaging / session-navigation-smoke.mjs
1 // Real packaged sidebar -> navigation owner -> service -> transcript regression.
2 // Usage: node desktop/packaging/session-navigation-smoke.mjs /path/Reasonix.app
3 import assert from "node:assert/strict";
4 import { createRequire } from "node:module";
5 import { createServer } from "node:http";
6 import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
7 import { join } from "node:path";
8 import { tmpdir } from "node:os";
9 import { packagedSmokeEnv } from "./smoke-env.mjs";
10 import { waitForSmokeCondition } from "./smoke-poll.mjs";
11
12 const require = createRequire(new URL("../electron/package.json", import.meta.url));
13 const { _electron } = require("playwright");
14 const home = mkdtempSync(join(tmpdir(), "reasonix-sidebar-navigation-"));
15 const server = createServer(async (req, res) => {
16 let raw = "";
17 for await (const chunk of req) raw += chunk;
18 const request = JSON.parse(raw || "{}");
19 const messages = JSON.stringify(request.messages ?? []);
20 const marker = messages.includes("NAV_BETA") ? "NAV_BETA" : "NAV_ALPHA";
21 res.writeHead(200, { "Content-Type": "text/event-stream" });
22 res.end(`data: ${JSON.stringify({ id: "fixture", choices: [{ index: 0, delta: { content: `ANSWER_${marker}` }, finish_reason: null }] })}\n\n`
23 + `data: ${JSON.stringify({ id: "fixture", choices: [{ index: 0, delta: {}, finish_reason: "stop" }] })}\n\ndata: [DONE]\n\n`);
24 });
25 await new Promise(resolve => server.listen(0, "127.0.0.1", resolve));
26 writeFileSync(join(home, "config.toml"), `default_model = "fixture/model"\n[desktop]\nprovider_access = ["fixture"]\n[[providers]]\nname = "fixture"\nkind = "openai"\nbase_url = "http://127.0.0.1:${server.address().port}/v1"\nmodels = ["model"]\ndefault = "model"\napi_key_env = "SIDEBAR_FIXTURE_KEY"\n`);
27 let application, page;
28 let failed = false;
29 try {
30 application = await _electron.launch({ executablePath: join(process.argv[2], "Contents/MacOS/Reasonix"),
31 env: { ...packagedSmokeEnv(process.env, home), SIDEBAR_FIXTURE_KEY: "local-fixture" } });
32 page = await application.firstWindow();
33 page.setDefaultTimeout(15000);
34 const errors = [];
35 page.on("pageerror", error => errors.push(error.message));
36 await page.waitForFunction(() => Boolean(window.reasonixDesktop));
37 await page.evaluate(() => {
38 window.__navigationSmokeEvents = [];
39 window.reasonixDesktop.on("topic:activation", event => {
40 window.__navigationSmokeEvents.push(event);
41 if (window.__navigationSmokeEvents.length > 64) window.__navigationSmokeEvents.shift();
42 });
43 });
44 const invoke = (method, args = []) => page.evaluate(({ method, args }) => window.reasonixDesktop.invoke(method, args), { method, args });
45 const active = async () => (await invoke("ListTabs")).find(tab => tab.active);
46 const transcriptContains = (text, expected = true) => page.waitForFunction(({ text, expected }) =>
47 (document.querySelector(".chat-transcript")?.textContent?.includes(text) ?? false) === expected, { text, expected });
48 const refs = {};
49 await invoke("CreateSession", ["global"]);
50 for (const marker of ["NAV_ALPHA", "NAV_BETA"]) {
51 console.log("Seeding", marker);
52 if (marker === "NAV_BETA") {
53 await page.locator(".sidebar__quick-action").click();
54 await transcriptContains("ANSWER_NAV_ALPHA", false);
55 }
56 const composer = page.locator("textarea").first();
57 for (let attempt = 0; attempt < 100; attempt++) {
58 await composer.fill(marker);
59 if (await page.locator(".composer__btn--send").isEnabled()) break;
60 await page.waitForTimeout(100);
61 }
62 await page.locator(".composer__btn--send").click();
63 await transcriptContains(`ANSWER_${marker}`);
64 await waitForSmokeCondition(async () => (await invoke("ListTabs")).every(tab => !tab.running));
65 refs[marker] = (await active()).session;
66 await invoke("RenameCanonicalSession", [refs[marker], marker]);
67 }
68 assert.notEqual(refs.NAV_ALPHA.sessionId, refs.NAV_BETA.sessionId);
69 const sessionRow = marker => page.locator(".project-tree__topic-main").filter({ has: page.getByText(marker, { exact: true }) });
70 for (const marker of ["NAV_ALPHA", "NAV_BETA", "NAV_ALPHA"]) {
71 console.log("Selecting", marker);
72 await sessionRow(marker).click();
73 await transcriptContains(`ANSWER_${marker}`);
74 const other = marker === "NAV_ALPHA" ? "NAV_BETA" : "NAV_ALPHA";
75 await transcriptContains(`ANSWER_${other}`, false);
76 assert.equal((await active()).session.sessionId, refs[marker].sessionId);
77 assert.equal(await sessionRow(marker).locator("xpath=..").evaluate(node => node.classList.contains("project-tree__topic--active")), true);
78 }
79 await page.evaluate(markers => {
80 for (const marker of markers) {
81 const label = [...document.querySelectorAll(".project-tree__topic-label")]
82 .find(candidate => candidate.textContent?.trim() === marker);
83 const row = label?.closest(".project-tree__topic-main");
84 if (!row) throw new Error(`missing project tree row ${marker}`);
85 row.click();
86 }
87 }, ["NAV_BETA", "NAV_ALPHA", "NAV_BETA"]);
88 await transcriptContains("ANSWER_NAV_BETA");
89 await transcriptContains("ANSWER_NAV_ALPHA", false);
90 assert.equal((await active()).session.sessionId, refs.NAV_BETA.sessionId);
91 assert.deepEqual(errors, []);
92 console.log("PASS packaged sidebar: create after completed turn, A/B/A selection and transcript agree, rapid clicks keep the last target; no page errors");
93 } catch (error) {
94 failed = true;
95 if (page) {
96 const state = await page.evaluate(async () => ({
97 tabs: await window.reasonixDesktop.invoke("ListTabs", []),
98 topics: await window.reasonixDesktop.invoke("ListProjectTopics", [{ scope: "global", limit: 50 }]).catch(error => ({ error: String(error) })),
99 sidebar: document.querySelector(".project-tree")?.textContent,
100 transcript: await (async () => {
101 const tab = (await window.reasonixDesktop.invoke("ListTabs", [])).find(tab => tab.active);
102 if (!tab) return null;
103 return window.reasonixDesktop.invoke("TranscriptSnapshotForTab", [tab.id, { records: 32 }]).catch(error => ({ error: String(error) }));
104 })(),
105 body: document.body.innerText,
106 activationEvents: window.__navigationSmokeEvents,
107 catalog: await window.reasonixDesktop.invoke("GetProjectTreeSnapshot", []).catch(error => ({ error: String(error) })),
108 })).catch(error => ({ error: String(error) }));
109 writeFileSync(join(home, "navigation-failure.json"), JSON.stringify(state, null, 2));
110 await page.screenshot({ path: join(home, "navigation-failure.png"), fullPage: true }).catch(() => {});
111 }
112 console.error(`Native navigation evidence retained at ${home}`);
113 throw error;
114 } finally {
115 await application?.close();
116 server.closeAllConnections();
117 await new Promise(resolve => server.close(resolve));
118 if (!failed) rmSync(home, { recursive: true, force: true });
119 }
120
120 lines Plain Text