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 (#8684)
* 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>
This commit is contained in:
1 parent
e01fcfb4b2
commit
8db538a7ca
14 files changed
+366
-38
No files matched your search
@@ -500,7 +500,7 @@ REA retains local observation data and removes only URL userinfo credentials, ex
|
||||
connection/frame inventories; matching direction/opcode sequences alone do
|
||||
not prove alignment after drops. Redirect resource types use only shared known metadata:
|
||||
an omitted type leaves equality unknown rather than proving a modification.
|
||||
- Console observations with a stack source retain call type, argument types, the CDP `Runtime.Timestamp` in milliseconds since Unix epoch, and source location. An absent or invalid timestamp is `null` and makes `console_events` incomplete; an observed zero remains zero. Argument types are listed by CDP argument position. An absent or non-array argument inventory is `null`, and a malformed argument keeps its position as `unknown`; either makes `console_events` incomplete, while an observed empty inventory remains `[]`. A malformed stack or top stack frame is not replaced by its caller: the console event is excluded, and a request initiator location or WebMCP `registration_source` is `null`; each is reported as `invalid_protocol_value`. This epoch-millisecond clock is separate from the monotonic-second timestamps in `observe-web-session`. When console text capture is selected, already-delivered primitive values are retained verbatim with their original `argument_index`; objects, getters, and remote properties are never expanded. Text capture is `unavailable` when the argument inventory is unknown.
|
||||
- Console observations with a stack source retain call type, argument types, the CDP `Runtime.Timestamp` in milliseconds since Unix epoch, and source location. An absent or invalid timestamp is `null` and makes `console_events` incomplete; an observed zero remains zero. Argument types are listed by CDP argument position. An absent or non-array argument inventory is `null`, and a malformed argument keeps its position as `unknown`; either makes `console_events` incomplete, while an observed empty inventory remains `[]`. A malformed stack or top stack frame is not replaced by its caller: the console event is excluded, and a request initiator location or WebMCP `registration_source` is `null`; each is reported as `invalid_protocol_value`. Source locations are CDP's zero-based lines and UTF-16 columns. A negative column, which Chrome reports on the first line of some inline scripts, is kept as reported and does not locate a column in the source URL; an invalid line or a non-integer column is `null` (WebMCP `registration_source` is `null`) and is reported as `invalid_protocol_value`. This epoch-millisecond clock is separate from the monotonic-second timestamps in `observe-web-session`. When console text capture is selected, already-delivered primitive values are retained verbatim with their original `argument_index`; objects, getters, and remote properties are never expanded. Text capture is `unavailable` when the argument inventory is unknown.
|
||||
- WebSocket observations retain direction, opcode, and payload byte length. A missing or malformed CDP opcode is returned as `null`, with opcode-dependent payload byte length and shape also `null`, and marks `websocket_frames` incomplete. Missing or wrong-typed payload data is not treated as empty; its byte length is `null`, and a text payload shape is unknown. Numeric opcode `0` and an observed empty payload remain distinct from missing data. Opcode is nullable in the public inspection schema, so clients must handle unknown protocol values. When shape capture is selected, text messages are classified as text or complete value-free JSON shape; binary bytes, hashes, prefixes, and raw payloads are never retained.
|
||||
- Storage observations always redact values. Key names, IndexedDB names, and cache names require `include_storage_keys`; stable content fingerprints require the additional `include_storage_fingerprints` selection. Cookie fingerprints cover cookies applicable to the current main-frame URL, including its path, rather than every cookie path on that origin. Other storage remains scoped to the origin. Cache fingerprints hash all decoded response bytes, including bodies above 64 KiB, subject to the [64 MiB per-message CDP limit](#request-boundary). Missing or undecodable cache bodies and partial IndexedDB remote objects make fingerprint coverage incomplete, so identical observations remain `unknown`.
|
||||
- Unavailable or malformed storage inventories mark `storage_keys` unavailable, even without fingerprints. Comparisons suppress inferred key additions and removals while preserving modifications proven by complete fingerprints on both sides. Previously exported captures are not retroactively corrected; recapture them to obtain these coverage markers.
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ Results retain:
|
||||
- Main-document script inventory, exact session script IDs, resource start coordinates, sourceURL/source-map declarations, producer hash and independently computed UTF-8 text digest.
|
||||
- Armed-window requests from the selected main frame and selected origins, complete reported initiator objects (including stack hierarchy/descriptions), derived callsites, exact script-ID associations and unresolved async parent IDs. Source association does not establish UI causality. Callsite and listener `line_number`/`column_number` values are CDP's zero-based lines and UTF-16 columns in the resource named by `url`. With a `script_id`, that is the script's enclosing resource: the script file for an external script, or the HTML document for an inline script. `resource_start` is where the script text starts in that resource. A callsite with an empty `script_id` comes from the request's reported initiator rather than a script call frame, such as markup that the parser read in a document or a module import in a script file. It still locates a position in `url`, but has no session-script association, so it cannot be joined to captured source or `resource_start`. `trace_web_source_location` takes one-based, script-relative lines and zero-based UTF-16 columns.
|
||||
|
||||
For an external script, where `url` is the script file, use `line = line_number + 1` and keep `column = column_number`; this also applies to an empty-`script_id` initiator position in a script file. For an inline script, which requires a `script_id` and its `resource_start`, use `line = line_number - resource_start.line_number + 1`. Subtract `resource_start.column_number` from `column_number` only when `line_number === resource_start.line_number`; keep the reported column on later lines. Columns remain zero-based. This conversion requires known source-start and column coordinates; preserve unavailable coordinates as unknown.
|
||||
For an external script, where `url` is the script file, use `line = line_number + 1` and keep `column = column_number`; this also applies to an empty-`script_id` initiator position in a script file. For an inline script, which requires a `script_id` and its `resource_start`, use `line = line_number - resource_start.line_number + 1`. Subtract `resource_start.column_number` from `column_number` only when `line_number === resource_start.line_number`; keep the reported column on later lines. Columns remain zero-based. This conversion requires known source-start and column coordinates; preserve unavailable coordinates as unknown. Chrome reports a negative start column for some inline scripts, such as one in a helper frame, and positions on that script's first line include it. REA keeps those columns as reported rather than failing the observation; a negative column does not locate a column in `url`, but the subtraction above still gives the script-relative column.
|
||||
|
||||
The coverage counter interval starts at backend acceptance before the armed receipt and ends at the resetting sample after the local timer. Counts may include execution during these command intervals; request events are restricted to the locally armed window. The result retains both backend timestamps and the distinct local clock.
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { mediaTypeEssence } from "../domain/analysisView/recordedMediaType.js";
|
||||
import { cdpResourceColumnSchema } from "../domain/cdpObservationSchemas.js";
|
||||
import {
|
||||
admitRecords,
|
||||
isHttpUrl,
|
||||
@@ -57,18 +58,22 @@ export const exclusionReasonForUrl = (
|
||||
? "disallowed_origin"
|
||||
: "unsupported_url";
|
||||
|
||||
/** Preserve omitted and invalid protocol offsets as unknown, never invented coordinates. */
|
||||
/**
|
||||
* Preserve omitted and invalid protocol offsets as unknown, never invented
|
||||
* coordinates. Columns follow the shared CDP resource-column policy, which
|
||||
* keeps a negative column Chrome reports.
|
||||
*/
|
||||
export const cdpSourceCoordinates = (frame: UnknownRecord | undefined) => {
|
||||
const valid = (value: unknown): value is number =>
|
||||
const validLine = (value: unknown): value is number =>
|
||||
typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
||||
const line = frame?.lineNumber;
|
||||
const column = frame?.columnNumber;
|
||||
const column = cdpResourceColumnSchema.safeParse(frame?.columnNumber);
|
||||
return {
|
||||
line: valid(line) ? line : null,
|
||||
column: valid(column) ? column : null,
|
||||
line: validLine(line) ? line : null,
|
||||
column: column.success ? column.data : null,
|
||||
invalid:
|
||||
(line !== undefined && !valid(line)) ||
|
||||
(column !== undefined && !valid(column)),
|
||||
(line !== undefined && !validLine(line)) ||
|
||||
(frame?.columnNumber !== undefined && !column.success),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
recordValue,
|
||||
type UnknownRecord,
|
||||
} from "./CdpCaptureValues.js";
|
||||
import { cdpResourceColumnSchema } from "../domain/cdpObservationSchemas.js";
|
||||
|
||||
const integer = z.int().min(0);
|
||||
/** Active runtime admission remains stricter than passive request observations. */
|
||||
@@ -25,7 +26,7 @@ export const runtimeRequestSchema = z.object({
|
||||
stack: z.unknown().optional(),
|
||||
url: z.string().optional(),
|
||||
lineNumber: integer.optional(),
|
||||
columnNumber: integer.optional(),
|
||||
columnNumber: cdpResourceColumnSchema.optional(),
|
||||
})
|
||||
.passthrough(),
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
numberValue,
|
||||
recordValue,
|
||||
} from "./CdpCaptureValues.js";
|
||||
import { cdpResourceColumnSchema } from "../domain/cdpObservationSchemas.js";
|
||||
|
||||
const integer = z.int().min(0);
|
||||
const contextAuxDataSchema = z
|
||||
@@ -31,9 +32,9 @@ export const runtimeScriptParsedSchema = scriptDeclaration.extend({
|
||||
scriptId: z.string().min(1),
|
||||
executionContextId: integer,
|
||||
startLine: integer,
|
||||
startColumn: integer,
|
||||
startColumn: cdpResourceColumnSchema,
|
||||
endLine: integer,
|
||||
endColumn: integer,
|
||||
endColumn: cdpResourceColumnSchema,
|
||||
hash: z.string().optional(),
|
||||
sourceMapURL: z.string().optional(),
|
||||
hasSourceURL: z.boolean().optional(),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { cdpResourceColumnSchema } from "../../domain/cdpObservationSchemas.js";
|
||||
|
||||
const integer = z.int().min(0);
|
||||
const range = z.object({
|
||||
@@ -30,7 +31,7 @@ export const runtimeStackSchema = z.object({
|
||||
scriptId: z.string(),
|
||||
url: z.string(),
|
||||
lineNumber: integer,
|
||||
columnNumber: integer,
|
||||
columnNumber: cdpResourceColumnSchema,
|
||||
functionName: z.string(),
|
||||
}),
|
||||
),
|
||||
@@ -49,7 +50,7 @@ export const runtimeListenersSchema = z.object({
|
||||
once: z.boolean(),
|
||||
scriptId: z.string(),
|
||||
lineNumber: integer,
|
||||
columnNumber: integer,
|
||||
columnNumber: cdpResourceColumnSchema,
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { utcDateTimeSchema } from "./offsetDateTime.js";
|
||||
import { browserCompletenessSchema } from "./browserCompleteness.js";
|
||||
import {
|
||||
cdpDomSnapshotSchema,
|
||||
cdpResourceColumnSchema,
|
||||
cdpScriptSourceSchema,
|
||||
cdpScriptMetadataSchema,
|
||||
} from "./cdpObservationSchemas.js";
|
||||
@@ -125,7 +126,7 @@ const browserNetworkRequestSchema = z.object({
|
||||
type: z.string(),
|
||||
url: z.string().nullable(),
|
||||
line: z.int().min(0).nullable(),
|
||||
column: z.int().min(0).nullable(),
|
||||
column: cdpResourceColumnSchema.nullable(),
|
||||
}),
|
||||
body_shapes: z.object({
|
||||
status: z.enum(["not_approved", "included", "partial", "unavailable"]),
|
||||
@@ -149,7 +150,7 @@ const browserConsoleEventSchema = z.object({
|
||||
),
|
||||
url: z.string().nullable(),
|
||||
line: z.int().min(0).nullable(),
|
||||
column: z.int().min(0).nullable(),
|
||||
column: cdpResourceColumnSchema.nullable(),
|
||||
text_capture: z.object({
|
||||
status: z.enum(["not_approved", "included", "unavailable"]),
|
||||
values: z.array(
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { z } from "zod";
|
||||
import { webTextArtifactSchema } from "./webContentArtifact.js";
|
||||
|
||||
/**
|
||||
* Zero-based UTF-16 column in a resource, as CDP reports it. CDP declares
|
||||
* columns as plain integers. Chrome reports a negative start column for some
|
||||
* inline scripts, and positions on a script's first line include that start
|
||||
* column, so a negative column is kept as reported. It does not locate a
|
||||
* column in the resource.
|
||||
*/
|
||||
export const cdpResourceColumnSchema = z.int();
|
||||
|
||||
/** Structural DOM facts shared by passive web and Electron observations. */
|
||||
export const cdpDomNodeSchema = z.object({
|
||||
index: z.number().int().min(0),
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
browserObservationEndpointSchema,
|
||||
} from "./browserObservation.js";
|
||||
import { browserVersionSchema } from "./browserObservationSchemas.js";
|
||||
import { cdpResourceColumnSchema } from "./cdpObservationSchemas.js";
|
||||
import { jsonValueSchema } from "./jsonValue.js";
|
||||
import { digestSchema, prefixedDigestSchema } from "./../domain/digests.js";
|
||||
|
||||
@@ -44,7 +45,7 @@ const webMcpToolSchema = z.object({
|
||||
.object({
|
||||
url: z.string(),
|
||||
line: z.int().min(0).nullable(),
|
||||
column: z.int().min(0).nullable(),
|
||||
column: cdpResourceColumnSchema.nullable(),
|
||||
})
|
||||
.nullable(),
|
||||
trust: z.literal("page-declared-untrusted"),
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
webRuntimeCommandTimeoutSchema,
|
||||
} from "./browserDeadline.js";
|
||||
import { jsonObjectSchema } from "./jsonValue.js";
|
||||
import { cdpResourceColumnSchema } from "./cdpObservationSchemas.js";
|
||||
|
||||
// CDP positions are zero-based and relative to the resource named by url.
|
||||
// With a script ID that is the script's enclosing resource: the script file,
|
||||
@@ -18,17 +19,18 @@ import { jsonObjectSchema } from "./jsonValue.js";
|
||||
// association to join captured source or resource_start.
|
||||
// trace_web_source_location takes one-based, script-relative lines, so the
|
||||
// advertised units keep callers from passing a position off by one or by an
|
||||
// inline script's offset.
|
||||
// inline script's offset. Columns are kept as reported even when Chrome reports
|
||||
// a negative one, so subtracting resource_start's column stays consistent.
|
||||
const SCRIPT_ID =
|
||||
"CDP session script ID; empty for a position a request initiator reported, which has no session-script association for joining captured source or resource_start.";
|
||||
const RESOURCE_LINE =
|
||||
"Zero-based line in the resource named by url, as reported by CDP: the script file for an external script (including a module-import initiator), or the HTML document for an inline script or parser markup.";
|
||||
const RESOURCE_COLUMN =
|
||||
"Zero-based UTF-16 column on that line of the resource named by url, as reported by CDP.";
|
||||
"Zero-based UTF-16 column on that line of the resource named by url, as reported by CDP. Chrome can report a negative column, for example on the first line of a script whose resource_start column is negative; it then does not locate a column in url.";
|
||||
const START_LINE =
|
||||
"Zero-based line where the script text starts in its enclosing resource (the HTML document for an inline script).";
|
||||
const START_COLUMN =
|
||||
"Zero-based UTF-16 column where the script text starts on that line of its enclosing resource.";
|
||||
"Zero-based UTF-16 column where the script text starts on that line of its enclosing resource. Chrome reports a negative column for some inline scripts; it is kept as reported and does not locate a column in that resource.";
|
||||
|
||||
/** Scope for one externally owned page; omitted origins select its current origin. */
|
||||
export const webRuntimeScopeSchema = z.strictObject({
|
||||
@@ -53,7 +55,7 @@ export const webRuntimeSourceSchema = z.object({
|
||||
resource_start: z
|
||||
.object({
|
||||
line_number: z.int().min(0).describe(START_LINE),
|
||||
column_number: z.int().min(0).describe(START_COLUMN),
|
||||
column_number: cdpResourceColumnSchema.describe(START_COLUMN),
|
||||
})
|
||||
.nullable(),
|
||||
source: z.discriminatedUnion("state", [
|
||||
@@ -77,7 +79,7 @@ export const webRuntimeLocationSchema = z.object({
|
||||
script_id: z.string().describe(SCRIPT_ID),
|
||||
url: z.string().nullable(),
|
||||
line_number: z.int().min(0).describe(RESOURCE_LINE),
|
||||
column_number: z.int().min(0).nullable().describe(RESOURCE_COLUMN),
|
||||
column_number: cdpResourceColumnSchema.nullable().describe(RESOURCE_COLUMN),
|
||||
function_name: z.string().nullable(),
|
||||
source_association: z.enum(["script_id", "unknown"]),
|
||||
});
|
||||
|
||||
@@ -2,7 +2,12 @@ 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 { startRuntimeBrowser } from "../../fixtures/webRuntime.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)(
|
||||
@@ -217,3 +222,90 @@ describe("runtime collector failure and uncertain source metadata", () => {
|
||||
).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 },
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -1,22 +1,21 @@
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { expect, it, onTestFinished } from "vitest";
|
||||
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
|
||||
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
|
||||
import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
|
||||
|
||||
it("reports malformed source coordinates as unknown through MCP", async () => {
|
||||
/** Report one call frame for a console event, a request initiator and a WebMCP registration. */
|
||||
const inspectSourceCoordinates = async (position: {
|
||||
readonly lineNumber: number;
|
||||
readonly columnNumber: number;
|
||||
}) => {
|
||||
const browser = await startFakeCdpBrowser({
|
||||
webMcpTools: true,
|
||||
webMcpStackTrace: (origin) => ({
|
||||
callFrames: [
|
||||
{ url: `${origin}/app.js`, lineNumber: -3, columnNumber: 7.5 },
|
||||
],
|
||||
callFrames: [{ url: `${origin}/app.js`, ...position }],
|
||||
}),
|
||||
commandEvents(command, origin) {
|
||||
const frame = {
|
||||
url: `${origin}/app.js`,
|
||||
lineNumber: -3,
|
||||
columnNumber: 7.5,
|
||||
};
|
||||
const frame = { url: `${origin}/app.js`, ...position };
|
||||
const session =
|
||||
command.sessionId === undefined ? {} : { sessionId: command.sessionId };
|
||||
if (command.method === "Runtime.enable")
|
||||
@@ -38,7 +37,7 @@ it("reports malformed source coordinates as unknown through MCP", async () => {
|
||||
...session,
|
||||
method: "Network.requestWillBeSent",
|
||||
params: {
|
||||
requestId: "bad-coordinates",
|
||||
requestId: "coordinate-request",
|
||||
timestamp: 1,
|
||||
type: "Script",
|
||||
request: { url: `${origin}/app.js`, method: "GET" },
|
||||
@@ -71,6 +70,18 @@ it("reports malformed source coordinates as unknown through MCP", async () => {
|
||||
name: "inspect_web_page",
|
||||
arguments: args,
|
||||
});
|
||||
const tools = await client.callTool({
|
||||
name: "discover_webmcp_tools",
|
||||
arguments: args,
|
||||
});
|
||||
return { client, page, tools };
|
||||
};
|
||||
|
||||
it("reports malformed source coordinates as unknown through MCP", async () => {
|
||||
const { page, tools } = await inspectSourceCoordinates({
|
||||
lineNumber: -3,
|
||||
columnNumber: 7.5,
|
||||
});
|
||||
expect(page.isError).not.toBe(true);
|
||||
expect(page.structuredContent).toMatchObject({
|
||||
normalized_result: {
|
||||
@@ -80,7 +91,7 @@ it("reports malformed source coordinates as unknown through MCP", async () => {
|
||||
network: {
|
||||
requests: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
request_id: "bad-coordinates",
|
||||
request_id: "coordinate-request",
|
||||
initiator: expect.objectContaining({ line: null, column: null }),
|
||||
}),
|
||||
]),
|
||||
@@ -99,10 +110,6 @@ it("reports malformed source coordinates as unknown through MCP", async () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const tools = await client.callTool({
|
||||
name: "discover_webmcp_tools",
|
||||
arguments: args,
|
||||
});
|
||||
expect(tools.isError).not.toBe(true);
|
||||
expect(tools.structuredContent).toMatchObject({
|
||||
normalized_result: {
|
||||
@@ -120,3 +127,69 @@ it("reports malformed source coordinates as unknown through MCP", async () => {
|
||||
},
|
||||
});
|
||||
}, 20_000);
|
||||
|
||||
it("keeps a negative column Chrome reports in passive source locations", async () => {
|
||||
const { client, page, tools } = await inspectSourceCoordinates({
|
||||
lineNumber: 21,
|
||||
columnNumber: -40,
|
||||
});
|
||||
const advertised = (await client.listTools()).tools;
|
||||
const ajv = new Ajv2020({ strict: false, validateFormats: false });
|
||||
for (const [name, response] of [
|
||||
["inspect_web_page", page],
|
||||
["discover_webmcp_tools", tools],
|
||||
] as const) {
|
||||
expect(response.isError).not.toBe(true);
|
||||
const outputSchema: Record<string, unknown> | undefined = advertised.find(
|
||||
(tool) => tool.name === name,
|
||||
)?.outputSchema;
|
||||
if (outputSchema === undefined)
|
||||
throw new Error(`Missing advertised output schema: ${name}`);
|
||||
expect(
|
||||
ajv.validate(outputSchema, response.structuredContent),
|
||||
JSON.stringify(ajv.errors),
|
||||
).toBe(true);
|
||||
}
|
||||
const location = { line: 21, column: -40 };
|
||||
expect(page.structuredContent).toMatchObject({
|
||||
normalized_result: {
|
||||
console: { events: [expect.objectContaining(location)] },
|
||||
network: {
|
||||
requests: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
request_id: "coordinate-request",
|
||||
initiator: expect.objectContaining(location),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(tools.structuredContent).toMatchObject({
|
||||
normalized_result: {
|
||||
tools: {
|
||||
items: [
|
||||
expect.objectContaining({
|
||||
registration_source: expect.objectContaining(location),
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
for (const [response, section] of [
|
||||
[page, "console_events"],
|
||||
[page, "network_initiators"],
|
||||
[tools, "webmcp_tools"],
|
||||
] as const)
|
||||
expect(response.structuredContent).toMatchObject({
|
||||
normalized_result: {
|
||||
completeness: {
|
||||
excluded: expect.not.arrayContaining([
|
||||
expect.objectContaining({
|
||||
section,
|
||||
reason: "invalid_protocol_value",
|
||||
}),
|
||||
]),
|
||||
},
|
||||
},
|
||||
});
|
||||
}, 20_000);
|
||||
@@ -11,8 +11,10 @@ import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
||||
import {
|
||||
negativeColumnScript,
|
||||
runtimeSourceA,
|
||||
runtimeSourceB,
|
||||
startNegativeColumnBrowser,
|
||||
startRuntimeBrowser,
|
||||
} from "../../fixtures/webRuntime.js";
|
||||
|
||||
@@ -179,6 +181,94 @@ cliTest(
|
||||
20_000,
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"delivers negative columns Chrome reports through MCP and the compiled CLI",
|
||||
async ({ cli }) => {
|
||||
const browser = await startNegativeColumnBrowser();
|
||||
const provider = new CdpWebRuntimeProvider();
|
||||
const session = createTestBinarySession(() => {
|
||||
throw new Error("Binary provider must not start");
|
||||
});
|
||||
const server = createServer(
|
||||
{ kind: "session", session },
|
||||
{ webRuntime: new WebRuntimeService(provider) },
|
||||
);
|
||||
const client = new Client({
|
||||
name: "runtime-negative-columns",
|
||||
version: "1",
|
||||
});
|
||||
onTestFinished(async () => {
|
||||
await client.close();
|
||||
await server.close();
|
||||
await provider.close();
|
||||
await session.close();
|
||||
await browser.close();
|
||||
});
|
||||
const [clientTransport, serverTransport] =
|
||||
InMemoryTransport.createLinkedPair();
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
const advertised = (await client.listTools()).tools.find(
|
||||
({ name }) => name === "inspect_web_event_listeners",
|
||||
)?.outputSchema;
|
||||
if (advertised === undefined)
|
||||
throw new Error("Missing advertised listener output schema");
|
||||
const outputSchema: Record<string, unknown> = advertised;
|
||||
const response = await client.callTool({
|
||||
name: "inspect_web_event_listeners",
|
||||
arguments: {
|
||||
cdp_endpoint: browser.endpoint,
|
||||
target_id: "allowed-page",
|
||||
allowed_origins: [browser.allowedOrigin],
|
||||
selector: "#selected",
|
||||
},
|
||||
});
|
||||
expect(response.isError).not.toBe(true);
|
||||
const ajv = new Ajv2020({ strict: false, validateFormats: false });
|
||||
expect(
|
||||
ajv.validate(outputSchema, response.structuredContent),
|
||||
JSON.stringify(ajv.errors),
|
||||
).toBe(true);
|
||||
const cliResult = await cli.run({
|
||||
arguments: [
|
||||
"inspect-web-event-listeners",
|
||||
browser.endpoint,
|
||||
"allowed-page",
|
||||
"#selected",
|
||||
"--allowed-origins",
|
||||
browser.allowedOrigin,
|
||||
"--json",
|
||||
],
|
||||
environment: process.env,
|
||||
});
|
||||
expect(cliResult.exitCode).toBe(0);
|
||||
for (const evidence of [
|
||||
toolContract("inspect_web_event_listeners").outputSchema.parse(
|
||||
response.structuredContent,
|
||||
),
|
||||
cliResult.json,
|
||||
])
|
||||
expect(parseEvidence(evidence).normalized_result).toMatchObject({
|
||||
listeners: [
|
||||
{
|
||||
location: {
|
||||
script_id: negativeColumnScript.scriptId,
|
||||
line_number: 21,
|
||||
column_number: -50,
|
||||
},
|
||||
},
|
||||
],
|
||||
sources: [
|
||||
{
|
||||
script_id: negativeColumnScript.scriptId,
|
||||
resource_start: { line_number: 21, column_number: -60 },
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
20_000,
|
||||
);
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "observe_web_execution" as const,
|
||||
|
||||
Vendored
+52
@@ -162,3 +162,55 @@ const runtimeEvents = (command: FakeCdpCommand, origin: string) => {
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
/** Positions Chrome 156 reported for an inline script in a helper frame. */
|
||||
export const negativeColumnScript = {
|
||||
scriptId: "helper-inline",
|
||||
executionContextId: 7,
|
||||
startLine: 21,
|
||||
startColumn: -60,
|
||||
endLine: 21,
|
||||
endColumn: -15,
|
||||
scriptLanguage: "JavaScript",
|
||||
} as const;
|
||||
export const negativeColumnScriptUrl = (origin: string) =>
|
||||
`${origin}/empty.html?cross-origin-isolated=1`;
|
||||
|
||||
/**
|
||||
* Replays that script and a listener on its first line. Positions on a
|
||||
* script's first line include its start column, so the listener's is negative.
|
||||
*/
|
||||
export const startNegativeColumnBrowser = () =>
|
||||
startRuntimeBrowser({
|
||||
commandEvents: (command, origin) =>
|
||||
command.method === "Debugger.enable"
|
||||
? [
|
||||
{
|
||||
...(command.sessionId === undefined
|
||||
? {}
|
||||
: { sessionId: command.sessionId }),
|
||||
method: "Debugger.scriptParsed",
|
||||
params: {
|
||||
...negativeColumnScript,
|
||||
url: negativeColumnScriptUrl(origin),
|
||||
},
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
commandResult: (command) =>
|
||||
command.method === "DOMDebugger.getEventListeners"
|
||||
? {
|
||||
listeners: [
|
||||
{
|
||||
type: "message",
|
||||
useCapture: false,
|
||||
passive: false,
|
||||
once: false,
|
||||
scriptId: negativeColumnScript.scriptId,
|
||||
lineNumber: 21,
|
||||
columnNumber: -50,
|
||||
},
|
||||
],
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
Reference in new issue
Block a user