repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
pi/agent/skills/optional/chrome-cdp/scripts/cdp.integration.test.mjs
Rawimport assert from "node:assert/strict";
import { spawn } from "node:child_process";
import net from "node:net";
import { createHash, randomBytes } from "node:crypto";
import { existsSync } from "node:fs";
import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
import { createServer } from "node:http";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const SCRIPT = resolve(__dirname, "cdp.mjs");
const TARGET_ID = "ABCDEF1234567890ABCDEF1234567890";
const TARGET_PREFIX = TARGET_ID.slice(0, 8);
const PNG_1X1 =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=";
function encodeFrame(text) {
const payload = Buffer.from(text);
if (payload.length < 126)
return Buffer.concat([Buffer.from([0x81, payload.length]), payload]);
if (payload.length <= 0xffff) {
const head = Buffer.alloc(4);
head[0] = 0x81;
head[1] = 126;
head.writeUInt16BE(payload.length, 2);
return Buffer.concat([head, payload]);
}
const head = Buffer.alloc(10);
head[0] = 0x81;
head[1] = 127;
head.writeBigUInt64BE(BigInt(payload.length), 2);
return Buffer.concat([head, payload]);
}
function decodeFrames(buffer) {
const messages = [];
let offset = 0;
while (offset + 2 <= buffer.length) {
const b1 = buffer[offset];
const b2 = buffer[offset + 1];
let len = b2 & 0x7f;
let header = 2;
if (len === 126) {
if (offset + 4 > buffer.length) break;
len = buffer.readUInt16BE(offset + 2);
header = 4;
} else if (len === 127) {
if (offset + 10 > buffer.length) break;
len = Number(buffer.readBigUInt64BE(offset + 2));
header = 10;
}
const masked = (b2 & 0x80) !== 0;
const maskOffset = offset + header;
const payloadOffset = maskOffset + (masked ? 4 : 0);
const end = payloadOffset + len;
if (end > buffer.length) break;
const payload = Buffer.from(buffer.subarray(payloadOffset, end));
if (masked) {
const mask = buffer.subarray(maskOffset, payloadOffset);
for (let i = 0; i < payload.length; i++) payload[i] ^= mask[i % 4];
}
if ((b1 & 0x0f) === 0x1) messages.push(payload.toString("utf8"));
offset = end;
}
return { messages, rest: buffer.subarray(offset) };
}
async function startFakeCdpServer(options = {}) {
const targetInfos = options.targetInfos ?? [
{
targetId: TARGET_ID,
type: "page",
title: "Fake Page",
url: "https://example.test/",
},
{
targetId: "CHROME0000000000000000000000000000",
type: "page",
title: "Chrome",
url: "chrome://inspect",
},
];
const state = {
targetInfos,
methods: [],
noLoadEvent: options.noLoadEvent ?? false,
noRespondMethods: new Set(options.noRespondMethods ?? []),
resourceEntries: options.resourceEntries ?? [
{
name: "https://example.test/app.js",
type: "script",
duration: 12,
size: 2048,
},
],
mouseEvents: [],
typed: [],
loadMoreRemaining: 2,
attached: 0,
attachError: options.attachError ?? false,
};
const sockets = new Set();
const server = createServer((req, res) => {
if (req.url === "/json/version") {
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ webSocketDebuggerUrl: `ws://127.0.0.1:${server.address().port}${path}` }));
return;
}
res.writeHead(404);
res.end();
});
server.on("upgrade", (req, socket) => {
sockets.add(socket);
socket.on("close", () => sockets.delete(socket));
const accept = createHash("sha1")
.update(
req.headers["sec-websocket-key"] +
"258EAFA5-E914-47DA-95CA-C5AB0DC85B11",
)
.digest("base64");
socket.write(
[
"HTTP/1.1 101 Switching Protocols",
"Upgrade: websocket",
"Connection: Upgrade",
`Sec-WebSocket-Accept: ${accept}`,
"",
"",
].join("\r\n"),
);
let buf = Buffer.alloc(0);
socket.on("data", (chunk) => {
buf = Buffer.concat([buf, chunk]);
const decoded = decodeFrames(buf);
buf = decoded.rest;
for (const message of decoded.messages) {
const req = JSON.parse(message);
handleRequest(socket, req, state);
}
});
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const { port } = server.address();
const path = `/devtools/browser/${randomBytes(4).toString("hex")}`;
return {
state,
port,
path,
wsUrl: `ws://127.0.0.1:${port}${path}`,
async close() {
for (const socket of sockets) socket.destroy();
await new Promise((resolve) => server.close(resolve));
},
};
}
function send(socket, payload) {
socket.write(encodeFrame(JSON.stringify(payload)));
}
function runtimeValue(expression, state) {
if (expression === "document.title") return "Fake Title";
if (expression === "document.readyState") return "complete";
if (expression === "window.devicePixelRatio") return 2;
if (expression === "document.documentElement.outerHTML")
return '<html><body><button id="go">Go</button><div class="card">Card</div></body></html>';
if (expression.includes('document.querySelector(".card")?.outerHTML'))
return '<div class="card">Card</div>';
if (expression.includes('document.querySelector(".missing")?.outerHTML'))
return "Element not found";
if (expression.includes("performance.getEntriesByType('resource')")) {
return JSON.stringify(state.resourceEntries);
}
if (
expression.includes('document.querySelector("#go")') &&
expression.includes("return { ok: true, tag: el.tagName")
) {
return { ok: true, tag: "BUTTON", text: "Go" };
}
if (
expression.includes('document.querySelector("#missing")') &&
expression.includes("return { ok: true, tag: el.tagName")
) {
return { ok: false, error: "Element not found: #missing" };
}
if (
expression.includes('document.querySelector("#more")') &&
expression.includes("exists: true")
) {
if (state.loadMoreRemaining > 0) {
state.loadMoreRemaining--;
return { exists: true, clicked: true };
}
return { exists: false, clicked: false };
}
return null;
}
function handleRequest(socket, req, state) {
state.methods.push(req.method);
if (state.noRespondMethods.has(req.method)) return;
const id = req.id;
const sid = req.sessionId;
switch (req.method) {
case "Target.getTargets":
return send(socket, { id, result: { targetInfos: state.targetInfos } });
case "Target.attachToTarget":
state.attached++;
if (state.attachError) return send(socket, { id, error: { message: "attach denied" } });
return send(socket, { id, result: { sessionId: "session-1" } });
case "Accessibility.getFullAXTree":
return send(socket, {
id,
sessionId: sid,
result: {
nodes: [
{
nodeId: "1",
role: { value: "RootWebArea" },
name: { value: "Fake Page" },
childIds: ["2", "3"],
},
{
nodeId: "2",
parentId: "1",
role: { value: "button" },
name: { value: "Go" },
},
{
nodeId: "3",
parentId: "1",
role: { value: "InlineTextBox" },
name: { value: "hidden text" },
},
],
},
});
case "Runtime.enable":
case "Page.enable":
return send(socket, { id, sessionId: sid, result: {} });
case "Runtime.evaluate":
if (req.params.expression === "throwError()") {
return send(socket, {
id,
sessionId: sid,
result: {
exceptionDetails: {
text: "Uncaught",
exception: { description: "Error: boom" },
},
},
});
}
return send(socket, {
id,
sessionId: sid,
result: {
result: { value: runtimeValue(req.params.expression, state) },
},
});
case "Page.getLayoutMetrics":
return send(socket, {
id,
sessionId: sid,
result: {
visualViewport: { clientWidth: 1600 },
cssVisualViewport: { clientWidth: 800 },
},
});
case "Emulation.getDeviceMetricsOverride":
return send(socket, {
id,
sessionId: sid,
result: { deviceScaleFactor: 2 },
});
case "Page.captureScreenshot":
return send(socket, { id, sessionId: sid, result: { data: PNG_1X1 } });
case "Page.navigate":
if (req.params.url === "chrome-error://fail")
return send(socket, { id, sessionId: sid, result: { errorText: "Navigation failed" } });
send(socket, { id, sessionId: sid, result: { loaderId: "loader-1" } });
if (!state.noLoadEvent) {
setTimeout(
() =>
send(socket, {
method: "Page.loadEventFired",
sessionId: sid,
params: { timestamp: Date.now() / 1000 },
}),
5,
);
}
return;
case "Input.dispatchMouseEvent":
state.mouseEvents.push(req.params.type);
return send(socket, { id, sessionId: sid, result: {} });
case "Input.insertText":
state.typed.push(req.params.text);
return send(socket, { id, sessionId: sid, result: {} });
case "DOM.getDocument":
return send(socket, {
id,
sessionId: sid,
result: { root: { nodeId: 1, nodeName: "#document" } },
});
default:
return send(socket, { id, sessionId: sid, result: {} });
}
}
function runCdp(args, env, options = {}) {
return new Promise((resolve, reject) => {
const child = spawn(process.execPath, [SCRIPT, ...args], {
env: { ...process.env, ...env },
cwd: __dirname,
stdio: ["ignore", "pipe", "pipe"],
...options,
});
let stdout = "";
let stderr = "";
child.stdout.setEncoding("utf8");
child.stderr.setEncoding("utf8");
child.stdout.on("data", (chunk) => {
stdout += chunk;
});
child.stderr.on("data", (chunk) => {
stderr += chunk;
});
child.on("error", reject);
child.on("close", (code) => resolve({ code, stdout, stderr }));
});
}
async function expectOk(args, env) {
const result = await runCdp(args, env);
assert.equal(
result.code,
0,
`${args.join(" ")}\nstdout=${result.stdout}\nstderr=${result.stderr}`,
);
return result.stdout.trimEnd();
}
async function expectFail(args, env) {
const result = await runCdp(args, env);
assert.notEqual(
result.code,
0,
`${args.join(" ")} unexpectedly passed\nstdout=${result.stdout}\nstderr=${result.stderr}`,
);
return `${result.stdout}${result.stderr}`.trimEnd();
}
function socketRoundTrip(socketPath, payload) {
return new Promise((resolve, reject) => {
const conn = net.connect(socketPath);
let data = "";
conn.setEncoding("utf8");
conn.on("connect", () => conn.write(payload));
conn.on("data", (chunk) => {
data += chunk;
if (data.includes("\n")) conn.end();
});
conn.on("end", () => resolve(data.trimEnd()));
conn.on("error", reject);
});
}
test("cdp CLI integration covers list + all page commands + daemon lifecycle", async () => {
const server = await startFakeCdpServer();
const stateDir = await mkdtemp(join(tmpdir(), "pi-chrome-cdp-test-"));
const env = {
PI_CHROME_CDP_WS_URL: server.wsUrl,
PI_CHROME_CDP_STATE_DIR: stateDir,
PI_CHROME_CDP_DAEMON_CONNECT_DELAY_MS: "5",
PI_CHROME_CDP_DAEMON_CONNECT_RETRIES: "80",
};
try {
const list = await expectOk(["list"], env);
assert.match(
list,
new RegExp(`^${TARGET_PREFIX}\\s+Fake Page\\s+https://example\\.test/`),
);
assert.doesNotMatch(list, /chrome:\/\/inspect/);
assert.equal((await stat(stateDir)).mode & 0o777, 0o700);
assert.ok(existsSync(join(stateDir, "pages.json")));
const snap = await expectOk(["snap", TARGET_PREFIX], env);
assert.match(snap, /\[RootWebArea\] Fake Page/);
assert.match(snap, /\[button\] Go/);
assert.doesNotMatch(snap, /InlineTextBox/);
assert.equal(
await expectOk(["eval", TARGET_PREFIX, "document.title"], env),
"Fake Title",
);
assert.equal(
await expectOk(["html", TARGET_PREFIX, ".card"], env),
'<div class="card">Card</div>',
);
assert.match(
await expectOk(["nav", TARGET_PREFIX, "https://example.test/next"], env),
/Navigated to https:\/\/example\.test\/next/,
);
assert.match(
await expectOk(["net", TARGET_PREFIX], env),
/script\s+https:\/\/example\.test\/app\.js/,
);
const defaultShot = await expectOk(["shot", TARGET_PREFIX], env);
const defaultScreenshot = join(stateDir, "screenshot.png");
assert.match(
defaultShot,
new RegExp(defaultScreenshot.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")),
);
assert.equal(await readFile(defaultScreenshot, "base64"), PNG_1X1);
const screenshot = join(stateDir, "shot.png");
const shot = await expectOk(["shot", TARGET_PREFIX, screenshot], env);
assert.match(
shot,
new RegExp(screenshot.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")),
);
assert.match(shot, /DPR\): 2/);
assert.equal(await readFile(screenshot, "base64"), PNG_1X1);
assert.equal(
await expectOk(["click", TARGET_PREFIX, "#go"], env),
'Clicked <BUTTON> "Go"',
);
assert.equal(
await expectOk(["clickxy", TARGET_PREFIX, "10", "20"], env),
"Clicked at CSS (10, 20)",
);
assert.equal(
await expectOk(["type", TARGET_PREFIX, "hello world"], env),
"Typed 11 characters",
);
assert.equal(
await expectOk(["loadall", TARGET_PREFIX, "#more", "1"], env),
'Clicked "#more" 2 time(s) until it disappeared',
);
assert.match(
await expectOk(["evalraw", TARGET_PREFIX, "DOM.getDocument", "{}"], env),
/"nodeName": "#document"/,
);
const batchJson = JSON.stringify([
{ cmd: "eval", args: ["document.title"] },
{ cmd: "html", args: [".card"] },
]);
const batch = JSON.parse(
await expectOk(["batch", TARGET_PREFIX, batchJson], env),
);
assert.deepEqual(
batch.map((item) => item.ok),
[true, true],
);
assert.equal(batch[0].result, "Fake Title");
assert.equal(batch[1].result, '<div class="card">Card</div>');
const daemonList = await expectOk(["list"], env);
assert.match(daemonList, /Fake Page/);
assert.ok(server.state.attached >= 1);
assert.deepEqual(server.state.mouseEvents, [
"mouseMoved",
"mousePressed",
"mouseReleased",
]);
assert.deepEqual(server.state.typed, ["hello world"]);
await expectOk(["stop", TARGET_PREFIX], env);
} finally {
await runCdp(["stop"], env).catch(() => {});
await rm(stateDir, { recursive: true, force: true });
await server.close();
}
});
test("cdp CLI safety caps are tunable and explicit", async () => {
const server = await startFakeCdpServer({
resourceEntries: [
{ name: "https://example.test/a.js", type: "script", duration: 1, size: 1 },
{ name: "https://example.test/b.js", type: "script", duration: 2, size: 2 },
],
});
const stateDir = await mkdtemp(join(tmpdir(), "pi-chrome-cdp-caps-test-"));
const env = {
PI_CHROME_CDP_WS_URL: server.wsUrl,
PI_CHROME_CDP_STATE_DIR: stateDir,
PI_CHROME_CDP_MAX_HTML_CHARS: "20",
PI_CHROME_CDP_MAX_NET_ENTRIES: "1",
};
try {
await expectOk(["list"], env);
const html = await expectOk(["html", TARGET_PREFIX], env);
assert.match(html, /page HTML truncated/);
const net = await expectOk(["net", TARGET_PREFIX], env);
assert.match(net, /network entries truncated: 2 entries > 1/);
} finally {
await runCdp(["stop"], env).catch(() => {});
await rm(stateDir, { recursive: true, force: true });
await server.close();
}
});
test("cdp CLI handles zero inspectable page targets", async () => {
const server = await startFakeCdpServer({
targetInfos: [
{
targetId: "CHROME0000000000000000000000000000",
type: "page",
title: "Chrome",
url: "chrome://inspect",
},
{
targetId: "WORKER0000000000000000000000000000",
type: "service_worker",
title: "Worker",
url: "https://example.test/sw.js",
},
],
});
const stateDir = await mkdtemp(join(tmpdir(), "pi-chrome-cdp-empty-test-"));
const env = {
PI_CHROME_CDP_WS_URL: server.wsUrl,
PI_CHROME_CDP_STATE_DIR: stateDir,
};
try {
assert.equal(await expectOk(["list"], env), "");
assert.deepEqual(JSON.parse(await readFile(join(stateDir, "pages.json"), "utf8")), []);
} finally {
await rm(stateDir, { recursive: true, force: true });
await server.close();
}
});
test("cdp CLI reports invalid DevToolsActivePort files", async () => {
const stateDir = await mkdtemp(join(tmpdir(), "pi-chrome-cdp-invalid-portfile-test-"));
const portFile = join(stateDir, "DevToolsActivePort");
await writeFile(portFile, "12345\n");
const env = {
PI_CHROME_CDP_DEVTOOLS_ACTIVE_PORT: portFile,
PI_CHROME_CDP_STATE_DIR: stateDir,
};
try {
assert.match(await expectFail(["list"], env), /DevToolsActivePort \(invalid\)/);
} finally {
await rm(stateDir, { recursive: true, force: true });
}
});
test("cdp CLI uses explicit websocket env before DevToolsActivePort files", async () => {
const server = await startFakeCdpServer();
const stateDir = await mkdtemp(join(tmpdir(), "pi-chrome-cdp-explicit-ws-test-"));
const badPortFile = join(stateDir, "DevToolsActivePort");
await writeFile(badPortFile, "9\n/devtools/browser/dead\n");
const env = {
PI_CHROME_CDP_WS_URL: server.wsUrl,
PI_CHROME_CDP_DEVTOOLS_ACTIVE_PORT: badPortFile,
PI_CHROME_CDP_STATE_DIR: stateDir,
};
try {
const list = await expectOk(["list"], env);
assert.match(list, /Fake Page/);
} finally {
await rm(stateDir, { recursive: true, force: true });
await server.close();
}
});
test("cdp CLI uses unique long prefixes and rejects ambiguous short prefixes", async () => {
const server = await startFakeCdpServer({
targetInfos: [
{
targetId: "ABCDEF12000000000000000000000000",
type: "page",
title: "First Page",
url: "https://one.example/",
},
{
targetId: "ABCDEF12FFFFFFFFFFFFFFFFFFFFFFFF",
type: "page",
title: "Second Page",
url: "https://two.example/",
},
],
});
const stateDir = await mkdtemp(join(tmpdir(), "pi-chrome-cdp-prefix-test-"));
const env = {
PI_CHROME_CDP_WS_URL: server.wsUrl,
PI_CHROME_CDP_STATE_DIR: stateDir,
PI_CHROME_CDP_DAEMON_CONNECT_DELAY_MS: "5",
PI_CHROME_CDP_DAEMON_CONNECT_RETRIES: "80",
};
try {
const list = await expectOk(["list"], env);
assert.match(list, /^ABCDEF120\s+First Page/m);
assert.match(list, /^ABCDEF12F\s+Second Page/m);
assert.match(await expectFail(["snap", "ABCDEF12"], env), /Ambiguous prefix/);
assert.match(await expectOk(["snap", "ABCDEF120"], env), /\[RootWebArea\] Fake Page/);
} finally {
await runCdp(["stop"], env).catch(() => {});
await rm(stateDir, { recursive: true, force: true });
await server.close();
}
});
test("cdp CLI rejects ambiguous daemon stop prefixes", async () => {
const stateDir = await mkdtemp(join(tmpdir(), "pi-chrome-cdp-stop-prefix-test-"));
const env = { PI_CHROME_CDP_STATE_DIR: stateDir };
await writeFile(join(stateDir, "cdp-ABCDEF12000000000000000000000000.sock"), "stale");
await writeFile(join(stateDir, "cdp-ABCDEF12FFFFFFFFFFFFFFFFFFFFFFFF.sock"), "stale");
try {
assert.match(await expectFail(["stop", "ABCDEF12"], env), /Ambiguous prefix/);
await expectOk(["stop"], env);
assert.equal(existsSync(join(stateDir, "cdp-ABCDEF12000000000000000000000000.sock")), false);
assert.equal(existsSync(join(stateDir, "cdp-ABCDEF12FFFFFFFFFFFFFFFFFFFFFFFF.sock")), false);
} finally {
await rm(stateDir, { recursive: true, force: true });
}
});
test("cdp CLI discovers websocket via DevToolsActivePort override", async () => {
const server = await startFakeCdpServer();
const stateDir = await mkdtemp(
join(tmpdir(), "pi-chrome-cdp-portfile-test-"),
);
const portFile = join(stateDir, "DevToolsActivePort");
await writeFile(portFile, `${server.port}\n${server.path}\n`);
const env = {
PI_CHROME_CDP_DEVTOOLS_ACTIVE_PORT: portFile,
PI_CHROME_CDP_STATE_DIR: stateDir,
};
try {
const list = await expectOk(["list"], env);
assert.match(list, /Fake Page/);
} finally {
await rm(stateDir, { recursive: true, force: true });
await server.close();
}
});
test("cdp CLI stop removes stale daemon sockets", async () => {
const stateDir = await mkdtemp(join(tmpdir(), "pi-chrome-cdp-stale-test-"));
const env = { PI_CHROME_CDP_STATE_DIR: stateDir };
const staleSocket = join(stateDir, `cdp-${TARGET_ID}.sock`);
await writeFile(staleSocket, "stale");
try {
await expectOk(["stop", TARGET_PREFIX], env);
assert.equal(existsSync(staleSocket), false);
} finally {
await rm(stateDir, { recursive: true, force: true });
}
});
test("cdp CLI validates command shape before browser work", async () => {
const stateDir = await mkdtemp(join(tmpdir(), "pi-chrome-cdp-args-test-"));
const env = { PI_CHROME_CDP_STATE_DIR: stateDir };
try {
assert.match(await expectOk(["--help"], env), /Usage: cdp <command>/);
assert.match(await expectFail(["bogus"], env), /Unknown command: bogus/);
assert.match(await expectFail(["snap"], env), /target ID required/);
assert.match(
await expectFail(["snap", TARGET_PREFIX], env),
/No page list cached/,
);
} finally {
await rm(stateDir, { recursive: true, force: true });
}
});
test("cdp CLI validates required page-command args before starting daemons", async () => {
const server = await startFakeCdpServer();
const stateDir = await mkdtemp(join(tmpdir(), "pi-chrome-cdp-preflight-test-"));
const env = {
PI_CHROME_CDP_WS_URL: server.wsUrl,
PI_CHROME_CDP_STATE_DIR: stateDir,
PI_CHROME_CDP_DAEMON_CONNECT_DELAY_MS: "5",
PI_CHROME_CDP_DAEMON_CONNECT_RETRIES: "20",
};
await writeFile(
join(stateDir, "pages.json"),
JSON.stringify([{ targetId: TARGET_ID, type: "page", title: "Fake Page", url: "https://example.test/" }]),
);
try {
assert.match(await expectFail(["eval", TARGET_PREFIX], env), /expression required/);
assert.match(await expectFail(["type", TARGET_PREFIX], env), /text required/);
assert.match(await expectFail(["nav", TARGET_PREFIX], env), /URL required/);
assert.match(await expectFail(["batch", TARGET_PREFIX], env), /batch JSON required/);
assert.match(await expectFail(["evalraw", TARGET_PREFIX], env), /CDP method required/);
assert.equal(server.state.attached, 0);
} finally {
await runCdp(["stop"], env).catch(() => {});
await rm(stateDir, { recursive: true, force: true });
await server.close();
}
});
test("cdp CLI reports CDP command and load-event timeouts", async () => {
const server = await startFakeCdpServer({ noRespondMethods: ["Never.respond"] });
const stateDir = await mkdtemp(join(tmpdir(), "pi-chrome-cdp-timeout-test-"));
const env = {
PI_CHROME_CDP_WS_URL: server.wsUrl,
PI_CHROME_CDP_STATE_DIR: stateDir,
PI_CHROME_CDP_CDP_TIMEOUT_MS: "5",
PI_CHROME_CDP_DAEMON_CONNECT_DELAY_MS: "5",
PI_CHROME_CDP_DAEMON_CONNECT_RETRIES: "80",
};
await writeFile(
join(stateDir, "pages.json"),
JSON.stringify([{ targetId: TARGET_ID, type: "page", title: "Fake Page", url: "https://example.test/" }]),
);
try {
assert.match(await expectFail(["evalraw", TARGET_PREFIX, "Never.respond", "{}"], env), /Timeout: Never\.respond/);
} finally {
await runCdp(["stop"], env).catch(() => {});
await rm(stateDir, { recursive: true, force: true });
await server.close();
}
const navServer = await startFakeCdpServer({ noLoadEvent: true });
const navStateDir = await mkdtemp(join(tmpdir(), "pi-chrome-cdp-load-timeout-test-"));
const navEnv = {
PI_CHROME_CDP_WS_URL: navServer.wsUrl,
PI_CHROME_CDP_STATE_DIR: navStateDir,
PI_CHROME_CDP_NAVIGATION_TIMEOUT_MS: "5",
PI_CHROME_CDP_DAEMON_CONNECT_DELAY_MS: "5",
PI_CHROME_CDP_DAEMON_CONNECT_RETRIES: "80",
};
await writeFile(
join(navStateDir, "pages.json"),
JSON.stringify([{ targetId: TARGET_ID, type: "page", title: "Fake Page", url: "https://example.test/" }]),
);
try {
assert.match(await expectFail(["nav", TARGET_PREFIX, "https://example.test/slow"], navEnv), /Timeout waiting for event: Page\.loadEventFired/);
} finally {
await runCdp(["stop"], navEnv).catch(() => {});
await rm(navStateDir, { recursive: true, force: true });
await navServer.close();
}
});
test("cdp CLI reports daemon startup connection and attach failures", async () => {
const stateDir = await mkdtemp(join(tmpdir(), "pi-chrome-cdp-daemon-fail-test-"));
const env = {
PI_CHROME_CDP_WS_URL: "ws://127.0.0.1:9/devtools/browser/dead",
PI_CHROME_CDP_STATE_DIR: stateDir,
PI_CHROME_CDP_DAEMON_CONNECT_DELAY_MS: "1",
PI_CHROME_CDP_DAEMON_CONNECT_RETRIES: "0",
};
await writeFile(
join(stateDir, "pages.json"),
JSON.stringify([{ targetId: TARGET_ID, type: "page", title: "Fake Page", url: "https://example.test/" }]),
);
try {
assert.match(await expectFail(["snap", TARGET_PREFIX], env), /Daemon failed to start/);
} finally {
await rm(stateDir, { recursive: true, force: true });
}
const server = await startFakeCdpServer({ attachError: true });
const attachStateDir = await mkdtemp(join(tmpdir(), "pi-chrome-cdp-attach-fail-test-"));
const attachEnv = {
PI_CHROME_CDP_WS_URL: server.wsUrl,
PI_CHROME_CDP_STATE_DIR: attachStateDir,
PI_CHROME_CDP_DAEMON_CONNECT_DELAY_MS: "1",
PI_CHROME_CDP_DAEMON_CONNECT_RETRIES: "0",
};
await writeFile(
join(attachStateDir, "pages.json"),
JSON.stringify([{ targetId: TARGET_ID, type: "page", title: "Fake Page", url: "https://example.test/" }]),
);
try {
assert.match(await expectFail(["snap", TARGET_PREFIX], attachEnv), /Daemon failed to start/);
assert.equal(server.state.attached, 1);
} finally {
await rm(attachStateDir, { recursive: true, force: true });
await server.close();
}
});
test("cdp daemon rejects invalid NDJSON and continues", async () => {
const server = await startFakeCdpServer();
const stateDir = await mkdtemp(join(tmpdir(), "pi-chrome-cdp-invalid-json-test-"));
const env = {
PI_CHROME_CDP_WS_URL: server.wsUrl,
PI_CHROME_CDP_STATE_DIR: stateDir,
PI_CHROME_CDP_DAEMON_CONNECT_DELAY_MS: "5",
PI_CHROME_CDP_DAEMON_CONNECT_RETRIES: "80",
};
await writeFile(
join(stateDir, "pages.json"),
JSON.stringify([{ targetId: TARGET_ID, type: "page", title: "Fake Page", url: "https://example.test/" }]),
);
try {
await expectOk(["snap", TARGET_PREFIX], env);
const socketPath = join(stateDir, `cdp-${TARGET_ID}.sock`);
const raw = await socketRoundTrip(socketPath, "{bad\n");
assert.deepEqual(JSON.parse(raw), { ok: false, error: "Invalid JSON request", id: null });
const list = JSON.parse(await socketRoundTrip(socketPath, JSON.stringify({ id: 7, cmd: "list" }) + "\n"));
assert.equal(list.id, 7);
assert.equal(list.ok, true);
assert.match(list.result, /Fake Page/);
assert.equal(await expectOk(["eval", TARGET_PREFIX, "document.title"], env), "Fake Title");
} finally {
await runCdp(["stop"], env).catch(() => {});
await rm(stateDir, { recursive: true, force: true });
await server.close();
}
});
test("cdp CLI reports daemon command validation errors and keeps daemon usable", async () => {
const server = await startFakeCdpServer();
const stateDir = await mkdtemp(join(tmpdir(), "pi-chrome-cdp-errors-test-"));
const env = {
PI_CHROME_CDP_WS_URL: server.wsUrl,
PI_CHROME_CDP_STATE_DIR: stateDir,
PI_CHROME_CDP_DAEMON_CONNECT_DELAY_MS: "5",
PI_CHROME_CDP_DAEMON_CONNECT_RETRIES: "80",
PI_CHROME_CDP_MAX_BATCH_COMMANDS: "1",
};
try {
await expectOk(["list"], env);
assert.match(await expectFail(["click", TARGET_PREFIX], env), /CSS selector required/);
assert.match(
await expectFail(["clickxy", TARGET_PREFIX, "x", "20"], env),
/x and y must be finite numbers/,
);
assert.match(await expectFail(["eval", TARGET_PREFIX, "throwError()"], env), /Uncaught/);
assert.match(await expectFail(["click", TARGET_PREFIX, "#missing"], env), /Element not found: #missing/);
assert.equal(await expectOk(["html", TARGET_PREFIX, ".missing"], env), "Element not found");
assert.match(await expectFail(["nav", TARGET_PREFIX, "chrome-error://fail"], env), /Navigation failed/);
assert.match(await expectFail(["clickxy", TARGET_PREFIX, "Infinity", "20"], env), /x and y must be finite numbers/);
assert.match(await expectFail(["loadall", TARGET_PREFIX, "#more", "NaN"], env), /interval must be a finite number/);
assert.match(
await expectFail(["evalraw", TARGET_PREFIX, "DOM.getDocument", "{bad"], env),
/Invalid JSON params/,
);
assert.match(
await expectFail(["batch", TARGET_PREFIX, JSON.stringify({ cmd: "eval" })], env),
/Batch JSON must be an array/,
);
assert.match(
await expectFail(["batch", TARGET_PREFIX, JSON.stringify([{ args: [] }])], env),
/Batch item missing cmd/,
);
assert.match(
await expectFail(["batch", TARGET_PREFIX, JSON.stringify([{ cmd: "nope" }])], env),
/Unknown command: nope/,
);
assert.match(
await expectFail(["batch", TARGET_PREFIX, JSON.stringify([{ cmd: "list" }])], env),
/Command not allowed in batch: list/,
);
assert.match(
await expectFail(["batch", TARGET_PREFIX, JSON.stringify([{ cmd: "eval" }, { cmd: "html" }])], env),
/Batch too large: 2 > 1/,
);
assert.equal(
await expectOk(["eval", TARGET_PREFIX, "document.title"], env),
"Fake Title",
);
} finally {
await runCdp(["stop"], env).catch(() => {});
await rm(stateDir, { recursive: true, force: true });
await server.close();
}
});