aboutsummaryrefslogtreecommitdiff
path: root/test-suite/unit
diff options
context:
space:
mode:
Diffstat (limited to 'test-suite/unit')
-rw-r--r--test-suite/unit/grant-scope.test.mjs121
-rw-r--r--test-suite/unit/session-cleanup.test.mjs14
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");