aboutsummaryrefslogtreecommitdiff
path: root/test-suite/unit/grant-scope.test.mjs
blob: e7ce044deec4a6164bbb8e378a9c9a7733b019c3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
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);
});