aboutsummaryrefslogtreecommitdiff
path: root/test/manual
diff options
context:
space:
mode:
Diffstat (limited to 'test/manual')
-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
9 files changed, 7 insertions, 211 deletions
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 });