diff --git a/tests/boundary/browser/cdpBrowserProvider.document_script.test.ts b/tests/boundary/browser/cdpBrowserProvider.document_script.test.ts index 07c867c9df..e843ed9694 100644 --- a/tests/boundary/browser/cdpBrowserProvider.document_script.test.ts +++ b/tests/boundary/browser/cdpBrowserProvider.document_script.test.ts @@ -381,11 +381,11 @@ it.each( reason: "invalid_protocol_value", count: 1, }); - expect( - result.value.completeness.attach_limited_sections.includes( - "webmcp_tools", - ), - ).toBe(scope !== "foreign"); + // Initial allowed-child replay coverage remains unknown even when a + // later event positively identifies that child's destination as foreign. + expect(result.value.completeness.attach_limited_sections).toContain( + "webmcp_tools", + ); expect( result.value.completeness.policy_filtered_sections.includes( "webmcp_tools", @@ -483,7 +483,7 @@ it.each( result.value.completeness.attach_limited_sections.includes( "webmcp_tools", ), - ).toBe(uncertain); + ).toBe(uncertain || valid); expect( result.value.completeness.policy_filtered_sections.includes( "webmcp_tools", @@ -523,11 +523,13 @@ describe("CdpBrowserProvider: transient WebMCP frames", () => { expect(result.value.completeness.attach_limited_sections).toContain( "webmcp_tools", ); - expect( - result.value.completeness.excluded.find( - ({ section }) => section === "webmcp_tools", - )?.count, - ).toBe(1); + // Initial foreign frame and its replay declaration are two exclusions; + // their count does not describe the transient allowed child. + expect(result.value.completeness.excluded).toContainEqual({ + section: "webmcp_tools", + reason: "out_of_target_scope", + count: 2, + }); }); it("accepts fresh child registrations after a stable frame commit", async () => { @@ -551,7 +553,9 @@ describe("CdpBrowserProvider: transient WebMCP frames", () => { expect(result.value.tools.items).toContainEqual( expect.objectContaining({ name: "child_tool" }), ); - expect(result.value.completeness.attach_limited_sections).not.toContain( + // Fresh registration restores this tool, but cannot recover pre-attach + // declarations from the initial allowed child frame. + expect(result.value.completeness.attach_limited_sections).toContain( "webmcp_tools", ); }); @@ -864,11 +868,43 @@ describe("CdpBrowserProvider WebMCP schema comparison", () => { $ref: "https://untrusted.invalid/schema.json", examples: [{ token: "caller-schema-value" }], }; - const options: { webMcpTools: boolean; webMcpInputSchema: JsonValue } = { - webMcpTools: true, - webMcpInputSchema: declaration, - }; - const browser = await startFakeCdpBrowser(options); + let inputSchema: JsonValue = declaration; + const browser = await startFakeCdpBrowser({ + // A root-only replay supplies complete owner correspondence. Foreign + // filtering and pre-existing child coverage are tested separately above. + eventsBeforeReply: ["WebMCP.enable"], + commandEvents: ({ method, sessionId }) => + method === "WebMCP.enable" + ? [ + { + method: "WebMCP.toolsAdded", + ...(sessionId === undefined ? {} : { sessionId }), + params: { + tools: [ + { + name: "schema_tool", + frameId: "frame-main", + inputSchema, + annotations: { consequential: false }, + }, + ], + }, + }, + ] + : undefined, + commandResult: ({ method }, origin) => + method === "Page.getFrameTree" + ? { + frameTree: { + frame: { + id: "frame-main", + loaderId: "loader-main", + url: `${origin}/app`, + }, + }, + } + : undefined, + }); trackBrowser(browser); const provider = new CdpBrowserProvider(); const input = { @@ -895,7 +931,7 @@ describe("CdpBrowserProvider WebMCP schema comparison", () => { trust: "page-declared-untrusted", }); const compare = async (schema: JsonValue) => { - options.webMcpInputSchema = schema; + inputSchema = schema; const after = await discover(); const diff = compareWebCaptures( compareWebCapturesInputSchema.parse({ @@ -908,7 +944,7 @@ describe("CdpBrowserProvider WebMCP schema comparison", () => { const reordered = Object.fromEntries(Object.entries(declaration).reverse()); const equivalent = await compare(reordered); expect(equivalent.dimension).toMatchObject({ - status: "unknown", + status: "unchanged", total_changes: 0, }); expect(equivalent.after.tools.items[0]?.input_schema_sha256).toBe( diff --git a/tests/boundary/browser/cdpElectronProvider.test.ts b/tests/boundary/browser/cdpElectronProvider.test.ts index 1c447e6a2e..e4ca0f6886 100644 --- a/tests/boundary/browser/cdpElectronProvider.test.ts +++ b/tests/boundary/browser/cdpElectronProvider.test.ts @@ -1108,7 +1108,9 @@ it.each( expect(result.value.scripts.items).toContainEqual( expect.objectContaining({ cdp_hash: id, - frame_id: parent === "valid" ? id : null, + frame_id: id, + frame_scope: parent === "valid" ? "authorized" : "unknown", + frame_claims: { execution_context_frame_id: id }, }), ); if (parent !== "valid") { @@ -1239,7 +1241,8 @@ it.each( expect(result.value.scripts.items).toContainEqual( expect.objectContaining({ cdp_hash: "local", - frame_id: phase === "invalidated" ? null : "local", + frame_id: "local", + frame_scope: phase === "invalidated" ? "unknown" : "authorized", }), ); expect(result.value.completeness.policy_filtered_sections).toContain( @@ -1476,17 +1479,23 @@ it.each([ expect.objectContaining({ cdp_hash: "before", frame_id: "child" }), expect.objectContaining({ cdp_hash: "after-replay", - frame_id: expectedOwner ? "child" : null, + frame_id: "child", + frame_scope: expectedOwner ? "authorized" : "unknown", }), expect.objectContaining({ cdp_hash: "descendant", - frame_id: expectedOwner ? "grandchild" : null, + frame_id: "grandchild", + frame_scope: expectedOwner ? "authorized" : "unknown", }), ]), ); if (kind === "post-detach") expect(result.value.scripts.items).toContainEqual( - expect.objectContaining({ cdp_hash: "after-detach", frame_id: null }), + expect.objectContaining({ + cdp_hash: "after-detach", + frame_id: "child", + frame_scope: "unknown", + }), ); expect( result.value.completeness.attach_limited_sections.includes("scripts"), diff --git a/tests/boundary/browser/cdpJsonShapeComparison.test.ts b/tests/boundary/browser/cdpJsonShapeComparison.test.ts index 8e9ea57615..4c9b747cd4 100644 --- a/tests/boundary/browser/cdpJsonShapeComparison.test.ts +++ b/tests/boundary/browser/cdpJsonShapeComparison.test.ts @@ -155,6 +155,12 @@ describe.each(["request", "response", "base64 response"])( before + " ".repeat(length - Buffer.byteLength(before)), after + " ".repeat(length - Buffer.byteLength(after)), ); + expect( + (await compareCaptures(...captures)).dimensions.network, + ).toMatchObject({ + status: "unknown", + total_changes: 0, + }); const result = await compareCaptures( completeNormalizedCapture(captures[0]), completeNormalizedCapture(captures[1]), @@ -181,6 +187,12 @@ describe.each(["request", "response", "base64 response"])( Buffer.byteLength(beforeBody) - Buffer.byteLength(changedBody), ); const [before, after] = comparableCaptures(source, beforeBody, afterBody); + expect( + (await compareCaptures(before, after)).dimensions.network, + ).toMatchObject({ + status: "unknown", + total_changes: 0, + }); const result = await compareCaptures( completeNormalizedCapture(before), completeNormalizedCapture(after), diff --git a/tests/boundary/browser/cdpResourceCompleteness.test.ts b/tests/boundary/browser/cdpResourceCompleteness.test.ts index c392617fc0..09f12feb48 100644 --- a/tests/boundary/browser/cdpResourceCompleteness.test.ts +++ b/tests/boundary/browser/cdpResourceCompleteness.test.ts @@ -215,7 +215,10 @@ describe("resource inventory omissions through CDP", () => { expect(capture.completeness.unavailable_sections).not.toContain( "resources", ); - expect(compareResources(capture, capture).status).toBe("unchanged"); + // Omitted contentSize is valid metadata, but cannot prove equal resource size. + expect(compareResources(capture, capture).status).toBe( + size === null ? "unknown" : "unchanged", + ); }, ); diff --git a/tests/boundary/browser/cdpWebMcpTransientIdentity.test.ts b/tests/boundary/browser/cdpWebMcpTransientIdentity.test.ts index 45d9026ee0..2427b12346 100644 --- a/tests/boundary/browser/cdpWebMcpTransientIdentity.test.ts +++ b/tests/boundary/browser/cdpWebMcpTransientIdentity.test.ts @@ -194,9 +194,11 @@ it.each([ expect( result.value.completeness.policy_filtered_sections.includes("webmcp_tools"), ).toBe(rawUrl !== "allowed"); - expect(result.value.completeness.attach_limited_sections).not.toContain( - "webmcp_tools", - ); + // The initial allowed child may have unreported pre-attach registrations. + // A literal #about:blank fragment does not make that coverage complete. + expect( + result.value.completeness.attach_limited_sections.includes("webmcp_tools"), + ).toBe(rawUrl === "allowed"); if (rawUrl === "allowed") expect(result.value.tools.items[0]?.frame_url).toBe( browser.allowedOrigin + "/child#about:blank", diff --git a/tests/boundary/cli/browserCli.test.ts b/tests/boundary/cli/browserCli.test.ts index a8b63ca643..6f0023f52b 100644 --- a/tests/boundary/cli/browserCli.test.ts +++ b/tests/boundary/cli/browserCli.test.ts @@ -222,6 +222,26 @@ it( redirectCapture(inspection, browser.allowedOrigin, 307), ); expect(knownDifference).toMatchObject({ + normalized_result: { + dimensions: { network: { status: "unknown", total_changes: 0 } }, + }, + }); + // A closed imported request inventory supplies the missing directional + // proof; two attachment-limited samples cannot pair repeated requests. + const closed = redirectCapture(inspection, browser.allowedOrigin, 302); + if (!isRecord(closed.inspection.completeness)) + throw new Error("Missing capture completeness"); + const limited = closed.inspection.completeness.attach_limited_sections; + if (!Array.isArray(limited)) throw new Error("Missing attachment coverage"); + closed.inspection.completeness.attach_limited_sections = limited.filter( + (section) => section !== "network_requests", + ); + expect( + await compareCapturesCli( + closed, + redirectCapture(inspection, browser.allowedOrigin, 307), + ), + ).toMatchObject({ normalized_result: { dimensions: { network: { status: "changed", total_changes: 1 } }, }, diff --git a/tests/boundary/cli/httpCaptureComparison.test.ts b/tests/boundary/cli/httpCaptureComparison.test.ts index 10d3733518..e49d5758d3 100644 --- a/tests/boundary/cli/httpCaptureComparison.test.ts +++ b/tests/boundary/cli/httpCaptureComparison.test.ts @@ -42,18 +42,27 @@ it("CLI compares incomplete HTTP responses without inventing changes", async () expect(finished.network.requests).toMatchObject([ { status: 200, mime_type: "application/json", encoded_data_length: 100 }, ]); - const paths = [pending, finished, changed].map((_, index) => + // Passive attachment cannot establish the complete same-URL request + // inventory. Closed imported captures can, and retain the positive oracle. + const closed = structuredClone(pending); + closed.completeness.attach_limited_sections = + closed.completeness.attach_limited_sections.filter( + (section) => section !== "network_requests", + ); + const captures = [pending, finished, changed, closed]; + const paths = captures.map((_, index) => join(root, `capture-${String(index)}.json`), ); await Promise.all( - [pending, finished, changed].map((inspection, index) => + captures.map((inspection, index) => writeFile(paths[index]!, JSON.stringify({ inspection })), ), ); for (const [left, right, status, count] of [ [0, 1, "unknown", 0], [1, 0, "unknown", 0], - [0, 2, "changed", 1], + [0, 2, "unknown", 0], + [3, 2, "changed", 1], ] as const) { const compared = await runCliJson([ "compare-web-captures", diff --git a/tests/boundary/mcp/browserComparisonInput.test.ts b/tests/boundary/mcp/browserComparisonInput.test.ts index 4930e81b43..fd15f96294 100644 --- a/tests/boundary/mcp/browserComparisonInput.test.ts +++ b/tests/boundary/mcp/browserComparisonInput.test.ts @@ -215,9 +215,11 @@ describe("browser comparison input boundary", () => { const parsedScenario = browserCaptureComparisonInputSchema.parse(withoutNormalization); expect(parsedScenario).toHaveProperty("normalization", { rules: [] }); + // The public example explicitly marks events missing and equality + // ineligible; normalization defaults cannot restore uncaptured evidence. expect(compareBrowserCaptures(parsedScenario)).toMatchObject({ comparison_kind: "browser_scenario", - overall_status: "unchanged", + overall_status: "unknown", }); } finally { await Promise.allSettled([ diff --git a/tests/boundary/process/browserScenarioProbeLifecycle.test.ts b/tests/boundary/process/browserScenarioProbeLifecycle.test.ts index 52960f90ae..9afbef027a 100644 --- a/tests/boundary/process/browserScenarioProbeLifecycle.test.ts +++ b/tests/boundary/process/browserScenarioProbeLifecycle.test.ts @@ -156,7 +156,17 @@ it("closes optional providers when MCP server startup throws", async () => { }), }); - expect(transport).toEqual({ ok: false }); + expect(transport).toMatchObject({ + ok: false, + cancelled: false, + handle: { close: expect.any(Function) }, + closeAndroid: expect.any(Function), + closeOptionalProviders: expect.any(Function), + }); + // Failed admission still returns partial owners for idempotent retries. + await transport.handle.close(); + await transport.closeOptionalProviders(); + await transport.closeAndroid(); expect(browserClose).toHaveBeenCalledTimes(1); expect(scenarioClose).toHaveBeenCalledTimes(1); expect(electronClose).toHaveBeenCalledTimes(1); diff --git a/tests/process-global/browserScenarioCleanup.test.ts b/tests/process-global/browserScenarioCleanup.test.ts index 5c1c0f2e20..ff7a2af357 100644 --- a/tests/process-global/browserScenarioCleanup.test.ts +++ b/tests/process-global/browserScenarioCleanup.test.ts @@ -439,7 +439,7 @@ it("preserves cancellation diagnostics while retrying retained cleanup", async ( const result = await serveScenario(); expect(result.output).toMatchObject({ code: "cleanup_incomplete", - category: "execution_failure", + category: "cancelled", details: { cleanup: "incomplete", diagnostics: {