From b49396d582857c318b920417ce0571920901d3e5 Mon Sep 17 00:00:00 2001 From: NIU-123370 <191000457+NIU-123370@users.noreply.github.com> Date: Sat, 17 Oct 2026 21:14:51 +0800 Subject: [PATCH 1/2] fix(browser): keep an undecodable Electron worker target uncertain The Electron worker inventory decided "not a worker" from a `type` value it could not read, so a target whose discriminator was absent, null, numeric or empty disappeared with no exclusion recorded: the same false-complete inventory #3378 reports, at the second producer. Admit the discriminator before the non-worker filter and count undecodable entries into the shared invalid_protocol_value ledger, and stop reading the inventory through a private Array.isArray check now that admitRecords owns that decision. The web producer landed on main in 4cc044be; its probe shapes were implemented but not pinned, so cover them there as well. Refs #3378. --- src/browser/CdpElectronWorkers.test.ts | 150 ++++++++++++++++++++++ src/browser/CdpElectronWorkers.ts | 30 ++++- src/browser/CdpPageCaptureWorkers.test.ts | 60 ++++++++- 3 files changed, 226 insertions(+), 14 deletions(-) create mode 100644 src/browser/CdpElectronWorkers.test.ts diff --git a/src/browser/CdpElectronWorkers.test.ts b/src/browser/CdpElectronWorkers.test.ts new file mode 100644 index 0000000000..3d090704a0 --- /dev/null +++ b/src/browser/CdpElectronWorkers.test.ts @@ -0,0 +1,150 @@ +import { fileURLToPath } from "node:url"; + +import { describe, expect, it } from "vitest"; + +import type { InspectElectronPageInput } from "../domain/javascript/electronObservation.js"; +import { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js"; +import type { CdpConnection } from "./CdpConnection.js"; +import type { CdpEndpointTarget } from "./CdpEndpoint.js"; +import { captureElectronWorkers } from "./CdpElectronWorkers.js"; + +/** `authorizedElectronFile` only admits a file the host can resolve. */ +const localFileUrl = `file://${fileURLToPath(import.meta.url)}`; + +const workerTarget = ( + over: Readonly> = {}, +): Readonly> => ({ + targetId: "worker-1", + type: "service_worker", + url: localFileUrl, + openerId: "electron-page", + attached: true, + ...over, +}); + +/** An Electron worker target whose `TargetInfo.type` is not delivered at all. */ +const targetWithoutType: Readonly> = { + targetId: "worker-1", + url: localFileUrl, + openerId: "electron-page", + attached: true, +}; + +const capture = async (inventory: unknown) => { + const completeness = new CdpCaptureCompleteness(); + const workers = await captureElectronWorkers({ + connection: { + async send() { + return inventory; + }, + } as unknown as CdpConnection, + target: { + id: "electron-page", + type: "page", + title: "Fixture", + url: localFileUrl, + attached: false, + } satisfies CdpEndpointTarget, + request: {} as InspectElectronPageInput, + frameIds: new Set(), + completeness, + limitations: [], + }); + return { workers, completeness: completeness.snapshot() }; +}; + +describe("Electron worker target inventory", () => { + it("preserves an explicit empty inventory", async () => { + const result = await capture({ targetInfos: [] }); + + expect(result.workers).toEqual([]); + expect(result.completeness.unavailable_sections).not.toContain("workers"); + expect(result.completeness.excluded).not.toContainEqual( + expect.objectContaining({ section: "workers" }), + ); + }); + + it.each([undefined, { length: 1 }, "worker-one"])( + "reports a missing or non-array target inventory as unknown: %s", + async (targetInfos) => { + const result = await capture({ targetInfos }); + + expect(result.workers).toEqual([]); + expect(result.completeness).toMatchObject({ + unavailable_sections: ["workers"], + excluded: [ + { section: "workers", reason: "invalid_protocol_value", count: null }, + ], + }); + }, + ); + + it("records a target entry that carries no type discriminator", async () => { + const result = await capture({ targetInfos: [targetWithoutType] }); + + expect(result.workers).toEqual([]); + expect(result.completeness.unavailable_sections).toContain("workers"); + expect(result.completeness.excluded).toContainEqual({ + section: "workers", + reason: "invalid_protocol_value", + count: 1, + }); + }); + + it.each([ + ["null", null], + ["numeric", 42], + ["empty", ""], + ] as const)( + "records a target entry whose type is an undecodable %s value", + async (_label, type) => { + const result = await capture({ targetInfos: [workerTarget({ type })] }); + + expect(result.workers).toEqual([]); + expect(result.completeness.excluded).toContainEqual({ + section: "workers", + reason: "invalid_protocol_value", + count: 1, + }); + }, + ); + + it("keeps a decodable non-worker type out of the malformed ledger", async () => { + const result = await capture({ + targetInfos: [workerTarget({ type: "page" })], + }); + + expect(result.workers).toEqual([]); + expect(result.completeness.unavailable_sections).not.toContain("workers"); + expect(result.completeness.excluded).not.toContainEqual( + expect.objectContaining({ section: "workers" }), + ); + }); + + it("retains valid worker records beside malformed entries", async () => { + const result = await capture({ + targetInfos: [ + null, + targetWithoutType, + { url: localFileUrl, type: "worker", openerId: "electron-page" }, + workerTarget({ targetId: "worker-2" }), + ], + }); + + expect(result.workers.map((worker) => worker.target_id)).toEqual([ + "worker-2", + ]); + expect(result.completeness.unavailable_sections).toContain("workers"); + expect(result.completeness.excluded).toContainEqual({ + section: "workers", + reason: "invalid_protocol_value", + count: 3, + }); + expect(result.completeness.excluded).not.toContainEqual( + expect.objectContaining({ + section: "workers", + reason: "out_of_target_scope", + }), + ); + }); +}); diff --git a/src/browser/CdpElectronWorkers.ts b/src/browser/CdpElectronWorkers.ts index 06a26eaf3a..f9f2361bb7 100644 --- a/src/browser/CdpElectronWorkers.ts +++ b/src/browser/CdpElectronWorkers.ts @@ -4,7 +4,11 @@ import type { } from "../domain/javascript/electronObservation.js"; import type { CdpConnection } from "./CdpConnection.js"; import { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js"; -import { recordValue, cdpStringValue } from "./CdpCaptureValues.js"; +import { + admitRecords, + recordValue, + cdpStringValue, +} from "./CdpCaptureValues.js"; import type { CdpEndpointTarget } from "./CdpEndpoint.js"; import { optionalCdpCommand } from "./CdpOptionalCommand.js"; import { authorizedElectronFile } from "./ElectronFileScope.js"; @@ -36,18 +40,24 @@ export const captureElectronWorkers = async (input: { } const workers: ElectronPageInspection["workers"] = []; // A missing target list or a non-record entry is unreadable, not empty. - const targetInfos = recordValue(result)?.targetInfos; - if (!Array.isArray(targetInfos)) { + const targetInfos = admitRecords(recordValue(result)?.targetInfos); + if (targetInfos === undefined) { input.completeness.exclude("workers", "invalid_protocol_value", null); return []; } + let malformedEntries = 0; for (const entry of targetInfos) { const target = recordValue(entry); if (target === undefined) { - input.completeness.exclude("workers", "invalid_protocol_value"); + malformedEntries += 1; + continue; + } + // An undecodable discriminator cannot prove the entry is not a worker. + const type = cdpStringValue(target.type); + if (type === undefined || type.length === 0) { + malformedEntries += 1; continue; } - const type = cdpStringValue(target.type) ?? ""; if (!type.includes("worker")) continue; const openerTargetId = boundedTargetField(target.openerId); const parentFrameId = boundedTargetField(target.parentFrameId); @@ -65,11 +75,11 @@ export const captureElectronWorkers = async (input: { } const targetId = boundedTargetField(target.targetId); if (targetId === null) { - input.completeness.exclude("workers", "invalid_protocol_value"); + malformedEntries += 1; continue; } if (typeof target.attached !== "boolean") { - input.completeness.exclude("workers", "invalid_protocol_value"); + malformedEntries += 1; continue; } workers.push({ @@ -81,6 +91,12 @@ export const captureElectronWorkers = async (input: { parent_frame_id: parentFrameId, }); } + if (malformedEntries > 0) + input.completeness.exclude( + "workers", + "invalid_protocol_value", + malformedEntries, + ); return workers.sort((left, right) => compareUnicodeCodePoints(left.target_id, right.target_id), ); diff --git a/src/browser/CdpPageCaptureWorkers.test.ts b/src/browser/CdpPageCaptureWorkers.test.ts index 77768ad997..9936dec1e3 100644 --- a/src/browser/CdpPageCaptureWorkers.test.ts +++ b/src/browser/CdpPageCaptureWorkers.test.ts @@ -43,6 +43,22 @@ const capture = async (inventory: unknown) => { return { workers, completeness: events.completeness.snapshot() }; }; +/** A page-owned worker `TargetInfo` with no `type` discriminator at all. */ +const targetWithoutType = () => ({ + targetId: "worker-1", + url: `${origin}/worker.js`, + openerId: "page", + attached: true, +}); + +const workerTarget = ( + over: Readonly> = {}, +): Readonly> => ({ + ...targetWithoutType(), + type: "service_worker", + ...over, +}); + describe("web worker target inventory", () => { it("preserves an explicit empty inventory", async () => { const result = await capture({ targetInfos: [] }); @@ -74,13 +90,7 @@ describe("web worker target inventory", () => { const result = await capture({ targetInfos: [ null, - { - targetId: "worker-1", - type: "service_worker", - url: `${origin}/worker.js`, - attached: true, - openerId: "page", - }, + workerTarget(), { targetId: "worker-missing-url", type: "worker", @@ -124,4 +134,40 @@ describe("web worker target inventory", () => { }), ); }); + + it.each([ + ["absent", undefined], + ["null", null], + ["numeric", 42], + ["empty", ""], + ] as const)( + "records a target entry whose type is %s instead of deciding it is not a worker", + async (label, type) => { + const result = await capture({ + targetInfos: [ + label === "absent" ? targetWithoutType() : workerTarget({ type }), + ], + }); + + expect(result.workers).toEqual([]); + expect(result.completeness.unavailable_sections).toContain("workers"); + expect(result.completeness.excluded).toContainEqual({ + section: "workers", + reason: "invalid_protocol_value", + count: 1, + }); + }, + ); + + it("keeps a decodable non-worker type out of the malformed ledger", async () => { + const result = await capture({ + targetInfos: [workerTarget({ type: "page" })], + }); + + expect(result.workers).toEqual([]); + expect(result.completeness.unavailable_sections).not.toContain("workers"); + expect(result.completeness.excluded).not.toContainEqual( + expect.objectContaining({ section: "workers" }), + ); + }); }); From 6d1b0c5f0de4c4dc3dbf0f1f8ad6e3a9a8f4ece1 Mon Sep 17 00:00:00 2001 From: NIU-123370 <191000457+NIU-123370@users.noreply.github.com> Date: Sat, 17 Oct 2026 21:32:49 +0800 Subject: [PATCH 2/2] test(browser): keep the Electron worker fixture URL escaped Re-prefixing a decoded path with `file://` hands `authorizedElectronFile` a URL it rejects whenever the checkout path carries `%` or `#`, which would degrade the valid-worker cases into `out_of_target_scope`. `import.meta.url` is already the escaped form. Refs #3378. --- src/browser/CdpElectronWorkers.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/browser/CdpElectronWorkers.test.ts b/src/browser/CdpElectronWorkers.test.ts index 3d090704a0..58191440a1 100644 --- a/src/browser/CdpElectronWorkers.test.ts +++ b/src/browser/CdpElectronWorkers.test.ts @@ -1,5 +1,3 @@ -import { fileURLToPath } from "node:url"; - import { describe, expect, it } from "vitest"; import type { InspectElectronPageInput } from "../domain/javascript/electronObservation.js"; @@ -9,7 +7,7 @@ import type { CdpEndpointTarget } from "./CdpEndpoint.js"; import { captureElectronWorkers } from "./CdpElectronWorkers.js"; /** `authorizedElectronFile` only admits a file the host can resolve. */ -const localFileUrl = `file://${fileURLToPath(import.meta.url)}`; +const localFileUrl = import.meta.url; const workerTarget = ( over: Readonly> = {},