test-suite/unit/bridge-resilience.test.mjs (view raw)
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 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
// Bridge transport resilience in the shipped worker, plus same-process (subagent) loading in index.ts.
// Worker timers are scaled 1000x down so the real 45s /next deadline elapses in ~45ms.
import assert from "node:assert/strict";
import fs from "node:fs";
import vm from "node:vm";
import { stripTypeScriptTypes } from "node:module";
import { test } from "node:test";
const workerSource = fs.readFileSync(new URL("../../extensions/chrome-profile-bridge/browser-extension/service_worker.js", import.meta.url), "utf8");
const indexSource = fs.readFileSync(new URL("../../extensions/chrome-profile-bridge/index.ts", import.meta.url), "utf8");
function loadWorker(fetchImpl) {
const listener = { addListener() {}, removeListener() {} };
const chrome = {
runtime: { id: "test", getManifest: () => ({ version: "0.0.1" }), getURL: (f) => `chrome-extension://test/${f}`, onInstalled: listener, onStartup: listener, reload() {} },
alarms: { create() {}, onAlarm: listener }, action: { onClicked: listener }, webNavigation: { onCommitted: listener },
debugger: { onDetach: listener }, scripting: {}, tabs: { onUpdated: listener },
};
const sandbox = {
chrome, console, AbortController, URL,
navigator: { userAgent: "unit-test" },
setTimeout: (fn, ms, ...args) => setTimeout(fn, Math.ceil((ms || 0) / 1000), ...args),
clearTimeout, setInterval: () => 0, clearInterval() {},
fetch: fetchImpl,
};
sandbox.self = sandbox;
vm.runInNewContext(workerSource, sandbox);
return sandbox;
}
function response(status, body, headers = {}) {
return { ok: status >= 200 && status < 300, status, headers: { get: (name) => headers[name.toLowerCase()] ?? null }, json: async () => body };
}
test("a half-open /next long poll is aborted at its deadline and polling resumes", async () => {
const requests = [];
let aborted = 0;
const worker = loadWorker((url, options = {}) => {
requests.push({ url, options });
if (requests.length === 1) {
// Zombie socket: never settles unless aborted.
return new Promise((_resolve, reject) => {
options.signal.addEventListener("abort", () => {
aborted++;
const error = new Error("The operation was aborted");
error.name = "AbortError";
reject(error);
});
});
}
return Promise.reject(new Error("bridge down"));
});
const first = worker.pollLoop();
assert.ok(requests[0]?.options.signal, "the /next fetch carries an abort signal");
// Resolves only after the scaled 45s deadline fires and the 2s backoff elapses.
await first;
assert.equal(aborted, 1, "the stalled /next fetch was aborted");
await worker.pollLoop();
assert.equal(requests.length, 2, "a later poll opens a fresh /next request instead of staying parked");
});
test("a command result is posted exactly once, retried on transport failure, and not retried on 4xx", async () => {
const posts = [];
let postStatus = [500, 200];
const worker = loadWorker(async (url, options = {}) => {
if (url.includes("/result")) {
posts.push(JSON.parse(options.body));
const status = postStatus.shift() ?? 200;
if (status === 0) throw new Error("fetch failed");
return response(status, {});
}
throw new Error("not polling in this test");
});
await worker.handleCommand({ id: "cmd-1", action: "tab.version", params: {} });
assert.equal(posts.length, 2, "one retry after HTTP 500");
assert.ok(posts.every((p) => p.id === "cmd-1" && p.ok === true), "the retry re-sends the success payload, not an error payload");
posts.length = 0;
postStatus = [0, 0, 0];
await worker.handleCommand({ id: "cmd-2", action: "tab.version", params: {} });
assert.equal(posts.length, 3, "network failures retry up to the attempt limit");
assert.ok(posts.every((p) => p.ok === true), "a failed success-post never turns into a second, error result");
posts.length = 0;
postStatus = [404];
await worker.handleCommand({ id: "cmd-3", action: "no.such.action", params: {} });
assert.equal(posts.length, 1, "4xx (unknown command id) is final");
assert.equal(posts[0].ok, false);
assert.match(posts[0].error, /Unknown action/);
});
test("cdp.call widens only its own command deadline", () => {
const worker = loadWorker(async () => { throw new Error("offline"); });
assert.equal(worker.commandTimeoutMs("page.click", { timeoutMs: 90_000 }), 25_000);
assert.equal(worker.commandTimeoutMs("cdp.call", {}), 25_000);
assert.equal(worker.commandTimeoutMs("cdp.call", { timeoutMs: 60_000 }), 65_000);
assert.equal(worker.commandTimeoutMs("cdp.call", { timeoutMs: 10_000_000 }), 125_000, "capped below the MV3 worker lifetime");
});
// ---- index.ts: a subagent session loads pi-chrome again in the same process. ----
function loadFactoryPrelude(globalState, root) {
const from = indexSource.indexOf("\tconst alreadyLoaded = globalState[PI_CHROME_GLOBAL_KEY];");
const to = indexSource.indexOf("\tconst bridge = new ChromeProfileBridge(", from);
assert.ok(from > 0 && to > from, "factory prelude section");
const body = stripTypeScriptTypes(`(() => {\n${indexSource.slice(from, to)}\nreturn true;\n})()`);
const warnings = [];
const token = Symbol(root);
const sandbox = {
globalState, currentRoot: root, instanceToken: token, PI_CHROME_GLOBAL_KEY: "loaded", PI_CHROME_VERSION: "9.9.9",
console: { warn: (msg) => warnings.push(msg) },
};
const loaded = vm.runInNewContext(body, sandbox);
return { loaded: loaded === true, token, warnings };
}
test("a same-root second load (subagent session) is not skipped, and does not steal the singleton flag", () => {
const globalState = {};
const parent = loadFactoryPrelude(globalState, "/pkg/pi-chrome");
assert.equal(parent.loaded, true);
assert.equal(globalState.loaded.token, parent.token);
const subagent = loadFactoryPrelude(globalState, "/pkg/pi-chrome");
assert.equal(subagent.loaded, true, "subagent gets its own pi-chrome instance and chrome_* tools");
assert.equal(subagent.warnings.length, 0);
assert.equal(globalState.loaded.token, parent.token, "only the first instance owns (and later clears) the flag");
});
test("a second install root is still treated as a duplicate", () => {
const globalState = {};
loadFactoryPrelude(globalState, "/npm/pi-chrome");
const other = loadFactoryPrelude(globalState, "/checkout/pi-chrome");
assert.equal(other.loaded, false);
assert.match(other.warnings[0], /already loaded from \/npm\/pi-chrome/);
});
test("a stale same-root flag from an older release does not block loading", () => {
const globalState = { loaded: { version: "0.15.19", root: "/pkg/pi-chrome" } };
assert.equal(loadFactoryPrelude(globalState, "/pkg/pi-chrome").loaded, true);
});
|