aboutsummaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/cdp/session-call-test.ts26
-rw-r--r--test/deep-research/agent-config-test.ts9
-rw-r--r--test/deep-research/readability-test.ts7
-rw-r--r--test/deep-research/serp-parser-test.ts8
-rw-r--r--test/manual/bridge-test.ts23
-rw-r--r--test/manual/daemon-transport-test.ts23
-rw-r--r--test/manual/e2e-test.ts33
-rw-r--r--test/manual/forms-test.ts21
-rw-r--r--test/manual/ipc-server-test.ts22
-rw-r--r--test/manual/profile-concurrency-test.ts22
-rw-r--r--test/manual/profile-e2e-test.ts40
-rw-r--r--test/manual/socket-guard-test.ts7
-rw-r--r--test/manual/window-binding-test.ts27
-rw-r--r--test/profile/browser-process-test.ts26
-rw-r--r--test/profile/launch-test.ts19
-rw-r--r--test/profile/list-test.ts18
-rw-r--r--test/profile/paths-test.ts17
-rw-r--r--test/profile/store-test.ts9
18 files changed, 15 insertions, 342 deletions
diff --git a/test/cdp/session-call-test.ts b/test/cdp/session-call-test.ts
index 7b1386b..6a14e17 100644
--- a/test/cdp/session-call-test.ts
+++ b/test/cdp/session-call-test.ts
@@ -14,13 +14,6 @@ type RecordedCall = {
readonly opts: RequestOpts | undefined;
};
-/**
- * A stub transport: no socket, no browser. `request()` looks up a canned
- * Result by method name (defaulting to `ok({})` for methods the test doesn't
- * care about — e.g. the six domain-enable calls switchTo fires as a side
- * effect). Every call is recorded so tests can assert on exactly what the
- * session sent, including the sessionId routing and the timeoutMs option.
- */
const makeStubTransport = (
responses: Record<string, Result<unknown, CdpError>> = {},
): { transport: CdpTransport; calls: RecordedCall[] } => {
@@ -32,8 +25,7 @@ const makeStubTransport = (
calls.push({ method, params, opts });
return Promise.resolve(responses[method] ?? ok({}));
},
- // No real events ever arrive; the session's background consumer just
- // awaits forever. No timers involved, so it doesn't keep the process alive.
+ // No real events ever arrive, so the session's background consumer awaits forever — with no timers involved, it does not keep the process alive.
events: (): AsyncIterable<CdpEvent> => (async function* (): AsyncGenerator<CdpEvent> {
await new Promise<never>(() => {});
})(),
@@ -84,8 +76,6 @@ describe("CdpSession.call / callOnTarget / callBrowser", () => {
const r = await session.call("Target.getTargetInfo", {});
assert.equal(r.success, false);
if (!r.success) {
- // Must stay "timeout" — decodeResult must never run on a failed
- // transport call and relabel it "invalid_response".
assert.equal(r.error.kind, "timeout");
assert.equal(r.error.message, "socket dead");
}
@@ -97,24 +87,21 @@ describe("CdpSession.call / callOnTarget / callBrowser", () => {
});
const session = createCdpSession(transport);
- // Establish an internal sessionId distinct from both null and the
- // explicit sid used below, via the public switchTo() — no attachFirstPage
- // bookkeeping required.
const switched = await session.switchTo("target-1");
assert.equal(switched.success, true);
- calls.length = 0; // discard switchTo's own requests (activate/attach/enable*6)
+ calls.length = 0;
await session.call("Page.navigate", { url: "http://example.com" });
await session.callOnTarget("Page.navigate", { url: "http://example.com" }, "explicit-sid");
await session.callBrowser("Target.getTargets");
const navigateCalls = calls.filter((c) => c.method === "Page.navigate");
- assert.equal(navigateCalls[0]?.opts?.sessionId, "session-xyz"); // call() uses the active session
- assert.equal(navigateCalls[1]?.opts?.sessionId, "explicit-sid"); // callOnTarget() uses the given sid, not the active one
+ assert.equal(navigateCalls[0]?.opts?.sessionId, "session-xyz");
+ assert.equal(navigateCalls[1]?.opts?.sessionId, "explicit-sid");
const browserCall = calls.find((c) => c.method === "Target.getTargets");
- assert.equal(browserCall?.opts?.sessionId, null); // callBrowser() forces null even though a session is active
+ assert.equal(browserCall?.opts?.sessionId, null);
});
test("opts.timeoutMs reaches the transport only when provided (exactOptionalPropertyTypes)", async () => {
@@ -127,8 +114,7 @@ describe("CdpSession.call / callOnTarget / callBrowser", () => {
await session.callBrowser("Target.getTargets");
const withoutTimeout = calls.filter((c) => c.method === "Target.getTargets")[1];
- // The key itself must be absent, not present-with-undefined — that's the
- // distinction exactOptionalPropertyTypes exists to enforce at the call site.
+ // The key must be absent, not present-with-undefined — the distinction exactOptionalPropertyTypes enforces at the call site.
assert.equal(withoutTimeout?.opts && "timeoutMs" in withoutTimeout.opts, false);
});
});
diff --git a/test/deep-research/agent-config-test.ts b/test/deep-research/agent-config-test.ts
index 5859060..025a844 100644
--- a/test/deep-research/agent-config-test.ts
+++ b/test/deep-research/agent-config-test.ts
@@ -1,12 +1,3 @@
-/**
- * S20: the web-search-researcher subagent config references only real tools.
- *
- * Guards against the pre-Phase-3 regression where the agent named the
- * non-existent web_search / web_fetch tools. Pure string checks over the
- * markdown file — no browser, no subagent runtime.
- *
- * Run: npm test
- */
import { test, before, describe } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
diff --git a/test/deep-research/readability-test.ts b/test/deep-research/readability-test.ts
index ba0e0e0..211229d 100644
--- a/test/deep-research/readability-test.ts
+++ b/test/deep-research/readability-test.ts
@@ -1,8 +1,3 @@
-/**
- * Unit tests for the pure readability extractor — no browser required.
- *
- * Run: npm test
- */
import { test, describe } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
@@ -13,7 +8,6 @@ const fixture = (name: string): PageCapture =>
JSON.parse(readFileSync(join(import.meta.dirname, "fixtures", name), "utf8")) as PageCapture;
describe("pure readability extractor", () => {
- // S10: boilerplate stripped, article content preserved
test("S10: article body text is present", () => {
const page = extractReadable(fixture("article-capture.json"));
assert.ok(page.text.includes("Chrome DevTools Protocol lets tools instrument"));
@@ -54,7 +48,6 @@ describe("pure readability extractor", () => {
assert.ok(page.wordCount > 40);
});
- // S11: no clear article node falls back to bounded body text
test("S11: structure-less page falls back to non-empty body text", () => {
const page = extractReadable(fixture("no-article-capture.json"));
assert.ok(page.text.length > 0);
diff --git a/test/deep-research/serp-parser-test.ts b/test/deep-research/serp-parser-test.ts
index af5ac54..433c650 100644
--- a/test/deep-research/serp-parser-test.ts
+++ b/test/deep-research/serp-parser-test.ts
@@ -1,8 +1,3 @@
-/**
- * Unit tests for the pure Google SERP parser — no browser required.
- *
- * Run: npm test
- */
import { test, describe } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
@@ -17,7 +12,6 @@ const captcha = fixture("google-serp-captcha.json");
const empty = fixture("google-serp-empty.json");
describe("pure Google SERP parser", () => {
- // S1: redirect unwrapping + internal/javascript anchors dropped
test("S1: /url?q= redirect is unwrapped to the real target", () => {
const results = parseGoogleSerp(ok.anchors, 10);
const urls = results.map((r) => r.url);
@@ -36,7 +30,6 @@ describe("pure Google SERP parser", () => {
assert.ok(!urls.some((u) => u.startsWith("javascript:")));
});
- // S2: dedupe + rank + limit
test("S2: trailing-slash duplicate collapses to one result", () => {
const results = parseGoogleSerp(ok.anchors, 10);
const realExampleCount = results.filter((r) => r.url.startsWith("https://real.example/x")).length;
@@ -53,7 +46,6 @@ describe("pure Google SERP parser", () => {
assert.ok(capped.length === 2 && capped[capped.length - 1]?.rank === 2);
});
- // S3: CAPTCHA / no-results classification
test("S3: populated SERP classifies as ok", () => {
assert.equal(classifySerp(ok.pageText, parseGoogleSerp(ok.anchors, 10).length), "ok");
});
diff --git a/test/manual/bridge-test.ts b/test/manual/bridge-test.ts
index 5402aae..d247f97 100644
--- a/test/manual/bridge-test.ts
+++ b/test/manual/bridge-test.ts
@@ -1,8 +1,3 @@
-/**
- * Manual integration test for CdpBridge against real Chrome.
- * Requires: Chrome with remote debugging enabled (chrome://inspect/#remote-debugging).
- * Run: npx tsx test/manual/bridge-test.ts
- */
import { createCdpBridge, type SendToClient } from "../../src/daemon/bridge";
import type { WireRequest, WireResponse, WireEvent } from "../../src/daemon/protocol";
@@ -17,11 +12,8 @@ async function main() {
console.log("Starting CdpBridge...");
const bridge = createCdpBridge();
- // ── Test 1: Connect to Chrome ──────────────────────────────────────────
- // start() now runs auto-retry in the background. Wait a moment for
- // the initial connection attempt to complete.
await bridge.start();
- await sleep(3000); // allow time for WebSocket handshake
+ await sleep(3000);
if (!bridge.isAlive()) {
console.error("Cannot continue — Chrome not reachable. Run /browser-setup first.");
console.error("(The bridge will keep retrying in the background.)");
@@ -30,7 +22,6 @@ async function main() {
console.log(" ✓ Connect to Chrome: ok");
check(bridge.isAlive(), "Bridge reports alive after connect");
- // ── Test 2: Send Target.getTargets ─────────────────────────────────────
const responses: WireResponse[] = [];
const events: WireEvent[] = [];
let eventCount = 0;
@@ -55,7 +46,6 @@ async function main() {
console.log(` Found ${r.targetInfos?.length ?? 0} targets`);
}
- // ── Test 3: Create a new tab ───────────────────────────────────────────
responses.length = 0;
const req2: WireRequest = {
type: "request", id: 2, method: "Target.createTarget",
@@ -75,7 +65,6 @@ async function main() {
process.exit(1);
}
- // ── Test 4: Attach to target ───────────────────────────────────────────
responses.length = 0;
const req3: WireRequest = {
type: "request", id: 3, method: "Target.attachToTarget",
@@ -89,7 +78,6 @@ async function main() {
const sessionId = (res3?.result as any)?.sessionId as string | undefined;
check(typeof sessionId === "string", `Session ID: ${sessionId ?? "FAILED"}`);
- // Verify session ownership was tracked
if (sessionId) {
const owner = bridge.getSessionOwner(sessionId);
check(owner === "pi-test", "Session ownership tracked correctly");
@@ -100,7 +88,6 @@ async function main() {
process.exit(1);
}
- // ── Test 5: Navigate tab via session ───────────────────────────────────
responses.length = 0;
const req4: WireRequest = {
type: "request", id: 4, method: "Page.navigate",
@@ -115,7 +102,6 @@ async function main() {
const frameId = (res4?.result as any)?.frameId as string | undefined;
check(typeof frameId === "string", `Navigated to example.com (frame: ${frameId ?? "FAILED"})`);
- // ── Test 6: Evaluate JS in the page ────────────────────────────────────
responses.length = 0;
const req5: WireRequest = {
type: "request", id: 5, method: "Runtime.evaluate",
@@ -130,7 +116,6 @@ async function main() {
const title = (res5?.result as any)?.result?.value as string | undefined;
check(title === "Example Domain", `Page title: "${title ?? "FAILED"}" (expected "Example Domain")`);
- // ── Test 7: Detach from target ─────────────────────────────────────────
responses.length = 0;
const req6: WireRequest = {
type: "request", id: 6, method: "Target.detachFromTarget",
@@ -142,11 +127,9 @@ async function main() {
const res6 = responses.find(r => r.id === 6);
check(res6 != null, "Target.detachFromTarget response received");
- // Session ownership should be released
const ownerAfter = bridge.getSessionOwner(sessionId);
check(ownerAfter === undefined, "Session ownership released after detach");
- // ── Test 8: Close target ───────────────────────────────────────────────
responses.length = 0;
const req7: WireRequest = {
type: "request", id: 7, method: "Target.closeTarget",
@@ -158,12 +141,8 @@ async function main() {
const res7 = responses.find(r => r.id === 7);
check(res7 != null, "Target.closeTarget response received");
- // ── Test 9: Remove client ──────────────────────────────────────────────
bridge.removeClient("pi-test");
- // After removal, no pending requests should remain — verify by checking
- // no errors are thrown when handling subsequent events.
- // ── Cleanup ────────────────────────────────────────────────────────────
console.log("\nStopping bridge...");
await bridge.stop();
console.log("Bridge stopped ✓");
diff --git a/test/manual/daemon-transport-test.ts b/test/manual/daemon-transport-test.ts
index e9660c8..5738e23 100644
--- a/test/manual/daemon-transport-test.ts
+++ b/test/manual/daemon-transport-test.ts
@@ -1,10 +1,3 @@
-/**
- * Integration test: IpcServer + DaemonTransport end-to-end.
- * Starts a real IpcServer, connects a DaemonTransport, sends CDP requests.
- * No Chrome required — the server simulates CDP responses.
- *
- * Run: npx tsx test/manual/daemon-transport-test.ts
- */
import { createIpcServer } from "../../src/daemon/server";
import { createDaemonTransport } from "../../src/cdp/daemon-transport";
import { DAEMON_SOCKET_PATH } from "../../src/daemon/protocol";
@@ -19,17 +12,13 @@ const check = (cond: boolean, label: string) => {
};
async function main() {
- // Clean up any stale socket
try { unlinkSync(DAEMON_SOCKET_PATH); } catch {}
- // ── Start IpcServer ────────────────────────────────────────────────────
console.log("Starting IpcServer...");
const server = createIpcServer();
- // When a request arrives, reply with a fake CDP response
server.onMessage((msg, client) => {
if (msg.type === "request") {
- // Simulate a CDP response
const reply: WireMessage = {
type: "response",
id: msg.id,
@@ -42,15 +31,12 @@ async function main() {
await server.start();
console.log("Server started ✓");
- // ── Create DaemonTransport ─────────────────────────────────────────────
const transport = createDaemonTransport("pi-test-transport");
- // ── Test 1: Connect ────────────────────────────────────────────────────
const connResult = await transport.connect("");
check(connResult.success, `Connect: ${connResult.success ? "ok" : connResult.error.message}`);
check(transport.state() === "open", `State is "open" (actual: "${transport.state()}")`);
- // ── Test 2: Send a request ─────────────────────────────────────────────
const reqResult = await transport.request("Page.navigate", { url: "https://example.com" });
check(reqResult.success, `Request succeeded: ${reqResult.success}`);
if (reqResult.success) {
@@ -59,11 +45,9 @@ async function main() {
check(data?.method === "Page.navigate", "Response echoes method");
}
- // ── Test 3: Send request with sessionId ────────────────────────────────
const req2 = await transport.request("Runtime.evaluate", { expression: "1+1" }, { sessionId: "abc123" });
check(req2.success, "Request with sessionId succeeded");
- // ── Test 4: Events ─────────────────────────────────────────────────────
const eventPromise = new Promise<boolean>((resolve) => {
const timeout = setTimeout(() => resolve(false), 2000);
(async () => {
@@ -75,7 +59,6 @@ async function main() {
})();
});
- // Simulate server sending an event
server.broadcast({
type: "event",
method: "Target.targetCreated",
@@ -85,7 +68,6 @@ async function main() {
const gotEvent = await eventPromise;
check(gotEvent, "Event received via transport.events()");
- // ── Test 5: Close transport ────────────────────────────────────────────
let closeFired = false;
transport.onClose(() => { closeFired = true; });
@@ -93,16 +75,13 @@ async function main() {
check(transport.state() === "closed", "State is closed after close()");
check(closeFired, "onClose handler fired");
- // ── Test 6: Reconnect ──────────────────────────────────────────────────
- await new Promise(r => setTimeout(r, 500)); // let server process socket close
+ await new Promise(r => setTimeout(r, 500));
const reconnectResult = await transport.connect("");
check(reconnectResult.success, `Reconnect: ${reconnectResult.success ? "ok" : reconnectResult.error.message}`);
- // ── Test 7: Request after reconnect ────────────────────────────────────
const req3 = await transport.request("Browser.getVersion", {});
check(req3.success, "Request after reconnect succeeded");
- // ── Cleanup ────────────────────────────────────────────────────────────
await transport.close();
await server.stop();
console.log("Cleanup complete ✓");
diff --git a/test/manual/e2e-test.ts b/test/manual/e2e-test.ts
index 963efaf..659409f 100644
--- a/test/manual/e2e-test.ts
+++ b/test/manual/e2e-test.ts
@@ -1,11 +1,3 @@
-/**
- * End-to-end integration test: full daemon pipeline.
- *
- * Starts the daemon (IpcServer + CdpBridge), connects via DaemonTransport,
- * exercises the full CDP request/response/event lifecycle.
- *
- * Run: npx tsx test/manual/e2e-test.ts
- */
import { createIpcServer } from "../../src/daemon/server";
import { createCdpBridge, type SendToClient } from "../../src/daemon/bridge";
import { createDaemonTransport } from "../../src/cdp/daemon-transport";
@@ -21,15 +13,12 @@ const check = (cond: boolean, label: string) => {
};
async function main() {
- // Clean up stale socket
try { unlinkSync(DAEMON_SOCKET_PATH); } catch {}
- // ── Start daemon ───────────────────────────────────────────────────────
console.log("Starting daemon (IpcServer + CdpBridge)...");
const ipcServer = createIpcServer();
const cdpBridge = createCdpBridge();
- // Wire IpcServer ↔ CdpBridge
ipcServer.onMessage((msg, client) => {
if (msg.type !== "request") return;
const send: SendToClient = (cid, resp) => ipcServer.send(cid, resp);
@@ -45,10 +34,7 @@ async function main() {
await ipcServer.start();
console.log("Daemon started ✓");
- // The bridge will try to connect to Chrome — might fail, that's ok for
- // this test. We'll simulate CDP responses from the server side.
- // ── Connect pi client via DaemonTransport ──────────────────────────────
console.log("\nConnecting pi client (DaemonTransport)...");
const transport = createDaemonTransport("pi-e2e-test");
@@ -56,17 +42,11 @@ async function main() {
check(connectResult.success, `Client connect: ${connectResult.success ? "ok" : connectResult.error.message}`);
check(transport.state() === "open", "Transport state is open");
- // ── Test 1: Request → simulated CDP response ───────────────────────────
- // The IpcServer's onMessage handler forwards requests to the bridge.
- // The bridge tries to send to Chrome (which may be down). For this test,
- // we intercept at the IpcServer level and simulate CDP responses.
- // Override the message handler to simulate CDP responses directly.
let rawRequests: WireRequest[] = [];
ipcServer.onMessage((msg, client) => {
if (msg.type === "request") {
rawRequests.push(msg);
- // Simulate a CDP success response
ipcServer.send(client.id, {
type: "response",
id: msg.id,
@@ -85,13 +65,11 @@ async function main() {
check(rawRequests.length === 1, "Server received exactly 1 request");
check(rawRequests[0]!.method === "Page.navigate", "Correct method forwarded");
- // ── Test 2: Request with sessionId ─────────────────────────────────────
rawRequests.length = 0;
const res2 = await transport.request("Runtime.evaluate", { expression: "1+1" }, { sessionId: "session-abc" });
check(res2.success, "Session-scoped request succeeded");
check(rawRequests[0]?.sessionId === "session-abc", "SessionId forwarded correctly");
- // ── Test 3: Multiple parallel requests ─────────────────────────────────
rawRequests.length = 0;
const [r3a, r3b, r3c] = await Promise.all([
transport.request("Target.getTargets", {}),
@@ -101,7 +79,6 @@ async function main() {
check(r3a.success && r3b.success && r3c.success, "All 3 parallel requests succeeded");
check(rawRequests.length === 3, "Server received all 3 requests");
- // ── Test 4: Events ─────────────────────────────────────────────────────
const eventPromise = new Promise<string | null>((resolve) => {
const timeout = setTimeout(() => resolve(null), 2000);
(async () => {
@@ -113,7 +90,6 @@ async function main() {
})();
});
- // Simulate a CDP event broadcast from the daemon
ipcServer.broadcast({
type: "event",
method: "Target.targetCreated",
@@ -123,33 +99,24 @@ async function main() {
const eventMethod = await eventPromise;
check(eventMethod === "Target.targetCreated", `Event received: ${eventMethod ?? "TIMEOUT"}`);
- // ── Test 5: Session persistence (detach simulation) ────────────────────
- // Simulate a session shutdown: close the transport, then reconnect.
- // The transport.close() sends deregister, server cleans up.
await transport.close();
check(transport.state() === "closed", "Transport closed");
- // Small delay for server cleanup
await new Promise(r => setTimeout(r, 500));
- // Reconnect — simulates next pi session
const reconnectResult = await transport.connect("");
check(reconnectResult.success, "Transport reconnected (simulated session restart)");
- // Send a request through the reconnected transport
const res4 = await transport.request("Target.getTargets", {});
check(res4.success, "Request after reconnect succeeded");
check(transport.state() === "open", "Transport state is open after reconnect");
- // ── Test 6: Server-side client count ───────────────────────────────────
check(ipcServer.clientCount() === 1, "Server reports 1 connected client");
- // ── Test 7: Client disconnect cleanup ──────────────────────────────────
await transport.close();
await new Promise(r => setTimeout(r, 500));
check(ipcServer.clientCount() === 0, "Server reports 0 clients after disconnect");
- // ── Cleanup ────────────────────────────────────────────────────────────
await ipcServer.stop();
console.log("\nDaemon stopped ✓");
diff --git a/test/manual/forms-test.ts b/test/manual/forms-test.ts
index ca6c596..43b2863 100644
--- a/test/manual/forms-test.ts
+++ b/test/manual/forms-test.ts
@@ -1,15 +1,3 @@
-/**
- * Manual integration test for the form-filling tools.
- *
- * Exercises browser_snapshot (ref surfacing) + browser_fill_form / browser_fill /
- * browser_select_option / browser_set_checked against a data:-URL fixture that
- * includes a plain input, a textarea, a "controlled" input (commits its model
- * only on the `input` event — the realistic failure mode for naive value-setting),
- * a native <select>, a checkbox, and a contenteditable.
- *
- * Requires the daemon running + a real Chrome (same as live-daemon-test.ts).
- * Run: npx tsx test/manual/forms-test.ts
- */
import { createDaemonTransport } from "../../src/cdp/daemon-transport";
import { createBrowserClient } from "../../src/client";
import { fillTool, selectOptionTool, setCheckedTool } from "../../src/domains/form";
@@ -22,7 +10,6 @@ const check = (cond: boolean, label: string): void => {
else { failed++; console.error(` ✗ ${label}`); }
};
-// Minimal handler context — the form/snapshot handlers only read `client`.
const mkCtx = (client: any): any => ({
client,
signal: undefined,
@@ -89,7 +76,6 @@ async function main(): Promise<void> {
const url = "data:text/html," + encodeURIComponent(FIXTURE);
const tab = await client.newTab(url);
check(tab.success, `Opened fixture tab: ${tab.success ? "ok" : tab.error.message}`);
- // Give the inline script a beat to wire up listeners.
await new Promise((r) => setTimeout(r, 300));
const byName = await publishRefs(client, ["plain", "ta", "controlled", "country", "agree", "bio"]);
@@ -103,7 +89,6 @@ async function main(): Promise<void> {
};
for (const [k, v] of Object.entries(refs)) check(typeof v === "string", `ref present for ${k} (${v})`);
- // ── Batch fill text fields ──────────────────────────────────────────────────
const fill = await fillFormTool.handler({
fields: [
{ ref: refs.plain!, value: "hello plain" },
@@ -115,31 +100,25 @@ async function main(): Promise<void> {
check(fill.success, "browser_fill_form succeeded");
if (fill.success) console.log(" " + fill.data.text);
- // ── Select + checkbox ───────────────────────────────────────────────────────
const sel = await selectOptionTool.handler({ ref: refs.sel!, label: "Canada" }, ctx);
check(sel.success, `browser_select_option by label: ${sel.success ? sel.data.text : (sel as any).error.message}`);
const chk = await setCheckedTool.handler({ ref: refs.chk!, checked: true }, ctx);
check(chk.success, `browser_set_checked: ${chk.success ? chk.data.text : (chk as any).error.message}`);
- // ── Verify committed state via JS reads ─────────────────────────────────────
const read = async (expr: string): Promise<unknown> => {
const r = await client.evaluateJs(expr);
return r.success ? r.data : `ERR:${r.error.message}`;
};
check((await read("document.getElementById('plain').value")) === "hello plain", "plain input value committed");
check((await read("document.getElementById('ta').value")) === "multi\nline", "textarea value committed");
- // The controlled input's MODEL (input-event-driven) must reflect the fill —
- // proves browser_fill fired the input event, not just set the DOM value.
check((await read("window.__model()")) === "ctrl-value", "controlled input model committed (input event fired)");
check((await read("document.getElementById('editable').textContent")) === "my bio", "contenteditable text committed");
check((await read("document.getElementById('sel').value")) === "ca", "select value committed");
check((await read("document.getElementById('chk').checked")) === true, "checkbox checked committed");
- // ── No-matching-option path ─────────────────────────────────────────────────
const badSel = await selectOptionTool.handler({ ref: refs.sel!, label: "Atlantis" }, ctx);
check(!badSel.success && (badSel as any).error.kind === "invalid_state", "select with no match → invalid_state error");
- // ── Stale ref path ──────────────────────────────────────────────────────────
const staleRef = refs.plain!;
const nav = await client.session().call("Page.navigate", { url: "data:text/html," + encodeURIComponent("<body>gone</body>") });
check(nav.success, "navigated away to invalidate refs");
diff --git a/test/manual/ipc-server-test.ts b/test/manual/ipc-server-test.ts
index b4da3df..1a67f1a 100644
--- a/test/manual/ipc-server-test.ts
+++ b/test/manual/ipc-server-test.ts
@@ -1,8 +1,3 @@
-/**
- * Manual smoke test for IpcServer.
- * Starts a server, connects a client, tests registration, send, broadcast, disconnect.
- * Run: npx tsx test/manual/ipc-server-test.ts
- */
import { createConnection } from "node:net";
import { createInterface } from "node:readline";
import { createIpcServer } from "../../src/daemon/server";
@@ -19,7 +14,6 @@ async function main() {
else { failed++; console.error(` ✗ ${label}`); }
};
- // ── Start server ──────────────────────────────────────────────────────
console.log("Starting IpcServer...");
const server = createIpcServer();
@@ -34,21 +28,18 @@ async function main() {
await server.start();
console.log("Server started ✓");
- // ── Connect client ────────────────────────────────────────────────────
console.log("\nConnecting client...");
const socket = createConnection(DAEMON_SOCKET_PATH);
const rl = createInterface({ input: socket, crlfDelay: Infinity });
const receivedByClient: WireMessage[] = [];
rl.on("line", (line) => {
- try { receivedByClient.push(JSON.parse(line)); } catch { /* ignore */ }
+ try { receivedByClient.push(JSON.parse(line)); } catch {}
});
await new Promise<void>((resolve) => socket.on("connect", resolve));
console.log("Client connected ✓");
- // ── Test 1: Registration ──────────────────────────────────────────────
- // Send register, expect registered ack.
socket.write(JSON.stringify({ type: "control", action: "register", clientId: TEST_CLIENT_ID }) + "\n");
await sleep(200);
@@ -56,7 +47,6 @@ async function main() {
check(ack != null, "Registration acknowledged");
check(connectCount === 1, "Connect handler fired");
- // ── Test 2: Send message to server ────────────────────────────────────
receivedByServer.length = 0;
const testRequest = { type: "request", id: 1, method: "Page.navigate", params: { url: "https://example.com" } };
socket.write(JSON.stringify(testRequest) + "\n");
@@ -66,7 +56,6 @@ async function main() {
check(received != null, "Server received request from client");
check((received as any)?.method === "Page.navigate", "Method matches");
- // ── Test 3: Server → client send ──────────────────────────────────────
receivedByClient.length = 0;
server.send(TEST_CLIENT_ID, { type: "response", id: 1, result: { frameId: "xyz" } });
await sleep(200);
@@ -74,7 +63,6 @@ async function main() {
const response = receivedByClient.find((m: any) => m.type === "response" && m.id === 1);
check(response != null, "Client received response from server");
- // ── Test 4: Broadcast ─────────────────────────────────────────────────
receivedByClient.length = 0;
const broadcastMsg: WireEvent = {
type: "event",
@@ -88,14 +76,12 @@ async function main() {
const bc = receivedByClient[0] as any;
check(bc?.method === "Target.targetCreated", "Broadcast content matches");
- // ── Test 5: Client disconnect ─────────────────────────────────────────
socket.destroy();
await sleep(500);
check(disconnectCount === 1, "Disconnect handler fired");
check(server.clientCount() === 0, "Client count is 0 after disconnect");
- // ── Test 6: Re-registration (same clientId after disconnect) ──────────
const socket2 = createConnection(DAEMON_SOCKET_PATH);
await new Promise<void>((resolve) => socket2.on("connect", resolve));
@@ -106,12 +92,10 @@ async function main() {
socket2.on("error", () => {}); // suppress EPIPE from server-side cleanup
await sleep(300);
- // ── Test 7: Max clients enforcement ───────────────────────────────────
- // Connect max clients, then one more should be rejected.
const testSockets: ReturnType<typeof createConnection>[] = [];
for (let i = 0; i < 20; i++) {
const s = createConnection(DAEMON_SOCKET_PATH);
- s.on("error", () => {}); // server may reject and destroy this socket
+ s.on("error", () => {});
testSockets.push(s);
await new Promise<void>((resolve) => s.on("connect", resolve));
try { s.write(JSON.stringify({ type: "control", action: "register", clientId: `pi-test-max-${i}` }) + "\n"); } catch {}
@@ -119,13 +103,11 @@ async function main() {
}
await sleep(300);
check(server.clientCount() <= 16, "Max client enforcement works");
- // Clean up test sockets (some already destroyed by server rejection)
for (const s of testSockets) {
try { s.destroy(); } catch {}
s.removeAllListeners();
}
- // ── Cleanup ───────────────────────────────────────────────────────────
console.log(`\nCleaning up...`);
await server.stop();
console.log("Server stopped ✓");
diff --git a/test/manual/profile-concurrency-test.ts b/test/manual/profile-concurrency-test.ts
index ab792c7..05e06bc 100644
--- a/test/manual/profile-concurrency-test.ts
+++ b/test/manual/profile-concurrency-test.ts
@@ -1,20 +1,3 @@
-/**
- * Concurrency regression for harness tab creation, against a real browser.
- *
- * browser_read_page is registered unserialized, so several isolated tabs can be
- * opened at the same moment. Two risks live here:
- *
- * 1. Each concurrent caller finding "no harness window" and creating its own
- * — two blank windows before profiles existed, two LAUNCHED profile windows
- * after. ensureHarnessWindow single-flights per client to prevent it.
- * 2. Callers sharing that single creation and all claiming the same seed tab.
- * Only the caller that started the creation may claim it; the rest must
- * open their own tab beside it.
- *
- * Runs pinned and unpinned inside a throwaway --user-data-dir.
- *
- * Run: npx tsx test/manual/profile-concurrency-test.ts
- */
import { spawn, execFileSync } from "node:child_process";
import { existsSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
@@ -62,7 +45,6 @@ const killTree = (pid: number | undefined): void => {
try {
execFileSync("taskkill.exe", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" });
} catch {
- // already gone
}
return;
}
@@ -72,7 +54,6 @@ const killTree = (pid: number | undefined): void => {
try {
process.kill(pid, "SIGKILL");
} catch {
- // already gone
}
}
};
@@ -101,7 +82,6 @@ async function scenario(exePath: string, label: string, pinned: boolean): Promis
const res = await fetch(`http://127.0.0.1:${PORT}/json/version`);
if (res.ok) endpoint = ((await res.json()) as { webSocketDebuggerUrl: string }).webSocketDebuggerUrl;
} catch {
- // not up yet
}
}
if (!endpoint) {
@@ -110,7 +90,6 @@ async function scenario(exePath: string, label: string, pinned: boolean): Promis
}
if (pinned) {
- // Materialise the profile the run pins to.
spawn(exePath, [...base, "--profile-directory=Profile 2", "about:blank"], {
detached: true,
stdio: "ignore",
@@ -166,7 +145,6 @@ async function scenario(exePath: string, label: string, pinned: boolean): Promis
try {
rmSync(udd, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 });
} catch {
- // a locked temp dir is not worth failing over
}
}
}
diff --git a/test/manual/profile-e2e-test.ts b/test/manual/profile-e2e-test.ts
index 3ac3ac2..6981b7a 100644
--- a/test/manual/profile-e2e-test.ts
+++ b/test/manual/profile-e2e-test.ts
@@ -1,22 +1,3 @@
-/**
- * End-to-end proof of profile pinning, against a REAL browser.
- *
- * Runs entirely inside a throwaway --user-data-dir, so the user's own browser
- * and profiles are never touched. Designed to run on Linux (under xvfb-run),
- * macOS, and Windows — the three OSes where the launch handshake relies on
- * Chromium's ProcessSingleton handing our argv to the running browser.
- *
- * What it asserts:
- * 1. two profiles in one browser enumerate from Local State
- * 2. they surface as DISTINCT browserContextIds over one CDP endpoint
- * 3. a sentinel launch opens a window in the requested profile and is
- * identifiable
- * 4. tabs spawned afterwards stay in that profile AND that window
- * 5. a spawned tab is attachable, navigable, and closable
- *
- * Run: npx tsx test/manual/profile-e2e-test.ts
- * CHROME_BIN=/path/to/chrome npx tsx test/manual/profile-e2e-test.ts
- */
import { spawn, execFileSync } from "node:child_process";
import { mkdtempSync, rmSync, existsSync } from "node:fs";
import { tmpdir } from "node:os";
@@ -39,7 +20,6 @@ const check = (cond: boolean, label: string): void => {
}
};
-/** Locate a Chrome/Chromium binary for the host OS. */
function findBrowser(): string | undefined {
const fromEnv = process.env["CHROME_BIN"] ?? process.env["CHROME_PATH"];
if (fromEnv && existsSync(fromEnv)) return fromEnv;
@@ -68,7 +48,6 @@ function findBrowser(): string | undefined {
const resolved = execFileSync("which", [name], { encoding: "utf8" }).trim();
if (resolved) return resolved;
} catch {
- // not on PATH
}
}
}
@@ -118,7 +97,6 @@ async function waitForEndpoint(deadlineMs = 30_000): Promise<boolean> {
const res = await fetch(`http://127.0.0.1:${PORT}/json/version`);
if (res.ok) return true;
} catch {
- // not up yet
}
await sleep(500);
}
@@ -150,31 +128,26 @@ async function main(): Promise<void> {
browser.unref();
const cleanup = (): void => {
- // Chrome spawns a tree of child processes. Leaving any alive keeps the
- // user-data-dir locked, which on Windows also blocks the directory removal.
+ // Any surviving child keeps the user-data-dir locked, which on Windows also blocks the directory removal.
if (process.platform === "win32") {
try {
execFileSync("taskkill.exe", ["/pid", String(browser.pid), "/T", "/F"], { stdio: "ignore" });
} catch {
- // already gone
}
} else {
try {
- // detached:true put the browser in its own process group; the negative
- // pid signals the whole group.
+ // detached:true put the browser in its own process group, so the negative pid signals the whole group.
process.kill(-browser.pid!, "SIGKILL");
} catch {
try {
browser.kill("SIGKILL");
} catch {
- // already gone
}
}
}
try {
rmSync(udd, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 });
} catch {
- // A locked file in a temp dir is not worth failing the run over.
}
};
@@ -185,9 +158,6 @@ async function main(): Promise<void> {
process.exit(1);
}
- // Create a SECOND profile in the same browser by launching into it. The
- // running instance handles this via ProcessSingleton — the very behaviour
- // profile pinning depends on.
const second = spawn(exePath, [...baseArgs, "--profile-directory=Profile 2", "about:blank"], {
detached: true,
stdio: "ignore",
@@ -201,8 +171,7 @@ async function main(): Promise<void> {
const windowOf = async (targetId: string): Promise<number | undefined> =>
(await cdp.call("Browser.getWindowForTarget", { targetId })).result?.windowId;
- // 1. enumeration. Chrome flushes Local State asynchronously — a profile
- // created seconds ago is not registered yet — so poll rather than read once.
+ // Chrome flushes Local State asynchronously, so a just-created profile is not registered yet — poll rather than read once.
let profiles = await listProfiles(udd);
const enumDeadline = Date.now() + 30_000;
while (Date.now() < enumDeadline && profiles.length < 2) {
@@ -217,11 +186,9 @@ async function main(): Promise<void> {
"E1: every profile has a 'Name (…)' label",
);
- // 2. distinct browser contexts
const contexts = new Set((await pageTargets()).map((t) => t.browserContextId));
check(contexts.size >= 2, `E2: profiles surface as distinct browserContextIds (${contexts.size} seen)`);
- // 3. sentinel launch into a chosen profile
const launched = await openProfileWindow({ exePath, profileDir: "Profile 2", explicitUserDataDir: udd });
check(launched.success, "E3: profile window launch dispatched");
if (!launched.success) throw new Error(launched.error);
@@ -240,7 +207,6 @@ async function main(): Promise<void> {
const seedContext = seed.browserContextId;
check(typeof seedContext === "string" && seedContext.length > 0, "E3: seed carries the profile's browserContextId");
- // 4. + 5. spawn a tab the way the target factory does
const attached = await cdp.call("Target.attachToTarget", { targetId: seed.targetId, flatten: true });
const sessionId = attached.result?.sessionId;
check(!!sessionId, "E4: seed page in a non-default profile is attachable");
diff --git a/test/manual/socket-guard-test.ts b/test/manual/socket-guard-test.ts
index ab519f9..1b6c4d7 100644
--- a/test/manual/socket-guard-test.ts
+++ b/test/manual/socket-guard-test.ts
@@ -1,6 +1,3 @@
-/**
- * Test: socket guard produces clear error when daemon not running.
- */
import { access } from "node:fs/promises";
import { setTimeout as sleep } from "node:timers/promises";
@@ -12,7 +9,6 @@ import { DAEMON_SOCKET_PATH } from "../../src/daemon/protocol";
async function main() {
const sock = DAEMON_SOCKET_PATH;
- // Simulate no daemon
try {
await access(sock);
console.log("FAIL: Socket exists unexpectedly");
@@ -22,13 +18,11 @@ async function main() {
console.log(' "Browser harness not initialized. Run /browser-setup first"');
}
- // Start daemon
const daemonEntry = join(dirname(fileURLToPath(import.meta.url)), "../../src/daemon/index.ts");
const proc = spawn("npx", ["tsx", daemonEntry], { detached: true, stdio: "ignore" });
proc.unref();
await sleep(2000);
- // Now socket should exist
try {
await access(sock);
console.log("✓ Socket exists after daemon spawn — tool wrapper proceeds silently");
@@ -37,7 +31,6 @@ async function main() {
process.exit(1);
}
- // Cleanup
proc.kill();
console.log("✓ All socket guard checks passed");
}
diff --git a/test/manual/window-binding-test.ts b/test/manual/window-binding-test.ts
index d743ae5..cad526a 100644
--- a/test/manual/window-binding-test.ts
+++ b/test/manual/window-binding-test.ts
@@ -1,15 +1,3 @@
-/**
- * Unit test (mock transport) for window-scoped session binding + tab teardown.
- *
- * Covers the two fixes:
- * 1. The session binds to a real Chrome windowId. New tabs land in that window;
- * tabs opened by an owned page (popups / window.open) are auto-adopted so the
- * session controls them; nothing outside the window is ever owned.
- * 2. closeOwnedTabs() closes every owned tab and clears the window binding — the
- * teardown the extension runs on session shutdown so no stale tabs survive.
- *
- * Run: npx tsx test/manual/window-binding-test.ts
- */
import { createBrowserClient } from "../../src/client";
import { createOwnershipRegistry } from "../../src/cdp/ownership";
import type { CdpTransport } from "../../src/cdp/transport";
@@ -24,7 +12,6 @@ const check = (cond: boolean, label: string): void => {
else { failed++; console.error(` ✗ ${label}`); }
};
-// ── A minimal in-memory CDP browser the mock transport drives. ──────────────
type FakeTarget = { targetId: string; type: string; url: string; title: string; windowId: number; openerId?: string };
const createMockTransport = (): CdpTransport & {
@@ -38,7 +25,6 @@ const createMockTransport = (): CdpTransport & {
let targetSeq = 0;
let windowSeq = 100;
- // Event queue plumbing so session.ts's consumeEvents() can await targetCreated.
const listeners: Array<() => void> = [];
let buffer: CdpEvent[] = [];
let waiter: (() => void) | null = null;
@@ -83,7 +69,6 @@ const createMockTransport = (): CdpTransport & {
const windowId = newWindow || !opener ? ++windowSeq : (targets.get(opener)?.windowId ?? ++windowSeq);
const t: FakeTarget = { targetId: id, type: "page", url: (params["url"] as string) ?? "about:blank", title: "", windowId, ...(opener ? { openerId: opener } : {}) };
targets.set(id, t);
- // Chrome fires targetCreated for every new target (discover:true).
push({ method: "Target.targetCreated", params: { targetInfo: { targetId: id, type: "page", url: t.url, openerId: opener } } });
return ok({ targetId: id });
}
@@ -123,7 +108,6 @@ const tick = (): Promise<void> => new Promise((r) => setTimeout(r, 20));
async function main(): Promise<void> {
console.log("window-binding-test\n");
- // ── Registry: windowId binding is stored + cleared. ───────────────────────
{
const reg = createOwnershipRegistry();
check(reg.harnessWindowId() === undefined, "windowId starts undefined");
@@ -137,7 +121,6 @@ async function main(): Promise<void> {
check(reg.harnessWindowId() === undefined && notified === 1, "clearing windowId notifies");
}
- // ── newTab: creates the dedicated window and captures its real windowId. ──
{
const transport = createMockTransport();
const client = createBrowserClient({ namespace: "t", transport });
@@ -154,16 +137,12 @@ async function main(): Promise<void> {
}
}
- // ── Popup adoption: a tab opened by an owned page is auto-owned. ──────────
{
const transport = createMockTransport();
const client = createBrowserClient({ namespace: "t", transport });
await client.start();
const cur = client.current()!;
- // A page the session controls opens a child target (window.open / _blank).
- // Chrome emits Target.targetCreated with openerId = our owned tab; the
- // session must adopt it so the tab is controllable and cleaned up later.
const child = await client.session().callBrowser("Target.createTarget", { url: "https://child.test", openerId: cur.targetId });
await tick();
check(child.success, "owned page opens a child target");
@@ -172,7 +151,6 @@ async function main(): Promise<void> {
check(client.owns(id), "child target opened by an owned tab is auto-adopted");
}
- // A target the user opens in their own window has no owned opener → never adopted.
const stranger = await client.session().callBrowser("Target.createTarget", { url: "https://user.test", newWindow: true });
await tick();
if (stranger.success) {
@@ -181,7 +159,6 @@ async function main(): Promise<void> {
}
}
- // ── Reattach self-heal: re-adopt live window-mates of a surviving owned tab. ─
{
const transport = createMockTransport();
transport.seed({ targetId: "own-1", type: "page", url: "https://a.test", title: "", windowId: 500 });
@@ -198,11 +175,8 @@ async function main(): Promise<void> {
check(!client.owns("user-9"), "reattach: a tab in a DIFFERENT window is never adopted");
}
- // ── Restart-collision guard: stale windowId must not adopt the user's tabs. ─
{
const transport = createMockTransport();
- // Chrome restarted: our owned tab id is dead; windowId 500 now belongs to
- // the user's own window full of their tabs.
transport.seed({ targetId: "user-a", type: "page", url: "https://user-a.test", title: "", windowId: 500 });
transport.seed({ targetId: "user-b", type: "page", url: "https://user-b.test", title: "", windowId: 500 });
const client = createBrowserClient({
@@ -216,7 +190,6 @@ async function main(): Promise<void> {
check(client.current() !== null && !["user-a", "user-b"].includes(client.current()!.targetId), "restart: attaches to a fresh harness tab, not a user tab");
}
- // ── Teardown: closeOwnedTabs closes every owned tab + clears binding. ─────
{
const transport = createMockTransport();
const client = createBrowserClient({ namespace: "t", transport });
diff --git a/test/profile/browser-process-test.ts b/test/profile/browser-process-test.ts
index 7a86c74..efcb9c8 100644
--- a/test/profile/browser-process-test.ts
+++ b/test/profile/browser-process-test.ts
@@ -1,14 +1,3 @@
-/**
- * Unit tests for browser-process parsing and candidate ranking — no browser.
- *
- * Ranking exists because a machine can run several Chromium browsers at once.
- * Picking the wrong one means launching a profile window into a browser the
- * harness is not connected to: the agent sees nothing, and a browser the user
- * never pointed us at gets a new window. (That is exactly what happened during
- * development before ranking existed.)
- *
- * Run: npm test
- */
import { test, before, after, describe } from "node:test";
import assert from "node:assert/strict";
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
@@ -23,7 +12,6 @@ import {
} from "../../src/profile/browser-process";
describe("browser-process parsing and candidate ranking", () => {
- // B1: --user-data-dir parsing
test("B1: bare value parsed", () => {
assert.equal(parseUserDataDirFlag("/opt/google/chrome/chrome --user-data-dir=/tmp/x --foo"), "/tmp/x");
});
@@ -47,7 +35,6 @@ describe("browser-process parsing and candidate ranking", () => {
assert.equal(parseUserDataDirFlag("chrome --user-data-dir="), undefined);
});
- // B2: an explicit --user-data-dir is decisive in both directions
test("B2: exact user-data-dir match outranks a flagless process", () => {
const target = "/tmp/pi-e2e-abc";
const match = rankBrowserCandidate({ exePath: "/opt/google/chrome/chrome", explicitUserDataDir: target }, target);
@@ -74,7 +61,6 @@ describe("browser-process parsing and candidate ranking", () => {
assert.equal(other, 0);
});
- // B3: browser family match decides between flagless processes
test("B3: Brave wins for a Brave user-data-dir", () => {
const braveDir = join(homedir(), ".config/BraveSoftware/Brave-Browser");
const brave = rankBrowserCandidate({ exePath: "/usr/bin/brave-browser" }, braveDir);
@@ -98,7 +84,6 @@ describe("browser-process parsing and candidate ranking", () => {
);
});
- // B4: no preference expressed → every candidate is equally acceptable
test("B4: without a target dir, candidates rank equally", () => {
assert.equal(
rankBrowserCandidate({ exePath: "/usr/bin/brave-browser" }),
@@ -106,7 +91,6 @@ describe("browser-process parsing and candidate ranking", () => {
);
});
- // B5: Windows path comparison ignores case and separator style
test("B5 win: case and separator differences still match", { skip: process.platform !== "win32" }, () => {
assert.equal(
rankBrowserCandidate(
@@ -120,9 +104,6 @@ describe("browser-process parsing and candidate ranking", () => {
);
});
- // B6: Linux's deleted-binary marker. An in-place `google-chrome` upgrade
- // unlinks the running binary, after which /proc/<pid>/exe reads
- // "…/chrome (deleted)" — a path spawn() rejects with ENOENT.
test("B6: marker stripped from a replaced binary's path", () => {
assert.equal(stripDeletedSuffix("/opt/google/chrome/chrome (deleted)"), "/opt/google/chrome/chrome");
});
@@ -135,7 +116,6 @@ describe("browser-process parsing and candidate ranking", () => {
assert.equal(stripDeletedSuffix(" (deleted)"), undefined);
});
- // B7: only a path that is actually on disk may be handed on as spawnable.
describe("B7", () => {
let root: string;
let real: string;
@@ -168,9 +148,6 @@ describe("browser-process parsing and candidate ranking", () => {
assert.equal(await spawnableExePath(""), undefined);
});
- // The launch gate must never pass on a path it cannot spawn — that is
- // what produced a 15s "couldn't open a window" timeout instead of a
- // named error.
test("B7: resolveBrowserExecutable repairs a replaced-binary path", async () => {
assert.equal(await resolveBrowserExecutable({ running: true, exePath: `${real} (deleted)` }), real);
});
@@ -183,8 +160,7 @@ describe("browser-process parsing and candidate ranking", () => {
},
);
- // A binary genuinely named "… (deleted)" must win over the stripped form.
- // Created last, so the checks above see only the ordinary case.
+ // A binary genuinely named "… (deleted)" must win over the stripped form; created last so the checks above see only the ordinary case.
test("B7: a real file named '… (deleted)' is preferred over stripping", async () => {
literal = join(root, "chrome (deleted)");
writeFileSync(literal, "#!/bin/sh\n");
diff --git a/test/profile/launch-test.ts b/test/profile/launch-test.ts
index 70ca8af..3883f98 100644
--- a/test/profile/launch-test.ts
+++ b/test/profile/launch-test.ts
@@ -1,14 +1,3 @@
-/**
- * Unit tests for openProfileWindow's failure reporting — no browser required.
- *
- * spawn() reports a missing or non-executable binary asynchronously, as an
- * 'error' event, not by throwing. When that event was ignored, a bad executable
- * path returned `ok` and the caller's 15s sentinel poll blamed the browser:
- * "couldn't open a window in <profile> automatically". These tests pin the
- * failure to its real cause instead.
- *
- * Run: npm test
- */
import { test, before, after, describe } from "node:test";
import assert from "node:assert/strict";
import { existsSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
@@ -28,8 +17,6 @@ describe("openProfileWindow's failure reporting", () => {
rmSync(root, { recursive: true, force: true });
});
- // L1: a nonexistent binary — the shape of an in-place browser upgrade,
- // where /proc/<pid>/exe still names the unlinked file.
describe("L1", () => {
const missing = (): string => join(root, "chrome (deleted)");
@@ -51,8 +38,6 @@ describe("openProfileWindow's failure reporting", () => {
});
});
- // L2: a file that exists but is not executable — same async 'error' path,
- // different errno (EACCES). Skipped where the check is meaningless.
test(
"L2: a non-executable file fails the launch",
{ skip: process.platform === "win32" || process.getuid?.() === 0 },
@@ -64,8 +49,6 @@ describe("openProfileWindow's failure reporting", () => {
},
);
- // L3: a failed launch leaves no sentinel file behind. The caller only calls
- // cleanup() on the success path, so the failure path owns its own tidying.
describe("L3", () => {
let before_: string | undefined;
@@ -91,8 +74,6 @@ describe("openProfileWindow's failure reporting", () => {
});
});
- // L4: a launcher that starts succeeds, and the sentinel it advertises is a
- // real, readable file — the page the browser is asked to open.
describe("L4", () => {
let r: Awaited<ReturnType<typeof openProfileWindow>>;
diff --git a/test/profile/list-test.ts b/test/profile/list-test.ts
index b4397e3..f1db39c 100644
--- a/test/profile/list-test.ts
+++ b/test/profile/list-test.ts
@@ -1,11 +1,3 @@
-/**
- * Unit tests for profile enumeration — no browser required.
- *
- * Builds throwaway user-data-dirs on disk so both tiers (Local State and the
- * Preferences scan) run against real files.
- *
- * Run: npm test
- */
import { test, after, describe } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
@@ -31,7 +23,6 @@ describe("profile enumeration", () => {
for (const root of roots) rmSync(root, { recursive: true, force: true });
});
- // L1: tier 1 — names, emails, ordering, last-used marker
const makeL1Udd = (): string => {
const udd = makeUdd();
writeLocalState(udd, {
@@ -73,7 +64,6 @@ describe("profile enumeration", () => {
assert.equal(profiles[0]?.lastUsed, false);
});
- // L2: profile with no signed-in account falls back to the directory
const makeL2Udd = (): string => {
const udd = makeUdd();
writeLocalState(udd, { profile: { info_cache: { "Profile 3": { name: "Work" } } } });
@@ -90,7 +80,6 @@ describe("profile enumeration", () => {
assert.equal(profiles[0]?.email, undefined);
});
- // L3: gaia_name backfills a missing name; ephemeral profiles are skipped
const makeL3Udd = (): string => {
const udd = makeUdd();
writeLocalState(udd, {
@@ -115,7 +104,6 @@ describe("profile enumeration", () => {
assert.equal(profiles[0]?.label, "Aman Kumar (gaia@example.com)");
});
- // L4: unlisted profiles are appended alphabetically after profiles_order
test("L4: ordered entries first, remainder alphabetical", async () => {
const udd = makeUdd();
writeLocalState(udd, {
@@ -132,7 +120,6 @@ describe("profile enumeration", () => {
assert.equal(profiles.map((p) => p.dir).join(","), "Profile 9,Default,Profile 3");
});
- // L5: corrupt Local State falls through to the Preferences scan
const makeL5Udd = (): string => {
const udd = makeUdd();
writeLocalState(udd, "{ this is not json");
@@ -140,7 +127,7 @@ describe("profile enumeration", () => {
profile: { name: "Fallback Name" },
account_info: [{ email: "fallback@example.com" }],
});
- writeProfileDir(udd, "Crash Reports"); // no Preferences → not a profile
+ writeProfileDir(udd, "Crash Reports");
return udd;
};
@@ -154,7 +141,6 @@ describe("profile enumeration", () => {
assert.equal(profiles[0]?.label, "Fallback Name (fallback@example.com)");
});
- // L6: missing info_cache also falls through to tier 2
test("L6: missing info_cache → Preferences scan", async () => {
const udd = makeUdd();
writeLocalState(udd, { profile: { last_used: "Default" } });
@@ -163,13 +149,11 @@ describe("profile enumeration", () => {
assert.equal(profiles[0]?.label, "Tier Two (Profile 1)");
});
- // L7: nothing readable → empty list, never a throw
test("L7: unreadable user-data-dir yields an empty list", async () => {
const profiles = await listProfiles(join(tmpdir(), "pi-profile-does-not-exist-xyz"));
assert.equal(profiles.length, 0);
});
- // L8: label formatting is total
test("L8: email wins the brackets", () => {
assert.equal(formatProfileLabel("A", "a@b.c", "Default"), "A (a@b.c)");
});
diff --git a/test/profile/paths-test.ts b/test/profile/paths-test.ts
index feaf7ae..1278446 100644
--- a/test/profile/paths-test.ts
+++ b/test/profile/paths-test.ts
@@ -1,17 +1,8 @@
-/**
- * Unit tests for path resolution — runs on Linux, macOS, and Windows in CI.
- *
- * Only the branch for the host platform can be exercised, so each assertion is
- * guarded by process.platform; the CI matrix is what gives all three coverage.
- *
- * Run: npm test
- */
import { test, describe } from "node:test";
import assert from "node:assert/strict";
import { homedir } from "node:os";
import { join } from "node:path";
-/** Re-import with a fresh module identity so env changes are picked up. */
const freshImport = async (): Promise<typeof import("../../src/profile/paths")> =>
import(`../../src/profile/paths?cache=${Math.random()}`);
@@ -28,7 +19,6 @@ const withEnv = async (key: string, value: string | undefined, fn: () => Promise
};
describe("path resolution", () => {
- // A1: agent dir default and override
test("A1: default agent dir is ~/.pi/agent", async () => {
await withEnv("PI_CODING_AGENT_DIR", undefined, async () => {
const { agentDir } = await freshImport();
@@ -43,8 +33,6 @@ describe("path resolution", () => {
});
});
- // pi's own expandTildePath only understands "~/" — never "~\" — so the agent
- // dir must resolve identically or the pin lands where pi does not look.
test("A1: '~/' in the agent-dir override expands like pi", async () => {
await withEnv("PI_CODING_AGENT_DIR", "~/custom-agent", async () => {
assert.equal((await freshImport()).agentDir(), `${homedir()}/custom-agent`);
@@ -57,7 +45,6 @@ describe("path resolution", () => {
});
});
- // A2: platform candidate list
test("A2: candidates are non-empty on this platform", async () => {
await withEnv("CHROME_USER_DATA_DIR", undefined, async () => {
const dirs = (await freshImport()).userDataDirCandidates();
@@ -133,7 +120,6 @@ describe("path resolution", () => {
});
});
- // A3: %LOCALAPPDATA% redirection is honoured (Windows-only behaviour)
test("A3 win: redirected LOCALAPPDATA is used", { skip: process.platform !== "win32" }, async () => {
await withEnv("LOCALAPPDATA", "D:\\Redirected\\Local", async () => {
const dirs = (await freshImport()).userDataDirCandidates();
@@ -141,8 +127,6 @@ describe("path resolution", () => {
});
});
- // A4: $CHROME_USER_DATA_DIR override leads the list. This value is ours to
- // interpret, so both tilde separators are accepted.
test("A4: env override is first and tilde-expanded", async () => {
await withEnv("CHROME_USER_DATA_DIR", join("~", "my-chrome-data"), async () => {
const dirs = (await freshImport()).userDataDirCandidates();
@@ -157,7 +141,6 @@ describe("path resolution", () => {
});
});
- // A5: browser naming
test("A5: Chrome recognised", async () => {
const { browserNameForUserDataDir } = await freshImport();
assert.equal(browserNameForUserDataDir("/home/u/.config/google-chrome"), "Chrome");
diff --git a/test/profile/store-test.ts b/test/profile/store-test.ts
index 8b0d85c..89afd00 100644
--- a/test/profile/store-test.ts
+++ b/test/profile/store-test.ts
@@ -1,11 +1,3 @@
-/**
- * Unit tests for the profile pin store — no browser required.
- *
- * Redirects pi's agent dir via $PI_CODING_AGENT_DIR so the real
- * ~/.pi/agent/browser-harness.json is never touched.
- *
- * Run: npm test
- */
import { test, before, after, describe } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
@@ -114,7 +106,6 @@ describe("profile pin store", () => {
const blocked = join(sandbox, "blocked");
mkdirSync(blocked, { recursive: true });
writeFileSync(join(blocked, "browser-harness.json"), "{}", "utf8");
- // Point the agent dir at a FILE — mkdir/write must fail cleanly.
process.env["PI_CODING_AGENT_DIR"] = join(blocked, "browser-harness.json");
const { writePin: writeAgain } = await import(`../../src/profile/store?nocache=${Date.now()}`);
const written = await writeAgain(pin);