Files
rea/scripts/lib/browser-socket-e2e.mjs
all-black-493 62c8b3da1b feat(browser): attach passive tools to browsers without HTTP discovery (#2346)
* feat(browser): attach passive tools to browsers without HTTP discovery

Accept a literal loopback browser WebSocket,
ws://127.0.0.1:PORT/devtools/browser/ID, as the cdp_endpoint of the six
passive browser tools. Discovery then reads Browser.getVersion and
Target.getTargets over that socket, and the target session adopts the
same socket, so a browser that asks for consent per connection prompts
once per call.

The socket identifier gates access, so Evidence records the endpoint as
ws://127.0.0.1:PORT/devtools/browser/[REDACTED], and runtime
reconciliation accepts that recorded form. HTTP endpoints and their
Evidence are unchanged. Electron, web runtime, scenario and Inspector
endpoints stay HTTP-only.

Refs #2288

* test(browser): match browser-socket captures against HTTP discovery

Check in the real Chrome lane that DevToolsActivePort names the same
browser socket as /json/version, and that inspecting the selected page
through that socket leaves DOM structure and scripts unchanged compared
with an inspection through HTTP discovery.

Refs #2288

* fix(browser): bound browser-socket handshake and report discovery timeouts

Run the browser-socket WebSocket handshake and both discovery commands
under one discovery deadline, so a port that accepts the connection but
never completes the upgrade times out instead of waiting forever.

Report a reached deadline as a timeout without carrying the internal
abort as its cause, so CLI and MCP project provider_timeout rather than
cancelled. Caller cancellation is still reported as cancelled.

Refs #2288
2026-10-14 00:36:37 +08:00

101 lines
3.3 KiB
JavaScript

import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import {
inspectWebPageInputSchema,
listBrowserTargetsInputSchema,
} from "../../dist/domain/browserObservation.js";
import { compareWebCapturesInputSchema } from "../../dist/domain/webCaptureDiffSchemas.js";
import { artifactCliEvidence } from "./artifact-e2e.mjs";
/** Verify browser-socket discovery and inspection against HTTP discovery. */
export async function verifyBrowserSocketTransport({
provider,
profile,
endpoint,
target,
origin,
}) {
const [port, path] = (
await readFile(join(profile, "DevToolsActivePort"), "utf8")
).split("\n");
const socket = `ws://127.0.0.1:${port}${path}`;
const version = await (await fetch(`${endpoint}/json/version`)).json();
assert.equal(
new URL(version.webSocketDebuggerUrl).pathname,
path,
"DevToolsActivePort and /json/version named different browser sockets",
);
const list = async (cdpEndpoint) => {
const listed = await provider.listTargets(
listBrowserTargetsInputSchema.parse({
cdp_endpoint: cdpEndpoint,
allowed_origins: [origin],
}),
);
if (!listed.ok) throw listed.error;
return listed.value;
};
const throughHttp = await list(endpoint);
const throughSocket = await list(socket);
// Target.getTargets reports attachment state that /json/list may omit.
const withoutAttachment = (targets) =>
targets.map(({ attached: _attached, ...rest }) => rest);
assert.ok(throughSocket.targets.length > 0, "Browser socket listed no page");
assert.deepEqual(
withoutAttachment(throughSocket.targets),
withoutAttachment(throughHttp.targets),
);
assert.ok(
throughSocket.targets.every(
({ attached }) => typeof attached === "boolean",
),
"Browser socket omitted target attachment state",
);
assert.equal(throughSocket.browser.product, throughHttp.browser.product);
const evidence = await artifactCliEvidence("inspect-web-page", socket, [
target,
"--allowed-origins",
origin,
"--observation-ms",
"100",
]);
assert.equal(evidence.normalized_result.target.target_id, target);
assert.ok(
evidence.normalized_result.dom.nodes.length > 0,
"Browser-socket inspection returned no DOM nodes",
);
assert.equal(
evidence.parameters.cdp_endpoint,
`ws://127.0.0.1:${port}/devtools/browser/[REDACTED]`,
);
assert.ok(
!JSON.stringify(evidence).includes(path),
"Browser-socket Evidence retained the socket identifier",
);
const throughHttpInspection = await provider.inspectPage(
inspectWebPageInputSchema.parse({
cdp_endpoint: endpoint,
allowed_origins: [origin],
target_id: target,
observation_ms: 100,
}),
);
if (!throughHttpInspection.ok) throw throughHttpInspection.error;
const compared = await provider.compareCaptures(
compareWebCapturesInputSchema.parse({
before: { inspection: throughHttpInspection.value },
after: { inspection: evidence.normalized_result },
}),
);
if (!compared.ok) throw compared.error;
assert.equal(compared.value.dimensions.dom_structure.status, "unchanged");
assert.equal(compared.value.dimensions.scripts.status, "unchanged");
return {
targets: throughSocket.targets.length,
revision: throughSocket.browser.revision,
};
}