aboutsummaryrefslogtreecommitdiff
path: root/test-suite/unit/grant-scope.test.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'test-suite/unit/grant-scope.test.mjs')
-rw-r--r--test-suite/unit/grant-scope.test.mjs121
1 files changed, 121 insertions, 0 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);
+});