Files
morluto 567b559474 fix(call-path): retain unresolved outgoing-call caveats
Preserve format and Evidence semantics at their owning boundaries, with focused regression coverage and documentation where applicable.

Fixes #6788
2026-10-23 23:36:21 +08:00

190 lines
6.2 KiB
TypeScript

import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { expect, it } from "vitest";
import { FUNCTION_COMPARISON_EXAMPLE } from "../../../src/contracts/functionComparisonExample.js";
import { callPathResultSchema } from "../../../src/domain/callPathSchemas.js";
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
import { functionDossierSchema } from "../../../src/domain/hopperValues.js";
import { createServer } from "../../../src/server/createServer.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
const observe = (
address: string,
callees: readonly string[],
unresolvedCalls: readonly { address: string; reason: string }[] = [],
) => {
const template = FUNCTION_COMPARISON_EXAMPLE.left;
if (template.subject === null)
throw new Error("Expected artifact-bound fixture");
const dossier = functionDossierSchema.parse(template.normalized_result);
return createEvidence(
{
path: template.subject.local_path,
sha256: template.subject.digest.sha256,
format: template.subject.format,
},
template.provider,
{
operation: "analyze_function",
parameters: { procedure: address },
result: {
...dossier,
procedure: { ...dossier.procedure, address },
callees: callees.map((callee) => ({
address: callee,
name: `fn_${callee}`,
classification: null,
body: dossier.procedure.body,
})),
callers: [],
unresolved_calls: [...unresolvedCalls],
limitations: [],
},
confidence: template.confidence,
authority: template.authority,
relationshipCoverage: { status: "complete" },
},
);
};
it("publishes explored extent without the terminal goal's callees", async () => {
const session = createTestBinarySession(() => {
throw new Error("Offline call paths must not launch a provider");
});
const server = createServer({ kind: "session", session });
const client = new Client({ name: "call-path-search-extent", version: "1" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const start = observe("0x1000", ["0x4000"]);
const goal = observe("0x4000", ["0x5000", "0x1000"]);
for (const evidence of [start, goal])
expect(session.recordEvidence(evidence).ok).toBe(true);
for (const { functions, target, hops, explored } of [
{
functions: [start],
target: "0x4000",
hops: 1,
explored: { nodes: 2, edges: 1, depth_reached: 1 },
},
{
functions: [start, goal],
target: "0x4000",
hops: 1,
explored: { nodes: 2, edges: 1, depth_reached: 1 },
},
{
functions: [start],
target: "0x1000",
hops: 0,
explored: { nodes: 1, edges: 0, depth_reached: 0 },
},
]) {
const reply = await client.callTool({
name: "build_call_path",
arguments: {
functions,
start: { address: "0x1000" },
goal: { address: target },
},
});
if (reply.isError) throw new Error(JSON.stringify(reply));
const result = callPathResultSchema.parse(
parseEvidence(reply.structuredContent).normalized_result,
);
expect(result).toMatchObject({
status: "found",
shortest_hops: hops,
search_scope: { exhaustive: true },
explored,
limitations: [],
});
}
} finally {
await Promise.allSettled([session.close(), client.close(), server.close()]);
}
});
it("preserves unresolved outgoing call caveats and bounds search completeness at expanded nodes", async () => {
const session = createTestBinarySession(() => {
throw new Error("Offline call paths must not launch a provider");
});
const server = createServer({ kind: "session", session });
const client = new Client({ name: "unresolved-call-path", version: "1" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const unresolved = {
address: "0x1008",
reason: "Computed target could not be resolved.",
};
const scenarios = [
{
start: observe("0x1000", [], [unresolved]),
terminal: undefined,
goal: "0x4000",
expected: {
status: "unknown",
search_scope: { exhaustive: false },
limitations: [
"Unresolved call at 0x1008: Computed target could not be resolved.",
],
},
},
{
start: observe("0x1000", ["0x4000"], [unresolved]),
terminal: undefined,
goal: "0x4000",
expected: {
status: "found",
shortest_hops: 1,
search_scope: { exhaustive: false },
limitations: [
"Unresolved call at 0x1008: Computed target could not be resolved.",
],
},
},
{
start: observe("0x1000", ["0x4000"]),
terminal: observe("0x4000", [], [unresolved]),
goal: "0x4000",
expected: {
status: "found",
shortest_hops: 1,
search_scope: { exhaustive: true },
limitations: [
"Unresolved call at 0x1008: Computed target could not be resolved.",
],
},
},
];
for (const scenario of scenarios) {
const functions = [
scenario.start,
...(scenario.terminal ? [scenario.terminal] : []),
];
for (const evidence of functions)
expect(session.recordEvidence(evidence).ok).toBe(true);
const reply = await client.callTool({
name: "build_call_path",
arguments: {
functions,
start: { address: "0x1000" },
goal: { address: scenario.goal },
},
});
if (reply.isError) throw new Error(JSON.stringify(reply));
const result = callPathResultSchema.parse(
parseEvidence(reply.structuredContent).normalized_result,
);
expect(result).toMatchObject(scenario.expected);
}
} finally {
await Promise.allSettled([session.close(), client.close(), server.close()]);
}
});