mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 19:23:24 +00:00
* test(mcp): check advertised schemas against a portable dialect in every profile * fix(mcp): re-emit advertised patterns in a portable regex grammar * fix(mcp): present discover_tools input through the selected profile The hot-cold catalog registered discover_tools directly with the SDK, so the default compact input profile still advertised its canonical schema, including page-size defaults clients apply on their own (#5322). Route every listed input schema, transport discovery included, through one server-owned presentation, and pass the selected presentation to the deferred server, which serves recorded tool schemas but not discovery. * fix(mcp): keep the Evidence union on described borrowed inputs borrowEvidenceInput installed its advertised projection on one schema instance. A call site's .describe() rebuilds the schema from its definition, so match_native_functions advertised left and right as unconstrained values in every input profile. Install the projection in the borrowed parser's constructor so derived schemas keep it, for both borrowed inputs and borrowed arrays. * fix(mcp): validate advertised dialect by selected profile * fix(mcp): preserve schema widening through references * feat(mcp): project compact input schemas into the portable advertised dialect One projection (advertisedDialectProjection.ts) owns the compact input advertisement: one type per node, items on every array, string literals as enums, oneOf widened to anyOf, allOf merged, tuples as element unions, exclusive bounds made inclusive, and every keyword outside the dialect dropped. Each rewrite is exact or widening; server validation is unchanged. The root description and sentences for dropped cross-field constraints move to the tool description, which Codex and Zed keep. The compact baseline falls to nullable values only (29), which the dialect cannot express without narrowing an input. The gateway shape repairs (typeArrayItemUnions, hasGatewayIncompatibleShape), the second dependentRequired-to-prose copy and the strict-gateway test denylist are removed; the dialect checker gates the inline presentation instead. * feat(mcp): omit output schemas from tools/list by default No surveyed model API reads an output schema, MCP client validators repeat REA's own result validation, and complete output schemas were about 92% of the default tools/list. REA_MCP_OUTPUT_SCHEMA_PROFILE now resolves to off through one owner (resolveMcpOutputSchemaProfile); compact and full remain selectable. structuredContent is still delivered, and the deferred server serves both off and full from the recorded catalog with the matching recorded identity. Tests and artifact verifiers that validate results against complete advertised output contracts select full explicitly. * docs: describe omitted output schemas as the default in README translations * fix(mcp): select schemas in verification workflows * fix(mcp): refuse zero-padded repeat bounds and record host-specific path patterns * fix(mcp): keep projected schemas permissive * fix(mcp): keep references valid when presentation widens their targets * fix(mcp): keep a single union branch's closure scoped to its properties * fix(mcp): select complete output schemas in the Electron verifier * fix(mcp): select complete output schemas in standalone verifier probes * fix(mcp): keep an allOf member's tuple tail with its own positions * fix(mcp): relax references into negations and conditionals presentation may remove * refactor(contracts): declare root discriminators as typed schema annotations * test(mcp): select the borrowed Evidence contract from its family * fix(mcp): keep astral literals and dynamic references widening in every mode * fix(mcp): omit astral literals and relax untracked references when presentation widens * fix(mcp): select complete output schemas in every verifier transport * fix(mcp): read reference pointers by schema position, not segment spelling * fix(mcp): protect references into collapsing boolean unions and keep referencing branches out of the collapse * fix(mcp): keep reduced schemas open when their root admits names by pattern * test(mcp): keep the reduced-schema pattern case outside the size-limited block * fix(mcp): omit patterns whose classes name astral characters * fix(mcp): decide union collapse and pointer protection from canonical branches * docs(mcp): document non-u astral matching and pin BMP agreement across V8 modes * fix(mcp): keep prototype-named properties as own properties in the dialect projection * test(mcp): select full output schemas in packet and signal schema checks * test(mcp): expect the advertised base64 schema to omit the unbounded pad-bit probe * fix(mcp): protect references into unevaluated keywords lost evaluation removes * fix(mcp): validate the doctor probe reply against the canonical output contract (#10655) * fix(mcp): validate the doctor probe reply against the canonical output contract * test(mcp): replay doctor probe validation under every output profile * test(mcp): accept typed-branch nullable unions in the advertised dialect (#10649) * test(mcp): select full output schemas in HTTP session workflows * fix(mcp): emit stack-safe classes with non-ASCII members and protect draft-07 reference siblings * fix(mcp): protect references into literal siblings of relaxed draft-07 references * feat(mcp): omit output schemas from tools/list by default (#10646) * feat(mcp): omit output schemas from tools/list by default No surveyed model API reads an output schema, MCP client validators repeat REA's own result validation, and complete output schemas were about 92% of the default tools/list. REA_MCP_OUTPUT_SCHEMA_PROFILE now resolves to off through one owner (resolveMcpOutputSchemaProfile); compact and full remain selectable. structuredContent is still delivered, and the deferred server serves both off and full from the recorded catalog with the matching recorded identity. Tests and artifact verifiers that validate results against complete advertised output contracts select full explicitly. * fix(mcp): select schemas in verification workflows * fix(mcp): select complete output schemas in the Electron verifier * fix(mcp): select complete output schemas in standalone verifier probes * fix(mcp): select complete output schemas in every verifier transport * test(mcp): select full output schemas in packet and signal schema checks * fix(mcp): validate the doctor probe reply against the canonical output contract (#10655) * fix(mcp): validate the doctor probe reply against the canonical output contract * test(mcp): replay doctor probe validation under every output profile * test(mcp): accept typed-branch nullable unions in the advertised dialect (#10649) * test(mcp): select full output schemas in HTTP session workflows * fix(mcp): keep the Evidence union on described borrowed inputs (#10597) * fix(mcp): keep the Evidence union on described borrowed inputs borrowEvidenceInput installed its advertised projection on one schema instance. A call site's .describe() rebuilds the schema from its definition, so match_native_functions advertised left and right as unconstrained values in every input profile. Install the projection in the borrowed parser's constructor so derived schemas keep it, for both borrowed inputs and borrowed arrays. * test(mcp): select the borrowed Evidence contract from its family * docs(mcp): admit baseline growth only for restored contract declarations * fix(mcp): state reduced multi-type properties as typed branches, not type lists The reduced fallback derived a property's types and emitted them as a type list, which Gemini, Moonshot and Ark reject. A property whose canonical schema is anyOf string and number therefore traded a typed union for a rejected construct when the inline projection fell back. Emit one type, or anyOf typed branches, so [T, null] becomes the dialect's nullable union. * feat(mcp)!: retire the full schema profiles and declare complete observations as a typed annotation (#10718) * refactor(mcp): declare complete observations as a typed annotation applied only on the wire * test(mcp): compare complete-observation declarations with their owning text * refactor(schema): couple Zod checks with their JSON Schema constraint keywords (#10725) * refactor(schema): couple Zod checks with their JSON Schema constraint keywords * refactor(schema): declare pattern approximations and object output roots as typed annotations (#10754) * refactor(schema): declare pattern approximations and object output roots as typed annotations * fix(schema): refuse pattern declarations JSON Schema cannot state * test(schema): select guided_workflows from its contract family * feat(mcp)!: retire the full input and output schema profiles (#10841) * feat(mcp)!: retire the full input and output schema profiles The compact input presentation is now the only advertised input schema, and output schemas are either omitted (`off`, the default) or bounded (`compact`). Catalog identity, the build-recorded digests and the recorded startup catalog collapse to those presentations: two digest profiles instead of six, one recorded catalog instead of two, and no input dialect baseline. Tests and verifiers that validated replies against listed full output schemas now validate against the canonical output contract, which REA already enforces server-side; tests that asserted canonical input restrictions read the canonical input schema instead of a full listing. BREAKING CHANGE: `REA_MCP_INPUT_SCHEMA_PROFILE=full` and `REA_MCP_OUTPUT_SCHEMA_PROFILE=full` are refused at startup and by `rea setup` with a configuration error naming the setting. Remove the setting from the client registration (or rerun setup with `REA_MCP_INPUT_SCHEMA_PROFILE=compact` / `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact`). `REA_MCP_INPUT_SCHEMA_PROFILE` still accepts `compact`. Server-side validation is unchanged. * test(mcp): validate replies against canonical output contracts at every listed-schema reader tools/list omits output schemas by default, so fixtures, verifiers and the Android analysis runners that read a listed outputSchema now bind the canonical contract instead. connectLocalToolsMcp and advertisedMcpToolSchemas hand the SDK a definition carrying the canonical output schema, so the SDK client and caller-supplied validators check every successful reply. * test(mcp): resolve Android runners' output contracts from the selected repository The Android efficiency runners imported the canonical output-schema helper from the harness checkout while --repository selects the revision under test. Import it from the selected repository, as the runners already do for the SDK and Ajv. * test(mcp): let Android runners compare revisions before the canonical output helper A selected revision without scripts/lib/canonical-output-schema.mjs could not start the Android runners. Choose the output contracts in one harness helper: the selected revision's canonical contracts when it has them, otherwise its listed schemas under the full output profile it still supports. * fix(scripts): keep caller launch settings when the real Hopper verifier builds its server environment The verifier deleted REA_VERIFY_SERVER_COMMAND and REA_VERIFY_SERVER_ARGS_JSON from process.env itself before reading them, so a caller selecting an installed REA command silently ran the checkout's dist/main.js. Copy the environment, as main does, so only the child's environment changes. * test(mcp): validate whole replies against canonical output contracts wherever a listed definition is held tools/list omits output schemas by default and the full output profile is gone, so tests that handed the SDK a listed definition no longer checked complete replies. Attach the canonical output schema with withCanonicalOutputSchema at every held REA tool definition. Environment-served tools (not REA contracts), and the deferred-runtime and doctor tests that exercise per-profile listings, keep their listed definitions. * test(mcp): record the advertised catalog summary without the retired full profiles Retiring the full input and output profiles leaves only the compact input and compact output listings, so the summary drops the full-profile digests and every pattern only the full listings carried. Regenerated with npm run verify:advertised-schemas -- --update-summary.
1007 lines
33 KiB
TypeScript
1007 lines
33 KiB
TypeScript
import { readFile, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { expect } from "vitest";
|
|
import {
|
|
cliTest,
|
|
type CliInvocation,
|
|
type TestCli,
|
|
} from "../../support/cli/cliFixture.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
|
|
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
|
import {
|
|
nativeMatchingNamedInventory as nativeMatchingInventory,
|
|
nativeMatchingDetails,
|
|
nativeMatchingTarget,
|
|
nativeMatchingProvider,
|
|
} from "../../fixtures/nativeFunctionMatching.js";
|
|
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
|
|
import { procedureInfoSchema } from "../../../src/domain/native/procedureInfo.js";
|
|
import {
|
|
createEvidenceBundle,
|
|
parseEvidenceBundle,
|
|
} from "../../../src/domain/evidenceBundle.js";
|
|
import { nativeFunctionMatchingSchema } from "../../../src/domain/native/functionMatching.js";
|
|
|
|
const createMatchingFixture = async () => {
|
|
const root = await createTestTempDirectory("rea-native-matching-");
|
|
const ancestor = createEvidence(undefined, nativeMatchingProvider, {
|
|
operation: "fixture_provenance",
|
|
parameters: {},
|
|
result: { source: "Synthetic ancestor" },
|
|
});
|
|
const unrelated = createEvidence(undefined, nativeMatchingProvider, {
|
|
operation: "fixture_unrelated",
|
|
parameters: {},
|
|
result: { source: "Unselected synthetic record" },
|
|
});
|
|
const dependency = createEvidence(undefined, nativeMatchingProvider, {
|
|
operation: "fixture_dependency",
|
|
parameters: {},
|
|
result: { source: "Synthetic dependency" },
|
|
evidenceLinks: [ancestor.evidence_id],
|
|
});
|
|
const left = nativeMatchingInventory(
|
|
"1",
|
|
[
|
|
{ address: "0x10", value: "main" },
|
|
{ address: "0x20", value: "old_name" },
|
|
],
|
|
[dependency.evidence_id],
|
|
);
|
|
const right = nativeMatchingInventory("2", [
|
|
{ address: "0x30", value: "main" },
|
|
{ address: "0x40", value: "new_name" },
|
|
]);
|
|
const leftDetails = [
|
|
...nativeMatchingDetails("1", "0x10", "9000", true),
|
|
...nativeMatchingDetails("1", "0x20", "c3", true),
|
|
];
|
|
const rightDetails = [
|
|
...nativeMatchingDetails("2", "0x30", "9001", true),
|
|
...nativeMatchingDetails("2", "0x40", "c3", true),
|
|
];
|
|
const ancestorsPath = join(root, "ancestors.json");
|
|
await writeFile(
|
|
ancestorsPath,
|
|
JSON.stringify(createEvidenceBundle([ancestor, unrelated])),
|
|
);
|
|
const importedPath = join(root, "dependencies.json");
|
|
await writeFile(
|
|
importedPath,
|
|
JSON.stringify(createEvidenceBundle([ancestor, dependency])),
|
|
);
|
|
return {
|
|
root,
|
|
ancestor,
|
|
dependency,
|
|
unrelated,
|
|
left,
|
|
right,
|
|
leftDetails,
|
|
rightDetails,
|
|
ancestorsPath,
|
|
importedPath,
|
|
};
|
|
};
|
|
|
|
cliTest(
|
|
"keeps the committed Evidence receipt when JSON Lines refuses the result",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-matching-jsonl-receipt-");
|
|
const request = join(root, "request.json");
|
|
const left = nativeMatchingInventory("1", [
|
|
{ address: "0x10", value: "main" },
|
|
]);
|
|
const right = nativeMatchingInventory("2", [
|
|
{ address: "0x20", value: "main" },
|
|
]);
|
|
for (const detail of ["summary", "complete"] as const) {
|
|
await writeFile(request, JSON.stringify({ left, right, detail }));
|
|
const output = join(root, `${detail}.json`);
|
|
const result = await cli.run({
|
|
arguments: [
|
|
"match-native-functions",
|
|
request,
|
|
"--evidence_output",
|
|
output,
|
|
"--format",
|
|
"jsonl",
|
|
],
|
|
});
|
|
expect(result.exitCode).toBe(1);
|
|
const receipt = {
|
|
path: output,
|
|
bytes: (await readFile(output)).byteLength,
|
|
};
|
|
expect(JSON.parse(result.stdout)).toMatchObject({
|
|
type: "error",
|
|
result: {
|
|
code: "invalid_request",
|
|
category: "invalid_input",
|
|
details: { diagnostics: { evidence_bundle: receipt } },
|
|
},
|
|
});
|
|
const retry = await cli.run({
|
|
arguments: [
|
|
"match-native-functions",
|
|
request,
|
|
"--evidence_output",
|
|
output,
|
|
"--json",
|
|
],
|
|
});
|
|
expect(retry.exitCode).toBe(1);
|
|
expect(retry.json).toMatchObject({ details: { reason: "exists" } });
|
|
expect(
|
|
parseEvidenceBundle(JSON.parse(await readFile(output, "utf8"))).records,
|
|
).toHaveLength(3);
|
|
}
|
|
},
|
|
);
|
|
const verifyPortableCli = async (
|
|
cli: TestCli,
|
|
fixture: Awaited<ReturnType<typeof createMatchingFixture>>,
|
|
input: Record<string, unknown>,
|
|
evidenceId: string,
|
|
) => {
|
|
const {
|
|
root,
|
|
ancestor,
|
|
dependency,
|
|
unrelated,
|
|
left,
|
|
right,
|
|
leftDetails,
|
|
rightDetails,
|
|
ancestorsPath,
|
|
} = fixture;
|
|
const bundlePath = join(root, "sources.json"),
|
|
inputPath = join(root, "request.json"),
|
|
outputPath = join(root, "result.json");
|
|
await writeFile(
|
|
bundlePath,
|
|
JSON.stringify(
|
|
createEvidenceBundle([
|
|
left,
|
|
right,
|
|
...leftDetails,
|
|
...rightDetails,
|
|
dependency,
|
|
]),
|
|
),
|
|
);
|
|
const original = await readFile(bundlePath);
|
|
await writeFile(inputPath, JSON.stringify({ ...input, detail: "complete" }));
|
|
const cliFull = await cli.run({
|
|
arguments: [
|
|
"match-native-functions",
|
|
inputPath,
|
|
"--source_bundle",
|
|
ancestorsPath,
|
|
"--source_bundle",
|
|
bundlePath,
|
|
"--json",
|
|
],
|
|
});
|
|
expect(cliFull.exitCode, cliFull.stderr).toBe(0);
|
|
expect(parseEvidence(cliFull.json).evidence_id).toBe(evidenceId);
|
|
await writeFile(inputPath, JSON.stringify(input));
|
|
const cliSummary = await cli.run({
|
|
arguments: [
|
|
"match-native-functions",
|
|
inputPath,
|
|
"--source_bundle",
|
|
ancestorsPath,
|
|
"--source_bundle",
|
|
bundlePath,
|
|
"--evidence_output",
|
|
outputPath,
|
|
"--json",
|
|
],
|
|
});
|
|
expect(cliSummary.exitCode, cliSummary.stderr).toBe(0);
|
|
expect(cliSummary.json).toMatchObject({
|
|
summary: {
|
|
normalized_result: { parent_evidence_id: evidenceId },
|
|
},
|
|
evidence_bundle: { path: outputPath },
|
|
});
|
|
const bundle = parseEvidenceBundle(
|
|
JSON.parse(await readFile(outputPath, "utf8")),
|
|
);
|
|
const ids = new Set(bundle.records.map((x) => x.evidence_id));
|
|
for (const record of bundle.records)
|
|
for (const id of record.evidence_links)
|
|
expect(
|
|
ids.has(id),
|
|
`Missing dependency ${id} of ${record.evidence_id}`,
|
|
).toBe(true);
|
|
expect(ids.has(ancestor.evidence_id)).toBe(true);
|
|
expect(ids.has(dependency.evidence_id)).toBe(true);
|
|
expect(ids.has(unrelated.evidence_id)).toBe(false);
|
|
expect(ids.has(evidenceId)).toBe(true);
|
|
expect(Buffer.compare(await readFile(bundlePath), original)).toBe(0);
|
|
const incomplete = await cli.run({
|
|
arguments: [
|
|
"match-native-functions",
|
|
inputPath,
|
|
"--source_bundle",
|
|
bundlePath,
|
|
"--evidence_output",
|
|
join(root, "incomplete.json"),
|
|
"--json",
|
|
],
|
|
});
|
|
expect(incomplete.json).toMatchObject({
|
|
code: "evidence_integrity_mismatch",
|
|
});
|
|
};
|
|
|
|
cliTest.for([{ profile: "compact" }] as const)(
|
|
"shared public CLI/MCP matching retains sources, pages exact facts, and feeds selected dossier pairs directly to compare_functions ($profile)",
|
|
async ({ profile }, { cli }) => {
|
|
const fixture = await createMatchingFixture();
|
|
const { left, right, leftDetails, rightDetails, importedPath } = fixture;
|
|
const { call } = await connectLocalToolsMcp({
|
|
REA_MCP_INPUT_SCHEMA_PROFILE: profile,
|
|
});
|
|
const imported = await call("import_evidence_bundle", {
|
|
path: importedPath,
|
|
});
|
|
expect(imported.isError, JSON.stringify(imported)).not.toBe(true);
|
|
const full = await call("match_native_functions", {
|
|
left,
|
|
right,
|
|
left_observations: leftDetails,
|
|
right_observations: rightDetails,
|
|
detail: "complete",
|
|
});
|
|
expect(full.isError, JSON.stringify(full)).not.toBe(true);
|
|
const evidence = parseEvidence(full.structuredContent),
|
|
report = nativeFunctionMatchingSchema.parse(evidence.normalized_result);
|
|
expect(report.summary).toEqual({
|
|
matched: 2,
|
|
only_left: 0,
|
|
only_right: 0,
|
|
unresolved: 0,
|
|
});
|
|
const reference = (evidence_id: string) => ({
|
|
kind: "retained-evidence",
|
|
evidence_id,
|
|
});
|
|
const input = {
|
|
left: reference(left.evidence_id),
|
|
right: reference(right.evidence_id),
|
|
left_observations: leftDetails.map((x) => reference(x.evidence_id)),
|
|
right_observations: rightDetails.map((x) => reference(x.evidence_id)),
|
|
};
|
|
const summary = await call("match_native_functions", input);
|
|
expect(summary.isError, JSON.stringify(summary)).not.toBe(true);
|
|
expect(
|
|
parseEvidence(summary.structuredContent).normalized_result,
|
|
).toMatchObject({
|
|
kind: "summary",
|
|
artifact: null,
|
|
parent_evidence_id: evidence.evidence_id,
|
|
summary: { summary: report.summary },
|
|
});
|
|
expect(parseEvidence(summary.structuredContent).locations).not.toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ kind: "artifact-path" }),
|
|
]),
|
|
);
|
|
const page = await call("inspect_analysis_view", {
|
|
source: reference(evidence.evidence_id),
|
|
view: {
|
|
kind: "function-matches",
|
|
collection: "functions",
|
|
sides: ["left"],
|
|
fields: ["name", "body_sha256"],
|
|
offset: 1,
|
|
limit: 1,
|
|
},
|
|
});
|
|
expect(page.isError, JSON.stringify(page)).not.toBe(true);
|
|
expect(
|
|
parseEvidence(page.structuredContent).normalized_result,
|
|
).toMatchObject({
|
|
artifact: null,
|
|
coverage: { total: 2, next_offset: null },
|
|
items: [
|
|
{
|
|
name: "old_name",
|
|
function_id: `${left.evidence_id}/1`,
|
|
address: "0x20",
|
|
},
|
|
],
|
|
});
|
|
const selectedPage = parseEvidence(page.structuredContent)
|
|
.normalized_result as {
|
|
items: Record<string, unknown>[];
|
|
};
|
|
expect(selectedPage.items[0]).not.toHaveProperty("limitations");
|
|
expect(parseEvidence(page.structuredContent).locations).not.toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ kind: "artifact-path" }),
|
|
]),
|
|
);
|
|
const withoutBytes = await call("match_native_functions", {
|
|
left: reference(left.evidence_id),
|
|
right: reference(right.evidence_id),
|
|
});
|
|
expect(withoutBytes.isError, JSON.stringify(withoutBytes)).not.toBe(true);
|
|
const missingParent = parseEvidence(withoutBytes.structuredContent)
|
|
.normalized_result as { parent_evidence_id: string };
|
|
const missingPage = await call("inspect_analysis_view", {
|
|
source: reference(missingParent.parent_evidence_id),
|
|
view: {
|
|
kind: "function-matches",
|
|
collection: "functions",
|
|
sides: ["left"],
|
|
names: ["main"],
|
|
},
|
|
});
|
|
expect(missingPage.isError, JSON.stringify(missingPage)).not.toBe(true);
|
|
expect(
|
|
parseEvidence(missingPage.structuredContent).normalized_result,
|
|
).toMatchObject({
|
|
items: [
|
|
{
|
|
name: "main",
|
|
body_sha256: null,
|
|
limitations: [
|
|
"No complete unambiguous bytes for the observed function body.",
|
|
],
|
|
},
|
|
],
|
|
});
|
|
const compared = await call(
|
|
"compare_functions",
|
|
report.matches[0]!.compare_functions_input!,
|
|
);
|
|
expect(compared.isError, JSON.stringify(compared)).not.toBe(true);
|
|
expect(parseEvidence(compared.structuredContent).operation).toBe(
|
|
"compare_functions",
|
|
);
|
|
await verifyPortableCli(cli, fixture, input, evidence.evidence_id);
|
|
const missingId = `ev_${"a".repeat(64)}`;
|
|
const missing = await call("match_native_functions", {
|
|
left: reference(missingId),
|
|
right: reference(right.evidence_id),
|
|
});
|
|
expect(parseMcpToolError(missing)).toMatchObject({
|
|
error: { details: { evidence_id: missingId, reason: "missing" } },
|
|
});
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"native matching rejects corrupt and misselected MCP sources without retaining them",
|
|
async () => {
|
|
const fixture = await createMatchingFixture();
|
|
const { left, right, leftDetails, root, importedPath } = fixture;
|
|
const { call } = await connectLocalToolsMcp();
|
|
const importedSources = await call("import_evidence_bundle", {
|
|
path: importedPath,
|
|
});
|
|
expect(importedSources.isError, JSON.stringify(importedSources)).not.toBe(
|
|
true,
|
|
);
|
|
const before = await call("get_evidence_bundle", {});
|
|
const corruptEnvelope = { ...left, normalized_result: [] };
|
|
const badCoordinate = nativeMatchingInventory("1", [
|
|
{ address: "not-an-address", value: "main" },
|
|
]);
|
|
const observationResult = procedureInfoSchema.parse(
|
|
nativeMatchingDetails("1", "0x10", "c3")[0]!.normalized_result,
|
|
);
|
|
const corruptObservation = createEvidence(
|
|
nativeMatchingTarget("1"),
|
|
nativeMatchingProvider,
|
|
{
|
|
operation: "procedure_info",
|
|
parameters: { procedure: "0x10" },
|
|
result: {
|
|
...observationResult,
|
|
entrypoint: "not-an-address",
|
|
},
|
|
},
|
|
);
|
|
for (const { request, expected } of [
|
|
{
|
|
request: { left: corruptEnvelope, right },
|
|
expected: { code: "evidence_integrity_mismatch" },
|
|
},
|
|
{
|
|
request: { left: badCoordinate, right },
|
|
expected: { code: "evidence_integrity_mismatch" },
|
|
},
|
|
{
|
|
request: { left, right, left_observations: [corruptObservation] },
|
|
expected: { code: "evidence_integrity_mismatch" },
|
|
},
|
|
{
|
|
request: { left: leftDetails[0], right },
|
|
expected: {
|
|
code: "invalid_request",
|
|
details: {
|
|
issues: [expect.objectContaining({ path: ["left"] })],
|
|
},
|
|
},
|
|
},
|
|
]) {
|
|
const rejected = await call("match_native_functions", {
|
|
...request,
|
|
detail: "complete",
|
|
});
|
|
expect(parseMcpToolError(rejected).error).toMatchObject(expected);
|
|
expect((await call("get_evidence_bundle", {})).structuredContent).toEqual(
|
|
before.structuredContent,
|
|
);
|
|
}
|
|
|
|
const corruptBundlePath = join(root, "corrupt-source.json");
|
|
await writeFile(
|
|
corruptBundlePath,
|
|
JSON.stringify(createEvidenceBundle([badCoordinate, right])),
|
|
);
|
|
const importedCorruptPayload = await call("import_evidence_bundle", {
|
|
path: corruptBundlePath,
|
|
});
|
|
expect(
|
|
importedCorruptPayload.isError,
|
|
JSON.stringify(importedCorruptPayload),
|
|
).not.toBe(true);
|
|
const retainedBefore = await call("get_evidence_bundle", {});
|
|
const retainedFailure = await call("match_native_functions", {
|
|
left: {
|
|
kind: "retained-evidence",
|
|
evidence_id: badCoordinate.evidence_id,
|
|
},
|
|
right: { kind: "retained-evidence", evidence_id: right.evidence_id },
|
|
detail: "complete",
|
|
});
|
|
expect(parseMcpToolError(retainedFailure).error).toMatchObject({
|
|
code: "evidence_integrity_mismatch",
|
|
details: {
|
|
reason: expect.stringContaining("Evidence validation failed"),
|
|
},
|
|
});
|
|
expect((await call("get_evidence_bundle", {})).structuredContent).toEqual(
|
|
retainedBefore.structuredContent,
|
|
);
|
|
|
|
const malformedOuterRequest = await call("match_native_functions", {
|
|
right,
|
|
detail: "complete",
|
|
});
|
|
expect(parseMcpToolError(malformedOuterRequest).error.code).toBe(
|
|
"invalid_request",
|
|
);
|
|
expect((await call("get_evidence_bundle", {})).structuredContent).toEqual(
|
|
retainedBefore.structuredContent,
|
|
);
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"native matching retains malformed inline field paths through MCP and CLI source admission",
|
|
async ({ cli }) => {
|
|
const left = nativeMatchingInventory("1", [
|
|
{ address: "0x10", value: "main" },
|
|
]);
|
|
const right = nativeMatchingInventory("2", [
|
|
{ address: "0x20", value: "main" },
|
|
]);
|
|
const root = await createTestTempDirectory("rea-matching-input-");
|
|
const bundlePath = join(root, "sources.json");
|
|
await writeFile(
|
|
bundlePath,
|
|
JSON.stringify(createEvidenceBundle([left, right])),
|
|
);
|
|
const { call } = await connectLocalToolsMcp();
|
|
for (const field of [
|
|
"left",
|
|
"right",
|
|
"left_observations",
|
|
"right_observations",
|
|
] as const) {
|
|
// Keep fresh-process coverage for both source-admission routes on left.
|
|
const run = (invocation: CliInvocation) =>
|
|
cli.run({ ...invocation, freshProcess: field === "left" });
|
|
const observation = field.endsWith("observations");
|
|
const request = {
|
|
left,
|
|
right,
|
|
detail: "complete",
|
|
[field]: observation ? ["not-evidence"] : "not-evidence",
|
|
};
|
|
const expected = {
|
|
code: "invalid_request",
|
|
details: {
|
|
issues: [
|
|
expect.objectContaining({
|
|
path: observation ? [field, 0] : [field],
|
|
reason: "invalid_type",
|
|
}),
|
|
],
|
|
},
|
|
};
|
|
expect(
|
|
parseMcpToolError(await call("match_native_functions", request)).error,
|
|
).toMatchObject(expected);
|
|
const requestPath = join(root, "request.json");
|
|
await writeFile(requestPath, JSON.stringify(request));
|
|
for (const sourceArguments of [[], ["--source_bundle", bundlePath]]) {
|
|
const rejected = await run({
|
|
arguments: [
|
|
"match-native-functions",
|
|
requestPath,
|
|
...sourceArguments,
|
|
"--json",
|
|
],
|
|
});
|
|
expect(rejected.exitCode).not.toBe(0);
|
|
expect(rejected.json).toMatchObject(expected);
|
|
}
|
|
}
|
|
const corrected = await call("match_native_functions", {
|
|
left,
|
|
right,
|
|
detail: "complete",
|
|
});
|
|
expect(corrected.isError).not.toBe(true);
|
|
expect(
|
|
parseEvidence(corrected.structuredContent).normalized_result,
|
|
).toMatchObject({ summary: { matched: 1 } });
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"native matching CLI inline and portable sources report the same validation errors",
|
|
async ({ cli }) => {
|
|
const fixture = await createMatchingFixture();
|
|
const { left, right, leftDetails, root } = fixture;
|
|
const badCoordinate = nativeMatchingInventory("1", [
|
|
{ address: "not-an-address", value: "main" },
|
|
]);
|
|
const corruptBundlePath = join(root, "corrupt-source.json");
|
|
await writeFile(
|
|
corruptBundlePath,
|
|
JSON.stringify(createEvidenceBundle([badCoordinate, right])),
|
|
);
|
|
const requestPath = join(root, "wrong-role-request.json");
|
|
const portablePath = join(root, "wrong-role-sources.json");
|
|
const wrongRole = leftDetails[0]!;
|
|
const inlineRequest = { left: wrongRole, right, detail: "complete" };
|
|
const portableRequest = {
|
|
left: { kind: "retained-evidence", evidence_id: wrongRole.evidence_id },
|
|
right: { kind: "retained-evidence", evidence_id: right.evidence_id },
|
|
detail: "complete",
|
|
};
|
|
await writeFile(requestPath, JSON.stringify(inlineRequest));
|
|
const inline = await cli.run({
|
|
arguments: ["match-native-functions", requestPath, "--json"],
|
|
});
|
|
await writeFile(
|
|
portablePath,
|
|
JSON.stringify(createEvidenceBundle([wrongRole, right])),
|
|
);
|
|
await writeFile(requestPath, JSON.stringify(portableRequest));
|
|
const portable = await cli.run({
|
|
arguments: [
|
|
"match-native-functions",
|
|
requestPath,
|
|
"--source_bundle",
|
|
portablePath,
|
|
"--json",
|
|
],
|
|
});
|
|
expect(inline.exitCode).not.toBe(0);
|
|
expect(portable.exitCode).toBe(inline.exitCode);
|
|
const wrongRoleError = {
|
|
code: "invalid_request",
|
|
details: {
|
|
issues: [expect.objectContaining({ path: ["left"] })],
|
|
},
|
|
};
|
|
expect(inline.json).toMatchObject(wrongRoleError);
|
|
expect(portable.json).toMatchObject(wrongRoleError);
|
|
|
|
const corruptRequest = { left: badCoordinate, right, detail: "complete" };
|
|
await writeFile(requestPath, JSON.stringify(corruptRequest));
|
|
const corruptInline = await cli.run({
|
|
arguments: ["match-native-functions", requestPath, "--json"],
|
|
});
|
|
const corruptPortableRequest = {
|
|
left: {
|
|
kind: "retained-evidence",
|
|
evidence_id: badCoordinate.evidence_id,
|
|
},
|
|
right: { kind: "retained-evidence", evidence_id: right.evidence_id },
|
|
detail: "complete",
|
|
};
|
|
await writeFile(requestPath, JSON.stringify(corruptPortableRequest));
|
|
const corruptPortable = await cli.run({
|
|
arguments: [
|
|
"match-native-functions",
|
|
requestPath,
|
|
"--source_bundle",
|
|
corruptBundlePath,
|
|
"--json",
|
|
],
|
|
});
|
|
expect(corruptInline.json).toMatchObject({
|
|
code: "evidence_integrity_mismatch",
|
|
});
|
|
expect(corruptPortable.json).toMatchObject({
|
|
code: "evidence_integrity_mismatch",
|
|
});
|
|
|
|
const malformedBundlePath = join(root, "malformed-outer-frame.json");
|
|
await writeFile(malformedBundlePath, JSON.stringify({ records: [left] }));
|
|
const malformedBundle = await cli.run({
|
|
arguments: [
|
|
"match-native-functions",
|
|
requestPath,
|
|
"--source_bundle",
|
|
malformedBundlePath,
|
|
"--json",
|
|
],
|
|
});
|
|
expect(malformedBundle.exitCode).not.toBe(0);
|
|
expect(malformedBundle.json).toMatchObject({
|
|
code: "evidence_integrity_mismatch",
|
|
});
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"native matching admits the request representation before authenticating observation carriers",
|
|
async () => {
|
|
// Reach the native matching representation boundary with this intentionally
|
|
// oversized carrier fixture, rather than stopping at MCP's lower default.
|
|
const { call } = await connectLocalToolsMcp({
|
|
REA_MCP_MAX_INPUT_ELEMENTS: "1048576",
|
|
});
|
|
const left = nativeMatchingInventory("1", [
|
|
{ address: "0x10", value: "main" },
|
|
]);
|
|
const right = nativeMatchingInventory("2", [
|
|
{ address: "0x20", value: "main" },
|
|
]);
|
|
const before = await call("get_evidence_bundle", {});
|
|
const observations = Array.from({ length: 524288 }, () => ({}));
|
|
const response = await call("match_native_functions", {
|
|
left,
|
|
right,
|
|
left_observations: observations,
|
|
detail: "complete",
|
|
});
|
|
expect(parseMcpToolError(response).error).toMatchObject({
|
|
code: "resource_constraint",
|
|
details: {
|
|
operation: "match_native_functions",
|
|
resource: "capacity",
|
|
},
|
|
});
|
|
expect((await call("get_evidence_bundle", {})).structuredContent).toEqual(
|
|
before.structuredContent,
|
|
);
|
|
},
|
|
);
|
|
|
|
cliTest.for([{ profile: "compact" }] as const)(
|
|
"ties byte qualifications to selected observations through paged and offline MCP ($profile)",
|
|
async ({ profile }) => {
|
|
const left = nativeMatchingInventory("1", [
|
|
{ address: "0x10", value: "left" },
|
|
]);
|
|
const right = nativeMatchingInventory("2", [
|
|
{ address: "0x20", value: "right" },
|
|
]);
|
|
const [leftInfo] = nativeMatchingDetails("1", "0x10", "c3");
|
|
const [rightInfo] = nativeMatchingDetails("2", "0x20", "c3");
|
|
const read = (side: "1" | "2", address: string, limitation: string) =>
|
|
createEvidence(nativeMatchingTarget(side), nativeMatchingProvider, {
|
|
operation: "read_bytes",
|
|
parameters: { address, length: 1 },
|
|
result: {
|
|
address,
|
|
requested_bytes: 1,
|
|
returned_bytes: 1,
|
|
bytes_hex: "c3",
|
|
complete: true,
|
|
},
|
|
limitations: [limitation],
|
|
});
|
|
const first = read("1", "0x10", "First selected source limitation");
|
|
const duplicate = read("1", "0x10", "Alternative source limitation");
|
|
const unused = read("1", "0x99", "Unused source limitation");
|
|
const other = read("2", "0x20", "Right source limitation");
|
|
const reference = (evidence_id: string) => ({
|
|
kind: "retained-evidence",
|
|
evidence_id,
|
|
});
|
|
const { call } = await connectLocalToolsMcp({
|
|
REA_MCP_INPUT_SCHEMA_PROFILE: profile,
|
|
REA_MCP_OUTPUT_SCHEMA_PROFILE: profile,
|
|
});
|
|
const execute = async (
|
|
firstRead: typeof first,
|
|
secondRead: typeof first,
|
|
) => {
|
|
const reply = await call("match_native_functions", {
|
|
left,
|
|
right,
|
|
left_observations: [leftInfo, firstRead, firstRead, secondRead, unused],
|
|
right_observations: [rightInfo, other],
|
|
detail: "complete",
|
|
});
|
|
expect(reply.isError, JSON.stringify(reply)).not.toBe(true);
|
|
return parseEvidence(reply.structuredContent);
|
|
};
|
|
const evidence = await execute(first, duplicate);
|
|
const report = nativeFunctionMatchingSchema.parse(
|
|
evidence.normalized_result,
|
|
);
|
|
const match = report.matches.find(
|
|
(item) => item.basis === "identical-bytes",
|
|
);
|
|
expect(match?.left[0]?.evidence_links).toContain(first.evidence_id);
|
|
expect(match?.left[0]?.evidence_links).not.toContain(duplicate.evidence_id);
|
|
expect(match?.right[0]?.evidence_links).toContain(other.evidence_id);
|
|
expect(report.byte_sources).toEqual([
|
|
{
|
|
side: "left",
|
|
evidence_id: first.evidence_id,
|
|
limitations: ["First selected source limitation"],
|
|
},
|
|
{
|
|
side: "right",
|
|
evidence_id: other.evidence_id,
|
|
limitations: ["Right source limitation"],
|
|
},
|
|
]);
|
|
const alternate = nativeFunctionMatchingSchema.parse(
|
|
(await execute(duplicate, first)).normalized_result,
|
|
);
|
|
expect(
|
|
alternate.matches.find((item) => item.basis === "identical-bytes")
|
|
?.left[0]?.evidence_links,
|
|
).toContain(duplicate.evidence_id);
|
|
expect(alternate.byte_sources?.map((item) => item.evidence_id)).toEqual([
|
|
duplicate.evidence_id,
|
|
other.evidence_id,
|
|
]);
|
|
const sourcePage = async (caller: typeof call, offset: number) => {
|
|
const reply = await caller("inspect_analysis_view", {
|
|
source: reference(evidence.evidence_id),
|
|
view: {
|
|
kind: "function-matches",
|
|
collection: "byte-sources",
|
|
offset,
|
|
limit: 1,
|
|
},
|
|
});
|
|
expect(reply.isError, JSON.stringify(reply)).not.toBe(true);
|
|
return parseEvidence(reply.structuredContent).normalized_result;
|
|
};
|
|
expect(await sourcePage(call, 0)).toMatchObject({
|
|
items: [
|
|
{
|
|
side: "left",
|
|
evidence_id: first.evidence_id,
|
|
limitations: ["First selected source limitation"],
|
|
},
|
|
],
|
|
coverage: { total: 2, next_offset: 1 },
|
|
});
|
|
expect(await sourcePage(call, 1)).toMatchObject({
|
|
items: [
|
|
{
|
|
side: "right",
|
|
evidence_id: other.evidence_id,
|
|
limitations: ["Right source limitation"],
|
|
},
|
|
],
|
|
coverage: { total: 2, next_offset: null },
|
|
});
|
|
const root = await createTestTempDirectory("rea-match-byte-sources-");
|
|
const path = join(root, "bundle.json");
|
|
const exported = await call("export_evidence_bundle", { path });
|
|
expect(exported.isError, JSON.stringify(exported)).not.toBe(true);
|
|
const offline = await connectLocalToolsMcp({
|
|
REA_MCP_INPUT_SCHEMA_PROFILE: profile,
|
|
REA_MCP_OUTPUT_SCHEMA_PROFILE: profile,
|
|
});
|
|
const imported = await offline.call("import_evidence_bundle", { path });
|
|
expect(imported.isError, JSON.stringify(imported)).not.toBe(true);
|
|
expect(await sourcePage(offline.call, 0)).toMatchObject({
|
|
items: [{ evidence_id: first.evidence_id }],
|
|
});
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"bounds byte-source pages and recovers an oversized source",
|
|
async () => {
|
|
const left = nativeMatchingInventory("1", [
|
|
{ address: "0x10", value: "left" },
|
|
]);
|
|
const right = nativeMatchingInventory("2", [
|
|
{ address: "0x20", value: "right" },
|
|
]);
|
|
const [info] = nativeMatchingDetails("1", "0x10", "00".repeat(256));
|
|
if (info === undefined) throw new Error("Missing synthetic body");
|
|
const reads = Array.from({ length: 256 }, (_, index) => {
|
|
const address = `0x${(16 + index).toString(16)}`;
|
|
return createEvidence(nativeMatchingTarget("1"), nativeMatchingProvider, {
|
|
operation: "read_bytes",
|
|
parameters: { address, length: 1 },
|
|
result: {
|
|
address,
|
|
requested_bytes: 1,
|
|
returned_bytes: 1,
|
|
bytes_hex: "00",
|
|
complete: true,
|
|
},
|
|
limitations: [
|
|
`${index}:` +
|
|
"x".repeat(index === 0 ? 1_100_000 : index === 1 ? 6000 : 4500),
|
|
],
|
|
});
|
|
});
|
|
const reference = (evidence_id: string) => ({
|
|
kind: "retained-evidence",
|
|
evidence_id,
|
|
});
|
|
const root = await createTestTempDirectory("rea-match-large-source-");
|
|
const path = join(root, "inputs.json");
|
|
await writeFile(
|
|
path,
|
|
JSON.stringify(createEvidenceBundle([left, right, info, ...reads])),
|
|
);
|
|
const { call } = await connectLocalToolsMcp();
|
|
const imported = await call("import_evidence_bundle", { path });
|
|
expect(imported.isError, JSON.stringify(imported)).not.toBe(true);
|
|
const result = await call("match_native_functions", {
|
|
left: reference(left.evidence_id),
|
|
right: reference(right.evidence_id),
|
|
left_observations: [info, ...reads].map((record) =>
|
|
reference(record.evidence_id),
|
|
),
|
|
detail: "summary",
|
|
});
|
|
expect(result.isError, JSON.stringify(result)).not.toBe(true);
|
|
const parent = parseEvidence(result.structuredContent)
|
|
.normalized_result as { parent_evidence_id: string };
|
|
const oversized = await call("inspect_analysis_view", {
|
|
source: reference(parent.parent_evidence_id),
|
|
view: { kind: "function-matches", collection: "byte-sources" },
|
|
});
|
|
expect(parseMcpToolError(oversized).error).toMatchObject({
|
|
code: "resource_constraint",
|
|
details: {
|
|
resource: "transport",
|
|
reported_limits: {
|
|
source_evidence_id: reads[0]!.evidence_id,
|
|
next_offset: 1,
|
|
},
|
|
},
|
|
});
|
|
let offset = 1;
|
|
const ids: string[] = [reads[0]!.evidence_id];
|
|
let limited = false;
|
|
while (true) {
|
|
const reply = await call("inspect_analysis_view", {
|
|
source: reference(parent.parent_evidence_id),
|
|
view: {
|
|
kind: "function-matches",
|
|
collection: "byte-sources",
|
|
offset,
|
|
limit: 1000,
|
|
},
|
|
});
|
|
expect(reply.isError, JSON.stringify(reply)).not.toBe(true);
|
|
const page = parseEvidence(reply.structuredContent).normalized_result as {
|
|
byte_limited: boolean;
|
|
items: { evidence_id: string; limitations: string[] }[];
|
|
coverage: { total: number; next_offset: number | null };
|
|
};
|
|
limited ||= page.byte_limited;
|
|
if (offset === 1)
|
|
expect(page.items[0]?.limitations).toEqual(reads[1]!.limitations);
|
|
ids.push(...page.items.map((item) => item.evidence_id));
|
|
expect(page.coverage.total).toBe(256);
|
|
if (page.coverage.next_offset === null) break;
|
|
expect(page.coverage.next_offset).toBe(ids.length);
|
|
offset = page.coverage.next_offset;
|
|
}
|
|
expect(limited).toBe(true);
|
|
expect(ids).toEqual(reads.map((record) => record.evidence_id));
|
|
const original = await call("get_evidence_bundle", {
|
|
detail: "records",
|
|
filters: { evidence_id: reads[0]!.evidence_id },
|
|
});
|
|
expect(original.isError, JSON.stringify(original)).not.toBe(true);
|
|
expect(JSON.stringify(original.structuredContent)).toContain(
|
|
reads[0]!.limitations[0],
|
|
);
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"oversized inline matching Evidence points back to the caller's original record",
|
|
async () => {
|
|
const left = nativeMatchingInventory("1", [
|
|
{ address: "0x10", value: "left" },
|
|
]);
|
|
const right = nativeMatchingInventory("2", [
|
|
{ address: "0x20", value: "right" },
|
|
]);
|
|
const [info] = nativeMatchingDetails("1", "0x10", "c3");
|
|
if (info === undefined) throw new Error("Missing synthetic body");
|
|
const read = createEvidence(
|
|
nativeMatchingTarget("1"),
|
|
nativeMatchingProvider,
|
|
{
|
|
operation: "read_bytes",
|
|
parameters: { address: "0x10", length: 1 },
|
|
result: {
|
|
address: "0x10",
|
|
requested_bytes: 1,
|
|
returned_bytes: 1,
|
|
bytes_hex: "c3",
|
|
complete: true,
|
|
},
|
|
limitations: ["x".repeat(1_100_000)],
|
|
},
|
|
);
|
|
const { call } = await connectLocalToolsMcp();
|
|
const match = await call("match_native_functions", {
|
|
left,
|
|
right,
|
|
left_observations: [info, read],
|
|
detail: "complete",
|
|
});
|
|
expect(match.isError, JSON.stringify(match)).not.toBe(true);
|
|
const matchingEvidence = parseEvidence(match.structuredContent);
|
|
const offline = await connectLocalToolsMcp();
|
|
const page = await offline.call("inspect_analysis_view", {
|
|
source: { kind: "inline", evidence: matchingEvidence },
|
|
view: { kind: "function-matches", collection: "byte-sources" },
|
|
});
|
|
const error = parseMcpToolError(page).error;
|
|
expect(error).toMatchObject({
|
|
code: "resource_constraint",
|
|
details: {
|
|
reported_limits: {
|
|
source_evidence_id: read.evidence_id,
|
|
next_offset: 1,
|
|
},
|
|
},
|
|
});
|
|
const report = nativeFunctionMatchingSchema.parse(
|
|
matchingEvidence.normalized_result,
|
|
);
|
|
expect(report.byte_sources).toEqual([
|
|
{
|
|
side: "left",
|
|
evidence_id: read.evidence_id,
|
|
limitations: read.limitations,
|
|
},
|
|
]);
|
|
const lookup = await offline.call("get_evidence_bundle", {
|
|
detail: "records",
|
|
filters: { evidence_id: read.evidence_id },
|
|
});
|
|
expect(lookup.isError, JSON.stringify(lookup)).not.toBe(true);
|
|
expect(
|
|
(lookup.structuredContent as { result: { records: unknown[] } }).result
|
|
.records,
|
|
).toEqual([]);
|
|
},
|
|
);
|