mirror of
https://github.com/morluto/rea.git
synced 2026-11-05 06:15:59 +00:00
Restore source-position-aware ambient aliases, scalar env/argv reads, candidate endpoint worker data, Chrome DOMSnapshot empty strings, and accumulated Ghidra LaunchSupport diagnostics. Align regression fixtures with declared current contracts, while preserving identity, provenance, cancellation, and cleanup checks. Assert workflow caveats independently at both established locations. Validation: original five source failures reproduced on main; 307 focused tests passed with one platform skip, 22 fixture follow-up tests and 22 workflow/MCP tests passed; full static and docs checks passed. Real public http-server CLI analyses preserve identical artifact identity, statistics, and 581-token selected output. Real Chromium confirms restored DOM form destinations; isolated real service-worker verification passes on main and the change. The complete local browser run retained an unclassified later marker timeout; current-head CI provides complete real-browser and recovery lane results. Native IDA/Ghidra execution and complete local suite execution are not claimed. Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Co-authored-by: N0zoM1z0 <161784452+N0zoM1z0@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
210 lines
7.8 KiB
TypeScript
210 lines
7.8 KiB
TypeScript
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
|
import {
|
|
McpServer,
|
|
STDIO_DEFAULT_MAX_BUFFER_SIZE,
|
|
} from "@modelcontextprotocol/server";
|
|
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import { compareWebCaptureEvidence } from "../../../src/application/BrowserObservationService.js";
|
|
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
|
|
import { toolContract } from "../../../src/contracts/toolContracts.js";
|
|
import {
|
|
browserCaptureComparisonInputSchema,
|
|
compareBrowserCaptures,
|
|
} from "../../../src/domain/browserCaptureComparison.js";
|
|
import { inspectWebPageInputSchema } from "../../../src/domain/browserObservation.js";
|
|
import { browserScenarioCaptureSchema } from "../../../src/domain/browserScenarioCapture.js";
|
|
import { ToolResultDelivery } from "../../../src/server/toolResult.js";
|
|
import { toolRegistrationOptions } from "../../../src/server/toolRegistrationOptions.js";
|
|
import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
|
|
|
|
const delivery = new ToolResultDelivery(STDIO_DEFAULT_MAX_BUFFER_SIZE);
|
|
|
|
const contract = toolContract("compare_web_captures");
|
|
|
|
async function capturePassivePair(
|
|
provider: CdpBrowserProvider,
|
|
browser: Awaited<ReturnType<typeof startFakeCdpBrowser>>,
|
|
) {
|
|
const produced = await provider.inspectPage(
|
|
inspectWebPageInputSchema.parse({
|
|
cdp_endpoint: browser.endpoint,
|
|
target_id: "allowed-page",
|
|
allowed_origins: [browser.allowedOrigin, "https://private.example.test"],
|
|
observation_ms: 0,
|
|
include_json_body_shapes: true,
|
|
include_websocket_shapes: true,
|
|
}),
|
|
);
|
|
if (!produced.ok) throw produced.error;
|
|
// Capture order must not turn identical frame identities into a DOM change.
|
|
produced.value.frames = [null, browser.allowedOrigin, ""].map(
|
|
(origin, index) => ({
|
|
frame_id: String(index),
|
|
parent_frame_id: null,
|
|
url: "about:blank",
|
|
origin,
|
|
}),
|
|
);
|
|
const reordered = structuredClone(produced.value);
|
|
reordered.frames.reverse();
|
|
return {
|
|
before: { inspection: produced.value },
|
|
after: { inspection: reordered },
|
|
};
|
|
}
|
|
|
|
describe("browser comparison input boundary", () => {
|
|
it("advertises complete producer captures and agrees with SDK validation on both comparison families", async () => {
|
|
const browser = await startFakeCdpBrowser({ sensitiveShapes: true });
|
|
const server = new McpServer({ name: "capture-input", version: "0" });
|
|
const client = new Client({ name: "capture-input", version: "0" });
|
|
const [clientTransport, serverTransport] =
|
|
InMemoryTransport.createLinkedPair();
|
|
let calls = 0;
|
|
const provider = new CdpBrowserProvider();
|
|
server.registerTool(
|
|
contract.name,
|
|
toolRegistrationOptions(contract),
|
|
async (input) => {
|
|
calls += 1;
|
|
const compared = await compareWebCaptureEvidence(provider, input);
|
|
return compared.ok
|
|
? delivery.toEvidenceToolResult(compared.value, contract, undefined)
|
|
: delivery.toErrorToolResult(
|
|
compared.error,
|
|
contract.returnsTargetContent,
|
|
);
|
|
},
|
|
);
|
|
try {
|
|
await server.connect(serverTransport);
|
|
await client.connect(clientTransport);
|
|
const advertised = (await client.listTools()).tools.find(
|
|
({ name }) => name === contract.name,
|
|
);
|
|
if (advertised === undefined) throw new Error("Missing comparison tool");
|
|
expect(advertised.inputSchema.type).toBe("object");
|
|
// Anthropic tool input schemas reject root combinators.
|
|
expect(advertised.inputSchema).not.toHaveProperty("anyOf");
|
|
const ajv = new Ajv2020({ strict: false, validateFormats: false });
|
|
expect(ajv.validateSchema(advertised.inputSchema)).toBe(true);
|
|
const validate = ajv.compile(advertised.inputSchema);
|
|
const passive = await capturePassivePair(provider, browser);
|
|
const scenario = contract.examples[0]?.input;
|
|
if (scenario === undefined) throw new Error("Missing scenario example");
|
|
const withoutNormalization = Object.fromEntries(
|
|
Object.entries(scenario).filter(([key]) => key !== "normalization"),
|
|
);
|
|
const scenarioCapture = browserScenarioCaptureSchema.parse(
|
|
scenario.before_scenario,
|
|
);
|
|
const invalid = [
|
|
{},
|
|
{ before: passive.before },
|
|
{ before: { inspection: {} }, after: passive.after },
|
|
{
|
|
before: passive.before,
|
|
after: { ...passive.after, webmcp: {} },
|
|
},
|
|
{ before_scenario: {}, after_scenario: {} },
|
|
{ before_scenario: scenario.before_scenario },
|
|
{ ...passive, unexpected: true },
|
|
{
|
|
...scenario,
|
|
before_scenario: { ...scenarioCapture, steps: [{}] },
|
|
},
|
|
{
|
|
...scenario,
|
|
normalization: {
|
|
rules: [
|
|
{ rule_id: "bad", artifacts: [], match: "x", replacement: "y" },
|
|
],
|
|
},
|
|
},
|
|
];
|
|
// Strict clients reject combinators and dependentRequired, so the
|
|
// projection states the groups in its description and leaves group
|
|
// completeness and exclusion to the canonical runtime parser.
|
|
expect(advertised.inputSchema.description).toContain(
|
|
"before + after; before_scenario + after_scenario + [normalization]",
|
|
);
|
|
const mixedGroups = [
|
|
{ ...passive, ...scenario },
|
|
{ ...passive, normalization: { rules: [] } },
|
|
{
|
|
before_scenario: scenario.before_scenario,
|
|
normalization: { rules: [] },
|
|
},
|
|
{
|
|
after_scenario: scenario.after_scenario,
|
|
normalization: { rules: [] },
|
|
},
|
|
];
|
|
for (const { input, advertisedValid, runtimeValid } of [
|
|
...[passive, scenario, withoutNormalization].map((input) => ({
|
|
input,
|
|
advertisedValid: true,
|
|
runtimeValid: true,
|
|
})),
|
|
...invalid.map((input) => ({
|
|
input,
|
|
advertisedValid: false,
|
|
runtimeValid: false,
|
|
})),
|
|
...mixedGroups.map((input) => ({
|
|
input,
|
|
advertisedValid: true,
|
|
runtimeValid: false,
|
|
})),
|
|
]) {
|
|
expect(validate(input), JSON.stringify(validate.errors)).toBe(
|
|
advertisedValid,
|
|
);
|
|
expect(
|
|
browserCaptureComparisonInputSchema.safeParse(input).success,
|
|
).toBe(runtimeValid);
|
|
const beforeCalls = calls;
|
|
const result = await client.callTool({
|
|
name: contract.name,
|
|
arguments: input,
|
|
});
|
|
expect(calls - beforeCalls).toBe(runtimeValid ? 1 : 0);
|
|
expect(result.isError === true).toBe(!runtimeValid);
|
|
if (runtimeValid)
|
|
expect(result.structuredContent).toMatchObject({
|
|
operation: contract.name,
|
|
normalized_result: compareBrowserCaptures(
|
|
browserCaptureComparisonInputSchema.parse(input),
|
|
),
|
|
});
|
|
}
|
|
const parsedPassive = browserCaptureComparisonInputSchema.parse(passive);
|
|
expect(parsedPassive).toEqual({
|
|
before: { ...passive.before, webmcp: null },
|
|
after: { ...passive.after, webmcp: null },
|
|
});
|
|
expect(compareBrowserCaptures(parsedPassive)).toMatchObject({
|
|
dimensions: { dom_structure: { status: "unchanged" } },
|
|
});
|
|
expect(compareBrowserCaptures(parsedPassive).overall_status).toBe(
|
|
"unknown",
|
|
);
|
|
const parsedScenario =
|
|
browserCaptureComparisonInputSchema.parse(withoutNormalization);
|
|
expect(parsedScenario).toHaveProperty("normalization", { rules: [] });
|
|
expect(compareBrowserCaptures(parsedScenario)).toMatchObject({
|
|
comparison_kind: "browser_scenario",
|
|
overall_status: "unchanged",
|
|
});
|
|
} finally {
|
|
await Promise.allSettled([
|
|
client.close(),
|
|
server.close(),
|
|
browser.close(),
|
|
]);
|
|
}
|
|
});
|
|
});
|