| 1 | // Real renderer navigation and cache accounting in an unchanged macOS bundle. |
| 2 | // Usage: node desktop/packaging/history-cache-soak.mjs /path/Reasonix.app |
| 3 | // The bench URL only enables the existing diagnostic hook; all data, RPCs, |
| 4 | // writer fences, and shell/service identity checks remain production paths. |
| 5 | import assert from "node:assert/strict"; |
| 6 | import { spawn } from "node:child_process"; |
| 7 | import { once } from "node:events"; |
| 8 | import { createHash } from "node:crypto"; |
| 9 | import { createRequire } from "node:module"; |
| 10 | import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "node:fs"; |
| 11 | import { join, resolve } from "node:path"; |
| 12 | import { tmpdir, platform, arch } from "node:os"; |
| 13 | import { packagedSmokeEnv } from "./smoke-env.mjs"; |
| 14 | import { parseServiceReady, waitForSmokeCondition } from "./smoke-poll.mjs"; |
| 15 | import { closeAndVerify } from "./smoke-lifecycle.mjs"; |
| 16 | |
| 17 | assert.equal(platform(), "darwin"); |
| 18 | const bundle = resolve(process.argv[2]); |
| 19 | const require = createRequire(new URL("../electron/package.json", import.meta.url)); |
| 20 | const { _electron } = require("playwright"); |
| 21 | const home = mkdtempSync(join(tmpdir(), "reasonix-history-cache-")); |
| 22 | const resultPath = `${home}.results.json`; |
| 23 | const dir = join(home, "sessions"); |
| 24 | mkdirSync(dir); |
| 25 | 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:1/v1"\nmodels = ["model"]\ndefault = "model"\napi_key_env = "HISTORY_FIXTURE_KEY"\n`); |
| 26 | const hash = path => createHash("sha256").update(readFileSync(path)).digest("hex"); |
| 27 | const fixtures = Array.from({ length: 8 }, (_, id) => { |
| 28 | const name = `cache-${id}`; |
| 29 | const messages = Array.from({ length: 160 }, (_, turn) => [ |
| 30 | { role: "user", content: `SOAK_${id}_QUESTION_${turn}` }, |
| 31 | { role: "assistant", content: `SOAK_${id}_ANSWER_${turn}\n\n` + `**History ${id}/${turn}** with Unicode 🧭 and inline \`code\`.\n\n`.repeat(12) |
| 32 | + `\n\`\`\`svg\n<svg xmlns="http://www.w3.org/2000/svg" width="120" height="40"><text x="2" y="25">${id}/${turn}</text></svg>\n\`\`\`\n` }, |
| 33 | ]).flat(); |
| 34 | const path = join(dir, `${name}.jsonl`); |
| 35 | writeFileSync(path, id % 2 === 0 ? messages.map(message => JSON.stringify(message)).join("\n") + "\n" |
| 36 | : '{"role":"user","content":"SUPERSEDED_CHECKPOINT"}\n'); |
| 37 | const files = [path]; |
| 38 | if (id % 2) { |
| 39 | const events = join(dir, `${name}.events.jsonl`); |
| 40 | writeFileSync(events, JSON.stringify({ messages, type: "replace", schema_version: 1 }) + "\n"); |
| 41 | files.push(events); |
| 42 | } |
| 43 | return { id, name, path, files: files.map(path => ({ path, digest: hash(path) })) }; |
| 44 | }); |
| 45 | const report = { build: JSON.parse(readFileSync(join(bundle, "Contents/Resources/build.json"), "utf8")), |
| 46 | machine: { platform: platform(), arch: arch() }, complete: false, samples: [], |
| 47 | scope: "Real UI navigation, bidirectional paging, cache accounting, worker drain and SVG object URL release for checkpoint/schema-1 fixtures; not whole-process memory or idle-runtime qualification." }; |
| 48 | let application, page, lease; |
| 49 | const invoke = (method, args = []) => page.evaluate(({ method, args }) => window.reasonixDesktop.invoke(method, args), { method, args }); |
| 50 | const stats = () => page.evaluate(() => window.__reasonixPerf.stats()); |
| 51 | async function sample(id, phase) { |
| 52 | await waitForSmokeCondition(async () => (await stats()).markdownWorker?.pending === 0); |
| 53 | const value = await stats(); |
| 54 | const cache = value.transcriptCache; |
| 55 | assert.ok(cache && cache.residentSessions > 0, "real renderer did not populate transcript cache"); |
| 56 | assert.ok(cache.residentSessions <= cache.maxResidentSessions, "resident-session budget exceeded"); |
| 57 | assert.ok(cache.bodyBytes <= cache.bodyBudgetBytes, "body budget exceeded"); |
| 58 | assert.ok(cache.markdownBytes <= cache.markdownBudgetBytes, "Markdown budget exceeded"); |
| 59 | assert.ok(cache.residentWindowEntries <= cache.residentSessions * cache.windowMaxPages * 32, "resident history window exceeded"); |
| 60 | const objectURLs = await page.evaluate(() => ({ ...window.__historyObjectURLs(), mountedSVG: document.querySelectorAll(".md-svg").length })); |
| 61 | report.samples.push({ fixture: id, phase, cache, markdownWorker: value.markdownWorker, objectURLs }); |
| 62 | assert.ok(objectURLs.active <= objectURLs.mountedSVG, "unmounted SVG retained an object URL"); |
| 63 | } |
| 64 | async function select(fixture) { |
| 65 | await page.locator(".project-tree__topic-main").filter({ has: page.getByText(fixture.name, { exact: true }) }).click(); |
| 66 | await waitForSmokeCondition(async () => (await invoke("ListTabs")).some(tab => tab.active && tab.sessionPath === fixture.path)); |
| 67 | await page.waitForFunction(id => document.querySelector(".chat-transcript")?.textContent?.includes(`SOAK_${id}_QUESTION_`), fixture.id); |
| 68 | const text = await page.locator(".chat-transcript").innerText(); |
| 69 | for (const other of fixtures.filter(other => other !== fixture)) assert.equal(text.includes(`SOAK_${other.id}_`), false, "previous source remained visible"); |
| 70 | assert.equal(text.includes("SUPERSEDED_CHECKPOINT"), false); |
| 71 | } |
| 72 | async function pageHistory(fixture) { |
| 73 | // Exercise the same buttons readers use, never call history RPCs directly or |
| 74 | // mutate scrollTop to bypass the transcript's navigation owner. |
| 75 | await page.locator(".chat-flow-scroll").hover(); |
| 76 | await page.mouse.wheel(0, -1000000); |
| 77 | await page.waitForFunction(() => document.querySelector(".chat-flow-scroll")?.getAttribute("data-scroll-mode") === "reader"); |
| 78 | for (let index = 0; index < 4; index++) { |
| 79 | const before = await page.locator(".chat-history-window").innerText(); |
| 80 | await page.locator(".chat-older").click(); |
| 81 | await page.waitForFunction(previous => { |
| 82 | const button = document.querySelector(".chat-older"); |
| 83 | return button && !button.disabled && document.querySelector(".chat-history-window")?.innerText !== previous; |
| 84 | }, before); |
| 85 | report.samples.push({ fixture: fixture.id, phase: "page", range: await page.locator(".chat-history-window").innerText(), cache: (await stats()).transcriptCache }); |
| 86 | } |
| 87 | assert.equal((await page.locator(".chat-column").innerText()).includes(`SOAK_${fixture.id}_QUESTION_159`), false, "newest page was not reclaimed"); |
| 88 | await sample(fixture.id, "older"); |
| 89 | await page.locator(".chat-history-newer button").last().click(); |
| 90 | await page.waitForFunction(id => document.querySelector(".chat-column")?.textContent?.includes(`SOAK_${id}_QUESTION_159`), fixture.id); |
| 91 | await sample(fixture.id, "latest"); |
| 92 | } |
| 93 | try { |
| 94 | lease = spawn("python3", ["-u", "-c", `import fcntl, sys |
| 95 | locks = [open(path, 'a+') for path in sys.argv[1:]] |
| 96 | for file in locks: fcntl.flock(file, fcntl.LOCK_EX | fcntl.LOCK_NB) |
| 97 | print('locked', flush=True) |
| 98 | sys.stdin.read() |
| 99 | `, ...fixtures.map(fixture => fixture.path + ".lease.lock")], { stdio: ["pipe", "pipe", "inherit"] }); |
| 100 | await Promise.race([once(lease.stdout, "data").then(([data]) => assert.match(String(data), /locked/)), |
| 101 | once(lease, "exit").then(() => { throw new Error("fixture writer exited before locking"); })]); |
| 102 | application = await _electron.launch({ executablePath: join(bundle, "Contents/MacOS/Reasonix"), |
| 103 | env: { ...packagedSmokeEnv(process.env, home), HISTORY_FIXTURE_KEY: "local-fixture" } }); |
| 104 | await waitForSmokeCondition(async () => { |
| 105 | for (const candidate of application.windows()) if (await candidate.evaluate(() => Boolean(window.reasonixDesktop)).catch(() => false)) { page = candidate; return true; } |
| 106 | return false; |
| 107 | }); |
| 108 | assert.equal(await invoke("Version"), report.build.version); |
| 109 | assert.equal(await application.evaluate(({ app }) => app.isPackaged && !process.env.REASONIX_DEV), true); |
| 110 | const url = new URL(page.url()); |
| 111 | url.searchParams.set("bench", "1"); |
| 112 | // Observe native URL ownership without altering app/cache/RPC behavior. |
| 113 | // Each navigation gets a fresh counter; only content-free counts are saved. |
| 114 | await page.addInitScript(() => { |
| 115 | const active = new Set(); |
| 116 | let created = 0, revoked = 0; |
| 117 | const create = URL.createObjectURL, revoke = URL.revokeObjectURL; |
| 118 | URL.createObjectURL = function(blob) { |
| 119 | const url = create.call(this, blob); |
| 120 | // Inline worker loaders revoke their JavaScript blob from inside the |
| 121 | // worker realm. A page-only observer cannot count those releases. Track |
| 122 | // SVG resources owned by the mounted history components specifically. |
| 123 | if (blob.type === "image/svg+xml") { active.add(url); created++; } |
| 124 | return url; |
| 125 | }; |
| 126 | URL.revokeObjectURL = function(url) { |
| 127 | if (active.delete(url)) revoked++; |
| 128 | return revoke.call(this, url); |
| 129 | }; |
| 130 | window.__historyObjectURLs = () => ({ kind: "image/svg+xml", created, revoked, active: active.size }); |
| 131 | }); |
| 132 | await page.goto(url.href); |
| 133 | await page.waitForFunction(() => Boolean(window.__reasonixPerf && window.reasonixDesktop)); |
| 134 | assert.equal(await invoke("Version"), report.build.version); |
| 135 | const folder = page.locator(".project-tree__folder-main").first(); |
| 136 | await folder.waitFor(); |
| 137 | if (await folder.getAttribute("aria-expanded") !== "true") await folder.click(); |
| 138 | await page.locator(".project-tree__topic-window-toggle").first().click(); |
| 139 | await page.getByText("cache-0", { exact: true }).waitFor(); |
| 140 | for (let round = 0; round < 4; round++) { |
| 141 | for (const fixture of round % 2 ? [...fixtures].reverse() : fixtures) { |
| 142 | await select(fixture); |
| 143 | await pageHistory(fixture); |
| 144 | } |
| 145 | console.log(`PASS native cache round ${round + 1}: eight sessions, older/latest paging and settled budgets`); |
| 146 | } |
| 147 | for (const fixture of [fixtures[0], fixtures[1], fixtures[0]]) { await select(fixture); await sample(fixture.id, "A-B-A"); } |
| 148 | assert.ok(report.samples.some(sample => sample.cache.historyEvictions > 0), "no resident-session eviction exercised"); |
| 149 | assert.ok(report.samples.some(sample => sample.cache.reclaimedPages > 0), "no page reclamation exercised"); |
| 150 | assert.ok(report.samples.some(sample => sample.cache.markdownBytes > 0), "no parsed Markdown cached"); |
| 151 | assert.ok(report.samples.some(sample => sample.objectURLs?.created > 0), "no native SVG preview URL created"); |
| 152 | assert.ok(report.samples.some(sample => sample.objectURLs?.revoked > 0), "no native SVG preview URL released"); |
| 153 | for (const fixture of fixtures) for (const file of fixture.files) assert.equal(hash(file.path), file.digest, "authoritative source changed"); |
| 154 | const ready = readFileSync(join(home, "desktop-shell/logs/shell.log"), "utf8").split("\n").reverse().map(parseServiceReady).find(Boolean); |
| 155 | assert.ok(ready); |
| 156 | await closeAndVerify(application, { shellPid: await application.evaluate(() => process.pid), servicePid: ready.pid }); |
| 157 | application = undefined; |
| 158 | report.complete = true; |
| 159 | } catch (error) { |
| 160 | report.error = String(error); |
| 161 | if (page) writeFileSync(join(home, "failure.json"), JSON.stringify(await page.evaluate(() => ({ text: document.querySelector(".chat-column")?.innerText, stats: window.__reasonixPerf?.stats(), objectURLs: window.__historyObjectURLs?.() })).catch(error => ({ error: String(error) })), null, 2)); |
| 162 | await page?.screenshot({ path: join(home, "failure.png"), fullPage: true }).catch(() => {}); |
| 163 | throw error; |
| 164 | } finally { |
| 165 | writeFileSync(resultPath, JSON.stringify(report, null, 2)); |
| 166 | await application?.close(); |
| 167 | if (lease && lease.exitCode === null) { const exited = once(lease, "exit"); lease.stdin.end(); await exited; } |
| 168 | if (report.complete) rmSync(home, { recursive: true, force: true }); |
| 169 | else console.error(`Native cache fixture retained at ${home}`); |
| 170 | console.log(`Native cache results: ${resultPath}`); |
| 171 | } |
| 172 |