Files
reverse-engineering-anything/tests/boundary/mcp/enhancedTools.traceFeatureCoverage.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

250 lines
7.7 KiB
TypeScript

import { afterEach, describe, expect, it } from "vitest";
import type { AnalysisOperationPort } from "../../../src/application/AnalysisProvider.js";
import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js";
import { EnhancedTools } from "../../../src/application/EnhancedTools.js";
import { AnalysisOutputError } from "../../../src/domain/analysisErrorCore.js";
import { err, ok } from "../../../src/domain/result.js";
import { closeEnhancedToolResources, connect } from "./enhancedToolsHarness.js";
afterEach(closeEnhancedToolResources);
const PROVIDER = {
id: "fixture",
name: "Fixture analysis provider",
version: "1",
} as const;
type Coverage = boolean | undefined;
interface Behavior {
/** What the provider reports for the references of each match address. */
readonly coverage?: Readonly<Record<string, Coverage>>;
readonly limitations?: readonly string[];
/** Also report the first string match as a procedure match. */
readonly duplicateProcedure?: boolean;
/** Fail the references read of this address. */
readonly failXrefsOf?: string;
readonly calls?: string[];
}
const containing = (address: string) => ({
query_address: address,
found: false,
procedure: null,
reason: "not_in_procedure",
});
/**
* A provider with two string matches (0x10 and 0x11), each referenced from
* one address (0x20 and 0x21).
*/
const provider = (behavior: Behavior = {}): AnalysisOperationPort => ({
execute: async (name, arguments_) => {
const address = String(arguments_.address);
behavior.calls?.push(`${name}:${address}`);
switch (name) {
case "search_strings":
return ok(
createAnalysisExecution(
[
{ address: "0x10", value: "hello" },
{ address: "0x11", value: "hello again" },
],
PROVIDER,
),
);
case "search_procedures":
return ok(
createAnalysisExecution(
behavior.duplicateProcedure === true
? [{ address: "0x10", value: "hello" }]
: [],
PROVIDER,
),
);
case "xrefs": {
if (address === behavior.failXrefsOf)
return err(new AnalysisOutputError("xrefs", "provider failed"));
const reported = behavior.coverage?.[address];
return ok(
createAnalysisExecution(
[address === "0x10" ? "0x20" : "0x21"],
PROVIDER,
{
limitations: behavior.limitations ?? [],
...(reported === undefined
? {}
: { referencesTruncated: reported }),
},
),
);
}
case "resolve_containing_procedure":
return ok(createAnalysisExecution(containing(address), PROVIDER));
default:
throw new Error(`Unexpected operation: ${name}`);
}
},
});
const expectedTrace = (
residual: readonly string[],
extraMatches: readonly object[] = [],
) => ({
query: "hello",
search_mode: "literal",
matches: [
{ type: "string", address: "0x10", value: "hello" },
{ type: "string", address: "0x11", value: "hello again" },
...extraMatches,
],
references: [
{
target_address: "0x10",
source_address: "0x20",
containing_procedure: containing("0x20"),
},
{
target_address: "0x11",
source_address: "0x21",
containing_procedure: containing("0x21"),
},
...extraMatches.map(() => ({
target_address: "0x10",
source_address: "0x20",
containing_procedure: containing("0x20"),
})),
],
truncated: residual.length > 0,
residual_unknowns: residual,
});
const unknownFor = (address: string) =>
`References to ${address} were truncated by the provider; additional references may exist.`;
const trace = async (analysis: AnalysisOperationPort) => {
const client = await connect(analysis);
const result = await client.callTool({
name: "trace_feature",
arguments: { query: "hello" },
});
expect(result.isError).not.toBe(true);
return result.structuredContent as { normalized_result: unknown };
};
describe("trace_feature reference coverage", () => {
it("marks the trace truncated for the target whose references the provider capped", async () => {
const output = await trace(
provider({ coverage: { "0x10": true, "0x11": false } }),
);
expect(
output.normalized_result,
"the received references are kept and only the capped target is a residual unknown",
).toEqual(expectedTrace([unknownFor("0x10")]));
});
it("does not carry a truncation over to the next target that reports nothing", async () => {
const output = await trace(provider({ coverage: { "0x10": true } }));
expect(output.normalized_result).toEqual(
expectedTrace([unknownFor("0x10")]),
);
});
it("reports a capped target once when it is matched twice", async () => {
const output = await trace(
provider({ coverage: { "0x10": true }, duplicateProcedure: true }),
);
expect(output.normalized_result).toEqual(
expectedTrace(
[unknownFor("0x10")],
[{ type: "procedure", address: "0x10", value: "hello" }],
),
);
});
it("resolves the containing procedure of a repeated reference source once", async () => {
const calls: string[] = [];
const output = await trace(provider({ duplicateProcedure: true, calls }));
expect(
calls.filter((call) => call.startsWith("resolve_containing_procedure:")),
"the source 0x20 is referenced by two matches of the same address and resolved once",
).toEqual([
"resolve_containing_procedure:0x20",
"resolve_containing_procedure:0x21",
]);
expect(
output.normalized_result,
"every reference is still reported with its containing procedure",
).toEqual(
expectedTrace(
[],
[{ type: "procedure", address: "0x10", value: "hello" }],
),
);
});
it("keeps the trace complete when the provider explicitly reports no cap", async () => {
const output = await trace(
provider({ coverage: { "0x10": false, "0x11": false } }),
);
expect(output.normalized_result).toEqual(expectedTrace([]));
});
it("does not change the trace of a provider that reports no coverage", async () => {
const output = await trace(
provider({
limitations: ["Upstream xrefs_to capped results at 1000 references."],
}),
);
expect(
output.normalized_result,
"a limitation text alone never truncates, and an absent field keeps the previous result",
).toEqual(expectedTrace([]));
});
it("never exposes the coverage field in the tool output", async () => {
const output = await trace(
provider({ coverage: { "0x10": true, "0x11": false } }),
);
expect(JSON.stringify(output)).not.toMatch(
/referencesTruncated|references_truncated/,
);
});
it("fails the trace when a references read fails and reads nothing further", async () => {
const calls: string[] = [];
const result = await new EnhancedTools(
provider({ failXrefsOf: "0x10", calls }),
).execute("trace_feature", { query: "hello" });
expect(result.ok).toBe(false);
expect(
calls.filter((call) => call.startsWith("xrefs:")),
"the next target is not read after the failure",
).toEqual(["xrefs:0x10"]);
});
it("stops without reading references when the trace is cancelled", async () => {
const calls: string[] = [];
const controller = new AbortController();
controller.abort();
const result = await new EnhancedTools(provider({ calls })).execute(
"trace_feature",
{ query: "hello" },
controller.signal,
);
expect(result.ok).toBe(false);
expect(calls.filter((call) => call.startsWith("xrefs:"))).toEqual([]);
});
});