Files
rea/tests/boundary/mcp/nativeDispatchAvailability.test.ts
T
morluto 50b6975d86 fix(session): admit direct native dispatch metadata providers
Share named metadata source operations between execution and capability discovery. Preserve symbol fallback and typed runtime failure behavior, and cover availability through public MCP workflows.

Fixes #5070
2026-10-19 03:19:33 +08:00

333 lines
10 KiB
TypeScript

import { rm } from "node:fs/promises";
import type { Client } from "@modelcontextprotocol/client";
import type { CallToolResult } from "@modelcontextprotocol/server";
import { afterEach, describe, expect, it } from "vitest";
import type {
AnalysisClient,
AnalysisProvider,
CapabilityDescriptor,
} from "../../../src/application/AnalysisProvider.js";
import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js";
import { AnalysisOutputError } from "../../../src/domain/analysisErrorCore.js";
import { inspectNativeDispatchMetadata } from "../../../src/domain/native/objcSwiftMetadata.js";
import { err, ok } from "../../../src/domain/result.js";
import type { ProviderRejectionCode } from "../../../src/contracts/providerSelection.js";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import { expandToolAvailability } from "../../../src/contracts/toolOutputSchemaPrimitives.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { createSessionMcpHarness } from "./sessionMcpHarness.js";
const resources: Array<{ close(): Promise<unknown> }> = [];
let directory: string | undefined;
afterEach(async () => {
await Promise.all(resources.splice(0).map((resource) => resource.close()));
if (directory !== undefined)
await rm(directory, { recursive: true, force: true });
directory = undefined;
});
const identity = {
id: "fixture",
name: "Fixture analysis provider",
version: "1",
};
const dispatchMetadata = inspectNativeDispatchMetadata([
{
address: "0x1000",
name: "-[BuildViewController buildTapped:]",
},
]);
const symbolInventory = [
{ address: "0x1000", name: "-[BuildViewController buildTapped:]" },
];
type Operation = "inspect_native_dispatch_metadata" | "list_names";
type Scenario = {
readonly name: string;
readonly capabilities: readonly {
readonly operation: Operation;
readonly available: boolean;
readonly reason?: string;
readonly availabilityCode?: ProviderRejectionCode;
}[];
readonly directRuntimeFailure?: boolean;
readonly unavailableReason?: "provider_unavailable" | "unsupported_host";
};
const directOnly: Scenario = {
name: "direct metadata only",
capabilities: [
{ operation: "inspect_native_dispatch_metadata", available: true },
],
};
const scenarios: readonly Scenario[] = [
directOnly,
{
name: "symbol-only fallback",
capabilities: [{ operation: "list_names", available: true }],
},
{ name: "neither operation", capabilities: [] },
{
name: "direct operation unavailable with symbol fallback",
capabilities: [
{
operation: "inspect_native_dispatch_metadata",
available: false,
reason: "Direct metadata inspection is unavailable.",
},
{ operation: "list_names", available: true },
],
},
{
name: "direct runtime failure with symbol fallback",
capabilities: [
{ operation: "inspect_native_dispatch_metadata", available: true },
{ operation: "list_names", available: true },
],
directRuntimeFailure: true,
},
{
name: "direct host denial with usable symbol fallback",
capabilities: [
{
operation: "inspect_native_dispatch_metadata",
available: false,
reason: "Direct metadata inspection requires another host.",
availabilityCode: "unsupported_host",
},
{ operation: "list_names", available: true },
],
},
{
name: "unavailable direct operation without symbol fallback",
capabilities: [
{
operation: "inspect_native_dispatch_metadata",
available: false,
reason: "Direct metadata inspection is unavailable.",
},
],
unavailableReason: "provider_unavailable",
},
];
describe("native dispatch availability at the public MCP boundary", () => {
it.each(scenarios)(
"preserves $name capability semantics",
async (scenario) => {
directory = await createTestTempDirectory(
"rea-native-dispatch-availability-",
);
const calls: Operation[] = [];
const { mcp, first } = await createSessionMcpHarness(
directory,
() => provider(scenario, calls),
resources,
);
const opened = await mcp.callTool({
name: "open_binary",
arguments: { path: first },
});
expect(opened.isError).not.toBe(true);
try {
const inventory = await readInventory(mcp);
const availability = dispatchAvailability(inventory);
const result = await mcp.callTool({
name: "inspect_native_dispatch_metadata",
arguments: { max_records: 10 },
});
assertExecution(scenario, result, calls);
if (scenario.unavailableReason !== undefined) {
expect(availability).toMatchObject({
available: false,
reason: scenario.unavailableReason,
remediation: "Direct metadata inspection is unavailable.",
});
} else if (scenario === directOnly) {
expect(availability).toMatchObject({
available: true,
reason: "available",
remediation: null,
});
} else if (scenario.capabilities.length === 0) {
expect(availability).toMatchObject({
available: false,
reason: "provider_missing",
});
} else {
expect(availability).toMatchObject({
available: true,
reason: "available",
});
}
} finally {
const closed = await mcp.callTool({
name: "close_binary",
arguments: {},
});
expect(closed.isError).not.toBe(true);
}
},
);
it("reports a target requirement before probing provider operations", async () => {
directory = await createTestTempDirectory("rea-native-dispatch-no-target-");
const calls: Operation[] = [];
const { mcp } = await createSessionMcpHarness(
directory,
() => provider(directOnly, calls),
resources,
);
const inventory = await readInventory(mcp);
expect(inventory.result.open).toBe(false);
expect(dispatchAvailability(inventory)).toMatchObject({
available: false,
reason: "target_required",
});
expect(calls).toEqual([]);
});
});
const readInventory = async (mcp: Client) => {
const result = await mcp.callTool({ name: "binary_session", arguments: {} });
return toolContract("binary_session").outputSchema.parse(
result.structuredContent,
);
};
const dispatchAvailability = (
inventory: Awaited<ReturnType<typeof readInventory>>,
) =>
expandToolAvailability(inventory.result.tool_availability).find(
(entry) => entry.name === "inspect_native_dispatch_metadata",
);
const assertExecution = (
scenario: Scenario,
result: CallToolResult,
calls: readonly Operation[],
): void => {
if (scenario.directRuntimeFailure) {
expect(result.isError).toBe(true);
expect(parseMcpToolError(result)).toMatchObject({
error: { details: { operation: "inspect_native_dispatch_metadata" } },
});
expect(calls).toEqual(["inspect_native_dispatch_metadata"]);
return;
}
if (
scenario.capabilities.length === 0 ||
scenario.unavailableReason !== undefined
) {
expect(result.isError).toBe(true);
expect(parseMcpToolError(result)).toMatchObject({
error: { code: "capability_unavailable" },
});
expect(calls).toEqual([]);
return;
}
expect(result.isError).not.toBe(true);
expect(result.structuredContent).toMatchObject({
normalized_result: {
result: {
objc_dispatch_implementations: [
{
class_name: "BuildViewController",
selector: "buildTapped:",
implementation_address: "0x1000",
},
],
},
},
});
if (
scenario.capabilities.some(
({ operation, available }) =>
operation === "inspect_native_dispatch_metadata" && available,
)
)
expect(calls).toEqual(["inspect_native_dispatch_metadata"]);
else expect(calls).toEqual(["list_names"]);
};
const provider = (scenario: Scenario, calls: Operation[]): AnalysisProvider => {
const capabilities: CapabilityDescriptor[] = scenario.capabilities.map(
(capability) => {
const common = {
provider: identity,
operation: capability.operation,
effects: {
mutatesArtifact: false,
launchesProcess: false,
mayShowUi: false,
mayAccessNetwork: false,
mayWriteFilesystem: false,
changesPermissions: false,
requiresRoot: false,
},
limitations: [],
};
return capability.available
? { ...common, available: true as const, reason: null }
: {
...common,
available: false as const,
reason: capability.reason ?? "Fixture capability is unavailable.",
...(capability.availabilityCode === undefined
? {}
: { availabilityCode: capability.availabilityCode }),
};
},
);
return {
identity: () => identity,
capabilities: () => capabilities,
createClient: (target, profile): AnalysisClient => ({
execute: async (operation) => {
if (operation === "health")
return ok(createAnalysisExecution(null, identity));
if (
operation !== "inspect_native_dispatch_metadata" &&
operation !== "list_names"
)
throw new Error(`Unexpected provider operation: ${operation}`);
calls.push(operation);
if (operation === "inspect_native_dispatch_metadata") {
if (scenario.directRuntimeFailure)
return err(
new AnalysisOutputError(
operation,
"fixture direct metadata runtime failure",
),
);
return ok(
createAnalysisExecution(
{
target_sha256: target.sha256,
provider: identity,
analysis_profile_digest: profile?.digest ?? null,
result: dispatchMetadata,
},
identity,
{ subject: target },
),
);
}
return ok(
createAnalysisExecution(symbolInventory, identity, {
subject: target,
...(profile === undefined ? {} : { analysisProfile: profile }),
}),
);
},
close: async () => ok(null),
}),
};
};