mirror of
https://github.com/morluto/rea.git
synced 2026-11-03 21:36:13 +00:00
* fix(browser): keep negative script columns Chrome reports Chrome reported a Debugger.scriptParsed event with startColumn -60 and endColumn -15 for an inline script in WordPress's helper frame. CDP declares these columns as plain integers, but REA's admission required min(0), so one script failed the whole observe-web-execution run with unreadable_output. CDP resource columns now share one representation, cdpResourceColumnSchema, for script start/end columns, initiator call frames, parser initiators and event listeners. Positions on a script's first line include its start column, so keeping the reported values keeps the documented script-relative conversion consistent. The domain contract and web runtime docs state that a negative column does not locate a column in the resource. Fixes #8674 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(browser): apply the CDP column policy to passive capture inspect_web_page and discover_webmcp_tools still read CDP columns through cdpSourceCoordinates, whose own >= 0 check turned a negative column into null and reported invalid_protocol_value. WebMCP discovery also dropped the tool's whole registration_source. cdpResourceColumnSchema now lives with the domain's CDP observation schemas. Runtime admission, the passive coordinate helper and the runtime, console, initiator and WebMCP output schemas all use it. Lines keep their non-negative rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
312 lines
9.9 KiB
TypeScript
312 lines
9.9 KiB
TypeScript
import { describe, expect, it, onTestFinished } from "vitest";
|
|
import { CdpWebRuntimeProvider } from "../../../src/browser/execution/CdpWebRuntimeProvider.js";
|
|
import { observeWebExecutionInputSchema } from "../../../src/domain/webExecution.js";
|
|
import { inspectWebEventListenersInputSchema } from "../../../src/domain/webEventListeners.js";
|
|
import {
|
|
negativeColumnScript,
|
|
negativeColumnScriptUrl,
|
|
startNegativeColumnBrowser,
|
|
startRuntimeBrowser,
|
|
} from "../../fixtures/webRuntime.js";
|
|
|
|
describe("runtime document identity across same-document URL changes", () => {
|
|
it.each(["execution", "listeners"] as const)(
|
|
"accepts a stable loader for %s while retaining the initial location",
|
|
async (operation) => {
|
|
const browser = await startRuntimeBrowser({
|
|
commandResult: (command, origin, reads) =>
|
|
command.method === "Page.getFrameTree"
|
|
? {
|
|
frameTree: {
|
|
frame: {
|
|
id: "runtime-main",
|
|
url: `${origin}/${reads === 1 ? "allowed" : "new-path?caller=value#fragment"}`,
|
|
loaderId: "same-document",
|
|
},
|
|
},
|
|
}
|
|
: undefined,
|
|
});
|
|
onTestFinished(() => browser.close());
|
|
const provider = new CdpWebRuntimeProvider();
|
|
const scope = {
|
|
cdp_endpoint: browser.endpoint,
|
|
target_id: "allowed-page",
|
|
};
|
|
const result =
|
|
operation === "execution"
|
|
? await provider.observeExecution(
|
|
observeWebExecutionInputSchema.parse({
|
|
...scope,
|
|
observation_ms: 5,
|
|
}),
|
|
)
|
|
: await provider.inspectEventListeners(
|
|
inspectWebEventListenersInputSchema.parse({
|
|
...scope,
|
|
selector: "#selected",
|
|
}),
|
|
);
|
|
if (!result.ok) throw result.error;
|
|
expect(result.value.target.initial_url).toBe(
|
|
`${browser.endpoint}/allowed`,
|
|
);
|
|
expect(result.value.target.loader_id).toBe("same-document");
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
{ loader: undefined, denied: false, reason: "target_changed" },
|
|
{ loader: "same-document", denied: true, reason: "target_not_allowed" },
|
|
])(
|
|
"preserves conservative identity and origin checks: $reason",
|
|
async ({ loader, denied, reason }) => {
|
|
const browser = await startRuntimeBrowser({
|
|
commandResult: (command, origin, reads) =>
|
|
command.method === "Page.getFrameTree"
|
|
? {
|
|
frameTree: {
|
|
frame: {
|
|
id: "runtime-main",
|
|
url:
|
|
reads === 1
|
|
? `${origin}/allowed`
|
|
: denied
|
|
? "https://unselected.example/new"
|
|
: `${origin}/new`,
|
|
...(loader === undefined ? {} : { loaderId: loader }),
|
|
},
|
|
},
|
|
}
|
|
: undefined,
|
|
});
|
|
onTestFinished(() => browser.close());
|
|
const result = await new CdpWebRuntimeProvider().inspectEventListeners(
|
|
inspectWebEventListenersInputSchema.parse({
|
|
cdp_endpoint: browser.endpoint,
|
|
target_id: "allowed-page",
|
|
selector: "#selected",
|
|
}),
|
|
);
|
|
if (result.ok) throw new Error("Unverified document or origin must fail");
|
|
expect(result.error).toMatchObject({ reason });
|
|
},
|
|
);
|
|
});
|
|
|
|
describe("runtime collector failure and uncertain source metadata", () => {
|
|
it.each(["Debugger.scriptParsed", "Network.requestWillBeSent"])(
|
|
"stops an armed long window immediately for malformed %s",
|
|
async (method) => {
|
|
const browser = await startRuntimeBrowser();
|
|
onTestFinished(() => browser.close());
|
|
const result = await new CdpWebRuntimeProvider().observeExecution(
|
|
observeWebExecutionInputSchema.parse({
|
|
cdp_endpoint: browser.endpoint,
|
|
target_id: "allowed-page",
|
|
observation_ms: 2_147_483_647,
|
|
}),
|
|
{
|
|
signal: AbortSignal.timeout(2_000),
|
|
progress: {
|
|
report: async (update) => {
|
|
if (update.completed === 1)
|
|
browser.emitEvent({
|
|
method,
|
|
params: {},
|
|
sessionId: "session-1",
|
|
});
|
|
},
|
|
},
|
|
},
|
|
);
|
|
if (result.ok) throw new Error("Malformed producer data must fail");
|
|
expect(result.error._tag).toBe("AnalysisOutputError");
|
|
expect(
|
|
browser.commands.some(
|
|
({ method: command }) => command === "Profiler.stopPreciseCoverage",
|
|
),
|
|
).toBe(true);
|
|
expect(
|
|
browser.commands.some(
|
|
({ method: command }) => command === "Target.detachFromTarget",
|
|
),
|
|
).toBe(true);
|
|
},
|
|
);
|
|
|
|
it("retains reported metadata without claiming ownership or reading unowned source", async () => {
|
|
const browser = await startRuntimeBrowser();
|
|
onTestFinished(() => browser.close());
|
|
const script = {
|
|
scriptId: "uncertain-script",
|
|
url: `${browser.endpoint}/declared.js?caller=value#fragment`,
|
|
executionContextId: 999,
|
|
hash: "observed-hash",
|
|
sourceMapURL: "../declared.map?query=value#fragment",
|
|
hasSourceURL: true,
|
|
scriptLanguage: "JavaScript",
|
|
startLine: 4,
|
|
startColumn: 8,
|
|
endLine: 8,
|
|
endColumn: 0,
|
|
};
|
|
const result = await new CdpWebRuntimeProvider().observeExecution(
|
|
observeWebExecutionInputSchema.parse({
|
|
cdp_endpoint: browser.endpoint,
|
|
target_id: "allowed-page",
|
|
observation_ms: 30,
|
|
}),
|
|
{
|
|
progress: {
|
|
report: async (update) => {
|
|
if (update.completed !== 1) return;
|
|
browser.emitEvent({
|
|
method: "Debugger.scriptParsed",
|
|
params: script,
|
|
sessionId: "session-1",
|
|
});
|
|
browser.emitEvent({
|
|
method: "Network.requestWillBeSent",
|
|
sessionId: "session-1",
|
|
params: {
|
|
requestId: "uncertain-request",
|
|
frameId: "runtime-main",
|
|
timestamp: 12,
|
|
request: { url: `${browser.endpoint}/request`, method: "GET" },
|
|
initiator: {
|
|
type: "script",
|
|
stack: {
|
|
callFrames: [
|
|
{
|
|
scriptId: script.scriptId,
|
|
url: script.url,
|
|
functionName: "unknown",
|
|
lineNumber: 4,
|
|
columnNumber: 8,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
});
|
|
},
|
|
},
|
|
},
|
|
);
|
|
if (!result.ok) throw result.error;
|
|
expect(
|
|
result.value.sources.find(
|
|
({ script_id }) => script_id === script.scriptId,
|
|
),
|
|
).toMatchObject({
|
|
url: script.url,
|
|
execution_context_id: 999,
|
|
frame_id: null,
|
|
producer_hash: script.hash,
|
|
source_map_url: script.sourceMapURL,
|
|
has_source_url: true,
|
|
language: "JavaScript",
|
|
resource_start: { line_number: 4, column_number: 8 },
|
|
source: { state: "excluded" },
|
|
});
|
|
expect(result.value.requests[0]?.callsites[0]?.source_association).toBe(
|
|
"unknown",
|
|
);
|
|
expect(
|
|
browser.commands.some(
|
|
({ method, params }) =>
|
|
method === "Debugger.getScriptSource" &&
|
|
params.scriptId === script.scriptId,
|
|
),
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
it("keeps negative columns Chrome reports for an inline script and its first-line callsites", async () => {
|
|
const browser = await startNegativeColumnBrowser();
|
|
onTestFinished(() => browser.close());
|
|
const scriptUrl = negativeColumnScriptUrl(browser.endpoint);
|
|
const result = await new CdpWebRuntimeProvider().observeExecution(
|
|
observeWebExecutionInputSchema.parse({
|
|
cdp_endpoint: browser.endpoint,
|
|
target_id: "allowed-page",
|
|
observation_ms: 30,
|
|
}),
|
|
{
|
|
progress: {
|
|
report: async (update) => {
|
|
if (update.completed !== 1) return;
|
|
for (const [requestId, initiator] of [
|
|
[
|
|
"script-request",
|
|
{
|
|
type: "script",
|
|
stack: {
|
|
callFrames: [
|
|
{
|
|
scriptId: negativeColumnScript.scriptId,
|
|
url: scriptUrl,
|
|
functionName: "",
|
|
lineNumber: 21,
|
|
columnNumber: -40,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
[
|
|
"parser-request",
|
|
{
|
|
type: "parser",
|
|
url: scriptUrl,
|
|
lineNumber: 21,
|
|
columnNumber: -30,
|
|
},
|
|
],
|
|
] as const)
|
|
browser.emitEvent({
|
|
method: "Network.requestWillBeSent",
|
|
sessionId: "session-1",
|
|
params: {
|
|
requestId,
|
|
frameId: "runtime-main",
|
|
timestamp: 12,
|
|
request: {
|
|
url: `${browser.endpoint}/${requestId}`,
|
|
method: "GET",
|
|
},
|
|
initiator,
|
|
},
|
|
});
|
|
},
|
|
},
|
|
},
|
|
);
|
|
if (!result.ok) throw result.error;
|
|
expect(
|
|
result.value.requests.map(({ callsites }) =>
|
|
callsites.map(({ script_id, line_number, column_number }) => ({
|
|
script_id,
|
|
line_number,
|
|
column_number,
|
|
})),
|
|
),
|
|
).toEqual([
|
|
[
|
|
{
|
|
script_id: negativeColumnScript.scriptId,
|
|
line_number: 21,
|
|
column_number: -40,
|
|
},
|
|
],
|
|
[{ script_id: "", line_number: 21, column_number: -30 }],
|
|
]);
|
|
expect(result.value.sources).toContainEqual(
|
|
expect.objectContaining({
|
|
script_id: negativeColumnScript.scriptId,
|
|
resource_start: { line_number: 21, column_number: -60 },
|
|
}),
|
|
);
|
|
});
|