aboutsummaryrefslogtreecommitdiff
path: root/test-suite
diff options
context:
space:
mode:
Diffstat (limited to 'test-suite')
-rw-r--r--test-suite/unit/background-policy.test.mjs12
-rw-r--r--test-suite/unit/grant-scope.test.mjs30
-rw-r--r--test-suite/unit/sandboxed-snapshot.test.mjs108
-rw-r--r--test-suite/unit/shared-window.test.mjs45
4 files changed, 158 insertions, 37 deletions
diff --git a/test-suite/unit/background-policy.test.mjs b/test-suite/unit/background-policy.test.mjs
index efbca78..ea08be0 100644
--- a/test-suite/unit/background-policy.test.mjs
+++ b/test-suite/unit/background-policy.test.mjs
@@ -297,6 +297,14 @@ function workerHarness({ withWindows = true } = {}) {
};
return h;
}
+// A new Pi tab must not take over the user's window: it is either inactive, or the only tab of a
+// new, unfocused window of the session's own (so it is visible to the agent without stealing focus).
+function assertNewTabStaysOutOfTheWay(h, tab) {
+ if (tab.active === false) return;
+ assert.notEqual(tab.windowId, 1, "an active new tab must not be in the user's window");
+ assert.ok(h.calls.some((c) => c.action === "windows.create" && c.params.focused === false), "its window was created unfocused");
+}
+
function assertNoFocus(h) {
assert.equal(h.focusedWindow(), 1);
assert.equal(h.tabs.get(1).active, true);
@@ -325,7 +333,7 @@ test("worker advertises support, keeps new tabs inactive, and rejects activation
assert.equal((await h.w.dispatch("tab.version", {})).capabilities.hardBackground, true);
for (const flags of [{}, { foreground: false }, { background: true, foreground: true }]) {
const result = await h.w.dispatch("tab.new", { ...flags, sessionKey: "alpha", groupTitle: "Pi Session: alpha" });
- assert.equal(result.tab.active, false);
+ assertNewTabStaysOutOfTheWay(h, result.tab);
await assert.rejects(h.w.dispatch("tab.activate", { ...flags, targetId: "missing" }), /background mode/);
}
assertNoFocus(h);
@@ -339,7 +347,7 @@ test("worker advertises support, keeps new tabs inactive, and rejects activation
test("background-only worker aliases force safe behavior even with foreground flags", async () => {
const h = workerHarness();
const opened = await h.w.dispatch("tab.new.background", { foreground: true, background: false });
- assert.equal(opened.tab.active, false);
+ assertNewTabStaysOutOfTheWay(h, opened.tab);
const shot = await h.w.dispatch("page.screenshot.background", { targetId: "2", foreground: true, background: false });
assert.equal(shot.method, "cdp");
assertNoFocus(h);
diff --git a/test-suite/unit/grant-scope.test.mjs b/test-suite/unit/grant-scope.test.mjs
index e7ce044..7717131 100644
--- a/test-suite/unit/grant-scope.test.mjs
+++ b/test-suite/unit/grant-scope.test.mjs
@@ -24,6 +24,8 @@ function piProcess(env) {
setTimeout, clearTimeout, setInterval, clearInterval,
});
let runtime;
+ // A grant an earlier runtime left on globalThis, e.g. a build that saved the env grant there.
+ const seedGrant = (until) => vm.runInContext(`globalThis.__piChromeProfileBridgeAuth__ = ${JSON.stringify({ until })}`, context);
// Evaluate the module again and run its factory, as pi does at startup, on /reload and on a
// session switch.
const load = () => {
@@ -65,7 +67,7 @@ function piProcess(env) {
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") };
+ return { load, replace, seedGrant, end: () => runtime?.shutdown("quit") };
}
test("PI_CHROME_AUTHORIZE grants every runtime without announcing it", async (t) => {
@@ -104,18 +106,38 @@ test("/chrome authorize survives /reload and ends with its session", async (t) =
}
});
-test("/chrome authorize over PI_CHROME_AUTHORIZE is announced, and a switch falls back to the env grant", async (t) => {
+test("with PI_CHROME_AUTHORIZE set, /chrome authorize does not bring the footer, 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\)$/);
+ assert.equal(first.chromeTools(), true);
+ assert.equal(first.footer(), undefined);
const reloaded = await pi.replace("reload");
- assert.match(reloaded.footer(), /^● Chrome Bridge \(\d+m\)$/);
+ assert.equal(reloaded.footer(), undefined);
const next = await pi.replace("new");
assert.equal(next.chromeTools(), true);
assert.equal(next.footer(), undefined);
});
+
+test("with PI_CHROME_AUTHORIZE set, a grant left on globalThis by an older build never shows the footer", async (t) => {
+ for (const until of ["indefinite", Date.now() + 3_600_000]) {
+ const pi = piProcess({ PI_CHROME_AUTHORIZE: "indefinite" });
+ t.after(() => pi.end());
+ pi.seedGrant(until);
+ const first = pi.load();
+ await first.start();
+ assert.equal(first.chromeTools(), true);
+ assert.equal(first.footer(), undefined, `footer at start, saved until ${until}`);
+ for (const reason of ["reload", "new", "resume", "fork"]) {
+ pi.seedGrant(until);
+ const next = await pi.replace(reason);
+ assert.equal(next.chromeTools(), true, `tools after ${reason}`);
+ assert.equal(next.footer(), undefined, `footer after ${reason}, saved until ${until}`);
+ }
+ await pi.end();
+ }
+});
diff --git a/test-suite/unit/sandboxed-snapshot.test.mjs b/test-suite/unit/sandboxed-snapshot.test.mjs
new file mode 100644
index 0000000..9774f21
--- /dev/null
+++ b/test-suite/unit/sandboxed-snapshot.test.mjs
@@ -0,0 +1,108 @@
+// chrome_snapshot and chrome_inspect on a page served with CSP `sandbox` (no allow-scripts), such as
+// raw.githubusercontent.com. There chrome.scripting.executeScript resolves with a null result
+// instead of throwing, so the access-denied CDP fallback never ran and the snapshot came back
+// undefined. The worker now redoes the injection over CDP Runtime.evaluate, which such pages allow.
+import assert from "node:assert/strict";
+import fs from "node:fs";
+import vm from "node:vm";
+import { test } from "node:test";
+
+const workerUrl = new URL("../../extensions/chrome-profile-bridge/browser-extension/service_worker.js", import.meta.url);
+const src = fs.readFileSync(workerUrl, "utf8");
+
+// Stands in for snapshot_injected.js when the worker reads it from the package for CDP injection.
+const INJECTED_STUB = `
+globalThis.__piChromeSnapshotPage = async (...args) => ({ mode: args[4], title: "raw page" });
+globalThis.__piChromeInspectTarget = async (uid) => ({ target: { uid } });
+`;
+
+function loadWorker({ scriptingRuns, cdpReturnsNothing = false }) {
+ const pageWorld = vm.createContext({ Promise, Error, String, JSON });
+ pageWorld.globalThis = pageWorld;
+ const calls = { scripting: 0, cdp: 0 };
+ const noop = () => {};
+ const listener = { addListener: noop, removeListener: noop };
+ const sandbox = {
+ console, JSON, Date, Math, Promise, Array, Object, String, Number, Boolean,
+ Error, TypeError, Map, Set, Symbol, structuredClone,
+ setTimeout, clearTimeout, setInterval: () => 0, clearInterval: noop,
+ fetch: async (url) => {
+ assert.match(String(url), /snapshot_injected\.js$/);
+ return { ok: true, text: async () => INJECTED_STUB };
+ },
+ navigator: { userAgent: "unit-test" },
+ WebSocket: function () {},
+ chrome: {
+ runtime: {
+ id: "unittestextension", getManifest: () => ({ version: "0.0.0" }),
+ getURL: (file) => `chrome-extension://unittestextension/${file}`,
+ onInstalled: listener, onStartup: listener, lastError: null,
+ },
+ alarms: { onAlarm: listener, create: noop, clear: noop, clearAll: noop },
+ action: { onClicked: listener },
+ debugger: { sendCommand: noop, attach: async () => {}, detach: async () => {}, getTargets: (cb) => cb([]) },
+ scripting: {
+ // A sandboxed document runs no extension script and reports a null result.
+ executeScript: async ({ files, func, args }) => {
+ calls.scripting++;
+ if (!scriptingRuns) return [{ frameId: 0, result: null }];
+ if (files) {
+ vm.runInContext(INJECTED_STUB, pageWorld);
+ return [{ frameId: 0, result: undefined }];
+ }
+ const fn = vm.runInContext(`(${func.toString()})`, pageWorld);
+ return [{ frameId: 0, result: await fn(...(args || [])) }];
+ },
+ },
+ tabs: { query: async () => [], get: async () => ({}), create: async () => ({}), update: async () => ({}), remove: async () => {} },
+ windows: { update: async () => {} },
+ webNavigation: { onCommitted: listener },
+ },
+ };
+ sandbox.globalThis = sandbox;
+ sandbox.self = sandbox;
+ vm.createContext(sandbox);
+ vm.runInContext(src, sandbox);
+ sandbox.attachDebugger = async () => ({});
+ sandbox.bringToFront = async () => {};
+ sandbox.getTabByParams = async () => ({ id: 7, windowId: 1, url: "https://raw.githubusercontent.com/o/r/main/f.swift" });
+ sandbox.cdp = async (_tabId, method, params) => {
+ assert.equal(method, "Runtime.evaluate");
+ calls.cdp++;
+ if (cdpReturnsNothing) return { result: { type: "undefined" } };
+ // returnByValue: JSON-able values come back as `value`; functions and undefined only as a type.
+ const value = await vm.runInContext(params.expression, pageWorld);
+ const json = JSON.stringify(value);
+ return json === undefined
+ ? { result: { type: typeof value } }
+ : { result: { type: "object", value: JSON.parse(json) } };
+ };
+ return { dispatch: sandbox.dispatch, calls };
+}
+
+test("snapshot of a sandboxed page falls back to CDP", async () => {
+ const { dispatch, calls } = loadWorker({ scriptingRuns: false });
+ const snapshot = await dispatch("page.snapshot", { mode: "text" });
+ assert.equal(snapshot?.title, "raw page");
+ assert.equal(snapshot?.mode, "text");
+ assert.equal(calls.scripting, 2, "scripting is tried first for both the injection and the call");
+ assert.equal(calls.cdp, 2, "both are redone over CDP");
+});
+
+test("inspect of a sandboxed page falls back to CDP", async () => {
+ const { dispatch } = loadWorker({ scriptingRuns: false });
+ const inspect = await dispatch("page.inspect", { uid: "e12" });
+ assert.equal(inspect?.target?.uid, "e12");
+});
+
+test("snapshot of an ordinary page does not touch CDP", async () => {
+ const { dispatch, calls } = loadWorker({ scriptingRuns: true });
+ const snapshot = await dispatch("page.snapshot", { mode: "auto" });
+ assert.equal(snapshot?.title, "raw page");
+ assert.equal(calls.cdp, 0);
+});
+
+test("a page that runs neither path reports an error instead of an empty snapshot", async () => {
+ const { dispatch } = loadWorker({ scriptingRuns: false, cdpReturnsNothing: true });
+ await assert.rejects(dispatch("page.snapshot", { mode: "text" }), /snapshot script returned no result in tab 7 \(https:\/\/raw\.githubusercontent\.com/);
+});
diff --git a/test-suite/unit/shared-window.test.mjs b/test-suite/unit/shared-window.test.mjs
index 7afeb77..66041f4 100644
--- a/test-suite/unit/shared-window.test.mjs
+++ b/test-suite/unit/shared-window.test.mjs
@@ -1,5 +1,5 @@
-// Agent sessions share one window: a new session's tab goes into the window that already holds a
-// "Pi Session:" group, and a window is created only when there is none.
+// Each session has its own window: a window shows only one tab, so sessions sharing one hid each
+// other's tabs (no real input, blank screenshots). A session's extra tabs go into its own window.
import assert from "node:assert/strict";
import fs from "node:fs";
import vm from "node:vm";
@@ -35,44 +35,27 @@ function worker({ groups = [], windows = { 5: { id: 5, type: "normal" } } } = {}
}
const piGroup = (windowId, name = "Pi Session: claude-code 1 other") => ({ id: 1, windowId, title: name });
+const ops = (calls) => calls.filter((c) => c.method !== "storage.set").map((c) => c.method);
-test("a new session's tab goes into the window that already holds another Pi session", async () => {
+test("a new session gets its own window even when another session's window exists", async () => {
const { w, calls } = worker({ groups: [piGroup(5)] });
await w.createAutomationTarget("claude-code 2 mine", "Pi Session: claude-code 2 mine");
- assert.deepEqual(calls.filter((c) => c.method !== "storage.set").map((c) => c.method), ["tabs.create"]);
- assert.equal(calls[0].props.windowId, 5);
+ assert.deepEqual(ops(calls), ["windows.create"]);
});
-test("with no Pi window yet, the first session creates one", async () => {
- const { w, calls } = worker({ groups: [] });
+test("a session that already has a group adds its tab to that group's window", async () => {
+ const { w, calls } = worker({ groups: [piGroup(5, "Pi Session: claude-code 2 mine")] });
await w.createAutomationTarget("claude-code 2 mine", "Pi Session: claude-code 2 mine");
- assert.deepEqual(calls.filter((c) => c.method !== "storage.set").map((c) => c.method), ["windows.create"]);
+ assert.deepEqual(ops(calls), ["tabs.create"]);
+ assert.equal(calls.find((c) => c.method === "tabs.create").props.windowId, 5);
});
-test("groups that are not Pi sessions do not attract agent tabs", async () => {
- const { w, calls } = worker({ groups: [{ id: 2, windowId: 5, title: "Work" }] });
- await w.createAutomationTarget("claude-code 2 mine", "Pi Session: claude-code 2 mine");
- assert.deepEqual(calls.filter((c) => c.method !== "storage.set").map((c) => c.method), ["windows.create"]);
-});
-
-test("a Pi group whose window is gone, or is not a normal window, is skipped", async () => {
- const { w, calls } = worker({ groups: [piGroup(7), piGroup(8)], windows: { 8: { id: 8, type: "popup" } } });
- await w.createAutomationTarget("claude-code 2 mine", "Pi Session: claude-code 2 mine");
- assert.deepEqual(calls.filter((c) => c.method !== "storage.set").map((c) => c.method), ["windows.create"]);
-});
-
-test("the shared window is never recorded as ours, so cleanup cannot close it", async () => {
- const { w, calls } = worker({ groups: [piGroup(5)] });
- await w.createAutomationTarget("claude-code 2 mine", "Pi Session: claude-code 2 mine");
- const stored = calls.filter((c) => c.method === "storage.set").map((c) => c.json).join("");
- assert.doesNotMatch(stored, /"windowId":5/);
-});
-
-test("chrome_tab new for a session with no group uses the shared window, not the focused one", async () => {
+test("chrome_tab new for a session with no group goes to the session's own window, not the focused one", async () => {
const { w, calls } = worker({ groups: [piGroup(5)] });
w.groupTab = async (tab) => tab;
w.bringToFront = async () => {};
- await w.dispatch("tab.new", { url: "about:blank", groupTitle: "Pi Session: claude-code 2 mine", sessionKey: "claude-code 2 mine" });
- const created = calls.find((c) => c.method === "tabs.create");
- assert.equal(created.props.windowId, 5);
+ await w.dispatch("tab.new", { url: "https://x.test/", groupTitle: "Pi Session: claude-code 2 mine", sessionKey: "claude-code 2 mine" });
+ assert.deepEqual(ops(calls), ["windows.create"]);
+ assert.equal(calls[0].props.url, "https://x.test/");
+ assert.equal(calls[0].props.focused, false);
});