From 6c06f406f69448a4181cb1be3558ab046714511d Mon Sep 17 00:00:00 2001 From: Arjun <57688398+amyanger@users.noreply.github.com> Date: Fri, 16 Oct 2026 18:16:20 +0100 Subject: [PATCH] fix(javascript): keep invalid V8 script events out of policy filtering Preserve invalid V8 script events as unavailable protocol data instead of merging them into target-policy exclusions. Keep independent producer counts, original Evidence, and existing target authority. Validated with 90 focused Linux tests, real Node Inspector CLI/MCP captures, complete static/architecture checks, and current-head automated review/CI. Fixes #4376. --- ...riptRuntimeReconciliationCaptureParsing.ts | 56 ++++++++---- .../browser/cdpScriptMetadata.test.ts | 89 +++++++++++++++++++ 2 files changed, 128 insertions(+), 17 deletions(-) diff --git a/src/domain/javascript/javascriptRuntimeReconciliationCaptureParsing.ts b/src/domain/javascript/javascriptRuntimeReconciliationCaptureParsing.ts index be428b06cb..a384b2e35e 100644 --- a/src/domain/javascript/javascriptRuntimeReconciliationCaptureParsing.ts +++ b/src/domain/javascript/javascriptRuntimeReconciliationCaptureParsing.ts @@ -12,6 +12,7 @@ import { import { classifyBrowserCompleteness, type BrowserCompleteness, + type BrowserExclusion, } from "../browserCompleteness.js"; import { electronPageInspectionSchema, @@ -350,9 +351,11 @@ const normalizeV8Inspection = ( })), }, workers: [], + // Route exclusions as CdpCaptureCompleteness does: denied locations are + // policy filtered, while invalid protocol values leave scripts unavailable. completeness: classifyBrowserCompleteness({ policyFilteredSections: new Set( - excludedV8Scripts(result) > 0 ? ["scripts"] : [], + result.scripts.excluded.unsupported_location > 0 ? ["scripts"] : [], ), attachLimitedSections: new Set([ "frames", @@ -361,17 +364,15 @@ const normalizeV8Inspection = ( "workers", ]), truncatedSections: new Set(result.capture.truncated ? ["scripts"] : []), - unavailableSections: new Set(["frames", "script_sources", "workers"]), - excluded: - excludedV8Scripts(result) === 0 - ? [] - : [ - { - section: "scripts", - reason: "out_of_target_scope", - count: excludedV8Scripts(result), - }, - ], + unavailableSections: new Set([ + "frames", + "script_sources", + "workers", + ...(result.scripts.excluded.invalid_protocol_value > 0 + ? (["scripts"] as const) + : []), + ]), + excluded: v8ScriptExclusions(result), droppedEvents: { scripts: result.capture.events_dropped, network_requests: 0, @@ -401,11 +402,32 @@ const runtimeLocation = ( return { url: location.specifier }; }; -const excludedV8Scripts = (result: JavaScriptRuntimeObservation): number => - Object.values(result.scripts.excluded).reduce( - (total, count) => total + count, - 0, - ); +const v8ScriptExclusions = ( + result: JavaScriptRuntimeObservation, +): BrowserExclusion[] => { + const { unsupported_location, invalid_protocol_value } = + result.scripts.excluded; + return [ + ...(unsupported_location > 0 + ? [ + { + section: "scripts", + reason: "out_of_target_scope", + count: unsupported_location, + } as const, + ] + : []), + ...(invalid_protocol_value > 0 + ? [ + { + section: "scripts", + reason: "invalid_protocol_value", + count: invalid_protocol_value, + } as const, + ] + : []), + ]; +}; const assertRuntimeParameters = ( evidence: Evidence, diff --git a/tests/boundary/browser/cdpScriptMetadata.test.ts b/tests/boundary/browser/cdpScriptMetadata.test.ts index 41d89fba41..765bfa122a 100644 --- a/tests/boundary/browser/cdpScriptMetadata.test.ts +++ b/tests/boundary/browser/cdpScriptMetadata.test.ts @@ -196,3 +196,92 @@ describe("Inspector script metadata", () => { }, ); }); + +describe("Inspector script exclusions", () => { + const reconcileV8Scripts = async ( + scriptEvents: readonly { method: string; params: object }[], + ) => { + const root = await fixtureRoot(); + const inspector = await startFakeV8Inspector({ + targetUrl: pathToFileURL(join(root, "app.js")).href, + scriptEvents, + }); + resources.push(() => inspector.close()); + const input = { + inspector_endpoint: inspector.endpoint, + target_id: inspector.targetId, + observation_ms: 50, + }; + const provider = new V8InspectorProvider(); + const result = await provider.observe(input); + if (!result.ok) throw result.error; + const [parsed] = parseRuntimeCaptures([ + createJavaScriptRuntimeObservationEvidence( + "observe_javascript_runtime", + input, + result.value, + provider.identity(), + ), + ]); + return { + producer: result.value.scripts.excluded, + completeness: parsed?.inspection.completeness, + }; + }; + const failedToParse = { + method: "Debugger.scriptFailedToParse", + params: { scriptId: "failed-script", url: "", executionContextId: 1 }, + }; + const missingUrl = { + method: "Debugger.scriptParsed", + params: { scriptId: "missing-url", executionContextId: 1 }, + }; + + it("keeps invalid protocol scripts out of policy filtering", async () => { + const { producer, completeness } = await reconcileV8Scripts([ + failedToParse, + missingUrl, + ]); + expect(producer).toEqual({ + unsupported_location: 0, + invalid_protocol_value: 2, + }); + expect(completeness).toMatchObject({ + status: "attach_limited", + conditions: ["attach_limited"], + policy_filtered_sections: [], + excluded: [ + { section: "scripts", reason: "invalid_protocol_value", count: 2 }, + ], + }); + expect(completeness?.unavailable_sections).toContain("scripts"); + }); + + it("reports unsupported and invalid script exclusions separately", async () => { + const { producer, completeness } = await reconcileV8Scripts([ + failedToParse, + { + method: "Debugger.scriptParsed", + params: { + scriptId: "data-script", + url: "data:text/javascript,0", + executionContextId: 1, + }, + }, + ]); + expect(producer).toEqual({ + unsupported_location: 1, + invalid_protocol_value: 1, + }); + expect(completeness).toMatchObject({ + status: "policy_filtered", + conditions: ["attach_limited", "policy_filtered"], + policy_filtered_sections: ["scripts"], + excluded: [ + { section: "scripts", reason: "invalid_protocol_value", count: 1 }, + { section: "scripts", reason: "out_of_target_scope", count: 1 }, + ], + }); + expect(completeness?.unavailable_sections).toContain("scripts"); + }); +});