Files
rea/tests/boundary/mcp/nativeValueTraceCompleteness.test.ts
Kaoru0822andClaude Opus 5.5 73ff6ca0c8 fix(native): separate value-trace page omission from traversal truncation (#8309)
trace_native_values set one truncated flag for two different facts: the
returned page omits retained nodes, and a budget stopped or narrowed the
traversal. A last page of a complete trace and a node- or edge-capped trace
both reported truncated: true with next_offset null and no unknown naming the
cap, so a caller could not tell whether to follow pages, raise a budget, or
treat the examined graph as exhausted.

Results now carry page_truncated for page omission and a traversal object
that is complete, or truncated with the observed reasons in schema order:
max_depth, max_functions, max_call_sites, max_nodes, max_edges,
node_payload_bytes, provider_pcode_truncated and ambiguous_call_candidates.
truncated keeps its previous meaning as the union of both.

A new budget owner admits nodes and edges and records every cause together
with a located unknown, only after a budget rejects work; an exactly filled
budget is no longer truncation. Depth and ambiguous-candidate limits are
resolved after traversal, so a destination analyzed or queued through a
shallower path is no longer reported as depth-limited. Once an edge is
rejected, remaining call sites in that procedure are not resolved.

Fixes #8283

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 08:59:58 -07:00

111 lines
3.9 KiB
TypeScript

import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { afterEach, expect, it } from "vitest";
import { valueTraceDiamond } from "../../../src/application/native/NativeValueTrace.fixture.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { nativeValueTraceSchema } from "../../../src/domain/native/nativeValueTrace.js";
import { advertisedMcpToolSchemas } from "../../fixtures/advertisedMcpTool.js";
import { createServer } from "../../../src/server/createServer.js";
import {
closeEnhancedToolResources,
resources,
} from "./enhancedToolsHarness.js";
afterEach(closeEnhancedToolResources);
it.each(["full", "compact"] as const)(
"separates page omission from named traversal budgets through the advertised %s MCP contract",
async (profile) => {
const server = createServer(
{ kind: "fixed", analysis: valueTraceDiamond().port },
{ environment: { REA_MCP_OUTPUT_SCHEMA_PROFILE: profile } },
);
const client = new Client({ name: "value-trace-test", version: "1.0.0" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
resources.push(client, server);
await server.connect(serverTransport);
await client.connect(clientTransport);
const { toolDefinition, ajv, outputSchema } =
await advertisedMcpToolSchemas(client, "trace_native_values");
if (profile === "full")
for (const field of [
"page_truncated",
"traversal",
"max_nodes",
"max_edges",
])
expect(JSON.stringify(outputSchema)).toContain(`"${field}"`);
const trace = async (arguments_: Record<string, unknown>) => {
const reply = await client.callTool(
{ name: "trace_native_values", arguments: arguments_ },
{ toolDefinition },
);
expect(reply.isError).not.toBe(true);
expect(
ajv.validate(outputSchema, reply.structuredContent),
JSON.stringify(ajv.errors),
).toBe(true);
return nativeValueTraceSchema.parse(
parseEvidence(reply.structuredContent).normalized_result,
);
};
const request = { procedure: "0x1000", max_depth: 1 };
const complete = await trace(request);
expect(complete).toMatchObject({
total_nodes: 11,
next_offset: null,
page_truncated: false,
traversal: { status: "complete" },
truncated: false,
});
const pages = [];
for (let offset: number | null = 0; offset !== null;) {
const page = await trace({ ...request, offset, limit: 4 });
expect(page).toMatchObject({
page_truncated: true,
traversal: { status: "complete" },
});
pages.push(page);
offset = page.next_offset;
}
expect(pages.map(({ nodes }) => nodes.length)).toEqual([4, 4, 3]);
expect(pages.flatMap(({ nodes }) => nodes.map(({ id }) => id))).toEqual(
complete.nodes.map(({ id }) => id),
);
for (const { budgets, offset, reason, unknown } of [
{
budgets: { max_nodes: 9 },
offset: 8,
reason: "max_nodes",
unknown: "Node budget reached",
},
{
budgets: { max_edges: 2 },
offset: 3,
reason: "max_edges",
unknown: "Edge budget reached",
},
] as const) {
const last = await trace({ ...request, ...budgets, offset, limit: 3 });
expect(last).toMatchObject({
nodes: [{}],
next_offset: null,
page_truncated: true,
traversal: { status: "truncated", reasons: [reason] },
truncated: true,
});
expect(last.unknowns.map(({ reason }) => reason)).toContain(unknown);
}
// Raising the named budget to the retained graph size recovers completeness.
expect(
await trace({ ...request, max_nodes: 11, max_edges: 9 }),
).toMatchObject({
traversal: { status: "complete" },
truncated: false,
unknowns: [],
});
},
);