Files
reverse-engineering-anything/tests/boundary/mcp/sessionAvailabilityRecovery.test.ts
T
Rishikesh Gowda K K 9044b6fb36 fix(kuna): mark project-dependent operations unavailable on export failure (#8748)
* fix(kuna): mark project-dependent operations unavailable on export failure

* fix(providers): preserve sibling operations in capability error copies

* fix(kuna): propagate sibling unavailability on export fallback

* fix(binary): preserve observed sibling recovery instructions across fallback observations

* fix(binary): build sibling unavailability from structured reasons without rewriting display text

* test(kuna): synchronize fallback test on settled export workspace retirement

* test(kuna): release and join export synchronization barrier across exit paths

* test(kuna): synchronize fallback test on observed export process exit

* test(kuna): gate fallback completion on settled export cache build

* test(kuna): preserve build promise chain and restore spy after teardown
2026-11-06 17:22:02 +08:00

1133 lines
39 KiB
TypeScript

import { expect, it, onTestFinished } from "vitest";
import {
createAnalysisExecution,
type AnalysisExecution,
type AnalysisOperation,
type AnalysisProvider,
type CapabilityDescriptor,
type ProviderIdentity,
} from "../../../src/application/AnalysisProvider.js";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import { expandToolAvailability } from "../../../src/contracts/toolOutputSchemaPrimitives.js";
import type { AnalysisError } from "../../../src/domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
AnalysisTimeoutError,
} from "../../../src/domain/analysisErrorCore.js";
import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js";
import {
inferredUnavailabilityReason,
runtimeFailureReason,
} from "../../../src/application/binary/SessionRuntimeAvailability.js";
import { ProviderAdapterError } from "../../../src/domain/providerAdapterError.js";
import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js";
import { err, ok, type Result } from "../../../src/domain/result.js";
import { silentLogger } from "../../../src/logger.js";
import { observed } from "../../fixtures/analysisExecution.js";
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
import { awaitAcknowledgement } from "../../fixtures/lifecycleAcknowledgement.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import {
createBinarySessionTargets,
createCacheProvider,
createDeferred,
createTestBinarySession,
} from "../../fixtures/binarySession.js";
it("restores MCP tool availability after cancelling a target switch", async () => {
const [first, second] = await createBinarySessionTargets();
const closing = createDeferred<void>();
const release = createDeferred<void>();
const cancellationObserved = createDeferred<void>();
const provider = createCacheProvider([]);
const resolveProfile = provider.resolveAnalysisProfile;
if (resolveProfile === undefined)
throw new Error("Missing test analysis profile");
const clients: { calls: number; closed: number }[] = [];
provider.createClient = () => {
const state = { calls: 0, closed: 0 };
const firstClient = clients.length === 0;
clients.push(state);
return {
execute: (operation) => {
state.calls += 1;
return Promise.resolve(
firstClient
? err(new ProviderAdapterError("fixture", operation))
: observed("restored-name"),
);
},
close: async () => {
state.closed += 1;
if (firstClient) {
closing.resolve();
await release.promise;
}
return ok(null);
},
};
};
const session = createTestBinarySession(provider, {
resolveAnalysisProfile: (target, options) => {
if (target.path === second) {
options?.signal?.addEventListener(
"abort",
() => cancellationObserved.resolve(),
{ once: true },
);
}
return resolveProfile(target, options);
},
});
const { client, close } = await createApplicationMcpHarness({
session,
clientIdentity: { name: "session-recovery", version: "1" },
serverOptions: { environment: {}, logger: silentLogger },
});
const controller = new AbortController();
const readStatus = async () => {
const reply = await client.callTool({
name: "binary_session",
arguments: {},
});
return toolContract("binary_session").outputSchema.parse(
reply.structuredContent,
).result;
};
try {
const opened = await client.callTool({
name: "open_binary",
arguments: { path: first },
});
expect(opened.isError).not.toBe(true);
const query = { address: "0x1000", document: "fixture" };
expect(
(await client.callTool({ name: "address_name", arguments: query }))
.isError,
).toBe(true);
const before = await readStatus();
expect(
expandToolAvailability(before.tool_availability).find(
(tool) => tool.name === "address_name",
),
).toMatchObject({
available: false,
reason: "provider_unavailable",
});
const switching = client.callTool(
{ name: "open_binary", arguments: { path: second } },
{ signal: controller.signal },
);
const cancelled = expect(switching).rejects.toThrow();
await closing.promise;
controller.abort();
await cancellationObserved.promise;
release.resolve();
await cancelled;
// Cancellation ends the client's wait before the server restores its session.
const settled = await session.withAdmittedAnalysis(
"binary_session",
undefined,
async () => null,
);
if (!settled.ok) throw settled.error;
const restored = await readStatus();
expect(restored).toMatchObject({ open: true, path: first });
expect(before.analysis_run?.run_id).toEqual(expect.any(String));
expect(restored.analysis_run?.run_id).toEqual(expect.any(String));
expect(restored.analysis_run?.run_id).not.toBe(before.analysis_run?.run_id);
expect(clients).toEqual([{ calls: 1, closed: 1 }]);
expect(
expandToolAvailability(restored.tool_availability).find(
(tool) => tool.name === "address_name",
),
).toMatchObject({
available: true,
reason: "available",
remediation: null,
});
const retried = await client.callTool({
name: "address_name",
arguments: query,
});
expect(retried.isError).not.toBe(true);
expect(retried.structuredContent).toMatchObject({
normalized_result: "restored-name",
});
expect(clients).toEqual([
{ calls: 1, closed: 1 },
{ calls: 1, closed: 0 },
]);
} finally {
controller.abort();
release.resolve();
await close();
}
});
it("keeps a retired run and its lineage visible over MCP until cleanup succeeds", async () => {
const [path] = await createBinarySessionTargets();
let allowed = false;
const provider = createCacheProvider([]);
provider.createClient = (_target, _profile, context) => {
if (context === undefined) throw new Error("missing analysis run context");
return {
execute: () => Promise.resolve(observed("fixture-name")),
runtimeLineageSnapshots: () => [
{
provider: provider.identity(),
observation: {
status: "unavailable" as const,
observedAt: "2026-10-16T00:00:00Z",
runId: context.runId,
launcherPid: 123,
processGroupId: 123,
reason: "fixture identity could not be revalidated",
},
},
],
close: () =>
Promise.resolve(
allowed
? ok(null)
: err(
new ProviderCleanupError("fixture", ["owned-runtime"], {
reason: "fixture cleanup denied",
}),
),
),
};
};
const session = createTestBinarySession(provider);
const { client, close } = await createApplicationMcpHarness({
session,
clientIdentity: { name: "retained-run", version: "1" },
serverOptions: { environment: {}, logger: silentLogger },
});
const readStatus = async () => {
const reply = await client.callTool({
name: "binary_session",
arguments: {},
});
return toolContract("binary_session").outputSchema.parse(
reply.structuredContent,
).result;
};
try {
expect((await readStatus()).analysis_run).toBeNull();
expect(
(await client.callTool({ name: "open_binary", arguments: { path } }))
.isError,
).not.toBe(true);
expect(
(
await client.callTool({
name: "address_name",
arguments: { address: "0x1000", document: "fixture" },
})
).isError,
).not.toBe(true);
const before = await readStatus();
expect(before.analysis_run).toMatchObject({
run_id: expect.any(String),
process_lineage: { status: "snapshots" },
});
for (let attempt = 0; attempt < 2; attempt++) {
expect(
(await client.callTool({ name: "close_binary", arguments: {} }))
.isError,
).toBe(true);
expect(await readStatus()).toMatchObject({
open: false,
cleanup_pending: true,
analysis_run: before.analysis_run,
});
}
allowed = true;
expect(
(await client.callTool({ name: "close_binary", arguments: {} })).isError,
).not.toBe(true);
expect(await readStatus()).toMatchObject({
open: false,
cleanup_pending: false,
analysis_run: null,
});
} finally {
allowed = true;
await close();
}
});
it("keeps provider tools available after one request reaches its deadline", async () => {
const [path] = await createBinarySessionTargets();
const provider = createCacheProvider([]);
let calls = 0;
provider.createClient = () => ({
execute: (operation) => {
calls += 1;
return Promise.resolve(
calls === 1
? err(
new AnalysisTimeoutError(operation, 5000, {
userMessage:
"Fixture decompilation exceeded its 5-second limit. The session remains usable.",
}),
)
: observed("after-timeout"),
);
},
close: () => Promise.resolve(ok(null)),
});
const session = createTestBinarySession(provider);
const { client, close } = await createApplicationMcpHarness({
session,
clientIdentity: { name: "request-timeout", version: "1" },
serverOptions: { environment: {}, logger: silentLogger },
});
const availability = async () => {
const reply = await client.callTool({
name: "binary_session",
arguments: {},
});
return expandToolAvailability(
toolContract("binary_session").outputSchema.parse(reply.structuredContent)
.result.tool_availability,
);
};
try {
expect(
(await client.callTool({ name: "open_binary", arguments: { path } }))
.isError,
).not.toBe(true);
const before = await availability();
const query = { address: "0x1000", document: "fixture" };
const timedOut = await client.callTool({
name: "address_name",
arguments: query,
});
expect(timedOut.isError).toBe(true);
expect(parseMcpToolError(timedOut)).toMatchObject({
error: { code: "provider_timeout", retryable: true },
});
// A request-level deadline is not a provider failure: nothing is marked
// unavailable, unlike the ProviderAdapterError case above.
expect(await availability()).toEqual(before);
const retried = await client.callTool({
name: "address_name",
arguments: query,
});
expect(retried.structuredContent).toMatchObject({
normalized_result: "after-timeout",
});
} finally {
await close();
}
});
type Reply = Result<AnalysisExecution, AnalysisError>;
type Responder = (operation: string) => Promise<Reply>;
const FIXTURE = {
id: "fixture",
name: "Fixture analysis provider",
version: "1",
} as const satisfies ProviderIdentity;
const AUXILIARY = {
id: "fixture-auxiliary",
name: "Auxiliary fixture provider",
version: "1",
} as const satisfies ProviderIdentity;
const liveCapability = (
provider: ProviderIdentity,
operation: Exclude<AnalysisOperation, "health">,
declaredRefusal?: string,
): CapabilityDescriptor => ({
provider,
operation,
...(declaredRefusal === undefined
? { available: true as const, reason: null }
: { available: false as const, reason: declaredRefusal }),
cachePolicy: "live",
effects: {
mutatesArtifact: false,
launchesProcess: false,
mayShowUi: false,
mayAccessNetwork: false,
mayWriteFilesystem: false,
changesPermissions: false,
requiresRoot: false,
},
limitations: [],
});
const adapterFailure = (provider: ProviderIdentity, operation: string) =>
Promise.resolve(err(new ProviderAdapterError(provider.id, operation)));
const recordedReason = (failure: AnalysisError): string => {
const reason = runtimeFailureReason(failure);
if (reason === undefined) throw new Error(`${failure._tag} is not recorded`);
return reason;
};
// Compare remediation with its owning formatters; the facts are their inputs.
const observedFailure = (operation: string, provider: string = FIXTURE.id) =>
recordedReason(new ProviderAdapterError(provider, operation));
const inferredFailure = (operation: string, provider: string = FIXTURE.id) =>
inferredUnavailabilityReason(provider, {
operation,
reason: observedFailure(operation, provider),
});
const refusal = (operation: string, reason: string) =>
recordedReason(
new AnalysisCapabilityUnavailableError(FIXTURE.id, operation, reason),
);
/** Hold provider replies; release and join every launched call. */
const heldCalls = (
client: Awaited<ReturnType<typeof createApplicationMcpHarness>>["client"],
) => {
const held = new Map<
string,
{ readonly reply: Promise<Reply>; readonly received: () => void }
>();
const launched: {
readonly release: (reply: Reply) => void;
readonly response: Promise<unknown>;
}[] = [];
return {
/** Serve a held reply when one is registered for this operation. */
respond: (operation: string): Promise<Reply> | undefined => {
const entry = held.get(operation);
if (entry === undefined) return undefined;
held.delete(operation);
entry.received();
return entry.reply;
},
dispatch: async (name: string, arguments_: Record<string, unknown>) => {
const reply = createDeferred<Reply>();
const received = createDeferred<void>();
held.set(name, { reply: reply.promise, received: received.resolve });
const response = client.callTool({ name, arguments: arguments_ });
launched.push({ release: reply.resolve, response });
await awaitAcknowledgement(received.promise, response, name);
return {
release: reply.resolve,
isError: async () => (await response).isError === true,
};
},
/** Release every held reply and wait for its MCP response. */
join: async () => {
for (const { release } of launched) release(observed("released"));
await Promise.allSettled(launched.map(({ response }) => response));
},
};
};
/** Connect a deep fixture and an auxiliary provider with scripted replies. */
const connectScripted = async (options: {
readonly operations: readonly Exclude<AnalysisOperation, "health">[];
readonly auxiliaryOperations?: readonly Exclude<
AnalysisOperation,
"health"
>[];
readonly respond: { current: Responder };
readonly respondAuxiliary?: { current: Responder };
/** Operations the fixture declares unavailable, with their declared reason. */
readonly declaredRefusals?: Readonly<Record<string, string>>;
readonly families?: string;
}) => {
const [path] = await createBinarySessionTargets();
const provider: AnalysisProvider = {
identity: () => FIXTURE,
resolveAnalysisProfile: () =>
Promise.resolve(
ok({ profile: createAnalysisProfile(FIXTURE, { fixture: true }) }),
),
capabilities: () =>
options.operations.map((operation) =>
liveCapability(
FIXTURE,
operation,
options.declaredRefusals?.[operation],
),
),
createClient: () => ({
execute: (operation) =>
operation === "health"
? Promise.resolve(observed(null))
: options.respond.current(operation),
close: () => Promise.resolve(ok(null)),
}),
};
const auxiliary: AnalysisProvider = {
identity: () => AUXILIARY,
capabilities: () =>
(options.auxiliaryOperations ?? []).map((operation) =>
liveCapability(AUXILIARY, operation),
),
createClient: () => ({
execute: (operation) =>
options.respondAuxiliary?.current(operation) ??
Promise.resolve(ok(createAnalysisExecution(null, AUXILIARY))),
close: () => Promise.resolve(ok(null)),
}),
};
const session = createTestBinarySession(provider, {
auxiliaryProviders: [auxiliary],
});
const { client, close } = await createApplicationMcpHarness({
session,
clientIdentity: { name: "provider-recovery", version: "1" },
serverOptions: {
environment:
options.families === undefined
? {}
: { REA_MCP_TOOL_FAMILIES: options.families },
logger: silentLogger,
},
});
onTestFinished(close);
expect(
(await client.callTool({ name: "open_binary", arguments: { path } }))
.isError,
).not.toBe(true);
const call = async (name: string, arguments_: Record<string, unknown>) =>
(await client.callTool({ name, arguments: arguments_ })).isError === true
? "error"
: "ok";
const tools = async () => {
const reply = await client.callTool({
name: "binary_session",
arguments: {},
});
return expandToolAvailability(
toolContract("binary_session").outputSchema.parse(reply.structuredContent)
.result.tool_availability,
);
};
const availability = async (names: readonly string[]) => {
const advertised = await tools();
return Object.fromEntries(
names.map((name) => {
const tool = advertised.find((entry) => entry.name === name);
if (tool === undefined) throw new Error(`${name} is not advertised`);
return [name, tool.available ? "available" : tool.remediation];
}),
);
};
return { client, call, availability, tools };
};
const DIRECT_OPERATIONS = [
"address_name",
"current_address",
"list_procedures",
"procedure_assembly",
"procedure_info",
] as const;
const ADDRESS = { address: "0x1000", document: "fixture" };
const PROCEDURE = { procedure: "main" };
it("preserves the user-facing recovery for an observed refusal and clears it after success", async () => {
let failing = true;
const respond = {
current: (operation: string): Promise<Reply> =>
failing
? Promise.resolve(
err(
new AnalysisCapabilityUnavailableError(
FIXTURE.id,
operation,
"project export failed",
{
userMessage:
"Project export failed. Resolve the cause and reopen the target.",
},
),
),
)
: Promise.resolve(observed("fixture-text")),
};
const { call, availability } = await connectScripted({
operations: ["address_name"],
respond,
});
expect(await call("address_name", ADDRESS)).toBe("error");
expect(await availability(["address_name"])).toEqual({
address_name:
"Project export failed. Resolve the cause and reopen the target.",
});
failing = false;
expect(await call("address_name", ADDRESS)).toBe("ok");
expect(await availability(["address_name"])).toEqual({
address_name: "available",
});
});
it("returns provider-inferred siblings to declared availability after one success", async () => {
let broken = false;
const respond = {
current: (operation: string): Promise<Reply> => {
if (operation === "procedure_info")
return Promise.resolve(
err(
new AnalysisCapabilityUnavailableError(
FIXTURE.id,
operation,
"fixture project is read-only",
),
),
);
if (broken) return adapterFailure(FIXTURE, operation);
return Promise.resolve(observed("fixture-text"));
},
};
const declared = "Fixture project was opened read-only.";
const { call, availability } = await connectScripted({
operations: [...DIRECT_OPERATIONS, "set_comment"],
auxiliaryOperations: ["list_documents"],
declaredRefusals: { set_comment: declared },
respond,
});
const refused = refusal("procedure_info", "fixture project is read-only");
expect(await call("procedure_info", PROCEDURE)).toBe("error");
broken = true;
expect(await call("address_name", ADDRESS)).toBe("error");
expect(await call("procedure_assembly", PROCEDURE)).toBe("error");
const names = [...DIRECT_OPERATIONS, "set_comment", "list_documents"];
expect(await availability(names)).toEqual({
address_name: observedFailure("address_name"),
current_address: inferredFailure("procedure_assembly"),
list_procedures: inferredFailure("procedure_assembly"),
procedure_assembly: observedFailure("procedure_assembly"),
// Separately observed and declared refusals keep their own reasons.
procedure_info: refused,
set_comment: declared,
list_documents: "available",
});
broken = false;
expect(await call("address_name", ADDRESS)).toBe("ok");
// One success is evidence the provider answers again, not that every
// operation is healthy: failures observed on other operations remain.
expect(await availability(names)).toEqual({
address_name: "available",
current_address: "available",
list_procedures: "available",
procedure_assembly: observedFailure("procedure_assembly"),
procedure_info: refused,
set_comment: declared,
list_documents: "available",
});
expect(await call("procedure_assembly", PROCEDURE)).toBe("ok");
expect(await availability(["procedure_assembly", "procedure_info"])).toEqual({
procedure_assembly: "available",
procedure_info: refused,
});
});
it("marks declared sibling operations unavailable when a shared prerequisite fails", async () => {
const respond = {
current: (operation: string): Promise<Reply> => {
if (operation === "procedure_assembly")
return Promise.resolve(
err(
new AnalysisCapabilityUnavailableError(
FIXTURE.id,
operation,
"project export failed",
{ siblingOperations: ["procedure_assembly", "procedure_info"] },
),
),
);
return Promise.resolve(observed("fixture-text"));
},
};
const { call, availability } = await connectScripted({
operations: DIRECT_OPERATIONS,
respond,
});
expect(await call("procedure_assembly", PROCEDURE)).toBe("error");
// Independent operations remain available.
expect(await call("address_name", ADDRESS)).toBe("ok");
expect(
await availability([
"procedure_assembly",
"procedure_info",
"address_name",
"list_procedures",
]),
).toEqual({
procedure_assembly: refusal("procedure_assembly", "project export failed"),
procedure_info: refusal("procedure_info", "project export failed"),
address_name: "available",
list_procedures: "available",
});
});
it("marks declared sibling operations unavailable when fallback reports export failure", async () => {
const respond = {
current: (operation: string): Promise<Reply> => {
if (operation === "procedure_pseudo_code")
return Promise.resolve(
ok(
createAnalysisExecution("int main() { return 0; }", FIXTURE, {
siblingUnavailability: new AnalysisCapabilityUnavailableError(
FIXTURE.id,
operation,
"project export failed",
{
siblingOperations: ["procedure_assembly", "procedure_info"],
},
),
}),
),
);
return Promise.resolve(observed("fixture-text"));
},
};
const { call, availability } = await connectScripted({
operations: [...DIRECT_OPERATIONS, "procedure_pseudo_code"],
respond,
});
expect(await call("procedure_pseudo_code", PROCEDURE)).toBe("ok");
// Independent operations remain available.
expect(await call("address_name", ADDRESS)).toBe("ok");
expect(
await availability([
"procedure_pseudo_code",
"procedure_assembly",
"procedure_info",
"address_name",
"list_procedures",
]),
).toEqual({
procedure_pseudo_code: "available",
procedure_assembly: refusal("procedure_assembly", "project export failed"),
procedure_info: refusal("procedure_info", "project export failed"),
address_name: "available",
list_procedures: "available",
});
});
it("preserves observed sibling recovery instructions when a later fallback reports sibling unavailability", async () => {
const recoveryMessage =
"Project export failed. Set REA_FIXTURE_TIMEOUT_MS and restart REA.";
const respond = {
current: (operation: string): Promise<Reply> => {
if (operation === "procedure_assembly")
return Promise.resolve(
err(
new AnalysisCapabilityUnavailableError(
FIXTURE.id,
operation,
"project export failed",
{
userMessage: recoveryMessage,
siblingOperations: ["procedure_assembly", "procedure_info"],
},
),
),
);
if (operation === "procedure_pseudo_code")
return Promise.resolve(
ok(
createAnalysisExecution("int main() { return 0; }", FIXTURE, {
siblingUnavailability: new AnalysisCapabilityUnavailableError(
FIXTURE.id,
operation,
"project export failed",
{
siblingOperations: ["procedure_assembly", "procedure_info"],
},
),
}),
),
);
return Promise.resolve(observed("fixture-text"));
},
};
const { call, availability } = await connectScripted({
operations: [...DIRECT_OPERATIONS, "procedure_pseudo_code"],
respond,
});
expect(await call("procedure_assembly", PROCEDURE)).toBe("error");
expect(await availability(["procedure_assembly"])).toEqual({
procedure_assembly: recoveryMessage,
});
// A later successful fallback must not overwrite the full recovery explanation.
expect(await call("procedure_pseudo_code", PROCEDURE)).toBe("ok");
expect(await availability(["procedure_assembly"])).toEqual({
procedure_assembly: recoveryMessage,
});
});
it("replaces inactive past sibling failures with the current export failure when fallback reports sibling unavailability", async () => {
let assemblyFails = true;
const oldRefusal = "Import slot 0x4000 cannot be disassembled.";
const exportFailureMessage =
"Kuna's project export failed, so procedure_assembly is unavailable in this session.";
const respond = {
current: (operation: string): Promise<Reply> => {
if (operation === "procedure_assembly") {
if (assemblyFails)
return Promise.resolve(
err(
new AnalysisCapabilityUnavailableError(
FIXTURE.id,
operation,
oldRefusal,
{ userMessage: oldRefusal },
),
),
);
return Promise.resolve(observed("mov eax, 1"));
}
if (operation === "procedure_pseudo_code")
return Promise.resolve(
ok(
createAnalysisExecution("int main() { return 0; }", FIXTURE, {
siblingUnavailability: new AnalysisCapabilityUnavailableError(
FIXTURE.id,
operation,
"project export failed",
{
siblingOperations: ["procedure_assembly"],
siblingReasons: {
procedure_assembly: exportFailureMessage,
},
},
),
}),
),
);
return Promise.resolve(observed("fixture-text"));
},
};
const { call, availability } = await connectScripted({
operations: [...DIRECT_OPERATIONS, "procedure_pseudo_code"],
respond,
});
// 1. Initial refusal on procedure_assembly
expect(await call("procedure_assembly", PROCEDURE)).toBe("error");
expect(await availability(["procedure_assembly"])).toEqual({
procedure_assembly: oldRefusal,
});
// 2. procedure_assembly succeeds later, resolving the active failure
assemblyFails = false;
expect(await call("procedure_assembly", PROCEDURE)).toBe("ok");
expect(await availability(["procedure_assembly"])).toEqual({
procedure_assembly: "available",
});
// 3. Fallback observes export failure: must show the new export failure, not resurrect oldRefusal
expect(await call("procedure_pseudo_code", PROCEDURE)).toBe("ok");
expect(await availability(["procedure_assembly"])).toEqual({
procedure_assembly: exportFailureMessage,
});
});
it("preserves source paths and diagnostics unchanged in structured sibling unavailability", async () => {
const pathWithOpName = "/var/log/procedure_pseudo_code/target.c";
const siblingMessage = `Kuna's project export failed, so procedure_assembly is unavailable in this session: Error compiling ${pathWithOpName}.`;
const respond = {
current: (operation: string): Promise<Reply> => {
if (operation === "procedure_pseudo_code")
return Promise.resolve(
ok(
createAnalysisExecution("int main() { return 0; }", FIXTURE, {
siblingUnavailability: new AnalysisCapabilityUnavailableError(
FIXTURE.id,
operation,
`export failed for ${pathWithOpName}`,
{
siblingOperations: ["procedure_assembly"],
siblingReasons: {
procedure_assembly: siblingMessage,
},
},
),
}),
),
);
return Promise.resolve(observed("fixture-text"));
},
};
const { call, availability } = await connectScripted({
operations: [...DIRECT_OPERATIONS, "procedure_pseudo_code"],
respond,
});
expect(await call("procedure_pseudo_code", PROCEDURE)).toBe("ok");
// The path containing "procedure_pseudo_code" must remain completely intact
expect(await availability(["procedure_assembly"])).toEqual({
procedure_assembly: siblingMessage,
});
});
it("keeps each provider's failures and ignores cancelled requests", async () => {
let fixtureBroken = false;
let auxiliaryBroken = true;
const respond = {
current: (operation: string): Promise<Reply> => {
if (operation === "list_procedures")
return Promise.resolve(err(new AnalysisCancelledError(operation)));
return fixtureBroken
? adapterFailure(FIXTURE, operation)
: Promise.resolve(observed("fixture-text"));
},
};
const respondAuxiliary = {
current: (operation: string): Promise<Reply> =>
auxiliaryBroken
? adapterFailure(AUXILIARY, operation)
: Promise.resolve(ok(createAnalysisExecution("doc", AUXILIARY))),
};
const { call, availability } = await connectScripted({
operations: ["address_name", "current_address", "list_procedures"],
auxiliaryOperations: ["current_document", "list_documents"],
respond,
respondAuxiliary,
});
const names = [
"address_name",
"current_address",
"list_procedures",
"current_document",
"list_documents",
];
expect(await call("list_documents", {})).toBe("error");
expect(await call("address_name", ADDRESS)).toBe("ok");
const auxiliaryFailure = {
current_document: inferredFailure("list_documents", AUXILIARY.id),
list_documents: observedFailure("list_documents", AUXILIARY.id),
};
// Another provider's success does not clear the auxiliary provider's state.
expect(await availability(names)).toEqual({
address_name: "available",
current_address: "available",
list_procedures: "available",
...auxiliaryFailure,
});
fixtureBroken = true;
expect(await call("address_name", ADDRESS)).toBe("error");
// A cancelled request is not evidence that the provider recovered.
expect(await call("list_procedures", {})).toBe("error");
const fixtureFailure = {
address_name: observedFailure("address_name"),
current_address: inferredFailure("address_name"),
list_procedures: inferredFailure("address_name"),
};
expect(await availability(names)).toEqual({
...fixtureFailure,
...auxiliaryFailure,
});
auxiliaryBroken = false;
expect(await call("current_document", {})).toBe("ok");
expect(await availability(names)).toEqual({
...fixtureFailure,
current_document: "available",
list_documents: auxiliaryFailure.list_documents,
});
});
it("keeps newer failures when older successes complete after them", async () => {
let reply: Responder = () => Promise.resolve(observed("text"));
const respond = { current: (operation: string) => reply(operation) };
const { client, call, availability } = await connectScripted({
operations: [...DIRECT_OPERATIONS, "current_procedure"],
respond,
});
const calls = heldCalls(client);
respond.current = (operation) => calls.respond(operation) ?? reply(operation);
const document = { document: "doc" };
const refuse: Responder = (operation) =>
Promise.resolve(
err(
new AnalysisCapabilityUnavailableError(
FIXTURE.id,
operation,
"fixture refusal",
),
),
);
try {
// Successes dispatched before an own refusal, before a provider failure,
// or between the two complete after both.
const olderAddress = await calls.dispatch("address_name", ADDRESS);
const olderCurrent = await calls.dispatch("current_address", document);
reply = refuse;
expect(await call("address_name", ADDRESS)).toBe("error");
expect(await call("current_procedure", document)).toBe("error");
const between = await calls.dispatch("current_procedure", document);
reply = (operation) => adapterFailure(FIXTURE, operation);
expect(await call("procedure_assembly", PROCEDURE)).toBe("error");
olderAddress.release(observed("old-name"));
olderCurrent.release(observed("0x1000"));
between.release(observed("main"));
for (const settled of [olderAddress, olderCurrent, between])
expect(await settled.isError()).toBe(false);
expect(
await availability([
"address_name",
"current_address",
"current_procedure",
"list_procedures",
]),
).toEqual({
address_name: refusal("address_name", "fixture refusal"),
current_address: inferredFailure("procedure_assembly"),
// Its own later success superseded the refusal, not the provider failure.
current_procedure: inferredFailure("procedure_assembly"),
list_procedures: inferredFailure("procedure_assembly"),
});
// A success dispatched behind a failure executed after it, even though
// the failure was not yet observed when the success was dispatched.
reply = () => Promise.resolve(observed("text"));
const failure = await calls.dispatch("procedure_assembly", PROCEDURE);
const queued = await calls.dispatch("current_address", document);
failure.release(
err(new ProviderAdapterError(FIXTURE.id, "procedure_assembly")),
);
expect(await failure.isError()).toBe(true);
queued.release(observed("0x1000"));
expect(await queued.isError()).toBe(false);
expect(
await availability([
"address_name",
"current_address",
"current_procedure",
"list_procedures",
"procedure_assembly",
]),
).toEqual({
address_name: refusal("address_name", "fixture refusal"),
current_address: "available",
current_procedure: "available",
list_procedures: "available",
procedure_assembly: observedFailure("procedure_assembly"),
});
} finally {
await calls.join();
}
});
it("keeps newer successes when an older failure completes last", async () => {
const respond = {
current: (_operation: string): Promise<Reply> =>
Promise.resolve(observed("text")),
};
const { client, call, availability } = await connectScripted({
operations: DIRECT_OPERATIONS,
respond,
});
const calls = heldCalls(client);
respond.current = (operation) =>
calls.respond(operation) ?? Promise.resolve(observed("text"));
const names = ["address_name", "current_address", "list_procedures"];
try {
// A sibling dispatched later already proved the provider answers.
const older = await calls.dispatch("address_name", ADDRESS);
const newer = await calls.dispatch("current_address", { document: "doc" });
newer.release(observed("0x1000"));
expect(await newer.isError()).toBe(false);
older.release(err(new ProviderAdapterError(FIXTURE.id, "address_name")));
expect(await older.isError()).toBe(true);
expect(await availability(names)).toEqual({
address_name: observedFailure("address_name"),
current_address: "available",
list_procedures: "available",
});
// A later success of the same operation supersedes its older failure.
const stale = await calls.dispatch("procedure_assembly", PROCEDURE);
expect(await call("procedure_assembly", PROCEDURE)).toBe("ok");
stale.release(
err(new ProviderAdapterError(FIXTURE.id, "procedure_assembly")),
);
expect(await stale.isError()).toBe(true);
expect(await availability([...names, "procedure_assembly"])).toEqual({
address_name: observedFailure("address_name"),
current_address: "available",
list_procedures: "available",
procedure_assembly: "available",
});
} finally {
await calls.join();
}
});
it("restores composed tools in a scoped catalog after one composed success", async () => {
let broken = true;
const respond = {
current: (operation: string): Promise<Reply> => {
if (broken) return adapterFailure(FIXTURE, operation);
if (operation === "procedure_address")
return Promise.resolve(observed("0x1"));
if (operation === "address_name")
return Promise.resolve(observed("fixture_name"));
return Promise.resolve(observed("return 0;"));
},
};
const { client, availability, tools } = await connectScripted({
operations: [
"address_name",
"procedure_address",
"procedure_callees",
"procedure_callers",
"procedure_pseudo_code",
"search_procedures",
"search_strings",
"xrefs",
],
respond,
families: "session,enhanced",
});
// Direct prerequisites are outside this scope; only composed tools can retry.
const batchItemStatus = async () => {
const reply = await client.callTool({
name: "batch_decompile",
arguments: { addresses: ["0x1"] },
});
expect(reply.isError).not.toBe(true);
return reply.structuredContent;
};
expect(await batchItemStatus()).toMatchObject({
normalized_result: { items: [{ status: "error" }] },
});
expect(await availability(["trace_feature", "get_call_graph"])).toEqual({
// The batch still decompiles the unresolved selector; its failure is newest.
trace_feature: inferredFailure("procedure_pseudo_code"),
get_call_graph: observedFailure("procedure_address"),
});
broken = false;
expect(await batchItemStatus()).toMatchObject({
normalized_result: { items: [{ status: "ok", pseudocode: "return 0;" }] },
});
expect(
await availability(["batch_decompile", "trace_feature", "get_call_graph"]),
).toEqual({
batch_decompile: "available",
trace_feature: "available",
get_call_graph: "available",
});
expect(
(await tools()).find(({ name }) => name === "get_call_graph"),
).toMatchObject({
default_mode_available: true,
modes: [
{ name: "forward", available: true, missing_operations: [] },
{ name: "backward", available: true, missing_operations: [] },
],
});
});