Preserve resource uncertainty through production MCP registration and the real SDK. Test surviving resource fields/exclusions, forward/reverse/self comparisons, and complete-empty controls. Production behavior is unchanged. Baron approved the unchanged head 07ea877a388ee2cd0586f04b1fb75bd104b484be; current required CI passes. Original contribution by @ShuaixinHuang. Co-authored-by: ShuaixinHuang <1836656514@qq.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
470 lines
15 KiB
TypeScript
470 lines
15 KiB
TypeScript
import { writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { describe, expect, it, onTestFinished } from "vitest";
|
|
|
|
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
|
|
import { inspectWebPageInputSchema } from "../../../src/domain/browserObservation.js";
|
|
import {
|
|
webPageInspectionSchema,
|
|
type WebPageInspection,
|
|
} from "../../../src/domain/browserObservationSchemas.js";
|
|
import { parseEvidence } from "../../../src/domain/evidence.js";
|
|
import { compareWebCaptures } from "../../../src/domain/webCaptureDiff.js";
|
|
import { compareWebCapturesInputSchema } from "../../../src/domain/webCaptureDiffSchemas.js";
|
|
import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
|
|
import { runCliJson } from "../../fixtures/cliJsonProcess.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { trackBrowser } from "./cdpBrowserProvider.support.js";
|
|
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
|
|
|
|
type ResourceMode =
|
|
| "valid"
|
|
| "missing"
|
|
| "null"
|
|
| "wrong-type"
|
|
| "invalid-entry"
|
|
| "child-missing"
|
|
| "child-invalid-entry"
|
|
| "child-wrong-type"
|
|
| "empty";
|
|
|
|
const startBrowser = async () => {
|
|
const state = {
|
|
mode: "valid" as ResourceMode,
|
|
peerFields: undefined as Record<string, unknown> | undefined,
|
|
};
|
|
const browser = await startFakeCdpBrowser({
|
|
commandResult(command, origin) {
|
|
const malformedChildren =
|
|
state.mode === "child-invalid-entry" ||
|
|
state.mode === "child-wrong-type";
|
|
if (command.method === "Page.getFrameTree" && malformedChildren)
|
|
return {
|
|
frameTree: {
|
|
frame: { id: "frame-main", url: origin + "/app" },
|
|
childFrames:
|
|
state.mode === "child-wrong-type"
|
|
? {}
|
|
: [
|
|
{
|
|
frame: {
|
|
id: "child",
|
|
parentId: "frame-main",
|
|
url: origin + "/child",
|
|
},
|
|
},
|
|
null,
|
|
],
|
|
},
|
|
};
|
|
if (command.method !== "Page.getResourceTree") return undefined;
|
|
const resource = (name: string) => ({
|
|
url: origin + "/" + name,
|
|
type: "Script",
|
|
mimeType: "text/javascript",
|
|
contentSize: 128,
|
|
...(name === "peer.js" ? state.peerFields : undefined),
|
|
});
|
|
let resources: unknown = [resource("keep.js"), resource("peer.js")];
|
|
if (state.mode === "missing") resources = undefined;
|
|
if (state.mode === "null") resources = null;
|
|
if (state.mode === "wrong-type") resources = {};
|
|
if (state.mode === "invalid-entry")
|
|
resources = [resource("keep.js"), null];
|
|
if (state.mode === "empty") resources = [];
|
|
const malformedChildFrames =
|
|
state.mode === "child-wrong-type"
|
|
? {}
|
|
: state.mode === "child-invalid-entry"
|
|
? [
|
|
{
|
|
frame: {
|
|
id: "child",
|
|
parentId: "frame-main",
|
|
url: origin + "/child",
|
|
},
|
|
resources: [resource("child.js")],
|
|
},
|
|
null,
|
|
]
|
|
: undefined;
|
|
return {
|
|
frameTree: {
|
|
frame: { id: "frame-main", url: origin + "/app" },
|
|
resources,
|
|
...(malformedChildFrames === undefined
|
|
? {}
|
|
: { childFrames: malformedChildFrames }),
|
|
...(state.mode === "child-missing"
|
|
? {
|
|
childFrames: [
|
|
{
|
|
frame: {
|
|
id: "child",
|
|
parentId: "frame-main",
|
|
url: origin + "/child",
|
|
},
|
|
},
|
|
],
|
|
}
|
|
: {}),
|
|
},
|
|
};
|
|
},
|
|
});
|
|
trackBrowser(browser);
|
|
const inspect = async () => {
|
|
const result = await new CdpBrowserProvider().inspectPage(
|
|
inspectWebPageInputSchema.parse({
|
|
cdp_endpoint: browser.endpoint,
|
|
allowed_origins: [browser.allowedOrigin],
|
|
target_id: "allowed-page",
|
|
observation_ms: 0,
|
|
}),
|
|
);
|
|
if (!result.ok) throw result.error;
|
|
return result.value;
|
|
};
|
|
return {
|
|
state,
|
|
inspect,
|
|
origin: browser.allowedOrigin,
|
|
endpoint: browser.endpoint,
|
|
};
|
|
};
|
|
|
|
const compareResources = (
|
|
before: WebPageInspection,
|
|
after: WebPageInspection,
|
|
) =>
|
|
compareWebCaptures(
|
|
compareWebCapturesInputSchema.parse({
|
|
before: { inspection: before },
|
|
after: { inspection: after },
|
|
}),
|
|
).dimensions.resources;
|
|
|
|
describe("resource inventory omissions through CDP", () => {
|
|
it.each([
|
|
{ fields: { type: undefined }, retained: false },
|
|
{ fields: { type: null }, retained: false },
|
|
{ fields: { type: 17 }, retained: false },
|
|
{ fields: { type: "" }, retained: false },
|
|
{ fields: { mimeType: undefined }, retained: false },
|
|
{ fields: { mimeType: null }, retained: false },
|
|
{ fields: { mimeType: 17 }, retained: false },
|
|
{ fields: { contentSize: -1 }, retained: true },
|
|
{ fields: { contentSize: null }, retained: true },
|
|
{ fields: { contentSize: "128" }, retained: true },
|
|
])(
|
|
"keeps malformed resource fields uncertain: $fields",
|
|
async ({ fields, retained }) => {
|
|
const { state, inspect, origin } = await startBrowser();
|
|
const before = await inspect();
|
|
state.peerFields = fields;
|
|
const after = await inspect();
|
|
for (const [left, right] of [
|
|
[before, after],
|
|
[after, before],
|
|
[after, after],
|
|
] as const)
|
|
expect(compareResources(left, right)).toMatchObject({
|
|
status: "unknown",
|
|
total_changes: 0,
|
|
changes: [],
|
|
});
|
|
expect(after.completeness.excluded).toContainEqual({
|
|
section: "resources",
|
|
reason: "invalid_protocol_value",
|
|
count: 1,
|
|
});
|
|
expect(after.resources.map(({ url }) => url).sort()).toEqual(
|
|
retained
|
|
? [origin + "/keep.js", origin + "/peer.js"]
|
|
: [origin + "/keep.js"],
|
|
);
|
|
if (retained)
|
|
expect(
|
|
after.resources.find(({ url }) => url.endsWith("/peer.js")),
|
|
).toMatchObject({
|
|
type: "Script",
|
|
mime_type: "text/javascript",
|
|
content_size: null,
|
|
});
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
{
|
|
fields: { type: "Other", mimeType: "", contentSize: undefined },
|
|
size: null,
|
|
},
|
|
{ fields: { contentSize: 0 }, size: 0 },
|
|
{ fields: { contentSize: 1.5 }, size: 1.5 },
|
|
])(
|
|
"accepts valid optional and empty fields: $fields",
|
|
async ({ fields, size }) => {
|
|
const { state, inspect } = await startBrowser();
|
|
state.peerFields = fields;
|
|
const capture = await inspect();
|
|
expect(capture.resources).toHaveLength(2);
|
|
expect(
|
|
capture.resources.find(({ url }) => url.endsWith("/peer.js"))
|
|
?.content_size,
|
|
).toBe(size);
|
|
expect(capture.completeness.unavailable_sections).not.toContain(
|
|
"resources",
|
|
);
|
|
expect(compareResources(capture, capture).status).toBe("unchanged");
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
"missing",
|
|
"null",
|
|
"wrong-type",
|
|
"invalid-entry",
|
|
"child-missing",
|
|
"child-invalid-entry",
|
|
"child-wrong-type",
|
|
] as const)(
|
|
"does not claim a resource removal or equality for %s",
|
|
async (mode) => {
|
|
const { state, inspect, origin } = await startBrowser();
|
|
const before = await inspect();
|
|
expect(before.resources).toHaveLength(2);
|
|
expect(before.completeness.unavailable_sections).not.toContain(
|
|
"resources",
|
|
);
|
|
state.mode = mode;
|
|
const after = await inspect();
|
|
for (const [left, right] of [
|
|
[before, after],
|
|
[after, before],
|
|
[after, after],
|
|
] as const)
|
|
expect(compareResources(left, right)).toMatchObject({
|
|
status: "unknown",
|
|
total_changes: 0,
|
|
changes: [],
|
|
});
|
|
expect(after.completeness.unavailable_sections).toContain("resources");
|
|
expect(after.completeness.excluded).toContainEqual({
|
|
section: "resources",
|
|
reason: "invalid_protocol_value",
|
|
count: mode === "invalid-entry" ? 1 : null,
|
|
});
|
|
if (mode === "child-invalid-entry" || mode === "child-wrong-type") {
|
|
expect(after.completeness.excluded).toContainEqual({
|
|
section: "frames",
|
|
reason: "invalid_protocol_value",
|
|
count: null,
|
|
});
|
|
expect(after.frames.map(({ frame_id }) => frame_id)).toContain(
|
|
"frame-main",
|
|
);
|
|
if (mode === "child-invalid-entry")
|
|
expect(after.frames.map(({ frame_id }) => frame_id)).toContain(
|
|
"child",
|
|
);
|
|
}
|
|
const expected =
|
|
mode === "invalid-entry"
|
|
? [origin + "/keep.js"]
|
|
: mode === "child-invalid-entry"
|
|
? [origin + "/child.js", origin + "/keep.js", origin + "/peer.js"]
|
|
: mode === "child-missing"
|
|
? [origin + "/keep.js", origin + "/peer.js"]
|
|
: mode === "child-wrong-type"
|
|
? [origin + "/keep.js", origin + "/peer.js"]
|
|
: [];
|
|
expect(after.resources.map(({ url }) => url).sort()).toEqual(expected);
|
|
},
|
|
);
|
|
|
|
it("distinguishes a complete empty inventory from an unavailable collection", async () => {
|
|
const { state, inspect } = await startBrowser();
|
|
const before = await inspect();
|
|
state.mode = "empty";
|
|
const after = await inspect();
|
|
expect(after.resources).toEqual([]);
|
|
expect(after.completeness.unavailable_sections).not.toContain("resources");
|
|
expect(compareResources(before, after)).toMatchObject({
|
|
status: "changed",
|
|
total_changes: 2,
|
|
changes: [{ change: "removed" }, { change: "removed" }],
|
|
});
|
|
expect(compareResources(after, after).status).toBe("unchanged");
|
|
});
|
|
});
|
|
|
|
describe("resource uncertainty in production MCP registration", () => {
|
|
it("delivers partial resources and preserves comparison uncertainty through the SDK", async () => {
|
|
const { state, origin, endpoint } = await startBrowser();
|
|
const harness = await createApplicationMcpHarness({
|
|
serverOptions: { browserObservation: new CdpBrowserProvider() },
|
|
});
|
|
onTestFinished(harness.close);
|
|
const capture = async () => {
|
|
const reply = await harness.client.callTool({
|
|
name: "inspect_web_page",
|
|
arguments: {
|
|
cdp_endpoint: endpoint,
|
|
allowed_origins: [origin],
|
|
target_id: "allowed-page",
|
|
observation_ms: 0,
|
|
},
|
|
});
|
|
expect(reply.isError).not.toBe(true);
|
|
expect(reply.structuredContent).toMatchObject({
|
|
operation: "inspect_web_page",
|
|
});
|
|
return webPageInspectionSchema.parse(
|
|
parseEvidence(reply.structuredContent).normalized_result,
|
|
);
|
|
};
|
|
const expectedResource = (
|
|
name: string,
|
|
contentSize: number | null = 128,
|
|
) => ({
|
|
resource_key: expect.any(String),
|
|
url: `${origin}/${name}`,
|
|
origin,
|
|
type: "Script",
|
|
mime_type: "text/javascript",
|
|
content_size: contentSize,
|
|
});
|
|
const resourceExclusions = (inspection: WebPageInspection) =>
|
|
inspection.completeness.excluded.filter(
|
|
({ section }) => section === "resources",
|
|
);
|
|
const before = await capture();
|
|
expect(
|
|
before.resources.toSorted((left, right) =>
|
|
left.url.localeCompare(right.url),
|
|
),
|
|
).toEqual([expectedResource("keep.js"), expectedResource("peer.js")]);
|
|
expect(before.completeness.unavailable_sections).not.toContain("resources");
|
|
expect(resourceExclusions(before)).toEqual([]);
|
|
for (const { mode, fields, resources, excludedCount } of [
|
|
{
|
|
mode: "missing",
|
|
fields: undefined,
|
|
resources: [],
|
|
excludedCount: null,
|
|
},
|
|
{
|
|
mode: "invalid-entry",
|
|
fields: undefined,
|
|
resources: [expectedResource("keep.js")],
|
|
excludedCount: 1,
|
|
},
|
|
{
|
|
mode: "valid",
|
|
fields: { type: undefined },
|
|
resources: [expectedResource("keep.js")],
|
|
excludedCount: 1,
|
|
},
|
|
{
|
|
mode: "valid",
|
|
fields: { contentSize: -1 },
|
|
resources: [
|
|
expectedResource("keep.js"),
|
|
expectedResource("peer.js", null),
|
|
],
|
|
excludedCount: 1,
|
|
},
|
|
{ mode: "empty", fields: undefined, resources: [], excludedCount: 0 },
|
|
] as const) {
|
|
state.mode = mode;
|
|
state.peerFields = fields;
|
|
const after = await capture();
|
|
expect(
|
|
after.resources.toSorted((left, right) =>
|
|
left.url.localeCompare(right.url),
|
|
),
|
|
).toEqual(resources);
|
|
expect(resourceExclusions(after)).toEqual(
|
|
mode === "empty"
|
|
? []
|
|
: [
|
|
{
|
|
section: "resources",
|
|
reason: "invalid_protocol_value",
|
|
count: excludedCount,
|
|
},
|
|
],
|
|
);
|
|
if (mode !== "empty")
|
|
expect(after.completeness.unavailable_sections).toContain("resources");
|
|
else
|
|
expect(after.completeness.unavailable_sections).not.toContain(
|
|
"resources",
|
|
);
|
|
for (const [left, right] of [
|
|
[before, after],
|
|
[after, before],
|
|
[after, after],
|
|
] as const) {
|
|
const reply = await harness.client.callTool({
|
|
name: "compare_web_captures",
|
|
arguments: {
|
|
before: { inspection: left },
|
|
after: { inspection: right },
|
|
},
|
|
});
|
|
expect(reply.isError).not.toBe(true);
|
|
expect(reply.structuredContent).toMatchObject({
|
|
operation: "compare_web_captures",
|
|
normalized_result: {
|
|
dimensions: {
|
|
resources: {
|
|
status:
|
|
mode !== "empty"
|
|
? "unknown"
|
|
: left === right
|
|
? "unchanged"
|
|
: "changed",
|
|
total_changes: mode === "empty" && left !== right ? 2 : 0,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("resource uncertainty in public CLI Evidence", () => {
|
|
it("preserves partial membership while complete inventories still prove removal", async () => {
|
|
const { state, inspect } = await startBrowser();
|
|
const root = await createTestTempDirectory("rea-resource-comparison-");
|
|
const before = join(root, "before.json");
|
|
const after = join(root, "after.json");
|
|
await writeFile(before, JSON.stringify({ inspection: await inspect() }));
|
|
for (const { mode, fields } of [
|
|
{ mode: "invalid-entry", fields: undefined },
|
|
{ mode: "valid", fields: { type: undefined } },
|
|
{ mode: "valid", fields: { contentSize: -1 } },
|
|
{ mode: "empty", fields: undefined },
|
|
] as const) {
|
|
state.mode = mode;
|
|
state.peerFields = fields;
|
|
await writeFile(after, JSON.stringify({ inspection: await inspect() }));
|
|
expect(
|
|
await runCliJson(["compare-web-captures", before, after, "--json"]),
|
|
).toMatchObject({
|
|
operation: "compare_web_captures",
|
|
normalized_result: {
|
|
dimensions: {
|
|
resources: {
|
|
status: mode === "empty" ? "changed" : "unknown",
|
|
total_changes: mode === "empty" ? 2 : 0,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
});
|
|
});
|