| 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; |
| 28 | try { |
| 29 | application = await _electron.launch({ executablePath: join(process.argv[2], "Contents/MacOS/Reasonix"), |
| 30 | env: { ...packagedSmokeEnv(process.env, home), SIDEBAR_FIXTURE_KEY: "local-fixture" } }); |
| 31 | const page = await application.firstWindow(); |
| 32 | page.setDefaultTimeout(15000); |
| 33 | const errors = []; |
| 34 | page.on("pageerror", error => errors.push(error.message)); |
| 35 | await page.waitForFunction(() => Boolean(window.reasonixDesktop)); |
| 36 | const invoke = (method, args = []) => page.evaluate(({ method, args }) => window.reasonixDesktop.invoke(method, args), { method, args }); |
| 37 | const active = async () => (await invoke("ListTabs")).find(tab => tab.active); |
| 38 | const transcriptContains = (text, expected = true) => page.waitForFunction(({ text, expected }) => |
| 39 | (document.querySelector(".chat-transcript")?.textContent?.includes(text) ?? false) === expected, { text, expected }); |
| 40 | const refs = {}; |
| 41 | await invoke("CreateSession", ["global"]); |
| 42 | for (const marker of ["NAV_ALPHA", "NAV_BETA"]) { |
| 43 | console.log("Seeding", marker); |
| 44 | if (marker === "NAV_BETA") { |
| 45 | await page.locator(".sidebar__quick-action").click(); |
| 46 | await transcriptContains("ANSWER_NAV_ALPHA", false); |
| 47 | } |
| 48 | const composer = page.locator("textarea").first(); |
| 49 | for (let attempt = 0; attempt < 100; attempt++) { |
| 50 | await composer.fill(marker); |
| 51 | if (await page.locator(".composer__btn--send").isEnabled()) break; |
| 52 | await page.waitForTimeout(100); |
| 53 | } |
| 54 | await page.locator(".composer__btn--send").click(); |
| 55 | await transcriptContains(`ANSWER_${marker}`); |
| 56 | await waitForSmokeCondition(async () => (await invoke("ListTabs")).every(tab => !tab.running)); |
| 57 | refs[marker] = (await active()).session; |
| 58 | await invoke("RenameCanonicalSession", [refs[marker], marker]); |
| 59 | } |
| 60 | assert.notEqual(refs.NAV_ALPHA.sessionId, refs.NAV_BETA.sessionId); |
| 61 | const sessionRow = marker => page.locator(".project-tree__topic-main").filter({ has: page.getByText(marker, { exact: true }) }); |
| 62 | for (const marker of ["NAV_ALPHA", "NAV_BETA", "NAV_ALPHA"]) { |
| 63 | await sessionRow(marker).click(); |
| 64 | await transcriptContains(`ANSWER_${marker}`); |
| 65 | const other = marker === "NAV_ALPHA" ? "NAV_BETA" : "NAV_ALPHA"; |
| 66 | await transcriptContains(`ANSWER_${other}`, false); |
| 67 | assert.equal((await active()).session.sessionId, refs[marker].sessionId); |
| 68 | assert.equal(await sessionRow(marker).locator("xpath=..").evaluate(node => node.classList.contains("project-tree__topic--active")), true); |
| 69 | } |
| 70 | await page.evaluate(markers => { |
| 71 | for (const marker of markers) { |
| 72 | const label = [...document.querySelectorAll(".project-tree__topic-label")] |
| 73 | .find(candidate => candidate.textContent?.trim() === marker); |
| 74 | const row = label?.closest(".project-tree__topic-main"); |
| 75 | if (!row) throw new Error(`missing project tree row ${marker}`); |
| 76 | row.click(); |
| 77 | } |
| 78 | }, ["NAV_BETA", "NAV_ALPHA", "NAV_BETA"]); |
| 79 | await transcriptContains("ANSWER_NAV_BETA"); |
| 80 | await transcriptContains("ANSWER_NAV_ALPHA", false); |
| 81 | assert.equal((await active()).session.sessionId, refs.NAV_BETA.sessionId); |
| 82 | assert.deepEqual(errors, []); |
| 83 | 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"); |
| 84 | } finally { |
| 85 | await application?.close(); |
| 86 | server.closeAllConnections(); |
| 87 | await new Promise(resolve => server.close(resolve)); |
| 88 | rmSync(home, { recursive: true, force: true }); |
| 89 | } |
| 90 |