Files
rea/tests/conformance/ida/providerContracts.test.ts
T
Mykola Dementiev 5f8af4ee5f feat(ida): resolve containing procedures and report the admitted program as the document (#2374)
* test(mcp): expect trace_feature to report references the provider truncated

A provider that reports a capped xrefs read makes the trace truncated with a residual unknown for that target. An explicit complete report and a provider that reports nothing leave the trace as before.

* fix(mcp): report references a provider truncated in trace_feature

Executions gain an optional flag for a capped xrefs read, set by the IDA adapter from the upstream cap. trace_feature marks the trace truncated with a residual unknown for that target only when the provider reports a truncation; a provider that reports nothing keeps its previous result. The trace_feature workflow profile is revised so earlier results are not replayed.

* refactor(ida): report reference truncation whenever the runner observed an xrefs cap

* test(mcp): cover session forwarding, per-target state, native reports and failures of the trace coverage

* test(mcp): format the session forwarding test

* test(ida): expect headless IDA to resolve containing procedures from lookups and segment facts

Cover positive and negative containment, segment boundaries including addresses above 2^53, failures of the lookup and of the segment survey, cancellation, input validation and the admitted program as the only document.

* test(ida): expect feature tracing and address context to work on headless IDA

Run trace_feature, including a capped reference read, and inspect_address_context with and without an explicit document through the IDA session client.

* test(mcp): expect trace_feature availability to follow the search operations it calls

* fix(mcp): derive trace_feature availability from the search operations it calls

* feat(ida): resolve containing procedures and report the admitted program as the document

The modern headless profile resolves an address through the function lookup and, when no function contains it, tells a mapped address from an unmapped one using the database-scoped minimal survey. A survey that is missing, truncated or inconsistent fails the read. list_documents reports the single admitted program. The attached profiles do not declare either operation.

* docs(ida): describe containing procedures, the admitted document and truncated feature traces

* test(ida): verify containing procedures, the admitted document and feature tracing on real IDA

* docs(testing): list the headless IDA workflows the real lane verifies

* test(ida): make each survey failure case isolate a single defect

* test(ida): pin large addresses, segment order, document identity, attached refusals and each trace_feature prerequisite

* test(mcp): expect a repeated reference source to be resolved once in trace_feature

* fix(mcp): resolve the containing procedure of a repeated reference source once

* test(ida): trace the fixture procedure and derive probe addresses from its facts in the real lane

* test(process): format processCaptureLifecycle.test.ts after the merge

* test(ida): declare the alignment each synthetic universal slice offset satisfies

* refactor(ida): move the real lane's feature trace assertions into a helper

* test(ida): expect the real lane's feature trace check to accept references to every matched address

* fix(ida): check each trace reference against its own matched address in the real lane

* test(ida): pin that the new operations reject a document other than the admitted program

* refactor(ida): move the real lane's mapped non-function probe into a helper

* test(ida): expect the real lane's mapped data probe to skip strings inside functions

* fix(ida): try the target's strings in order for the mapped non-function probe

* test(ida): declare the real lane helper types for the verifier tests

* test(ida): expect the real lane to reject a listed string that resolves as unmapped

* fix(ida): fail the real lane when a listed string resolves as unmapped
2026-10-13 20:56:20 +08:00

140 lines
4.6 KiB
TypeScript

import { STDIO_DEFAULT_MAX_BUFFER_SIZE } from "@modelcontextprotocol/server";
import { rm } from "node:fs/promises";
import { afterEach, describe, expect, it } from "vitest";
import { createIdaTarget, RecordingIdaMcp } from "../../fixtures/idaMcp.js";
import { IdaSessionClient } from "../../../src/ida/IdaSessionClient.js";
import {
IDA_OPERATIONS,
idaCapabilities,
} from "../../../src/ida/IdaProviderCapabilities.js";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import type { JsonValue } from "../../../src/domain/jsonValue.js";
import { ToolResultDelivery } from "../../../src/server/toolResult.js";
const delivery = new ToolResultDelivery(STDIO_DEFAULT_MAX_BUFFER_SIZE);
const roots: string[] = [];
afterEach(async () => {
for (const root of roots.splice(0))
await rm(root, { recursive: true, force: true });
});
/** Pinned upstream tools that navigate an active IDA GUI as a side effect. */
const GUI_NAVIGATING_TOOLS: ReadonlySet<string> = new Set([
"disassemble_function",
"decompile_function",
]);
const parametersFor = (
operation: string,
): Readonly<Record<string, JsonValue>> => {
if (operation.startsWith("search_")) return { pattern: "a.b" };
if (operation === "xrefs" || operation === "resolve_containing_procedure")
return { address: "0x1000" };
if (operation.startsWith("list_")) return {};
return { procedure: "main" };
};
/** Operations only the headless profile declares. */
const HEADLESS_ONLY: ReadonlySet<string> = new Set([
"resolve_containing_procedure",
"list_documents",
]);
describe.each(["attached", "headless"] as const)(
"IDA %s caller-visible contracts",
(mode) => {
const operations = IDA_OPERATIONS.filter(
(operation) => mode === "headless" || !HEADLESS_ONLY.has(operation),
);
it.each(operations)(
"declares %s UI effects matching the upstream tools it reaches",
async (operation) => {
const { root, target } = await createIdaTarget();
roots.push(root);
const producer = new RecordingIdaMcp(target, mode);
const client = new IdaSessionClient(
{
command: "fixture",
args: [],
env: {},
mode,
workspaceRoot: root,
timeoutMs: 1000,
},
target,
producer,
);
try {
await client.execute(operation, parametersFor(operation));
} finally {
await client.close();
}
const descriptor = idaCapabilities(mode, true).find(
(capability) => capability.operation === operation,
);
expect(descriptor?.effects.mayShowUi).toBe(
producer.calls.some(({ name }) => GUI_NAVIGATING_TOOLS.has(name)),
);
},
);
it.each(operations)(
"serializes %s against its named advertised output contract",
async (operation) => {
const { root, target } = await createIdaTarget();
roots.push(root);
const client = new IdaSessionClient(
{
command: "fixture",
args: [],
env: {},
mode,
workspaceRoot: root,
timeoutMs: 1000,
},
target,
new RecordingIdaMcp(target, mode),
);
try {
const parameters = parametersFor(operation);
const execution = await client.execute(operation, parameters);
if (mode === "headless" && operation === "procedure_callers") {
expect(execution.ok).toBe(false);
if (!execution.ok)
expect(execution.error._tag).toBe(
"AnalysisCapabilityUnavailableError",
);
return;
}
if (!execution.ok) throw execution.error;
const evidence = createEvidence(target, execution.value.provider, {
operation,
parameters,
result: execution.value.result,
rawResult: execution.value.rawResult,
locations: execution.value.locations,
limitations: execution.value.limitations,
});
const contract = toolContract(operation);
const serialized = delivery.toEvidenceToolResult(
evidence,
contract,
undefined,
);
expect(
contract.outputSchema.safeParse(serialized.structuredContent)
.success,
).toBe(true);
expect(serialized.content[0]).toEqual({
type: "text",
text: JSON.stringify(serialized.structuredContent),
});
} finally {
await client.close();
}
},
);
},
);