Files
jb fd63b0ccec
fast-browser scenarios / Offline scenario contract tests (push) Failing after 44s
fast-browser scenarios / Local Chromium headless scenarios (push) Failing after 23s
fast-browser-with-jev
2026-09-20 12:24:08 -04:00

955 lines
39 KiB
TypeScript

import assert from "node:assert/strict";
import { EventEmitter } from "node:events";
import { existsSync, mkdtempSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { hostname, tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import { createBrowserSessionManager, type BrowserChildProcess, type BrowserSpawn } from "./browser-session.ts";
import { FastBrowserConfigError, type FastBrowserConfig } from "./config.ts";
import { profileDirectory } from "./profiles.ts";
class FakeChild extends EventEmitter implements BrowserChildProcess {
constructor() {
super();
queueMicrotask(() => this.emit("spawn"));
}
pid?: number;
ownedProcessGroup?: BrowserChildProcess["ownedProcessGroup"];
killed: string[] = [];
killBehavior?: (signal: string) => void;
kill(signal?: NodeJS.Signals | number): boolean {
const value = String(signal);
this.killed.push(value);
if (this.killBehavior) this.killBehavior(value);
else this.emit("close", null, signal);
return true;
}
}
function dirs() {
const root = mkdtempSync(join(tmpdir(), "fast-browser-session-test-"));
const leases = join(root, "leases");
mkdirSync(leases, { mode: 0o700 });
return { root, leases, cleanup: () => rmSync(root, { recursive: true, force: true }) };
}
function config(overrides: Partial<NonNullable<FastBrowserConfig["browser"]>> = {}): FastBrowserConfig {
return {
allowedOrigins: ["https://fixture.test"],
browser: {
mode: "launch",
product: "vivaldi",
executablePath: "/fixture/vivaldi",
startUrl: "https://fixture.test/start",
...overrides,
},
};
}
class FakeProcessGroup {
alive = true;
owned = true;
leaderKilled: string[] = [];
killed: string[] = [];
killBehavior?: (signal: string) => void;
leaderKillBehavior?: (signal: string) => void;
killLeader(signal?: NodeJS.Signals | number): boolean {
const value = String(signal);
this.leaderKilled.push(value);
this.leaderKillBehavior?.(value);
return true;
}
kill(signal?: NodeJS.Signals | number): boolean {
const value = String(signal);
this.killed.push(value);
this.killBehavior?.(value);
return true;
}
isAlive(): boolean { return this.alive; }
isOwned(): boolean { return this.owned; }
}
function launchFake(children: FakeChild[], seen: { command: string; args: readonly string[]; options: Parameters<BrowserSpawn>[2] }[]): BrowserSpawn {
return (command, args, options) => {
const child = new FakeChild();
children.push(child);
seen.push({ command, args, options });
return child;
};
}
async function waitUntil(check: () => boolean, timeoutMs = 250): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (!check()) {
if (Date.now() >= deadline) throw new Error("test condition did not become true before the deadline");
await new Promise((resolve) => setImmediate(resolve));
}
}
function fakeLinuxProcessAdapter(options: {
foreign?: boolean;
foreignUid?: boolean;
foreignSession?: boolean;
foreignExecutable?: boolean;
reordered?: boolean;
reparentBroker?: boolean;
lateOrphan?: boolean;
executableMisses?: number;
executableTransitions?: number;
reuseAfterOwnedCensus?: boolean;
reusedRoot?: boolean;
} = {}) {
const pid = process.pid + 1;
const executable = "/fixture/chromium";
const uid = 1000;
let profile = "/fixture/profile";
let rootAlive = true;
let groupAlive = true;
let executableReads = 0;
let leaderReuseArmed = false;
let leaderOwnedCensusCount = 0;
let leaderReused = false;
const killed: string[] = [];
const leaderKilled: string[] = [];
const root = () => ({ pid, ppid: 1, pgrp: pid, session: pid, startTime: "root", state: "S", uid, executable, commandLine: `${executable} --user-data-dir=${profile}`, owned: false });
const gpu = () => ({ pid: pid + 1, ppid: pid, pgrp: pid, session: pid, startTime: "gpu", state: "S", uid, executable, commandLine: `${executable} --type=gpu-process --user-data-dir=${profile}`, owned: false });
const broker = () => ({ pid: pid + 2, ppid: options.reparentBroker && !rootAlive ? 1 : pid + 1, pgrp: pid, session: pid, startTime: "broker", state: "S", uid, executable, commandLine: `${executable} --type=broker`, owned: false });
const lateOrphan = () => ({ pid: pid + 4, ppid: pid + 1, pgrp: pid, session: pid, startTime: "late-orphan", state: "S", uid, executable, commandLine: `${executable} --user-data-dir=${profile} --type=late-helper`, owned: false });
const foreign = () => ({
pid: pid + 3,
ppid: 999_999,
pgrp: pid,
session: options.foreignSession ? pid + 99 : pid,
startTime: "foreign",
state: "S",
uid: options.foreignUid ? uid + 1 : uid,
executable: options.foreignExecutable ? "/fixture/other" : executable,
commandLine: `${executable} --type=broker`,
owned: false,
});
return {
pid,
killed,
get executableReads() { return executableReads; },
armLeaderReuse() {
leaderReuseArmed = true;
leaderOwnedCensusCount = 0;
},
setProfile(value: string) { profile = value; },
rootExit() { rootAlive = false; },
adapter: {
identity(value: number) {
if (value !== pid) return undefined;
if (leaderReused) return { ...root(), startTime: "foreign-root" };
if (rootAlive) return root();
return options.reusedRoot ? { ...root(), startTime: "foreign-root" } : undefined;
},
executable(value: number) {
if (value === pid) {
const read = executableReads++;
if (read < (options.executableMisses ?? 0)) return undefined;
if (read < (options.executableMisses ?? 0) + (options.executableTransitions ?? 0)) return "/fixture/pre-exec";
}
return executable;
},
uid(_value: number) { return uid; },
members(_groupId: number) {
const members = !rootAlive && options.lateOrphan ? [broker(), lateOrphan()] : [gpu(), broker()];
if (rootAlive) members.unshift(root());
if (options.foreign) members.push(foreign());
if (leaderReuseArmed && options.reuseAfterOwnedCensus) {
leaderOwnedCensusCount += 1;
if (leaderOwnedCensusCount === 2) leaderReused = true;
}
return options.reordered ? members.reverse() : members;
},
groupAlive(_groupId: number) { return groupAlive; },
killGroup(_groupId: number, signal?: NodeJS.Signals | number) {
killed.push(String(signal));
groupAlive = false;
return true;
},
killProcess(_pid: number, signal?: NodeJS.Signals | number) {
leaderKilled.push(String(signal));
groupAlive = false;
return true;
},
},
leaderKilled,
};
}
test("malformed child PIDs never reach process-group or root signals", async () => {
for (const pid of [0, 1, -1, process.pid, Number.NaN, 1.5, Number.MAX_SAFE_INTEGER + 1]) {
const p = dirs();
let child!: FakeChild;
const manager = createBrowserSessionManager({
leaseDirectory: p.leases,
spawn: () => {
child = new FakeChild();
child.pid = pid;
child.ownedProcessGroup = new FakeProcessGroup();
return child;
},
mkdtemp: (prefix) => mkdtempSync(join(p.root, prefix)),
readiness: async () => 19199,
});
await assert.rejects(manager.open(config(), new AbortController().signal), /startup cleanup failed/);
assert.deepEqual(child.killed, []);
await assert.rejects(manager.closeAll(), /cleanup failed/);
assert.deepEqual(child.killed, []);
p.cleanup();
}
});
test("owned Linux census accepts a profile-less Chromium broker through owned ancestry", async () => {
const p = dirs();
const fixture = fakeLinuxProcessAdapter({ reordered: true, reparentBroker: true });
let child!: FakeChild;
const manager = createBrowserSessionManager({
leaseDirectory: p.leases,
linuxProcessAdapter: fixture.adapter,
spawn: () => { child = new FakeChild(); child.pid = fixture.pid; return child; },
mkdtemp: (prefix) => mkdtempSync(join(p.root, prefix)),
readiness: async (directory) => { fixture.setProfile(directory); return 19200; },
});
const session = await manager.open(config(), new AbortController().signal);
fixture.rootExit();
child.emit("close", 0, null);
await waitUntil(() => fixture.killed.length === 1);
assert.deepEqual(fixture.killed, ["SIGTERM"]);
await waitUntil(() => {
try { manager.get(session.id); return false; }
catch (error) { return error instanceof Error && /no longer available/.test(error.message); }
});
assert.throws(() => manager.get(session.id), /no longer available: browser process closed/);
p.cleanup();
});
test("startup retries ownership while the executable identity stabilizes", async () => {
const p = dirs();
const fixture = fakeLinuxProcessAdapter({ executableMisses: 2 });
const manager = createBrowserSessionManager({
leaseDirectory: p.leases,
linuxProcessAdapter: fixture.adapter,
spawn: () => { const child = new FakeChild(); child.pid = fixture.pid; return child; },
mkdtemp: (prefix) => mkdtempSync(join(p.root, prefix)),
readiness: async (directory) => { fixture.setProfile(directory); return 19201; },
});
const session = await manager.open(config(), new AbortController().signal);
assert.ok(fixture.executableReads >= 3);
await manager.close(session.id);
p.cleanup();
});
test("a transitioning executable identity is retried before handoff", async () => {
const p = dirs();
const fixture = fakeLinuxProcessAdapter({ executableTransitions: 1 });
const manager = createBrowserSessionManager({
leaseDirectory: p.leases,
linuxProcessAdapter: fixture.adapter,
spawn: () => { const child = new FakeChild(); child.pid = fixture.pid; return child; },
mkdtemp: (prefix) => mkdtempSync(join(p.root, prefix)),
readiness: async (directory) => { fixture.setProfile(directory); return 19202; },
});
const session = await manager.open(config(), new AbortController().signal);
assert.ok(fixture.executableReads >= 2);
await manager.close(session.id);
p.cleanup();
});
test("persistent executable uncertainty does not hand out an uncloseable session", async () => {
const p = dirs();
const fixture = fakeLinuxProcessAdapter({ executableMisses: Number.MAX_SAFE_INTEGER });
const manager = createBrowserSessionManager({
leaseDirectory: p.leases,
linuxProcessAdapter: fixture.adapter,
spawn: () => { const child = new FakeChild(); child.pid = fixture.pid; return child; },
mkdtemp: (prefix) => mkdtempSync(join(p.root, prefix)),
readiness: async (directory) => { fixture.setProfile(directory); return 19202; },
});
await assert.rejects(manager.open(config(), new AbortController().signal), /startup cleanup failed/);
assert.deepEqual(fixture.killed, []);
await assert.rejects(manager.closeAll(), /cleanup failed/);
p.cleanup();
});
test("manual setup captures process provenance before later root exit", async () => {
const p = dirs();
const fixture = fakeLinuxProcessAdapter({ reordered: true, reparentBroker: true });
let child!: FakeChild;
const manager = createBrowserSessionManager({
profileRoot: join(p.root, "profiles"),
leaseDirectory: p.leases,
linuxProcessAdapter: fixture.adapter,
spawn: (_command, args) => {
fixture.setProfile(String(args.find((arg) => arg.startsWith("--user-data-dir="))).slice("--user-data-dir=".length));
child = new FakeChild();
child.pid = fixture.pid;
queueMicrotask(() => child.emit("spawn"));
return child;
},
});
const session = await manager.openSetup(config({ profile: "evebot" }), new AbortController().signal);
fixture.rootExit();
child.emit("close", 0, null);
await waitUntil(() => fixture.killed.length === 1);
assert.throws(() => manager.get(session.id), /no longer available: browser process closed/);
p.cleanup();
});
test("foreign UID/session/executable/parent lookalikes remain unkillable", async () => {
for (const variant of [{}, { foreignUid: true }, { foreignSession: true }, { foreignExecutable: true }]) {
const p = dirs();
const fixture = fakeLinuxProcessAdapter({ foreign: true, ...variant });
const manager = createBrowserSessionManager({
leaseDirectory: p.leases,
linuxProcessAdapter: fixture.adapter,
spawn: () => { const child = new FakeChild(); child.pid = fixture.pid; return child; },
mkdtemp: (prefix) => mkdtempSync(join(p.root, prefix)),
readiness: async (directory) => { fixture.setProfile(directory); return 19202; },
});
await assert.rejects(manager.open(config(), new AbortController().signal), /startup cleanup failed/);
assert.deepEqual(fixture.killed, []);
await assert.rejects(manager.closeAll(), /cleanup failed/);
p.cleanup();
}
});
test("a new member with an absent reused parent remains unkillable after leader exit", async () => {
const p = dirs();
const fixture = fakeLinuxProcessAdapter({ lateOrphan: true, reparentBroker: true });
let child!: FakeChild;
const manager = createBrowserSessionManager({
leaseDirectory: p.leases,
linuxProcessAdapter: fixture.adapter,
spawn: () => { child = new FakeChild(); child.pid = fixture.pid; return child; },
mkdtemp: (prefix) => mkdtempSync(join(p.root, prefix)),
readiness: async (directory) => { fixture.setProfile(directory); return 19203; },
});
const session = await manager.open(config(), new AbortController().signal);
fixture.rootExit();
child.emit("close", 0, null);
await new Promise((resolve) => setTimeout(resolve, 10));
assert.deepEqual(fixture.killed, []);
assert.doesNotThrow(() => manager.get(session.id));
await assert.rejects(manager.closeAll(), /cleanup failed/);
p.cleanup();
});
test("a leader exit before the first ownership census remains fail-closed", async () => {
const p = dirs();
const fixture = fakeLinuxProcessAdapter();
const manager = createBrowserSessionManager({
leaseDirectory: p.leases,
linuxProcessAdapter: fixture.adapter,
spawn: () => { const child = new FakeChild(); child.pid = fixture.pid; return child; },
mkdtemp: (prefix) => mkdtempSync(join(p.root, prefix)),
readiness: async (directory) => { fixture.setProfile(directory); fixture.rootExit(); return 19203; },
});
await assert.rejects(manager.open(config(), new AbortController().signal), /startup cleanup failed/);
assert.deepEqual(fixture.killed, []);
await assert.rejects(manager.closeAll(), /cleanup failed/);
p.cleanup();
});
test("a reused Linux leader identity remains unkillable", async () => {
const p = dirs();
const fixture = fakeLinuxProcessAdapter({ reusedRoot: true });
let child!: FakeChild;
const manager = createBrowserSessionManager({
leaseDirectory: p.leases,
linuxProcessAdapter: fixture.adapter,
spawn: () => { child = new FakeChild(); child.pid = fixture.pid; return child; },
mkdtemp: (prefix) => mkdtempSync(join(p.root, prefix)),
readiness: async (directory) => { fixture.setProfile(directory); return 19204; },
});
const session = await manager.open(config(), new AbortController().signal);
fixture.rootExit();
child.emit("close", 0, null);
await new Promise((resolve) => setTimeout(resolve, 10));
assert.deepEqual(fixture.killed, []);
assert.doesNotThrow(() => manager.get(session.id));
await assert.rejects(manager.closeAll(), /cleanup failed/);
p.cleanup();
});
test("isolates launched sessions and cleans one without touching another", async () => {
const p = dirs();
const children: FakeChild[] = [];
const seen: { command: string; args: readonly string[]; options: Parameters<BrowserSpawn>[2] }[] = [];
let port = 19200;
const manager = createBrowserSessionManager({
leaseDirectory: p.leases,
spawn: launchFake(children, seen),
mkdtemp: (prefix) => mkdtempSync(join(p.root, prefix)),
readiness: async () => port++,
});
const first = await manager.open(config(), new AbortController().signal);
const second = await manager.open(config(), new AbortController().signal);
assert.notEqual(first.id, second.id);
assert.notEqual(first.config.cdpUrl, second.config.cdpUrl);
assert.equal(first.owned, true);
assert.match(seen[0]!.args.join(" "), /--remote-debugging-port=0/);
await manager.close(first.id);
assert.equal(children[0]!.killed.length, 1);
assert.equal(children[1]!.killed.length, 0);
assert.doesNotThrow(() => manager.get(second.id));
await manager.closeAll();
p.cleanup();
});
test("unrestricted origin policy permits a launch start URL on any HTTP(S) origin", async () => {
const p = dirs();
const manager = createBrowserSessionManager({
leaseDirectory: p.leases,
spawn: launchFake([], []),
mkdtemp: (prefix) => mkdtempSync(join(p.root, prefix)),
readiness: async () => 19201,
});
const session = await manager.open({
...config({ startUrl: "https://other.example/start" }),
allowedOrigins: "*",
}, new AbortController().signal);
await manager.close(session.id);
p.cleanup();
});
test("headless is explicit and Vivaldi has no product fallback", async () => {
const p = dirs();
const seen: { command: string; args: readonly string[]; options: Parameters<BrowserSpawn>[2] }[] = [];
const manager = createBrowserSessionManager({
leaseDirectory: p.leases,
spawn: launchFake([], seen),
mkdtemp: (prefix) => mkdtempSync(join(p.root, prefix)),
readiness: async () => 19201,
});
await manager.open(config({ headless: true }), new AbortController().signal);
assert.equal(seen[0]!.command, "/fixture/vivaldi");
assert.ok(seen[0]!.args.includes("--headless=new"));
assert.ok(seen[0]!.args.some((arg) => arg.startsWith("--user-data-dir=")));
assert.equal(seen[0]!.args.some((arg) => arg.startsWith("--disable-") || arg === "--password-store=basic" || arg === "--use-mock-keychain"), false);
await manager.closeAll();
p.cleanup();
});
test("launch environment excludes provider keys but keeps desktop context", async () => {
const p = dirs();
const seen: { command: string; args: readonly string[]; options: Parameters<BrowserSpawn>[2] }[] = [];
const previous = {
TYPESAFE_API_KEY: process.env.TYPESAFE_API_KEY,
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
XDG_SESSION_TYPE: process.env.XDG_SESSION_TYPE,
};
process.env.TYPESAFE_API_KEY = "secret-provider-key";
process.env.OPENAI_API_KEY = "secret-openai-key";
process.env.XDG_SESSION_TYPE = "wayland";
try {
const manager = createBrowserSessionManager({
leaseDirectory: p.leases,
spawn: launchFake([], seen),
mkdtemp: (prefix) => mkdtempSync(join(p.root, prefix)),
readiness: async () => 19308,
});
await manager.open(config(), new AbortController().signal);
assert.equal(seen[0]!.options.env.TYPESAFE_API_KEY, undefined);
assert.equal(seen[0]!.options.env.OPENAI_API_KEY, undefined);
assert.equal(seen[0]!.options.env.XDG_SESSION_TYPE, "wayland");
await manager.closeAll();
} finally {
if (previous.TYPESAFE_API_KEY === undefined) delete process.env.TYPESAFE_API_KEY;
else process.env.TYPESAFE_API_KEY = previous.TYPESAFE_API_KEY;
if (previous.OPENAI_API_KEY === undefined) delete process.env.OPENAI_API_KEY;
else process.env.OPENAI_API_KEY = previous.OPENAI_API_KEY;
if (previous.XDG_SESSION_TYPE === undefined) delete process.env.XDG_SESSION_TYPE;
else process.env.XDG_SESSION_TYPE = previous.XDG_SESSION_TYPE;
p.cleanup();
}
});
test("attach leases normalize loopback aliases across managers", async () => {
const p = dirs();
const first = createBrowserSessionManager({ leaseDirectory: p.leases });
const second = createBrowserSessionManager({ leaseDirectory: p.leases });
const base = { allowedOrigins: ["https://fixture.test"], cdpUrl: "http://localhost:19300" };
const session = await first.open(base, new AbortController().signal);
assert.equal(session.owned, false);
await assert.rejects(second.open({ ...base, cdpUrl: "http://127.0.0.1:19300" }, new AbortController().signal), /already has a lease.*stale/i);
await first.close(session.id);
const reopened = await second.open({ ...base, cdpUrl: "http://[::1]:19300" }, new AbortController().signal);
await second.close(reopened.id);
p.cleanup();
});
test("named automation profiles retain data and remove stale CDP metadata only after leasing", async () => {
const p = dirs();
const root = join(p.root, "profiles");
const profile = profileDirectory("evebot", { profileRoot: root });
writeFileSync(join(profile, "DevToolsActivePort"), "9222\n/devtools/browser/stale\n");
writeFileSync(join(profile, "sentinel-cookie.txt"), "persistent-login-cookie");
const seen: { command: string; args: readonly string[]; options: Parameters<BrowserSpawn>[2] }[] = [];
const manager = createBrowserSessionManager({
leaseDirectory: p.leases,
profileRoot: root,
spawn: launchFake([], seen),
readiness: async (directory) => {
assert.equal(existsSync(join(directory, "DevToolsActivePort")), false);
return 19301;
},
});
const session = await manager.open(config({ profile: "evebot" }), new AbortController().signal);
await manager.close(session.id);
assert.equal(existsSync(profile), true);
assert.equal(readFileSync(join(profile, "sentinel-cookie.txt"), "utf8"), "persistent-login-cookie");
const reopenedManager = createBrowserSessionManager({
leaseDirectory: p.leases,
profileRoot: root,
spawn: launchFake([], []),
readiness: async (directory) => {
assert.equal(readFileSync(join(directory, "sentinel-cookie.txt"), "utf8"), "persistent-login-cookie");
return 19305;
},
});
const reopened = await reopenedManager.open(config({ profile: "evebot" }), new AbortController().signal);
await reopenedManager.close(reopened.id);
const failedManager = createBrowserSessionManager({
leaseDirectory: p.leases,
profileRoot: root,
spawn: launchFake([], []),
readiness: async () => { throw new Error("startup failure"); },
});
await assert.rejects(failedManager.open(config({ profile: "evebot" }), new AbortController().signal), /startup failure/);
assert.equal(readFileSync(join(profile, "sentinel-cookie.txt"), "utf8"), "persistent-login-cookie");
p.cleanup();
});
test("manual setup without saved launch/profile settings gives actionable configuration errors", async () => {
const manager = createBrowserSessionManager({
spawn: () => { throw new Error("must not launch for missing configuration"); },
});
const signal = new AbortController().signal;
for (const [input, field] of [
[{ allowedOrigins: ["https://fixture.test"], cdpUrl: "http://127.0.0.1:9222" }, "browser.mode"],
[config(), "browser.profile"],
] as const) {
await assert.rejects(manager.openSetup(input as FastBrowserConfig, signal), (error: unknown) => {
assert(error instanceof FastBrowserConfigError);
assert(error.message.includes(field));
assert.match(error.message, /Changes save automatically/);
return true;
});
}
});
test("manual setup is visible, has no CDP flags, and cannot be automated", async () => {
const p = dirs();
const root = join(p.root, "profiles");
let child!: FakeChild;
const seen: { command: string; args: readonly string[]; options: Parameters<BrowserSpawn>[2] }[] = [];
const manager = createBrowserSessionManager({
profileRoot: root,
spawn: (command, args, options) => {
child = new FakeChild();
seen.push({ command, args, options });
queueMicrotask(() => child.emit("spawn"));
return child;
},
});
const input = config({ profile: "evebot", headless: true });
const session = await manager.openSetup(input, new AbortController().signal);
assert.equal(session.owned, true);
assert.equal(session.config.browser?.headless, false);
assert.equal(input.browser?.headless, true);
assert.equal(seen[0]!.args.some((arg) => arg.includes("remote-debugging") || arg.startsWith("--headless")), false);
assert.throws(() => manager.get(session.id), /manual-only.*cannot be automated/i);
const automatedManager = createBrowserSessionManager({
profileRoot: root,
leaseDirectory: join(p.root, "automation-leases"),
spawn: launchFake([], []),
readiness: async () => 19306,
});
await assert.rejects(automatedManager.open(config({ profile: "evebot" }), new AbortController().signal), /active or stale lease/i);
await manager.close(session.id);
assert.equal(existsSync(profileDirectory("evebot", { profileRoot: root })), true);
const automated = await automatedManager.open(config({ profile: "evebot" }), new AbortController().signal);
await automatedManager.close(automated.id);
p.cleanup();
});
test("different named profiles can launch in parallel", async () => {
const p = dirs();
const root = join(p.root, "profiles");
let port = 19307;
const manager = createBrowserSessionManager({
profileRoot: root,
leaseDirectory: p.leases,
spawn: () => new FakeChild(),
readiness: async () => port++,
});
const [first, second] = await Promise.all([
manager.open(config({ profile: "evebot" }), new AbortController().signal),
manager.open(config({ profile: "otherbot" }), new AbortController().signal),
]);
assert.notEqual(first.config.cdpUrl, second.config.cdpUrl);
await manager.closeAll();
p.cleanup();
});
test("post-ready exit aborts and cleans its session", async () => {
const p = dirs();
let child!: FakeChild;
const manager = createBrowserSessionManager({
leaseDirectory: p.leases,
spawn: () => { child = new FakeChild(); return child; },
mkdtemp: (prefix) => mkdtempSync(join(p.root, prefix)),
readiness: async () => 19302,
});
const session = await manager.open(config(), new AbortController().signal);
child.emit("close", 0, null);
await new Promise((resolve) => setTimeout(resolve, 10));
assert.equal(session.signal.aborted, true);
assert.throws(() => manager.get(session.id), /no longer available: browser process closed \(0\)/);
p.cleanup();
});
test("recent exit diagnostics are safe and expire", async () => {
const p = dirs();
let child!: FakeChild;
const manager = createBrowserSessionManager({
leaseDirectory: p.leases,
recentExitTtlMs: 50,
spawn: () => { child = new FakeChild(); return child; },
mkdtemp: (prefix) => mkdtempSync(join(p.root, prefix)),
readiness: async () => 19315,
});
const session = await manager.open(config(), new AbortController().signal);
child.emit("error", new Error("stderr=https://secret.example/private?token=secret"));
await waitUntil(() => {
try { manager.get(session.id); return false; }
catch (error) { return error instanceof Error && /no longer available/.test(error.message); }
});
assert.throws(() => manager.get(session.id), (error: unknown) => {
assert(error instanceof Error);
assert.match(error.message, /browser process/);
assert.doesNotMatch(error.message, /stderr|https?:|secret/i);
return true;
});
await new Promise((resolve) => setTimeout(resolve, 60));
assert.throws(() => manager.get(session.id), /Unknown browser session/);
p.cleanup();
});
test("normal close and parent abort retain safe closed-session reasons", async () => {
const p = dirs();
const manager = createBrowserSessionManager({
leaseDirectory: p.leases,
spawn: launchFake([], []),
mkdtemp: (prefix) => mkdtempSync(join(p.root, prefix)),
readiness: async () => 19316,
});
const normal = await manager.open(config(), new AbortController().signal);
await manager.close(normal.id);
assert.throws(() => manager.get(normal.id), /no longer available: session closed/);
const parent = new AbortController();
const aborted = await manager.open(config(), parent.signal);
parent.abort(new Error("secret=https://private.example/token"));
await waitUntil(() => {
try { manager.get(aborted.id); return false; }
catch (error) { return error instanceof Error && /no longer available/.test(error.message); }
});
assert.throws(() => manager.get(aborted.id), (error: unknown) => {
assert(error instanceof Error);
assert.match(error.message, /session aborted/);
assert.doesNotMatch(error.message, /secret|https?:|private/i);
return true;
});
p.cleanup();
});
test("a root exit does not release a live owned descendant group", async () => {
const p = dirs();
const group = new FakeProcessGroup();
let child!: FakeChild;
const manager = createBrowserSessionManager({
leaseDirectory: p.leases,
spawn: () => {
child = new FakeChild();
child.pid = process.pid + 1;
child.ownedProcessGroup = group;
return child;
},
mkdtemp: (prefix) => mkdtempSync(join(p.root, prefix)),
readiness: async () => 19309,
});
const session = await manager.open(config(), new AbortController().signal);
child.emit("close", 0, null);
await waitUntil(() => group.killed.length === 1);
assert.deepEqual(group.leaderKilled, []);
assert.deepEqual(group.killed, ["SIGTERM"]);
assert.equal(group.alive, true);
assert.doesNotThrow(() => manager.get(session.id));
group.alive = false;
await assert.doesNotReject(manager.closeAll());
assert.throws(() => manager.get(session.id), /no longer available: browser process closed \(0\)/);
p.cleanup();
});
test("owned group cleanup escalates to bounded SIGKILL", async () => {
const p = dirs();
const group = new FakeProcessGroup();
group.leaderKillBehavior = () => { /* keep descendants alive for escalation */ };
group.killBehavior = (signal) => { if (signal === "SIGKILL") group.alive = false; };
const manager = createBrowserSessionManager({
leaseDirectory: p.leases,
closeTimeoutMs: 10,
spawn: () => {
const child = new FakeChild();
child.pid = process.pid + 1;
child.ownedProcessGroup = group;
return child;
},
mkdtemp: (prefix) => mkdtempSync(join(p.root, prefix)),
readiness: async () => 19310,
});
const session = await manager.open(config(), new AbortController().signal);
await manager.close(session.id);
assert.deepEqual(group.leaderKilled, ["SIGTERM"]);
assert.deepEqual(group.killed, ["SIGTERM", "SIGKILL"]);
p.cleanup();
});
test("verified leader shutdown precedes descendant escalation", async () => {
const p = dirs();
const group = new FakeProcessGroup();
group.leaderKillBehavior = () => { group.alive = false; };
const manager = createBrowserSessionManager({
leaseDirectory: p.leases,
closeTimeoutMs: 10,
spawn: () => {
const child = new FakeChild();
child.pid = process.pid + 1;
child.ownedProcessGroup = group;
return child;
},
mkdtemp: (prefix) => mkdtempSync(join(p.root, prefix)),
readiness: async () => 19317,
});
const session = await manager.open(config(), new AbortController().signal);
await manager.close(session.id);
assert.deepEqual(group.leaderKilled, ["SIGTERM"]);
assert.deepEqual(group.killed, []);
p.cleanup();
});
test("native ownership adapter signals the verified leader before its group", async () => {
const p = dirs();
const fixture = fakeLinuxProcessAdapter();
const manager = createBrowserSessionManager({
leaseDirectory: p.leases,
linuxProcessAdapter: fixture.adapter,
spawn: () => { const child = new FakeChild(); child.pid = fixture.pid; return child; },
mkdtemp: (prefix) => mkdtempSync(join(p.root, prefix)),
readiness: async (directory) => { fixture.setProfile(directory); return 19318; },
});
const session = await manager.open(config(), new AbortController().signal);
await manager.close(session.id);
assert.deepEqual(fixture.leaderKilled, ["SIGTERM"]);
assert.deepEqual(fixture.killed, []);
p.cleanup();
});
test("leader reuse between census and final probe produces no positive leader signal", async () => {
const p = dirs();
const fixture = fakeLinuxProcessAdapter({ reuseAfterOwnedCensus: true });
const manager = createBrowserSessionManager({
leaseDirectory: p.leases,
linuxProcessAdapter: fixture.adapter,
spawn: () => { const child = new FakeChild(); child.pid = fixture.pid; return child; },
mkdtemp: (prefix) => mkdtempSync(join(p.root, prefix)),
readiness: async (directory) => { fixture.setProfile(directory); return 19319; },
});
const session = await manager.open(config(), new AbortController().signal);
fixture.armLeaderReuse();
await assert.rejects(manager.close(session.id), /leader ownership changed|ownership could not be confirmed/);
assert.deepEqual(fixture.leaderKilled, []);
assert.deepEqual(fixture.killed, []);
p.cleanup();
});
test("leader exit skips reused leader PID and escalates only to the proven group", async () => {
const p = dirs();
const fixture = fakeLinuxProcessAdapter({ reparentBroker: true });
let child!: FakeChild;
const manager = createBrowserSessionManager({
leaseDirectory: p.leases,
linuxProcessAdapter: fixture.adapter,
spawn: () => { child = new FakeChild(); child.pid = fixture.pid; return child; },
mkdtemp: (prefix) => mkdtempSync(join(p.root, prefix)),
readiness: async (directory) => { fixture.setProfile(directory); return 19319; },
});
const session = await manager.open(config(), new AbortController().signal);
fixture.rootExit();
await manager.close(session.id);
assert.deepEqual(fixture.leaderKilled, []);
assert.deepEqual(fixture.killed, ["SIGTERM"]);
p.cleanup();
});
test("reused leader identity refuses both leader and group signals", async () => {
const p = dirs();
const fixture = fakeLinuxProcessAdapter({ reusedRoot: true });
const manager = createBrowserSessionManager({
leaseDirectory: p.leases,
linuxProcessAdapter: fixture.adapter,
spawn: () => { const child = new FakeChild(); child.pid = fixture.pid; return child; },
mkdtemp: (prefix) => mkdtempSync(join(p.root, prefix)),
readiness: async (directory) => { fixture.setProfile(directory); return 19320; },
});
const session = await manager.open(config(), new AbortController().signal);
fixture.rootExit();
await assert.rejects(manager.close(session.id), /ownership could not be confirmed/);
assert.deepEqual(fixture.leaderKilled, []);
assert.deepEqual(fixture.killed, []);
p.cleanup();
});
test("SIGKILL waits for confirmation and unkillable cleanup preserves state", async () => {
const p = dirs();
let child!: FakeChild;
const manager = createBrowserSessionManager({
leaseDirectory: p.leases,
closeTimeoutMs: 30,
spawn: () => {
child = new FakeChild();
child.killBehavior = (signal) => { if (signal === "SIGKILL") setTimeout(() => child.emit("close", null, signal), 15); };
return child;
},
mkdtemp: (prefix) => mkdtempSync(join(p.root, prefix)),
readiness: async () => 19303,
});
const session = await manager.open(config(), new AbortController().signal);
await manager.close(session.id);
assert.deepEqual(child.killed, ["SIGTERM", "SIGKILL"]);
p.cleanup();
});
test("uncertain SingletonLock cleanup keeps the named profile lease", async () => {
const p = dirs();
const root = join(p.root, "profiles");
let child!: FakeChild;
const manager = createBrowserSessionManager({
profileRoot: root,
leaseDirectory: p.leases,
spawn: () => {
child = new FakeChild();
child.pid = 29991;
const group = new FakeProcessGroup();
group.leaderKillBehavior = () => { group.alive = false; };
group.killBehavior = () => { group.alive = false; child.emit("close", null, "SIGTERM"); };
child.ownedProcessGroup = group;
return child;
},
readiness: async () => {
symlinkSync(`${hostname()}-29992`, join(root, "evebot", "SingletonLock"));
return 19311;
},
});
const session = await manager.open(config({ profile: "evebot" }), new AbortController().signal);
await assert.rejects(manager.close(session.id), /cleanup failed|SingletonLock/i);
assert.equal(readlinkSync(join(root, "evebot", "SingletonLock")), `${hostname()}-29992`);
const second = createBrowserSessionManager({ profileRoot: root, leaseDirectory: join(p.root, "other-leases"), spawn: launchFake([], []), readiness: async () => 19312 });
await assert.rejects(second.open(config({ profile: "evebot" }), new AbortController().signal), /active or stale lease/i);
// The foreign marker remains untouched; fixture cleanup removes the temp root.
assert.equal(readlinkSync(join(root, "evebot", "SingletonLock")), `${hostname()}-29992`);
p.cleanup();
});
test("a proven owned marker is removed and the named profile can be reused", async () => {
const p = dirs();
const root = join(p.root, "profiles");
let nextPid = 29993;
const spawn = () => {
const child = new FakeChild();
child.pid = nextPid++;
const group = new FakeProcessGroup();
group.leaderKillBehavior = () => { group.alive = false; child.emit("close", null, "SIGTERM"); };
group.killBehavior = () => { group.alive = false; child.emit("close", null, "SIGTERM"); };
child.ownedProcessGroup = group;
return child;
};
let firstChild!: FakeChild;
const manager = createBrowserSessionManager({
profileRoot: root,
leaseDirectory: p.leases,
spawn: () => {
firstChild = spawn();
return firstChild;
},
readiness: async () => {
symlinkSync(`${hostname()}-${firstChild.pid}`, join(root, "evebot", "SingletonLock"));
return 19313;
},
});
const first = await manager.open(config({ profile: "evebot" }), new AbortController().signal);
await manager.close(first.id);
assert.equal(existsSync(join(root, "evebot", "SingletonLock")), false);
const reopenedManager = createBrowserSessionManager({
profileRoot: root,
leaseDirectory: join(p.root, "reopen-leases"),
spawn: launchFake([], []),
readiness: async () => 19314,
});
const reopened = await reopenedManager.open(config({ profile: "evebot" }), new AbortController().signal);
await reopenedManager.close(reopened.id);
p.cleanup();
});
test("failed startup retains an unkillable child for closeAll retry", async () => {
const p = dirs();
const controller = new AbortController();
let child!: FakeChild;
const profiles: string[] = [];
const manager = createBrowserSessionManager({
closeTimeoutMs: 5,
spawn: () => { child = new FakeChild(); child.killBehavior = () => undefined; return child; },
mkdtemp: (prefix) => { const path = mkdtempSync(join(p.root, prefix)); profiles.push(path); return path; },
readiness: async (_path, signal) => {
await new Promise<void>((resolve) => signal.addEventListener("abort", () => resolve(), { once: true }));
throw new Error("startup cancelled");
},
});
const opening = manager.open(config(), controller.signal);
await new Promise((resolve) => setImmediate(resolve));
controller.abort();
await assert.rejects(opening, /startup cleanup failed/);
assert.equal(existsSync(profiles[0]!), true);
await assert.rejects(manager.closeAll(), /cleanup failed/);
child.emit("close", null, "SIGKILL");
await manager.closeAll();
assert.equal(existsSync(profiles[0]!), false);
p.cleanup();
});
test("closeAll blocks opens while cleaning and reports cleanup failures", async () => {
const p = dirs();
let child!: FakeChild;
const manager = createBrowserSessionManager({
leaseDirectory: p.leases,
closeTimeoutMs: 5,
spawn: () => { child = new FakeChild(); child.killBehavior = () => undefined; return child; },
mkdtemp: (prefix) => mkdtempSync(join(p.root, prefix)),
readiness: async () => 19304,
});
const session = await manager.open(config(), new AbortController().signal);
const closing = manager.closeAll();
await assert.rejects(manager.open(config(), new AbortController().signal), /manager is closing/);
await assert.rejects(closing, /cleanup failed/);
assert.doesNotThrow(() => manager.get(session.id));
child.emit("close", null, "SIGKILL");
await manager.closeAll();
p.cleanup();
});