diff options
Diffstat (limited to 'test-suite/unit')
| -rw-r--r-- | test-suite/unit/grant-scope.test.mjs | 121 | ||||
| -rw-r--r-- | test-suite/unit/session-cleanup.test.mjs | 14 |
2 files changed, 132 insertions, 3 deletions
diff --git a/test-suite/unit/grant-scope.test.mjs b/test-suite/unit/grant-scope.test.mjs new file mode 100644 index 0000000..e7ce044 --- /dev/null +++ b/test-suite/unit/grant-scope.test.mjs @@ -0,0 +1,121 @@ +// Which grant a session starts with, and whether the "● Chrome Bridge" footer announces it, across +// /reload and session switches. Pi re-evaluates the extension for each runtime but keeps globalThis +// for the life of the process, so each runtime here is a fresh evaluation of the shipped module in +// one shared context. The bridge binds an ephemeral loopback port; no Chrome is involved. +import assert from "node:assert/strict"; +import fs from "node:fs"; +import vm from "node:vm"; +import { createRequire, stripTypeScriptTypes } from "node:module"; +import { test } from "node:test"; + +const indexUrl = new URL("../../extensions/chrome-profile-bridge/index.ts", import.meta.url); +const stubbed = stripTypeScriptTypes(fs.readFileSync(indexUrl, "utf8"), { mode: "transform" }) + .replace(/^import \{ Text \} from "@earendil-works\/pi-tui";$/m, "const Text = class {};") + .replace(/^import \{ Type \} from "typebox";$/m, "const Type = new Proxy({}, { get: () => (value = {}) => value });") + .replace(/^import \{([^}]*)\} from "(node:[^"]+)";$/gm, (_m, names, mod) => `const {${names.replace(/\btype\s+\w+\s*,?/g, "")}} = require("${mod}");`) + .replace(/^export default function/m, "module.exports.default = function"); +assert.doesNotMatch(stubbed, /^import /m, "every import must be stubbed"); + +// One pi process: a shared context, so globalThis outlives each runtime. +function piProcess(env) { + const fakeProcess = Object.create(process, { env: { value: { ...env, PI_CHROME_BRIDGE_PORT: "0" } } }); + const context = vm.createContext({ + process: fakeProcess, console, Buffer, URL, AbortController, fetch, + setTimeout, clearTimeout, setInterval, clearInterval, + }); + let runtime; + // Evaluate the module again and run its factory, as pi does at startup, on /reload and on a + // session switch. + const load = () => { + const module = { exports: {} }; + vm.runInContext(`(function (module, exports, require, __dirname) {\n${stubbed}\n})`, context)( + module, module.exports, createRequire(indexUrl), new URL(".", indexUrl).pathname, + ); + const handlers = {}, commands = {}, status = {}; + let active = []; + const pi = { + on: (name, fn) => { handlers[name] = fn; }, + registerTool() {}, registerEntryRenderer() {}, appendEntry() {}, + registerCommand: (name, command) => { commands[name] = command; }, + getActiveTools: () => active, + setActiveTools: (tools) => { active = tools; }, + }; + module.exports.default(pi); + const ctx = { + cwd: process.cwd(), + ui: { + setStatus: (key, value) => { status[key] = value; }, + notify() {}, confirm: async () => true, theme: { fg: (_color, text) => text }, + }, + }; + runtime = { + footer: () => status.chrome, + chromeTools: () => active.some((name) => name.startsWith("chrome_")), + start: () => handlers.session_start({ type: "session_start" }, ctx), + shutdown: (reason) => handlers.session_shutdown({ type: "session_shutdown", reason }), + chrome: (args) => commands.chrome.handler(args, ctx), + }; + return runtime; + }; + // End the current runtime for `reason`, then start the next one. + const replace = async (reason) => { + await runtime.shutdown(reason); + const next = load(); + await next.start(); + return next; + }; + // Stops the bridge and timers of the live runtime, so a failed assertion cannot hang the run. + return { load, replace, end: () => runtime?.shutdown("quit") }; +} + +test("PI_CHROME_AUTHORIZE grants every runtime without announcing it", async (t) => { + const pi = piProcess({ PI_CHROME_AUTHORIZE: "indefinite" }); + t.after(() => pi.end()); + const first = pi.load(); + await first.start(); + assert.equal(first.chromeTools(), true); + assert.equal(first.footer(), undefined); + for (const reason of ["reload", "new", "resume", "fork"]) { + const next = await pi.replace(reason); + assert.equal(next.chromeTools(), true, `tools after ${reason}`); + assert.equal(next.footer(), undefined, `footer after ${reason}`); + } +}); + +test("/chrome authorize survives /reload and ends with its session", async (t) => { + const pi = piProcess({}); + t.after(() => pi.end()); + const first = pi.load(); + await first.start(); + assert.equal(first.chromeTools(), false); + await first.chrome("authorize indefinite"); + assert.equal(first.chromeTools(), true); + assert.equal(first.footer(), "● Chrome Bridge (indefinite)"); + + const reloaded = await pi.replace("reload"); + assert.equal(reloaded.chromeTools(), true); + assert.equal(reloaded.footer(), "● Chrome Bridge (indefinite)"); + + for (const reason of ["new", "resume", "fork"]) { + await (await pi.replace("reload")).chrome("authorize indefinite"); + const next = await pi.replace(reason); + assert.equal(next.chromeTools(), false, `tools after ${reason}`); + assert.equal(next.footer(), undefined, `footer after ${reason}`); + } +}); + +test("/chrome authorize over PI_CHROME_AUTHORIZE is announced, and a switch falls back to the env grant", async (t) => { + const pi = piProcess({ PI_CHROME_AUTHORIZE: "indefinite" }); + t.after(() => pi.end()); + const first = pi.load(); + await first.start(); + await first.chrome("authorize 15m"); + assert.match(first.footer(), /^● Chrome Bridge \(\d+m\)$/); + + const reloaded = await pi.replace("reload"); + assert.match(reloaded.footer(), /^● Chrome Bridge \(\d+m\)$/); + + const next = await pi.replace("new"); + assert.equal(next.chromeTools(), true); + assert.equal(next.footer(), undefined); +}); diff --git a/test-suite/unit/session-cleanup.test.mjs b/test-suite/unit/session-cleanup.test.mjs index 7e3285f..604e691 100644 --- a/test-suite/unit/session-cleanup.test.mjs +++ b/test-suite/unit/session-cleanup.test.mjs @@ -19,7 +19,7 @@ function harness(send, sessionKey = "session:test") { const events = []; let shutdown; const token = Symbol(); - const globalState = { loaded: { token } }; + const globalState = { loaded: { token }, auth: { until: "indefinite" } }; const context = { AbortController, Promise, setTimeout(fn, ms) { const key = Symbol(); timers.set(key, { fn, ms }); return key; }, @@ -27,7 +27,7 @@ function harness(send, sessionKey = "session:test") { bridge: { send(...args) { events.push("send"); return send(...args); }, stop() { events.push("stop"); } }, sessionCtx: {}, sessionKeyFor: () => sessionKey ?? undefined, clearAuthExpiryTimer() {}, clearCountdownInterval() {}, - globalState, PI_CHROME_GLOBAL_KEY: "loaded", instanceToken: token, + globalState, PI_CHROME_GLOBAL_KEY: "loaded", PI_CHROME_AUTH_KEY: "auth", instanceToken: token, pi: { on(name, handler) { assert.equal(name, "session_shutdown"); shutdown = handler; } }, }; vm.runInNewContext(`${helper}\n${listener}`, context); @@ -78,6 +78,14 @@ for (const reason of ["quit", "new", "resume", "fork"]) { const h = harness(() => Promise.resolve({})); await h.shutdown({ reason }); assert.deepEqual(h.events, ["send", "stop"]); + // A /chrome authorize grant stays with its session: a session switch drops it, quit leaves it + // for a parent session sharing this process. + assert.equal("auth" in h.globalState, reason === "quit", `grant after ${reason}`); +} +{ + const h = harness(() => Promise.resolve({})); + await h.shutdown({ reason: "reload" }); + assert.deepEqual(h.globalState.auth, { until: "indefinite" }, "grant after reload"); } for (const send of [() => Promise.reject(new Error("offline")), () => { throw new Error("offline"); }]) { const h = harness(send); @@ -90,4 +98,4 @@ for (const send of [() => Promise.reject(new Error("offline")), () => { throw ne await h.shutdown({ reason: "exit" }); assert.deepEqual(h.events, ["stop"]); } -console.log("session-cleanup: shutdown ordering, reload, deadline, failures, and missing identity passed"); +console.log("session-cleanup: shutdown ordering, reload, deadline, failures, missing identity, and grant scope passed"); |
