Files
reverse-engineering-anything/src/ida/IdaSessionClient.test.ts
T
morluto 352a846127 test: reduce brittle assertions and test-only runtime paths (#9289)
* test: focus assertions on behavioral evidence

Prune redundant and wording-coupled checks while retaining diagnostic, parser, recovery, and cleanup coverage.

* refactor: remove test-only wrappers and duplicate checks

Exercise production operations through test fixtures, remove unused adapters, and keep managed-member uncertainty guidance under its existing owner.

* test: focus process validation on diagnostic facts

* refactor: remove test-only runtime helpers

Move semantic evidence fixture setup out of runtime and remove unused Windows settle recovery helpers while checking configured defaults independently.

* test: focus artifact diagnostics on format facts

* test: verify retained cleanup guidance delivery
2026-11-08 02:02:31 +08:00

2270 lines
78 KiB
TypeScript

import {
mkdir,
readFile,
readdir,
realpath,
rm,
symlink,
writeFile,
} from "node:fs/promises";
import { basename, dirname, join, sep } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { z } from "zod";
import {
createIdaTarget,
RecordingIdaMcp,
} from "../../tests/fixtures/idaMcp.js";
import { nativeTools } from "./IdaNativeProtocol.js";
import { IdaSessionClient } from "./IdaSessionClient.js";
import {
DEFAULT_REGEX_MATCHING_DEADLINE,
REGEX_MATCHING_DEADLINE_SETTING,
} from "../config/regexMatchingDeadline.js";
import { functionDossierSchema } from "../domain/native/functionDossier.js";
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
import {
AnalysisProtocolError,
AnalysisResourceConstraintError,
AnalysisCancelledError,
AnalysisTimeoutError,
} from "../domain/analysisErrorCore.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { WindowsPrivateRuntime } from "../windows/WindowsPrivateRuntime.js";
const roots: string[] = [];
const windowsRuntimes = new Set<WindowsPrivateRuntime>();
beforeEach(() => {
if (process.platform !== "win32") return;
const create = WindowsPrivateRuntime.create;
vi.spyOn(WindowsPrivateRuntime, "create").mockImplementation(
(parent, prefix) => {
const runtime = create(parent, prefix);
windowsRuntimes.add(runtime);
return runtime;
},
);
});
afterEach(async () => {
try {
// These pure producer fixtures spawn no IDA worker. After the retention
// assertions, release only the actual native owners this test created.
for (const runtime of windowsRuntimes) {
await runtime.close();
windowsRuntimes.delete(runtime);
}
for (const root of roots.splice(0))
await rm(root, { recursive: true, force: true });
} finally {
vi.restoreAllMocks();
}
});
/** A failed close whose diagnostic names exactly these unverified sessions. */
const reportsSessions = (...sessions: readonly object[]) => ({
error: {
diagnostics: { upstream_sessions: { state: "unverified", sessions } },
},
});
const LIVE_PEER = {
session_id: "unrelated-live-peer",
backend: "worker",
owned: false,
} as const;
const fixture = async (
mode: "attached" | "headless" = "attached",
timeoutMs = 1000,
protocol?: "native",
) => {
const { root, target } = await createIdaTarget();
roots.push(root);
const producer = new RecordingIdaMcp(target, mode);
const client = new IdaSessionClient(
{
command: "fixture",
args: [],
env: {},
mode,
...(protocol === undefined ? {} : { protocol }),
timeoutMs,
workspaceRoot: root,
},
target,
producer,
);
return { root, target, producer, client };
};
it.each([
new Error("HTTP 404: fixture response"),
new ProviderAdapterError("ida", "transport", {
diagnostics: { reason: "fixture response" },
}),
new AnalysisTimeoutError("ida_mcp_connect", 1000),
new AnalysisProtocolError("Malformed MCP response"),
new AnalysisResourceConstraintError(
"ida_mcp_connect",
"transport",
"fixture limit",
{ max_response_bytes: 1 },
),
])(
"retains the selected HTTP endpoint on a connection failure",
async (cause) => {
const { root, target } = await createIdaTarget();
roots.push(root);
const producer = new RecordingIdaMcp(target, "attached");
vi.spyOn(producer, "connect").mockRejectedValue(cause);
const selected = "HTTP://LOCALHOST:01234/mcp?cursor=kept";
const client = new IdaSessionClient(
{
url: selected,
headers: { Authorization: "Bearer fixture-secret" },
mode: "attached",
timeoutMs: 1000,
},
target,
producer,
);
const result = await client.execute("list_procedures", {});
if (result.ok) throw new Error("Expected connection failure");
expect(result.error.diagnostics).toMatchObject({ endpoint: selected });
expect(JSON.stringify(projectAnalysisError(result.error))).not.toContain(
"fixture-secret",
);
await client.close();
},
);
it.each([
"fixture-secret",
"fixture%2Dsecret",
"fixture%2dsecret",
"%66%69%78%74%75%72%65-secret",
])(
"redacts a known transport credential in the selected endpoint: %s",
async (encoded) => {
const { root, target } = await createIdaTarget();
roots.push(root);
const producer = new RecordingIdaMcp(target, "attached");
vi.spyOn(producer, "connect").mockRejectedValue(
new Error("fixture failure"),
);
const client = new IdaSessionClient(
{
url: `http://localhost:1234/mcp?token=${encoded}&cursor=keep`,
headers: { Authorization: "Bearer fixture-secret" },
mode: "attached",
timeoutMs: 1000,
},
target,
producer,
);
const result = await client.execute("list_procedures", {});
if (result.ok) throw new Error("Expected connection failure");
const output = JSON.stringify(projectAnalysisError(result.error));
expect(output).not.toContain("fixture-secret");
expect(output).not.toContain(encoded);
expect(output).toContain("http://localhost:1234/mcp?token=");
expect(output).toContain("&cursor=keep");
await client.close();
},
);
it.each(["initialize", "transport"])(
"preserves producer outcome facts when attributing a cached %s failure",
async (operation) => {
const { producer, client } = await fixture();
const primary = new AnalysisCancelledError("provider_startup");
const source = new ProviderAdapterError("ida", operation, {
cleanupPrimary: primary,
limit: {
setting: "regex.matching_deadline_ms",
effective_value: 100,
unit: "ms",
minimum: 1,
maximum: 1000,
applies_at: "next_operation",
agent_editable: true,
scope: "rea-runtime",
rea_restart_required: false,
},
diagnostics: { producer_fact: "retained" },
});
const connect = vi.spyOn(producer, "connect").mockRejectedValue(source);
for (const requested of ["list_procedures", "list_strings"] as const) {
const result = await client.execute(requested, {});
if (result.ok) throw new Error("Expected startup failure");
expect(result.error).toBeInstanceOf(ProviderAdapterError);
expect(result.error).toMatchObject({
operation: requested,
limit: source.limit,
cleanupPrimary: { operation: requested },
diagnostics: { producer_fact: "retained" },
});
expect(result.error.cleanupPrimary).toBeInstanceOf(
AnalysisCancelledError,
);
}
expect(connect).toHaveBeenCalledTimes(1);
expect(source.operation).toBe(operation);
expect(source.cleanupPrimary).toBe(primary);
await client.close();
},
);
describe("headless IDA producer pagination", () => {
it("follows reported cursors for modern inventories, pseudocode, and bare-hex assembly without losing pages", async () => {
const { producer, client } = await fixture("headless");
producer.overrideCall = (name, args) => {
const first = args.offset === 0;
const cursor = first ? { next: 1 } : { done: true };
if (name === "decompile")
return { code: first ? "first line" : "second line", cursor };
if (name === "disasm")
return {
asm: {
name: "main",
start_ea: "0x1000",
lines: [
{
addr: first ? "1000" : "1001",
instruction: first ? "push rbp" : "ret",
},
],
},
cursor,
};
if (name === "find_regex")
return {
matches: [
{
addr: first ? "0x3000" : "0x3002",
string: first ? "a.b" : "a+b",
},
],
cursor,
};
if (name === "list_funcs") {
const page = z
.object({ offset: z.number() })
.parse(args.queries).offset;
return [
{
data: [
{
addr: page === 0 ? "0x1000" : "0x2000",
name: page === 0 ? "main" : "other",
size: "0x20",
},
],
next_offset: page === 0 ? 1 : null,
},
];
}
return undefined;
};
const inventory = await client.execute("list_procedures", {});
expect(inventory.ok && inventory.value.result).toHaveLength(2);
const strings = await client.execute("list_strings", {});
expect(strings.ok && strings.value.result).toHaveLength(2);
const result = await client.execute("analyze_function", {
procedure: "main",
});
if (!result.ok) throw result.error;
const dossier = functionDossierSchema.parse(result.value.result);
expect(dossier.pseudocode).toBe("first line\nsecond line");
expect(dossier.assembly).toEqual(["0x1000: push rbp", "0x1001: ret"]);
expect(dossier).toMatchObject({
procedure: {
classification: null,
body: {
available: false,
reason: expect.stringMatching(
/does not report complete function body ranges/iu,
),
},
},
native_api: null,
native_value_flow: null,
});
await client.close();
});
});
describe("headless IDA modern string search failures", () => {
it("rejects a producer error before returning page matches and retains both replies as partial evidence", async () => {
const { producer, client } = await fixture("headless");
producer.overrideCall = (name, args) => {
if (name !== "find_regex") return undefined;
if (args.offset === 0)
return {
matches: [{ addr: "0x3000", string: "complete page" }],
cursor: { next: 1 },
error: null,
};
return {
matches: [{ addr: "0x3001", string: "untrusted partial page" }],
cursor: { done: true },
error: "Search fixture failed",
};
};
try {
const failed = await client.execute("search_strings", {
pattern: "needle",
});
expect(failed.ok).toBe(false);
if (!failed.ok) {
expect(failed.error).toBeInstanceOf(AnalysisProtocolError);
expect(failed.error.message).toContain("Search fixture failed");
expect(failed.error.partialObservation).toMatchObject({
normalized_result: {
status: "partial",
failed_operation: "search_strings",
observations: expect.arrayContaining([
expect.objectContaining({
tool: "find_regex",
result: expect.objectContaining({
matches: [{ addr: "0x3000", string: "complete page" }],
error: null,
}),
}),
expect.objectContaining({
tool: "find_regex",
result: expect.objectContaining({
matches: [
{ addr: "0x3001", string: "untrusted partial page" },
],
error: "Search fixture failed",
}),
}),
]),
},
});
}
expect(
producer.calls
.filter(({ name }) => name === "find_regex")
.map(({ args }) => args.offset),
).toEqual([0, 1]);
producer.overrideCall = (name) =>
name === "find_regex"
? { matches: [], cursor: { done: true }, error: null }
: undefined;
const empty = await client.execute("search_strings", {
pattern: "needle",
});
expect(empty.ok && empty.value.result).toEqual([]);
} finally {
expect((await client.close()).ok).toBe(true);
}
});
});
describe("attached IDA readiness and queued cancellation", () => {
it("allows ready-only work after an attached provider has initialized", async () => {
const { producer, client } = await fixture("attached");
await expect(client.executeIfReady("list_procedures", {})).resolves.toBe(
undefined,
);
const startup = await client.execute("list_procedures", {});
if (!startup.ok) throw startup.error;
const result = await client.executeIfReady("list_procedures", {});
if (result === undefined)
throw new Error("expected ready attached IDA dispatch");
if (!result.ok) throw result.error;
expect(result.value.result).toEqual([
expect.objectContaining({ address: "0x1000", value: "main" }),
]);
expect(
producer.calls.filter(({ name }) => name === "idb_open"),
).toHaveLength(0);
await client.close();
});
it("returns cold and pending startup calls immediately, then cancels queued work before dispatch", async () => {
const { producer, client } = await fixture("headless");
await expect(client.executeIfReady("list_procedures", {})).resolves.toBe(
undefined,
);
expect(producer.connects).toBe(0);
let enterOpen: (() => void) | undefined;
let finishOpen: (() => void) | undefined;
const openEntered = new Promise<void>((resolve) => {
enterOpen = resolve;
});
const openGate = new Promise<void>((resolve) => {
finishOpen = resolve;
});
producer.beforeCall = async (name) => {
if (name === "idb_open") {
enterOpen?.();
await openGate;
}
};
const startup = client.execute("list_procedures", {});
await openEntered;
await expect(client.executeIfReady("list_strings", {})).resolves.toBe(
undefined,
);
expect(
producer.calls.filter(({ name }) => name === "idb_open"),
).toHaveLength(1);
finishOpen?.();
const started = await startup;
if (!started.ok) throw started.error;
const warm = await client.executeIfReady("list_procedures", {});
if (warm === undefined) throw new Error("expected ready IDA dispatch");
if (!warm.ok) throw warm.error;
expect(warm.value.result).toEqual([
expect.objectContaining({ address: "0x1000", value: "main" }),
]);
let enterRead: (() => void) | undefined;
let finishRead: (() => void) | undefined;
const readEntered = new Promise<void>((resolve) => {
enterRead = resolve;
});
const readGate = new Promise<void>((resolve) => {
finishRead = resolve;
});
producer.beforeCall = async (name) => {
if (name === "list_funcs") {
enterRead?.();
await readGate;
}
};
const active = client.execute("list_procedures", {});
await readEntered;
const controller = new AbortController();
const queued = client.executeIfReady(
"list_strings",
{},
{
signal: controller.signal,
},
);
controller.abort();
await expect(queued).resolves.toMatchObject({
ok: false,
error: { _tag: "AnalysisCancelledError" },
});
finishRead?.();
const activeResult = await active;
if (!activeResult.ok) throw activeResult.error;
expect(
producer.calls.filter(({ name }) => name === "list_funcs"),
).toHaveLength(3);
expect(producer.calls.some(({ name }) => name === "find_regex")).toBe(
false,
);
await client.close();
});
});
describe("attached IDA input identity and validation", () => {
it("accepts a normalized spelling of the bound document path", async () => {
const { target, producer, client } = await fixture();
const equivalent =
process.platform === "win32"
? target.path.toLowerCase().replaceAll("\\", "/")
: target.path.split(sep).join(`${sep}.${sep}`);
const result = await client.execute("list_procedures", {
document: equivalent,
});
expect(result.ok).toBe(true);
expect(producer.calls.some(({ name }) => name === "list_functions")).toBe(
true,
);
await client.close();
});
it.skipIf(process.platform === "win32")(
"rejects a document whose symlink traversal names a different input",
async () => {
const { root, target, producer, client } = await fixture();
await mkdir(join(root, "foreign", "inner"), { recursive: true });
await writeFile(
join(root, "foreign", basename(target.path)),
"foreign input",
);
await symlink(join(root, "foreign", "inner"), join(root, "link"));
const document = `${root}/link/../${basename(target.path)}`;
expect(await realpath(document)).not.toBe(await realpath(target.path));
const result = await client.execute("list_procedures", { document });
expect(result).toMatchObject({
ok: false,
error: { _tag: "AnalysisInputError" },
});
expect(producer.connects).toBe(0);
await client.close();
},
);
it.skipIf(process.platform === "win32")(
"refuses an opened worker whose input only matches after lexical parent collapse",
async () => {
const { root, producer, client } = await fixture("headless");
await mkdir(join(root, "foreign", "inner"), { recursive: true });
let privateInput = "";
producer.beforeCall = async (name, args) => {
if (name !== "idb_open") return;
privateInput = z.string().parse(args.input_path);
await writeFile(
join(root, "foreign", basename(privateInput)),
"foreign input",
);
await symlink(
join(root, "foreign", "inner"),
join(dirname(privateInput), "link"),
);
};
producer.overrideCall = (name, args) => {
if (name !== "idb_open") return undefined;
producer.database = z.string().parse(args.preferred_session_id);
producer.inputPath = `${dirname(privateInput)}/link/../${basename(privateInput)}`;
return {
success: true,
session: {
session_id: producer.database,
input_path: producer.inputPath,
},
};
};
const result = await client.execute("list_procedures", {});
expect(await realpath(producer.inputPath)).not.toBe(
await realpath(privateInput),
);
expect(result.ok).toBe(false);
expect(producer.calls.some(({ name }) => name === "list_funcs")).toBe(
false,
);
producer.inputPath = privateInput;
await client.close();
},
);
it("rejects an unrelated document before connecting or opening a provider", async () => {
const { producer, client } = await fixture();
const result = await client.execute("list_procedures", {
document: "/unrelated/input",
});
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error._tag).toBe("AnalysisInputError");
expect(result.error.message).toContain("list_procedures");
}
expect(producer.connects).toBe(0);
expect(producer.calls).toEqual([]);
await client.close();
});
it("preserves hexadecimal-looking function names as names unless the caller uses an explicit 0x address", async () => {
const { producer, client } = await fixture();
producer.overrideCall = (name, args) =>
name === "get_function_by_name" && args.name === "f"
? { address: "0x1000", name: "f", size: "0x20" }
: undefined;
const found = await client.execute("procedure_address", { procedure: "f" });
expect(found.ok && found.value.result).toBe("0x1000");
expect(
producer.calls.some(({ name }) => name === "get_function_by_address"),
).toBe(false);
await client.close();
});
it("preserves omitted optional arguments and rejects explicit null before connecting", async () => {
const { producer, client } = await fixture();
for (const [operation, parameters] of [
["list_procedures", { document: null }],
["list_strings", { address: null }],
["procedure_pseudo_code", { procedure: null }],
] as const) {
const result = await client.execute(operation, parameters);
expect(result.ok).toBe(false);
if (!result.ok)
expect(result.error).toMatchObject({
_tag: "AnalysisInputError",
operation,
issues: [{ path: Object.keys(parameters), reason: "invalid_type" }],
});
}
expect(producer.connects).toBe(0);
expect(producer.calls).toEqual([]);
expect((await client.execute("list_procedures", {})).ok).toBe(true);
expect((await client.execute("list_strings", {})).ok).toBe(true);
expect(
(await client.execute("procedure_pseudo_code", { procedure: "main" })).ok,
).toBe(true);
expect(
producer.calls.filter(({ name }) => name === "decompile_function"),
).toHaveLength(1);
await client.close();
});
});
describe("attached IDA function observations", () => {
it("resolves interior addresses and call sites to canonical entries, preserves externals, and reports unknown body evidence", async () => {
const { producer, client } = await fixture();
const result = await client.execute("analyze_function", {
procedure: "0x1004",
});
if (!result.ok) throw result.error;
const dossier = functionDossierSchema.parse(result.value.result);
expect(dossier.procedure.address).toBe("0x1000");
expect(dossier.callers.map((caller) => caller.address)).toEqual(["0x1000"]);
expect(dossier.callees.map((callee) => callee.address)).toEqual(["0x2000"]);
expect(dossier.procedure.body.available).toBe(false);
expect(dossier.comments).toEqual([]);
expect(result.value.rawResult).toMatchObject({
observations: expect.arrayContaining([
expect.objectContaining({ tool: "get_callers" }),
]),
});
expect(result.value.provider.version).toBe("1");
expect(producer.serverInfo()).toMatchObject({
version: "sdk-version-is-not-ida",
});
await client.close();
expect(producer.calls.some(({ name }) => name.startsWith("idb_"))).toBe(
false,
);
});
it("returns complete paged inventory and distinguishes literal from regex search", async () => {
const { producer, client } = await fixture();
producer.overrideCall = (name, args) =>
name === "list_functions"
? {
data: [
{
address: args.offset === 0 ? "0x1000" : "0x2000",
name: "main",
size: "0x10",
},
],
next_offset: args.offset === 0 ? 100 : null,
}
: undefined;
const inventory = await client.execute("list_procedures", {});
expect(inventory.ok && inventory.value.result).toHaveLength(2);
for (const [pattern, mode, count] of [
["a.b", "literal", 1],
["a+b", "literal", 0],
["a.b", "regex", 1],
] as const) {
const found = await client.execute("search_strings", { pattern, mode });
expect(found.ok && found.value.result).toHaveLength(count);
}
expect(
(await client.execute("search_strings", { pattern: "[", mode: "regex" }))
.ok,
).toBe(false);
await client.close();
});
});
describe("attached IDA regex supervision", () => {
it.each(["search_procedures", "search_strings"] as const)(
"preserves Unicode case semantics and inventory order in %s",
async (operation) => {
const { producer, client } = await fixture();
const values = ["skip", "ÄPFEL", "äpfel"];
producer.overrideCall = (name) =>
name === "list_functions"
? {
data: values.map((name, index) => ({
address: `0x${String(1000 + index)}`,
name,
size: "0x10",
})),
next_offset: null,
}
: name === "list_strings"
? {
data: values.map((string, index) => ({
address: `0x${String(3000 + index)}`,
length: string.length,
string,
})),
next_offset: null,
}
: undefined;
try {
for (const case_sensitive of [false, true]) {
const found = await client.execute(operation, {
pattern: "^ä",
mode: "regex",
case_sensitive,
});
expect(found.ok && found.value.result).toEqual(
values.flatMap((value, index) =>
index === 0 || (case_sensitive && index === 1)
? []
: [
{
address: `0x${String((operation === "search_strings" ? 3000 : 1000) + index)}`,
value,
},
],
),
);
}
} finally {
await client.close();
}
},
);
it.each(["search_procedures", "search_strings"] as const)(
"cancels CPU-bound %s matching and keeps the session usable",
async (operation) => {
const { producer, client } = await fixture();
const value = "a".repeat(26) + "!";
producer.overrideCall = (name) =>
name === "list_functions"
? {
data: [{ address: "0x1000", name: value, size: "0x10" }],
next_offset: null,
}
: name === "list_strings"
? {
data: [
{ address: "0x3000", length: value.length, string: value },
],
next_offset: null,
}
: undefined;
await client.execute("health", {});
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 50);
try {
const result = await client.execute(
operation,
{ pattern: "^(a+)+$", mode: "regex" },
{ signal: controller.signal },
);
expect(result.ok).toBe(false);
if (!result.ok)
expect(result.error._tag).toBe("AnalysisCancelledError");
const recovered = await client.execute(operation, {
pattern: "!",
mode: "literal",
});
expect(recovered.ok && recovered.value.result).toHaveLength(1);
} finally {
clearTimeout(timer);
await client.close();
}
},
15_000,
);
it("bounds regex matching without caller cancellation and preserves subsequent searches", async () => {
const { producer, client } = await fixture();
const value = "a".repeat(40) + "!";
producer.overrideCall = (name) =>
name === "list_strings"
? {
data: [{ address: "0x3000", length: value.length, string: value }],
next_offset: null,
}
: undefined;
try {
const result = await client.execute("search_strings", {
pattern: "^(a+)+$",
mode: "regex",
});
expect(result.ok).toBe(false);
if (
result.ok ||
!(result.error instanceof AnalysisResourceConstraintError)
)
throw new Error("Expected a supervised regex deadline");
expect(result.error).toMatchObject({
operation: "search_strings",
resource: "cpu",
reportedLimits: {
matching_deadline_ms: DEFAULT_REGEX_MATCHING_DEADLINE.deadlineMs,
matching_deadline_source: DEFAULT_REGEX_MATCHING_DEADLINE.source,
},
});
expect(result.error.message).toContain(REGEX_MATCHING_DEADLINE_SETTING);
const recovered = await client.execute("search_strings", {
pattern: "!$",
mode: "regex",
});
expect(recovered.ok && recovered.value.result).toEqual([
expect.objectContaining({ address: "0x3000", value }),
]);
if (recovered.ok)
expect(recovered.value.limitations.join("\n")).toContain(
REGEX_MATCHING_DEADLINE_SETTING,
);
} finally {
await client.close();
}
}, 15_000);
});
describe("attached IDA function failure handling", () => {
it("rejects mismatched identities and target switches rather than returning a successful observation", async () => {
const { producer, client, target } = await fixture();
producer.inputSha256 = "a".repeat(64);
const mismatch = await client.execute("health", {});
expect(mismatch.ok).toBe(false);
if (!mismatch.ok) {
expect(projectAnalysisError(mismatch.error)).toMatchObject({
code: "unreadable_output",
retryable: false,
message: expect.stringContaining("SHA-256"),
remediation: { action: expect.stringContaining("headless mode") },
details: {
operation: "health",
diagnostics: {
identity_check: "attached_sha256",
reason: expect.stringContaining(target.path),
},
},
});
}
expect(producer.calls.map(({ name }) => name)).toEqual(["get_metadata"]);
producer.inputSha256 = target.sha256;
producer.beforeCall = async (name) => {
if (name === "decompile_function") producer.imageBase = "0x2000";
};
const changed = await client.execute("procedure_pseudo_code", {
procedure: "main",
});
expect(changed.ok).toBe(false);
if (!changed.ok) expect(changed.error.message).toContain("changed");
await client.close();
});
it("rejects malformed producer data, non-advancing cursors, unsupported mutation, and missing explicit addresses", async () => {
const { producer, client } = await fixture();
expect(
(
await client.execute("set_address_name", {
address: "0x1000",
name: "changed",
})
).ok,
).toBe(false);
expect(producer.connects).toBe(0);
expect((await client.execute("xrefs", {})).ok).toBe(false);
const invalidAddress = await client.execute("xrefs", {
address: "not-an-address",
});
expect(invalidAddress.ok).toBe(false);
if (!invalidAddress.ok)
expect(invalidAddress.error._tag).toBe("AnalysisInputError");
expect(producer.connects).toBe(0);
producer.overrideCall = (name) =>
name === "list_functions" ? { data: [], next_offset: 0 } : undefined;
expect((await client.execute("list_procedures", {})).ok).toBe(false);
producer.overrideCall = (name) =>
name === "decompile_function" ? { unexpected: true } : undefined;
const malformed = await client.execute("procedure_pseudo_code", {
procedure: "main",
});
expect(malformed.ok).toBe(false);
if (!malformed.ok) expect(malformed.error._tag).toBe("AnalysisOutputError");
await client.close();
});
it("drains in-flight read-only work on cancellation before closing its proxy", async () => {
const { producer, client } = await fixture();
await client.execute("health", {});
let release: (() => void) | undefined;
let notify: (() => void) | undefined;
const started = new Promise<void>((resolve) => {
notify = resolve;
});
producer.beforeCall = async (name) => {
if (name === "decompile_function") {
notify?.();
await new Promise<void>((resolve) => {
release = resolve;
});
}
};
const controller = new AbortController();
const call = client.execute(
"procedure_pseudo_code",
{ procedure: "main" },
{ signal: controller.signal },
);
await started;
controller.abort();
const close = client.close();
expect(producer.closes).toBe(0);
release?.();
const result = await call;
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error._tag).toBe("AnalysisCancelledError");
expect((await close).ok).toBe(true);
expect(producer.closes).toBe(1);
});
});
const busyHealth = (busyTool?: JsonValue): JsonValue => ({
status: "busy",
uptime_sec: 1.041,
...(busyTool === undefined ? {} : { busy_tool: busyTool }),
busy_sec: 0,
queued_calls: 0,
});
const healthCalls = (producer: { calls: { name: string }[] }) =>
producer.calls.filter(({ name }) => name === "server_health").length;
const expectCleanClose = async (
producer: { closes: number },
client: { close: () => Promise<{ ok: boolean }> },
): Promise<void> => {
expect((await client.close()).ok, "the owned worker is released").toBe(true);
expect(producer.closes, "the upstream connection is closed once").toBe(1);
};
describe("headless IDA transient busy health", () => {
it("re-reads server_health until the worker reports ok", async () => {
const { producer, client } = await fixture("headless");
let busyReads = 2;
producer.overrideCall = (name) =>
name === "server_health" && busyReads-- > 0
? busyHealth("config_json_get")
: undefined;
const result = await client.execute("health", {});
expect(result.ok, "a transient busy worker is waited out").toBe(true);
expect(
healthCalls(producer),
"two busy reads and the ok read that settles the identity",
).toBe(3);
await expectCleanClose(producer, client);
});
it("returns a retryable timeout naming the busy tool at the registration deadline", async () => {
const { producer, client } = await fixture("headless", 300);
const busy = busyHealth("config_json_get");
producer.overrideCall = (name) =>
name === "server_health" ? busy : undefined;
const started = Date.now();
const result = await client.execute("health", {});
const elapsed = Date.now() - started;
if (result.ok) throw new Error("expected a busy timeout");
expect(result.error._tag, "the neutral timeout class is used").toBe(
"AnalysisTimeoutError",
);
const projected = projectAnalysisError(result.error);
expect(projected.code, "the outcome is a provider timeout").toBe(
"provider_timeout",
);
expect(projected.retryable, "the caller may retry").toBe(true);
expect(
projected.details,
"the timeout is attributed to the requested operation",
).toMatchObject({
operation: "health",
timeout_ms: 300,
});
expect(
projected.message,
"the public error message names the busy tool",
).toContain("config_json_get");
expect(
`${result.error.message} ${projected.message}`,
"no Hopper wording leaks in",
).not.toContain("Hopper");
expect(
result.error.cause,
"reattribution preserves the producing timeout",
).toMatchObject({
_tag: "AnalysisTimeoutError",
operation: "server_health",
cause: busy,
diagnostics: { busy_tool: "config_json_get" },
});
expect(
elapsed,
"the wait runs to the deadline, not less",
).toBeGreaterThanOrEqual(290);
expect(
healthCalls(producer),
"reads are spaced by the retry delay, not a tight loop",
).toBeLessThanOrEqual(6);
await expectCleanClose(producer, client);
});
it("never invents a busy tool the worker did not report", async () => {
const { producer, client } = await fixture("headless", 300);
producer.overrideCall = (name) =>
name === "server_health" ? busyHealth() : undefined;
const result = await client.execute("health", {});
if (result.ok) throw new Error("expected a busy timeout");
expect(
projectAnalysisError(result.error).details,
"the public operation stays independent of an omitted busy tool",
).toMatchObject({
operation: "health",
});
expect(result.error.diagnostics).toMatchObject({ busy_tool: "unknown" });
expect(projectAnalysisError(result.error).message).toContain(
"busy tool was unknown",
);
await expectCleanClose(producer, client);
});
it.each([
["free-form text", "token=abc123 /private<home> secret"],
["an over-long name", "x".repeat(200)],
])(
"reports %s from the worker as unknown instead of echoing it",
async (_label, tool) => {
const { producer, client } = await fixture("headless", 300);
producer.overrideCall = (name) =>
name === "server_health" ? busyHealth(tool) : undefined;
const result = await client.execute("health", {});
if (result.ok) throw new Error("expected a busy timeout");
expect(
projectAnalysisError(result.error).details,
"only a tool-name-shaped value is repeated",
).toMatchObject({
operation: "health",
});
expect(result.error.diagnostics).toMatchObject({ busy_tool: "unknown" });
expect(
projectAnalysisError(result.error).message,
"the producer text never reaches the public error message",
).not.toContain(tool);
await expectCleanClose(producer, client);
},
);
it("ends the wait at once when the caller aborts", async () => {
const { producer, client } = await fixture("headless", 30_000);
producer.overrideCall = (name) =>
name === "server_health" ? busyHealth("config_json_get") : undefined;
const controller = new AbortController();
const started = Date.now();
const call = client.execute("health", {}, { signal: controller.signal });
while (healthCalls(producer) < 2) {
if (Date.now() - started > 5_000) throw new Error("never polled");
await new Promise((resolve) => setTimeout(resolve, 10));
}
controller.abort();
const result = await call;
if (result.ok) throw new Error("expected cancellation");
expect(result.error._tag, "cancellation keeps today's outcome").toBe(
"AnalysisCancelledError",
);
expect(
Date.now() - started,
"the wait does not run to the registration deadline",
).toBeLessThan(5_000);
await expectCleanClose(producer, client);
});
});
describe("headless IDA busy health after an operation", () => {
const withInventory = (
producer: RecordingIdaMcp,
afterObservation: (read: number) => JsonValue | undefined,
) => {
let health = 0;
producer.overrideCall = (name) => {
if (name === "list_funcs")
return [
{
data: [{ addr: "0x1000", name: "main", size: "0x20" }],
next_offset: null,
},
];
if (name !== "server_health") return undefined;
health += 1;
return health < 2 ? undefined : afterObservation(health);
};
};
it("waits out a busy observation taken after the operation", async () => {
const { producer, client } = await fixture("headless");
withInventory(producer, (read) =>
read === 2 ? busyHealth("config_json_get") : undefined,
);
const result = await client.execute("list_procedures", {});
expect(result.ok, "the operation succeeds once the worker settles").toBe(
true,
);
expect(healthCalls(producer), "before, busy after, ok after").toBe(3);
await expectCleanClose(producer, client);
});
it("maps a cancellation during that wait to cancelled", async () => {
const { producer, client } = await fixture("headless", 30_000);
withInventory(producer, () => busyHealth("config_json_get"));
const controller = new AbortController();
const call = client.execute(
"list_procedures",
{},
{ signal: controller.signal },
);
const started = Date.now();
while (healthCalls(producer) < 3) {
if (Date.now() - started > 5_000) throw new Error("never polled");
await new Promise((resolve) => setTimeout(resolve, 10));
}
controller.abort();
const result = await call;
if (result.ok) throw new Error("expected cancellation");
expect(result.error._tag, "the after-observation wait is cancellable").toBe(
"AnalysisCancelledError",
);
await expectCleanClose(producer, client);
});
});
describe("headless IDA non-busy health payloads", () => {
it.each([
["an unknown status", { status: "starting" }],
["a non-string busy_tool", busyHealth(5)],
["a payload without a status", { uptime_sec: 1 }],
])(
"keeps %s as unreadable output without waiting",
async (_label, payload) => {
const { producer, client } = await fixture("headless");
producer.overrideCall = (name) =>
name === "server_health" ? jsonValueSchema.parse(payload) : undefined;
const result = await client.execute("health", {});
if (result.ok) throw new Error("expected unreadable output");
expect(
projectAnalysisError(result.error).code,
"only the narrow busy shape enters the wait",
).toBe("unreadable_output");
expect(healthCalls(producer), "no re-read happened").toBe(1);
await expectCleanClose(producer, client);
},
);
it("keeps the busy-shape reason when a busy payload is invalid", async () => {
const { producer, client } = await fixture("headless");
producer.overrideCall = (name) =>
name === "server_health" ? busyHealth(5) : undefined;
const result = await client.execute("health", {});
if (result.ok) throw new Error("expected unreadable output");
expect(
projectAnalysisError(result.error).details,
"the diagnostic names the invalid busy_tool, not the ok-schema fields",
).toMatchObject({ reason: expect.stringContaining("busy_tool") });
expect(
JSON.stringify(projectAnalysisError(result.error).details),
"the generic ok-schema field errors are not reported for a busy payload",
).not.toContain("input_path");
await expectCleanClose(producer, client);
});
it("stops waiting as soon as a later read is malformed", async () => {
const { producer, client } = await fixture("headless");
let reads = 0;
producer.overrideCall = (name) =>
name === "server_health"
? reads++ === 0
? busyHealth("config_json_get")
: busyHealth(5)
: undefined;
const result = await client.execute("health", {});
if (result.ok) throw new Error("expected unreadable output");
expect(
projectAnalysisError(result.error).code,
"a malformed re-read is not treated as busy",
).toBe("unreadable_output");
expect(
healthCalls(producer),
"exactly the busy read and the bad read",
).toBe(2);
await expectCleanClose(producer, client);
});
});
describe("IDA compatibility profile mismatch", () => {
it("reports the native profile for an incompatible catalog", async () => {
const { producer, client } = await fixture("attached", 1000, "native");
producer.tools = [];
const result = await client.execute("health", {});
if (result.ok) throw new Error("Expected incompatible native catalog");
expect(projectAnalysisError(result.error).details).toMatchObject({
diagnostics: {
mode: "attached",
expected_profile: "native",
missing_tools: expect.arrayContaining(["decompile"]),
},
});
expect(producer.calls).toEqual([]);
expect((await client.close()).ok).toBe(true);
});
it("names every missing tool so the caller can identify the upstream profile", async () => {
const { producer, client } = await fixture("headless");
producer.tools = [
"idb_open",
"idb_list",
"server_health",
"list_funcs",
"lookup_funcs",
"find_regex",
"decompile",
"disasm",
"callees",
"xrefs_to",
];
const result = await client.execute("health", {});
expect(result.ok, "an incomplete profile cannot serve analysis").toBe(
false,
);
if (result.ok) return;
const projected = projectAnalysisError(result.error);
expect(
projected.details,
"the absent tool and the expected profile are both reported",
).toMatchObject({
provider_id: "ida",
diagnostics: {
mode: "headless",
expected_profile: "database-supervisor",
missing_tools: ["idb_close"],
},
});
});
it("reports the legacy profile when an attached producer serves modern tools", async () => {
const { producer, client } = await fixture("attached");
producer.tools = ["server_health", "lookup_funcs", "decompile", "disasm"];
const result = await client.execute("health", {});
expect(result.ok, "an incomplete profile cannot serve analysis").toBe(
false,
);
if (result.ok) return;
expect(
projectAnalysisError(result.error).details,
"attached mode names the legacy profile it expected",
).toMatchObject({
diagnostics: { mode: "attached", expected_profile: "legacy-1.4" },
});
});
});
describe("headless IDA lifecycle boundaries", () => {
it("never closes an unowned worker even when open echoes the requested private identity", async () => {
const { producer, client } = await fixture("headless");
producer.owned = false;
expect((await client.execute("health", {})).ok).toBe(false);
const closed = await client.close();
expect(closed.ok).toBe(false);
if (!closed.ok) expect(closed.error.cleanupIncomplete).toBe(true);
expect(producer.calls.some(({ name }) => name === "idb_close")).toBe(false);
});
});
describe("headless IDA session ownership and cleanup", () => {
it("shares an in-flight close across concurrent cleanup callers", async () => {
const { producer, client } = await fixture("headless");
expect((await client.execute("health", {})).ok).toBe(true);
let release: (() => void) | undefined;
let notify: (() => void) | undefined;
const closeStarted = new Promise<void>((resolve) => {
notify = resolve;
});
producer.beforeCall = async (name) => {
if (name !== "idb_close") return;
notify?.();
await new Promise<void>((resolve) => {
release = resolve;
});
};
const first = client.close();
await closeStarted;
const second = client.close();
release?.();
const results = await Promise.all([first, second]);
expect(results.every((result) => result.ok)).toBe(true);
expect(
producer.calls.filter(({ name }) => name === "idb_close"),
).toHaveLength(1);
expect(producer.closes).toBe(1);
});
it("retains the workspace after an unconfirmed open, even when inventory is empty, and never retries startup implicitly", async () => {
const { producer, client, root } = await fixture("headless");
producer.beforeCall = async (name) => {
if (name === "idb_open")
throw new Error("Timed out while the worker may still be opening");
};
expect((await client.execute("health", {})).ok).toBe(false);
expect((await client.execute("health", {})).ok).toBe(false);
expect(
producer.calls.filter(({ name }) => name === "idb_open"),
).toHaveLength(1);
const closed = await client.close();
expect(closed.ok).toBe(false);
if (!closed.ok) expect(closed.error.cleanupIncomplete).toBe(true);
expect(
(await readdir(root)).some((name) => name.startsWith("rea-ida-")),
).toBe(true);
expect(producer.closes).toBe(0);
});
it("opens a private digest-verified copy, scopes requests, closes without saving, and removes only its workspace", async () => {
const { producer, client, root, target } = await fixture("headless");
const original = await readFile(target.path);
const result = await client.execute("analyze_function", {
procedure: "main",
});
if (!result.ok) throw result.error;
expect(producer.inputPath).not.toBe(target.path);
expect(Buffer.compare(await readFile(producer.inputPath), original)).toBe(
0,
);
expect(
functionDossierSchema
.parse(result.value.result)
.limitations.some((value) => value.includes("Direct callers")),
).toBe(true);
const open = producer.calls.find(({ name }) => name === "idb_open");
expect(open?.args).toMatchObject({
mode: "force_headless",
run_auto_analysis: true,
});
for (const call of producer.calls.filter(
({ name }) => !["idb_open", "idb_list"].includes(name),
))
expect(call.args.database).toBe(producer.database);
expect((await client.close()).ok).toBe(true);
expect(
producer.calls.find(({ name }) => name === "idb_close")?.args.save,
).toBe(false);
expect(await readdir(root)).toEqual(["target.elf"]);
expect(Buffer.compare(await readFile(target.path), original)).toBe(0);
});
it("retains the private workspace and reports cleanup failure when worker release is unverified", async () => {
const { producer, client, root } = await fixture("headless");
await client.execute("health", {});
const sessionId = producer.database;
producer.failClose = true;
const result = await client.close();
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error.cleanupIncomplete).toBe(true);
expect(result).toMatchObject(
reportsSessions({
session_id: sessionId,
backend: "worker",
owned: true,
input_path: producer.inputPath,
}),
);
expect(
(await readdir(root)).some((name) => name.startsWith("rea-ida-")),
).toBe(true);
});
});
describe("headless IDA blanked-identity inventory rows", () => {
it("releases cleanly when the closed worker lingers in inventory without an identity", async () => {
const { producer, client, root } = await fixture("headless");
expect((await client.execute("health", {})).ok).toBe(true);
let released = false;
producer.beforeCall = async (name) => {
if (name === "idb_close") released = true;
};
// A worker observed mid-teardown: identity cleared, private input retained.
producer.overrideCall = (name) =>
name === "idb_list" && released
? {
sessions: [
{
session_id: "",
input_path: producer.inputPath,
owned: false,
backend: "worker",
is_active: false,
},
],
count: 1,
}
: undefined;
expect((await client.close()).ok).toBe(true);
expect(await readdir(root)).toEqual(["target.elf"]);
});
it("still reports a same-input gui row whose identity is blank", async () => {
const { producer, client, root } = await fixture("headless");
expect((await client.execute("health", {})).ok).toBe(true);
let released = false;
producer.beforeCall = async (name) => {
if (name === "idb_close") released = true;
};
// A gui binding belongs to an IDA instance REA does not control, so the
// blanked-identity exception must not reach it.
producer.overrideCall = (name) =>
name === "idb_list" && released
? {
sessions: [
{
session_id: "",
input_path: producer.inputPath,
owned: false,
backend: "gui",
is_active: false,
},
],
count: 1,
}
: undefined;
const result = await client.close();
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error.cleanupIncomplete).toBe(true);
expect(
(await readdir(root)).some((name) => name.startsWith("rea-ida-")),
).toBe(true);
});
it("still reports a same-input peer that keeps a session identity", async () => {
const { producer, client, root } = await fixture("headless");
expect((await client.execute("health", {})).ok).toBe(true);
let released = false;
producer.beforeCall = async (name) => {
if (name === "idb_close") released = true;
};
producer.overrideCall = (name) =>
name === "idb_list" && released
? {
sessions: [
{
session_id: "unrelated-live-peer",
input_path: producer.inputPath,
owned: false,
backend: "worker",
is_active: false,
},
],
count: 1,
}
: undefined;
const result = await client.close();
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error.cleanupIncomplete).toBe(true);
expect(
(await readdir(root)).some((name) => name.startsWith("rea-ida-")),
).toBe(true);
});
});
describe("headless IDA reaped worker cleanup", () => {
it("recovers a reaped worker only after inventory confirms its session and private input are absent", async () => {
const { producer, client, root } = await fixture("headless");
expect((await client.execute("health", {})).ok).toBe(true);
const sessionId = producer.database;
producer.beforeCall = async (name) => {
if (name === "idb_close") producer.database = undefined;
};
producer.overrideCall = (name) =>
name === "idb_close"
? { error: `Session not found: ${sessionId}` }
: undefined;
expect((await client.close()).ok).toBe(true);
expect(producer.calls.map(({ name }) => name).slice(-2)).toEqual([
"idb_close",
"idb_list",
]);
expect(producer.closes).toBe(1);
expect(await readdir(root)).toEqual(["target.elf"]);
});
it("retains resources when an absent-session close is followed by a live private-input peer", async () => {
const { producer, client, root } = await fixture("headless");
expect((await client.execute("health", {})).ok).toBe(true);
const sessionId = producer.database;
producer.beforeCall = async (name) => {
if (name === "idb_close") producer.database = undefined;
if (name === "idb_list" && producer.database === undefined) {
producer.database = "unrelated-live-peer";
producer.owned = false;
}
};
producer.overrideCall = (name) =>
name === "idb_close"
? { error: `Session not found: ${sessionId}` }
: undefined;
const result = await client.close();
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error.cleanupIncomplete).toBe(true);
// The released session must not be named as the remaining worker.
expect(result).toMatchObject(reportsSessions(LIVE_PEER));
expect(producer.closes).toBe(0);
expect(
(await readdir(root)).some((name) => name.startsWith("rea-ida-")),
).toBe(true);
// A retry whose inventory is unavailable keeps the observed blocker.
producer.beforeCall = async (name) => {
if (name === "idb_list") throw new Error("inventory unavailable");
};
const retried = await client.close();
expect(retried.ok).toBe(false);
expect(retried).toMatchObject(reportsSessions(LIVE_PEER));
});
it("retries independent inventory verification after an absent-session response", async () => {
const { producer, client, root } = await fixture("headless");
expect((await client.execute("health", {})).ok).toBe(true);
const sessionId = producer.database;
let inventoryUnavailable = true;
producer.beforeCall = async (name) => {
if (name === "idb_close") producer.database = undefined;
if (name === "idb_list" && inventoryUnavailable) {
inventoryUnavailable = false;
throw new Error("Inventory temporarily unavailable");
}
};
producer.overrideCall = (name) =>
name === "idb_close"
? { error: `Session not found: ${sessionId}` }
: undefined;
expect((await client.close()).ok).toBe(false);
expect(producer.closes).toBe(0);
expect((await client.close()).ok).toBe(true);
expect(
producer.calls.filter(({ name }) => name === "idb_close"),
).toHaveLength(1);
expect(producer.closes).toBe(1);
expect(await readdir(root)).toEqual(["target.elf"]);
});
it("does not treat a different missing session identity as proof of release", async () => {
const { producer, client, root } = await fixture("headless");
expect((await client.execute("health", {})).ok).toBe(true);
producer.beforeCall = async (name) => {
if (name === "idb_close") producer.database = undefined;
};
producer.overrideCall = (name) =>
name === "idb_close"
? { error: "Session not found: another-session" }
: undefined;
const result = await client.close();
expect(result.ok).toBe(false);
expect(
producer.calls.filter(({ name }) => name === "idb_list"),
).toHaveLength(1);
expect(producer.closes).toBe(0);
expect(
(await readdir(root)).some((name) => name.startsWith("rea-ida-")),
).toBe(true);
});
});
/** A rejected open response, with helpers for separately inventoried workers. */
const rejectedOpenFixture = async () => {
const { producer, client } = await fixture("headless");
producer.overrideCall = (name) =>
name === "idb_open" ? { error: "open did not complete" } : undefined;
expect((await client.execute("health", {})).ok).toBe(false);
const open = producer.calls.find(({ name }) => name === "idb_open")?.args;
const entry = (session_id: string, owned: boolean) => ({
session_id,
input_path: String(open?.input_path),
owned,
backend: "worker",
is_active: true,
});
return {
producer,
client,
requested: String(open?.preferred_session_id),
entry,
};
};
describe("headless IDA cleanup diagnostics", () => {
it("does not name a worker whose close was confirmed when inventory then fails", async () => {
const { producer, client } = await fixture("headless");
expect((await client.execute("health", {})).ok).toBe(true);
producer.beforeCall = async (name) => {
if (name === "idb_list") throw new Error("inventory unavailable");
};
const result = await client.close();
expect(result.ok).toBe(false);
expect(producer.calls.map(({ name }) => name)).toContain("idb_close");
expect(result).toMatchObject(reportsSessions());
});
it("retains peers and confirmed ownership from successful startup when close fails", async () => {
const { producer, client } = await fixture("headless");
producer.overrideCall = (name) => {
if (name === "idb_list")
return {
sessions: [
{
session_id: String(producer.database),
input_path: producer.inputPath,
owned: true,
backend: "worker",
is_active: true,
},
{
session_id: "startup-peer",
input_path: producer.inputPath,
owned: false,
backend: "worker",
is_active: false,
},
],
};
if (name === "idb_close") return { error: "close rejected" };
return undefined;
};
expect((await client.execute("health", {})).ok).toBe(true);
const result = await client.close();
expect(result).toMatchObject(
reportsSessions(
{
session_id: producer.database,
backend: "worker",
owned: true,
input_path: producer.inputPath,
},
{
session_id: "startup-peer",
backend: "worker",
owned: false,
input_path: producer.inputPath,
is_active: false,
},
),
);
});
it("preserves an observed conflicting input binding after close", async () => {
const { producer, client } = await fixture("headless");
expect((await client.execute("health", {})).ok).toBe(true);
const requested = String(producer.database);
producer.overrideCall = (name) =>
name === "idb_list"
? {
sessions: [
{
session_id: requested,
input_path: "/different/input",
owned: false,
backend: "worker",
is_active: true,
},
],
}
: undefined;
const result = await client.close();
expect(result).toMatchObject(
reportsSessions({
session_id: requested,
backend: "worker",
owned: false,
input_path: "/different/input",
}),
);
});
it("keeps peers seen beside the requested worker when its close fails", async () => {
const { producer, client, requested, entry } = await rejectedOpenFixture();
producer.overrideCall = (name) => {
if (name === "idb_list")
return {
sessions: [
entry(requested, true),
entry("unrelated-live-peer", false),
],
};
if (name === "idb_close") return { error: "close rejected" };
return undefined;
};
const result = await client.close();
expect(result.ok).toBe(false);
expect(result).toMatchObject(
reportsSessions(
{ session_id: requested, backend: "worker", owned: true },
LIVE_PEER,
),
);
});
});
describe("headless IDA cleanup diagnostics for observed peers", () => {
it("drops the requested worker from blockers once its close is confirmed", async () => {
const { producer, client, requested, entry } = await rejectedOpenFixture();
let inventories = 0;
producer.beforeCall = async (name) => {
if (name === "idb_list" && ++inventories > 1)
throw new Error("inventory unavailable");
};
producer.overrideCall = (name) => {
if (name === "idb_list")
return {
sessions: [
entry(requested, true),
entry("unrelated-live-peer", false),
],
};
if (name === "idb_close")
return {
success: true,
session_id: requested,
saved: false,
owned: true,
backend: "worker",
};
return undefined;
};
const result = await client.close();
expect(result.ok).toBe(false);
expect(result).toMatchObject(reportsSessions(LIVE_PEER));
});
it("reports a blocking GUI session by its backend", async () => {
const { producer, client } = await fixture("headless");
expect((await client.execute("health", {})).ok).toBe(true);
const sessionId = producer.database;
producer.overrideCall = (name) => {
if (name === "idb_close")
return { error: `Session not found: ${sessionId}` };
if (name === "idb_list")
return {
sessions: [
{
session_id: "",
input_path: producer.inputPath,
owned: false,
backend: "gui",
is_active: true,
},
],
};
return undefined;
};
const result = await client.close();
expect(result.ok).toBe(false);
expect(result).toMatchObject(
reportsSessions({ session_id: "", backend: "gui", owned: false }),
);
});
});
/** A completed open response is not proof that its private Worker was ever released. */
describe("headless IDA unresolved successful-open ownership", () => {
it.each(["matching", "foreign-input", "malformed"] as const)(
"retains the private input after %s success with incomplete initial discovery",
async (kind) => {
const { producer, client, root } = await fixture("headless");
let hidden = true;
let ownedId: string | undefined;
let privateInput: string | undefined;
producer.beforeCall = async (name, args) => {
if (name !== "idb_open") return;
ownedId = z.string().parse(args.preferred_session_id);
privateInput = z.string().parse(args.input_path);
producer.database = ownedId;
producer.inputPath = privateInput;
};
producer.overrideCall = (name) => {
if (name === "idb_open") {
const originalId = z.string().parse(ownedId);
const originalInput = z.string().parse(privateInput);
if (kind === "foreign-input")
return {
success: true,
session: {
session_id: "foreign-session",
input_path: "/foreign/input",
},
};
if (kind === "malformed")
return { success: true, session: { session_id: originalId } };
return {
success: true,
session: { session_id: originalId, input_path: originalInput },
};
}
if (name === "idb_list" && hidden) return { sessions: [] };
return undefined;
};
expect((await client.execute("health", {})).ok).toBe(false);
const incomplete = await client.close();
expect(incomplete.ok).toBe(false);
if (!incomplete.ok) expect(incomplete.error.cleanupIncomplete).toBe(true);
expect(producer.closes).toBe(0);
expect(producer.database).toBe(ownedId);
expect(
(await readdir(root)).some((name) => name.startsWith("rea-ida-")),
).toBe(true);
expect(producer.calls.some(({ name }) => name === "idb_close")).toBe(
false,
);
// A later positive private ownership record permits ordinary save=false
// cleanup, without ever adopting the foreign open-response identity.
hidden = false;
expect((await client.close()).ok).toBe(true);
expect(producer.calls.filter(({ name }) => name === "idb_close")).toEqual(
[{ name: "idb_close", args: { database: ownedId, save: false } }],
);
expect(producer.closes).toBe(1);
expect(await readdir(root)).toEqual(["target.elf"]);
},
);
it("preserves cleanup after a completed, explicit idb_open rejection", async () => {
const { producer, client, root } = await fixture("headless");
producer.overrideCall = (name) =>
name === "idb_open"
? { error: "Input was rejected before creating a session" }
: undefined;
expect((await client.execute("health", {})).ok).toBe(false);
expect((await client.close()).ok).toBe(true);
expect(producer.calls.some(({ name }) => name === "idb_close")).toBe(false);
expect(producer.closes).toBe(1);
expect(await readdir(root)).toEqual(["target.elf"]);
});
it("retains cleanup when a rejected open leaves the requested ID on another input", async () => {
const { producer, client, requested } = await rejectedOpenFixture();
producer.overrideCall = (name) =>
name === "idb_list"
? {
sessions: [
{
session_id: requested,
input_path: "/different/input",
owned: false,
backend: "worker",
is_active: true,
},
],
}
: undefined;
const result = await client.close();
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error.cleanupIncomplete).toBe(true);
expect(producer.closes).toBe(0);
});
});
describe("headless IDA cleanup verification", () => {
it("retries failed worker release through the retained connection before removing its workspace", async () => {
const { producer, client, root } = await fixture("headless");
expect((await client.execute("health", {})).ok).toBe(true);
producer.failClose = true;
expect((await client.close()).ok).toBe(false);
expect(producer.closes).toBe(0);
expect(
(await readdir(root)).some((name) => name.startsWith("rea-ida-")),
).toBe(true);
producer.failClose = false;
expect((await client.close()).ok).toBe(true);
expect(
producer.calls.filter(({ name }) => name === "idb_close"),
).toHaveLength(2);
expect(producer.closes).toBe(1);
expect(await readdir(root)).toEqual(["target.elf"]);
});
it("retries post-close inventory verification without closing an already released database again", async () => {
const { producer, client, root } = await fixture("headless");
expect((await client.execute("health", {})).ok).toBe(true);
let inventoryFails = true;
producer.beforeCall = async (name) => {
if (
name === "idb_list" &&
producer.database === undefined &&
inventoryFails
)
throw new Error(
"Worker inventory unavailable after acknowledged close",
);
};
expect((await client.close()).ok).toBe(false);
expect(producer.closes).toBe(0);
expect(
(await readdir(root)).some((name) => name.startsWith("rea-ida-")),
).toBe(true);
inventoryFails = false;
expect((await client.close()).ok).toBe(true);
expect(
producer.calls.filter(({ name }) => name === "idb_close"),
).toHaveLength(1);
expect(await readdir(root)).toEqual(["target.elf"]);
});
it("releases a privately owned worker after malformed open output using verified session inventory", async () => {
const { producer, client, root } = await fixture("headless");
let ownedId: string | undefined;
producer.beforeCall = async (name, args) => {
if (name === "idb_open") {
ownedId = z.string().parse(args.preferred_session_id);
producer.database = ownedId;
producer.inputPath = z.string().parse(args.input_path);
}
};
producer.overrideCall = (name) =>
name === "idb_open"
? jsonValueSchema.parse({
success: true,
session: { session_id: producer.database },
})
: undefined;
expect((await client.execute("health", {})).ok).toBe(false);
expect((await client.close()).ok).toBe(true);
expect(ownedId).toEqual(expect.any(String));
expect(
producer.calls.find(({ name }) => name === "idb_close")?.args.database,
).toBe(ownedId);
expect(await readdir(root)).toEqual(["target.elf"]);
});
});
it("retains a conflicting requested session ID during pre-close recovery", async () => {
const { producer, client, root } = await fixture("headless");
producer.overrideCall = (name) =>
name === "idb_list"
? {
sessions: [
{
session_id: String(producer.database),
input_path: "/conflicting/input",
owned: true,
backend: "worker",
is_active: false,
},
],
}
: undefined;
expect((await client.execute("health", {})).ok).toBe(false);
const requested = String(producer.database);
const result = await client.close();
expect(result).toMatchObject(
reportsSessions({
session_id: requested,
input_path: "/conflicting/input",
owned: true,
backend: "worker",
is_active: false,
}),
);
expect(producer.calls.some(({ name }) => name === "idb_close")).toBe(false);
expect(
(await readdir(root)).some((name) => name.startsWith("rea-ida-")),
).toBe(true);
});
it.each(["unavailable", "malformed"] as const)(
"does not invent a worker after explicit rejection and %s inventory",
async (kind) => {
const { producer, client } = await fixture("headless");
producer.overrideCall = (name) =>
name === "idb_open"
? { error: "rejected before worker creation" }
: name === "idb_list" && kind === "malformed"
? {}
: undefined;
expect((await client.execute("health", {})).ok).toBe(false);
producer.beforeCall = async (name) => {
if (name === "idb_list" && kind === "unavailable")
throw new Error("inventory unavailable");
};
const result = await client.close();
if (result.ok) throw new Error("Expected retained cleanup");
expect(result.error).not.toHaveProperty("diagnostics.upstream_sessions");
},
);
it.each(["idb_close", "idb_list", "connection_close"])(
"preserves transport stderr through cleanup at %s",
async (stage) => {
const { producer, client } = await fixture("headless");
expect((await client.execute("health", {})).ok).toBe(true);
const failure = new ProviderAdapterError("ida", "transport", {
diagnostics: {
reason: "upstream transport closed",
},
capturedOutput: {
stdout: "",
stderr: "IDA shutdown diagnostic",
truncated: false,
},
});
const originalClose = producer.close.bind(producer);
producer.beforeCall = async (name) => {
if (name === stage) throw failure;
};
if (stage === "connection_close")
producer.close = async () => {
throw failure;
};
try {
const result = await client.close();
if (result.ok) throw new Error("expected cleanup failure");
expect(result.error.capturedOutput?.stderr).toBe(
"IDA shutdown diagnostic",
);
expect(result.error.diagnostics).toMatchObject({
failure_reason: "upstream transport closed",
});
expect(result.error.cleanupIncomplete).toBe(true);
} finally {
delete producer.beforeCall;
producer.close = originalClose;
await client.close();
}
},
);
it("binds retained transport diagnostics to the requested operation and recovery", async () => {
const { producer, client } = await fixture("attached");
await client.execute("health", {});
producer.beforeCall = async (name) => {
if (name === "list_functions")
throw new ProviderAdapterError("ida", "transport", {
diagnostics: { reason: "upstream closed" },
capturedOutput: {
stdout: "",
stderr: "IDA diagnostic",
truncated: false,
},
});
};
const result = await client.execute("list_procedures", {});
if (result.ok) throw new Error("expected upstream failure");
expect(projectAnalysisError(result.error)).toMatchObject({
details: {
operation: "list_procedures",
diagnostics: {
reason: "upstream closed",
recovery: expect.stringContaining("docs/providers/ida-provider.md"),
},
captured_output: { stderr: "IDA diagnostic" },
},
});
await client.close();
});
describe("attached IDA legacy string inventory", () => {
it("fetches legacy strings and literal searches in single upstream calls", async () => {
const { producer, client } = await fixture();
const all = Array.from({ length: 250 }, (_, i) => ({
address: `0x${(0x3000 + i).toString(16)}`,
length: 5,
string: `hello-${i}`,
}));
producer.overrideCall = (name, args) => {
if (name !== "list_strings") return undefined;
const offset = Number(args.offset ?? 0);
const count = Number(args.count ?? 100);
const data = count === 0 ? all : all.slice(offset, offset + count);
return {
data,
next_offset:
count === 0 || offset + count >= all.length ? null : offset + count,
};
};
const listed = await client.execute("list_strings", {});
expect(listed.ok && listed.value.result).toHaveLength(250);
expect(
producer.calls.filter(({ name }) => name === "list_strings")[0]?.args
.count,
).toBe(0);
expect(
producer.calls.filter(({ name }) => name === "list_strings"),
).toHaveLength(1);
await client.close();
});
});
it("retains transport diagnostics on a cancellation regardless of stderr", async () => {
const { producer, client } = await fixture("attached");
await client.execute("health", {});
const controller = new AbortController();
producer.beforeCall = async (name) => {
if (name === "list_functions") {
controller.abort();
throw new AnalysisCancelledError("list_functions", {
cause: controller.signal.reason,
diagnostics: { reason: "upstream closed after cancellation" },
capturedOutput: {
stdout: "",
stderr: "shutdown diagnostic",
truncated: false,
},
});
}
};
const result = await client.execute(
"list_procedures",
{},
{ signal: controller.signal },
);
if (result.ok) throw new Error("expected cancellation");
expect(projectAnalysisError(result.error)).toMatchObject({
code: "cancelled",
details: {
diagnostics: { reason: "upstream closed after cancellation" },
captured_output: { stderr: "shutdown diagnostic" },
},
});
await client.close();
});
it.each([false, null] as const)(
"keeps valid headless readiness %s distinct from malformed output",
async (ready) => {
const { producer, client } = await fixture("headless");
producer.overrideCall = (name) =>
name === "server_health"
? {
status: "ok",
input_path: producer.inputPath,
idb_path: `${producer.inputPath}.i64`,
module: "target.elf",
imagebase: "0x1000",
auto_analysis_ready: ready,
}
: undefined;
const result = await client.execute("health", {});
if (result.ok) throw new Error("Expected an unready worker refusal");
expect(projectAnalysisError(result.error)).toMatchObject({
code: "capability_unavailable",
retryable: ready === false,
details: { diagnostics: { auto_analysis_ready: ready } },
});
delete producer.overrideCall;
expect((await client.execute("health", {})).ok).toBe(true);
await expectCleanClose(producer, client);
},
);
it.each([false, true])(
"preserves an established IDA provider failure with late abort=%s",
async (lateAbort) => {
const { producer, client } = await fixture("attached");
expect((await client.execute("health", {})).ok).toBe(true);
const controller = new AbortController();
const failure = new ProviderAdapterError("ida", "transport", {
diagnostics: { reason: "upstream closed before cancellation" },
});
producer.beforeCall = async (name) => {
if (name !== "list_functions") return;
if (lateAbort) queueMicrotask(() => controller.abort({ caller: "late" }));
throw failure;
};
const result = await client.execute(
"list_procedures",
{},
{ signal: controller.signal },
);
if (result.ok) throw new Error("Expected provider failure");
expect(projectAnalysisError(result.error)).toMatchObject({
code: "execution_failure",
details: {
operation: "list_procedures",
diagnostics: { reason: "upstream closed before cancellation" },
},
});
expect(result.error.cause).toBe(failure);
await expectCleanClose(producer, client);
},
);
it.each(["before", "during"] as const)(
"preserves the exact caller abort reason %s an IDA operation",
async (phase) => {
const { producer, client } = await fixture(
phase === "before" ? "attached" : "headless",
);
const controller = new AbortController();
const reason = { caller: "selected opaque cancellation" };
if (phase === "before") controller.abort(reason);
else
producer.beforeCall = async (name) => {
if (name === "list_funcs") controller.abort(reason);
};
const result = await client.execute(
"list_procedures",
{},
{ signal: controller.signal },
);
if (result.ok) throw new Error("Expected cancellation");
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toBe(reason);
if (phase === "before") expect(producer.connects).toBe(0);
else
expect(result.error.partialObservation).toMatchObject({
operation: "list_procedures",
});
await expectCleanClose(producer, client);
},
);
it("attributes native IDA metadata timeout to the requested analysis operation", async () => {
const { producer, client } = await fixture("attached", 1000, "native");
producer.tools = [...nativeTools];
const timeout = new AnalysisTimeoutError("ida_mcp_read_resource", 40, {
diagnostics: { uri: "ida://idb/metadata" },
});
producer.readResource = async () => {
throw timeout;
};
const result = await client.execute("procedure_pseudo_code", {
procedure: "main",
});
if (result.ok) throw new Error("Expected metadata timeout");
expect(projectAnalysisError(result.error)).toMatchObject({
code: "provider_timeout",
details: {
operation: "procedure_pseudo_code",
timeout_ms: 40,
diagnostics: { uri: "ida://idb/metadata" },
},
recovery: { operation: "procedure_pseudo_code" },
});
expect(result.error.cause).toBe(timeout);
await expectCleanClose(producer, client);
});