test(browser): preserve partial capture and ownership contracts

This commit is contained in:
morluto
2026-10-24 07:21:06 +08:00
parent 9d569c344a
commit 4becc220b8
10 changed files with 137 additions and 34 deletions
@@ -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(
@@ -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"),
@@ -155,6 +155,12 @@ describe.each<BodySource>(["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<BodySource>(["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),
@@ -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",
);
},
);
@@ -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",
+20
View File
@@ -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 } },
},
@@ -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",
@@ -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([
@@ -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);
@@ -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: {