mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 09:52:27 +00:00
* fix(objdump): reflect observed launch failure in discovery availability Distinguish a statically registered standalone command from an executable whose launch has failed in the active server session. Discovery leaves unprobed commands optimistically available, reports an observed launch failure as provider_unavailable with the command remediation, keeps the tool callable for retries, and clears the failure after a subsequent successful launch. Fixes #8826. * test(reverse): verify overlapping command launch dispatch ordering * test(reverse): unblock gated test workers before awaiting service closure * test(reverse): bound lifecycle acknowledgement and join launched workers * fix(reverse): distinguish host capacity exhaustion from command launch failures * fix(reverse): preserve host capacity across Linux process ownership preparation * fix(reverse): exclude process ownership admission failures from command launch state * fix(reverse): preserve Darwin process ownership preparation error across command launch * fix(reverse): disambiguate cleanup declaration in provider observation * fix(reverse): preserve command deadline timeout during Darwin ownership preparation * fix(reverse): isolate Darwin baseline ps errors and provide platform-aware admission guidance * fix(reverse): rely on typed baseline admission failures and use owning repair formatter in tests * test(reverse): use owning formatters for process ownership remediation assertions * fix(reverse): restrict Linux ownership prerequisite remediation to missing ps and preserve system error codes * fix(reverse): retain typed process ownership prerequisite with preserved error codes * fix(reverse): remove unvalidated error label fallbacks and keep admission tags out of system_code * fix(reverse): keep Windows native runner failures out of command-launch state * fix(process): preserve current_process_unreported refusal in Linux provider readiness * fix(process): distinguish no-code ps failure from missing ps and cover rizin launch failure * fix(windows): classify process_spawn host-control rejections as RuntimeControlUnavailableError
1219 lines
37 KiB
TypeScript
1219 lines
37 KiB
TypeScript
import { sqliteDatabaseCapability } from "./sqlite/SqliteDatabaseCapability.js";
|
|
import { persistentDebuggerHostSupported } from "../process/PersistentDebuggerHost.js";
|
|
import { pcapCaptureHostSupported } from "../domain/pcap/pcapAnalysis.js";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
buildCapabilityInventory,
|
|
compactToolAvailability,
|
|
} from "./CapabilityInventory.js";
|
|
import { TOOL_CONTRACTS } from "../contracts/toolContracts.js";
|
|
import { cleanupRetryRemediation } from "../domain/analysisErrorRecovery.js";
|
|
import {
|
|
expandToolAvailability,
|
|
toolAvailabilityInventory,
|
|
toolAvailabilitySummaryInventory,
|
|
type ToolAvailability,
|
|
} from "../contracts/toolOutputSchemaPrimitives.js";
|
|
|
|
it("reports module syntax and native resolution availability separately", () => {
|
|
const input = { open: false, capabilities: [] };
|
|
const unavailable = buildCapabilityInventory(input, {
|
|
processCaptureEnabled: false,
|
|
}).find((tool) => tool.name === "trace_web_module_imports");
|
|
expect(unavailable).toMatchObject({
|
|
available: true,
|
|
default_mode_available: false,
|
|
modes: [
|
|
expect.objectContaining({
|
|
name: "sources-without-literal-imports",
|
|
available: true,
|
|
}),
|
|
expect.objectContaining({
|
|
name: "native-literal-resolution",
|
|
available: false,
|
|
}),
|
|
],
|
|
});
|
|
const enabled = buildCapabilityInventory(input, {
|
|
processCaptureEnabled: false,
|
|
webModuleResolutionEnabled: true,
|
|
}).find((tool) => tool.name === "trace_web_module_imports");
|
|
expect(enabled).toMatchObject({
|
|
available: true,
|
|
default_mode_available: true,
|
|
});
|
|
});
|
|
|
|
const enabledPolicy: Parameters<typeof buildCapabilityInventory>[1] = {
|
|
processCaptureEnabled: true,
|
|
browserObservationEnabled: true,
|
|
browserScenarioEnabled: true,
|
|
electronObservationEnabled: true,
|
|
v8InspectorObservationEnabled: true,
|
|
};
|
|
|
|
it("adds the retaining close owner's prerequisite without guessing from a pending flag", () => {
|
|
const retry = { operation: "close_binary" } as const;
|
|
const input = { open: false, capabilities: [], cleanup_pending: true };
|
|
const unavailable = buildCapabilityInventory(
|
|
{ ...input, cleanup_retry: retry },
|
|
enabledPolicy,
|
|
).find(({ name }) => name === "procedure_assembly");
|
|
expect(unavailable).toMatchObject({
|
|
available: false,
|
|
reason: "target_required",
|
|
remediation: expect.stringContaining(cleanupRetryRemediation(retry)),
|
|
});
|
|
const unknownOwner = buildCapabilityInventory(input, enabledPolicy).find(
|
|
({ name }) => name === "procedure_assembly",
|
|
);
|
|
const original = buildCapabilityInventory(
|
|
{ open: false, capabilities: [] },
|
|
enabledPolicy,
|
|
).find(({ name }) => name === "procedure_assembly");
|
|
expect(unknownOwner).toEqual(original);
|
|
expect(unknownOwner?.reason).toBe("target_required");
|
|
});
|
|
|
|
const status = (
|
|
options: {
|
|
readonly open?: boolean;
|
|
readonly kind?: "executable" | "database" | "archive" | "artifact";
|
|
readonly format?: string;
|
|
readonly capabilities?: readonly {
|
|
readonly operation: string;
|
|
readonly available: boolean;
|
|
readonly reason: string | null;
|
|
readonly availability_code?: "unsupported_host";
|
|
}[];
|
|
} = {},
|
|
) => ({
|
|
open: options.open ?? false,
|
|
...(options.kind === undefined ? {} : { kind: options.kind }),
|
|
...(options.format === undefined ? {} : { format: options.format }),
|
|
capabilities:
|
|
options.capabilities === undefined || options.capabilities.length === 0
|
|
? []
|
|
: [
|
|
{
|
|
provider: { id: "fixture", name: "Fixture", version: "1" },
|
|
shared_limitations: [],
|
|
groups: options.capabilities.map((capability) => ({
|
|
operations: [capability.operation],
|
|
effects: [],
|
|
limitations: [],
|
|
available: capability.available,
|
|
reason: capability.reason,
|
|
availability_code: capability.available
|
|
? null
|
|
: (capability.availability_code ?? null),
|
|
})),
|
|
},
|
|
],
|
|
});
|
|
|
|
const entry = (
|
|
name: string,
|
|
sessionStatus: ReturnType<typeof status>,
|
|
policy = enabledPolicy,
|
|
) => {
|
|
const found = buildCapabilityInventory(sessionStatus, policy).find(
|
|
(candidate) => candidate.name === name,
|
|
);
|
|
if (found === undefined) throw new Error(`missing capability ${name}`);
|
|
return found;
|
|
};
|
|
|
|
it("reports the active resource session's dispatch policy without limiting independent tools", () => {
|
|
const resourceStatus = {
|
|
...status({ open: true }),
|
|
session_kind: "bundle_resources",
|
|
capabilities: [
|
|
{
|
|
provider: { id: "bundle-resources", name: "Resources", version: "1" },
|
|
shared_limitations: [],
|
|
groups: [
|
|
{
|
|
operations: [
|
|
"inspect_plist",
|
|
"decode_interface_builder",
|
|
"inspect_asset_catalog",
|
|
"inspect_keyed_archive",
|
|
],
|
|
effects: [],
|
|
limitations: [],
|
|
available: true,
|
|
reason: null,
|
|
availability_code: null,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
const entries = buildCapabilityInventory(resourceStatus, enabledPolicy);
|
|
for (const name of [
|
|
"inspect_artifact",
|
|
"extract_artifact",
|
|
"trace_dylib_resolution",
|
|
"get_navigation_context",
|
|
])
|
|
expect(entries.find((item) => item.name === name)).toMatchObject({
|
|
available: false,
|
|
reason: "target_unsupported",
|
|
remediation: expect.stringContaining("open_binary"),
|
|
});
|
|
for (const name of [
|
|
"inspect_plist",
|
|
"decode_interface_builder",
|
|
"inspect_asset_catalog",
|
|
"inspect_keyed_archive",
|
|
"inspect_pe_imports",
|
|
"inspect_desktop_application",
|
|
"close_binary",
|
|
])
|
|
expect(entries.find((item) => item.name === name)).toMatchObject({
|
|
available: true,
|
|
});
|
|
expect(
|
|
entries.find((item) => item.name === "inspect_apk_signature"),
|
|
).toMatchObject({
|
|
available: true,
|
|
reason: "available",
|
|
remediation: null,
|
|
});
|
|
const sqlite = sqliteDatabaseCapability(process.versions.node);
|
|
expect(
|
|
entries.find((item) => item.name === "inspect_sqlite_database"),
|
|
).toMatchObject(
|
|
sqlite.available
|
|
? { available: true }
|
|
: {
|
|
available: false,
|
|
reason: "provider_unavailable",
|
|
remediation: sqlite.reason,
|
|
},
|
|
);
|
|
});
|
|
|
|
it("derives closed-session address and explicit-path APK availability from their contracts", () => {
|
|
expect(entry("inspect_address_context", status())).toMatchObject({
|
|
available: false,
|
|
reason: "target_required",
|
|
remediation: expect.any(String),
|
|
});
|
|
expect(entry("inspect_apk_signature", status())).toMatchObject({
|
|
available: true,
|
|
reason: "available",
|
|
remediation: null,
|
|
});
|
|
expect(
|
|
entry(
|
|
"inspect_address_context",
|
|
status({
|
|
open: true,
|
|
kind: "executable",
|
|
capabilities: [
|
|
{
|
|
operation: "inspect_address_context",
|
|
available: true,
|
|
reason: null,
|
|
},
|
|
],
|
|
}),
|
|
),
|
|
).toMatchObject({ available: true, reason: "available" });
|
|
expect(entry("inspect_apk_signature", status({ open: true }))).toMatchObject({
|
|
available: true,
|
|
reason: "available",
|
|
});
|
|
for (const kind of ["artifact", "archive"] as const) {
|
|
expect(
|
|
entry("inspect_address_context", status({ open: true, kind })),
|
|
).toMatchObject({
|
|
available: false,
|
|
reason: "target_unsupported",
|
|
remediation: expect.stringContaining("explicit document"),
|
|
});
|
|
expect(
|
|
entry("inspect_apk_signature", status({ open: true, kind })),
|
|
).toMatchObject({
|
|
available: true,
|
|
reason: "available",
|
|
});
|
|
}
|
|
});
|
|
|
|
describe("ADB and Apktool host availability", () => {
|
|
it("uses only their selected owner probes", () => {
|
|
const ready = {
|
|
status: "available" as const,
|
|
code: null,
|
|
reason: null,
|
|
diagnostics: {},
|
|
};
|
|
const configuredButInvalid = {
|
|
status: "unavailable" as const,
|
|
code: "not_configured" as const,
|
|
reason: "REA_ADB_PATH is invalid",
|
|
remediation: "Set REA_ADB_PATH to an absolute adb executable path.",
|
|
diagnostics: {},
|
|
};
|
|
const readyInventory = buildCapabilityInventory(status(), {
|
|
...enabledPolicy,
|
|
adbAnalysisAvailability: ready,
|
|
apktoolAnalysisAvailability: ready,
|
|
});
|
|
expect(
|
|
readyInventory.find(({ name }) => name === "inspect_adb_client"),
|
|
).toMatchObject({ available: true, reason: "available" });
|
|
expect(
|
|
readyInventory.find(({ name }) => name === "inspect_apktool_client"),
|
|
).toMatchObject({ available: true, reason: "available" });
|
|
|
|
const unavailableInventory = buildCapabilityInventory(status(), {
|
|
...enabledPolicy,
|
|
adbAnalysisAvailability: configuredButInvalid,
|
|
apktoolAnalysisAvailability: {
|
|
status: "unavailable",
|
|
code: "unsupported_version",
|
|
reason: "Apktool 2.6.3 is older than 2.7.0",
|
|
remediation: "Repair the fixture provider.",
|
|
diagnostics: {},
|
|
},
|
|
});
|
|
expect(
|
|
unavailableInventory.find(({ name }) => name === "inspect_adb_client"),
|
|
).toMatchObject({ available: false, reason: "provider_missing" });
|
|
expect(
|
|
unavailableInventory.find(
|
|
({ name }) => name === "decode_android_resources",
|
|
),
|
|
).toMatchObject({ available: false, reason: "provider_unavailable" });
|
|
});
|
|
});
|
|
|
|
describe("capability failure reasons", () => {
|
|
it("keeps an unavailable Android runtime distinct from a missing provider", () => {
|
|
const inventory = buildCapabilityInventory(status(), {
|
|
...enabledPolicy,
|
|
androidAnalysisAvailability: {
|
|
status: "unavailable",
|
|
code: "runtime_unavailable",
|
|
reason: "The JADX native self-test failed.",
|
|
remediation: "Repair the fixture provider.",
|
|
diagnostics: {},
|
|
},
|
|
});
|
|
expect(
|
|
inventory.find(({ name }) => name === "inspect_android_package"),
|
|
).toMatchObject({ available: false, reason: "provider_unavailable" });
|
|
});
|
|
|
|
it("admits physical universal Mach-O targets for native Mach-O operations", () => {
|
|
expect(
|
|
entry(
|
|
"inspect_native_dispatch_metadata",
|
|
status({
|
|
open: true,
|
|
kind: "executable",
|
|
format: "mach-o-universal",
|
|
capabilities: [
|
|
{
|
|
operation: "inspect_native_dispatch_metadata",
|
|
available: true,
|
|
reason: null,
|
|
},
|
|
],
|
|
}),
|
|
),
|
|
).toMatchObject({ available: true, reason: "available" });
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
label: "requires a target",
|
|
name: "current_address",
|
|
sessionStatus: status(),
|
|
reason: "target_required",
|
|
remediation: "open_binary",
|
|
},
|
|
{
|
|
label: "rejects an incompatible target family",
|
|
name: "current_address",
|
|
sessionStatus: status({ open: true, kind: "archive" }),
|
|
reason: "target_unsupported",
|
|
remediation: "native executable",
|
|
},
|
|
{
|
|
label: "reports a missing provider operation",
|
|
name: "current_address",
|
|
sessionStatus: status({ open: true, kind: "executable" }),
|
|
reason: "provider_missing",
|
|
remediation: "provider",
|
|
},
|
|
{
|
|
label: "distinguishes an unsupported host",
|
|
name: "current_address",
|
|
sessionStatus: status({
|
|
open: true,
|
|
kind: "executable",
|
|
capabilities: [
|
|
{
|
|
operation: "current_address",
|
|
available: false,
|
|
availability_code: "unsupported_host",
|
|
reason: "Operation requires macOS",
|
|
},
|
|
],
|
|
}),
|
|
reason: "unsupported_host",
|
|
remediation: "macOS",
|
|
},
|
|
{
|
|
label: "retains a provider failure",
|
|
name: "current_address",
|
|
sessionStatus: status({
|
|
open: true,
|
|
kind: "executable",
|
|
capabilities: [
|
|
{
|
|
operation: "current_address",
|
|
available: false,
|
|
reason: "Provider session is unhealthy",
|
|
},
|
|
],
|
|
}),
|
|
reason: "provider_unavailable",
|
|
remediation: "unhealthy",
|
|
},
|
|
{
|
|
label: "reports an available provider operation",
|
|
name: "current_address",
|
|
sessionStatus: status({
|
|
open: true,
|
|
kind: "executable",
|
|
capabilities: [
|
|
{
|
|
operation: "current_address",
|
|
available: true,
|
|
reason: null,
|
|
},
|
|
],
|
|
}),
|
|
reason: "available",
|
|
remediation: null,
|
|
},
|
|
])("$label", ({ name, sessionStatus, reason, remediation }) => {
|
|
const availability = entry(name, sessionStatus);
|
|
expect(availability).toMatchObject({
|
|
reason,
|
|
available: reason === "available",
|
|
});
|
|
if (remediation === null) expect(availability.remediation).toBeNull();
|
|
else expect(availability.remediation).toContain(remediation);
|
|
});
|
|
});
|
|
|
|
describe("compact tool availability", () => {
|
|
it("round-trips complete facts, omitted modes, and per-group client requirements", () => {
|
|
const full = buildCapabilityInventory(status(), enabledPolicy);
|
|
const alternateRequirements = {
|
|
required: ["roots"],
|
|
optional: [],
|
|
missing_required: ["roots"],
|
|
missing_optional: [],
|
|
} satisfies ToolAvailability["client_requirements"];
|
|
const withOverride = full.map((entry, index) =>
|
|
index === 1
|
|
? {
|
|
...entry,
|
|
available: false as const,
|
|
reason: "client_capability_missing" as const,
|
|
remediation: "Use an MCP client with roots support.",
|
|
client_requirements: alternateRequirements,
|
|
}
|
|
: entry,
|
|
);
|
|
const compact = compactToolAvailability(withOverride);
|
|
const expanded = expandToolAvailability(compact).toSorted((left, right) =>
|
|
left.name.localeCompare(right.name),
|
|
);
|
|
|
|
expect(expanded).toEqual(
|
|
withOverride.toSorted((left, right) =>
|
|
left.name.localeCompare(right.name),
|
|
),
|
|
);
|
|
expect(compact.client_requirements).toEqual(
|
|
withOverride[0]?.client_requirements,
|
|
);
|
|
expect(
|
|
compact.groups.find((group) => group.client_requirements !== undefined)
|
|
?.client_requirements,
|
|
).toEqual(alternateRequirements);
|
|
expect(compact.groups.every((group) => group.names.length > 0)).toBe(true);
|
|
const names = compact.groups.flatMap((group) => group.names).sort();
|
|
expect(names).toEqual(TOOL_CONTRACTS.map(({ name }) => name).sort());
|
|
expect(new Set(names).size).toBe(names.length);
|
|
expect(expanded.some((entry) => !Object.hasOwn(entry, "modes"))).toBe(true);
|
|
});
|
|
|
|
it("preserves an empty inventory with an explicit empty client baseline", () => {
|
|
const compact = compactToolAvailability([]);
|
|
expect(compact).toEqual({
|
|
client_requirements: {
|
|
required: [],
|
|
optional: [],
|
|
missing_required: [],
|
|
missing_optional: [],
|
|
},
|
|
groups: [],
|
|
});
|
|
expect(expandToolAvailability(compact)).toEqual([]);
|
|
});
|
|
|
|
it("rejects malformed compact facts and accepts an equivalent explicit baseline", () => {
|
|
const compact = compactToolAvailability(
|
|
buildCapabilityInventory(status(), enabledPolicy),
|
|
);
|
|
const firstGroup = compact.groups[0];
|
|
expect(firstGroup).toBeDefined();
|
|
if (firstGroup === undefined) throw new Error("Expected tool groups");
|
|
const availableIndex = compact.groups.findIndex((group) => group.available);
|
|
if (availableIndex < 0) throw new Error("Expected an available tool group");
|
|
|
|
const malformedInventories = [
|
|
{
|
|
...compact,
|
|
groups: compact.groups.map((group, index) =>
|
|
index === 0
|
|
? { ...group, effects: [...group.effects, "unknown_effect"] }
|
|
: group,
|
|
),
|
|
},
|
|
{
|
|
...compact,
|
|
groups: compact.groups.map((group, index) =>
|
|
index === 0
|
|
? {
|
|
...group,
|
|
annotations: [...group.annotations, "unknown_hint"],
|
|
}
|
|
: group,
|
|
),
|
|
},
|
|
{
|
|
...compact,
|
|
groups: compact.groups.map((group, index) =>
|
|
index === 0 ? { ...group, names: [] } : group,
|
|
),
|
|
},
|
|
{
|
|
...compact,
|
|
groups: compact.groups.map((group, index) =>
|
|
index === 0 ? { ...group, names: [""] } : group,
|
|
),
|
|
},
|
|
{
|
|
...compact,
|
|
groups: compact.groups.map((group, index) =>
|
|
index === availableIndex
|
|
? { ...group, reason: "provider_missing" }
|
|
: group,
|
|
),
|
|
},
|
|
{
|
|
...compact,
|
|
groups: compact.groups.map((group, index) =>
|
|
index === availableIndex
|
|
? { ...group, remediation: "unexpected remediation" }
|
|
: group,
|
|
),
|
|
},
|
|
];
|
|
for (const malformed of malformedInventories)
|
|
expect(toolAvailabilityInventory.safeParse(malformed).success).toBe(
|
|
false,
|
|
);
|
|
|
|
const inheritedGroup = compact.groups.find(
|
|
(group) => group.client_requirements === undefined,
|
|
);
|
|
expect(inheritedGroup).toBeDefined();
|
|
if (inheritedGroup === undefined)
|
|
throw new Error("Expected an inherited client-requirements group");
|
|
const explicitBaseline = {
|
|
...compact,
|
|
groups: compact.groups.map((group) =>
|
|
group === inheritedGroup
|
|
? { ...group, client_requirements: compact.client_requirements }
|
|
: group,
|
|
),
|
|
};
|
|
const parsedOverride =
|
|
toolAvailabilityInventory.safeParse(explicitBaseline);
|
|
expect(parsedOverride.success).toBe(true);
|
|
if (parsedOverride.success)
|
|
expect(expandToolAvailability(parsedOverride.data)).toEqual(
|
|
expandToolAvailability(compact),
|
|
);
|
|
});
|
|
});
|
|
|
|
it("reports target restrictions for navigation modes even when providers are declared", () => {
|
|
const capabilities = [
|
|
"current_document",
|
|
"current_address",
|
|
"resolve_containing_procedure",
|
|
].map((operation) => ({ operation, available: true, reason: null }));
|
|
for (const kind of ["artifact", "archive"] as const) {
|
|
const navigation = entry(
|
|
"get_navigation_context",
|
|
status({ open: true, kind, capabilities }),
|
|
);
|
|
expect(navigation).toMatchObject({
|
|
available: false,
|
|
reason: "target_unsupported",
|
|
default_mode_available: false,
|
|
modes: [
|
|
{ name: "current_selection", available: false },
|
|
{ name: "explicit_document", available: false },
|
|
],
|
|
});
|
|
for (const mode of navigation.modes ?? [])
|
|
expect(mode.remediation).toBe(navigation.remediation);
|
|
}
|
|
});
|
|
|
|
describe("partial navigation availability", () => {
|
|
it("requires a target before diagnosing navigation provider modes", () => {
|
|
expect(entry("get_navigation_context", status())).toMatchObject({
|
|
available: false,
|
|
reason: "target_required",
|
|
default_mode_available: false,
|
|
modes: [
|
|
{ name: "current_selection", available: false },
|
|
{ name: "explicit_document", available: false },
|
|
],
|
|
});
|
|
expect(
|
|
TOOL_CONTRACTS.find((tool) => tool.name === "get_navigation_context"),
|
|
).toMatchObject({
|
|
requiresActiveTarget: true,
|
|
});
|
|
});
|
|
it("allows explicit documents when the provider has no current-document getter", () => {
|
|
const capabilities = [
|
|
"current_document",
|
|
"current_address",
|
|
"resolve_containing_procedure",
|
|
].map((operation) => ({
|
|
operation,
|
|
available: true,
|
|
reason: null,
|
|
}));
|
|
expect(
|
|
entry(
|
|
"get_navigation_context",
|
|
status({ open: true, kind: "executable", capabilities }),
|
|
),
|
|
).toMatchObject({
|
|
available: true,
|
|
reason: "available",
|
|
default_mode_available: true,
|
|
modes: [
|
|
{ name: "current_selection", available: true },
|
|
{ name: "explicit_document", available: true },
|
|
],
|
|
});
|
|
expect(
|
|
entry(
|
|
"get_navigation_context",
|
|
status({
|
|
open: true,
|
|
kind: "executable",
|
|
capabilities: capabilities.slice(1),
|
|
}),
|
|
),
|
|
).toMatchObject({
|
|
available: true,
|
|
reason: "available",
|
|
default_mode_available: false,
|
|
modes: [
|
|
{ name: "current_selection", available: false },
|
|
{ name: "explicit_document", available: true },
|
|
],
|
|
});
|
|
expect(
|
|
entry(
|
|
"get_navigation_context",
|
|
status({
|
|
open: true,
|
|
kind: "executable",
|
|
capabilities: capabilities.slice(1, 2),
|
|
}),
|
|
),
|
|
).toMatchObject({
|
|
available: false,
|
|
reason: "provider_missing",
|
|
remediation: expect.stringContaining("resolve_containing_procedure"),
|
|
});
|
|
});
|
|
|
|
it("preserves the most specific navigation mode failure", () => {
|
|
expect(
|
|
entry(
|
|
"get_navigation_context",
|
|
status({
|
|
open: true,
|
|
kind: "executable",
|
|
capabilities: [
|
|
{
|
|
operation: "current_address",
|
|
available: false,
|
|
availability_code: "unsupported_host",
|
|
reason: "Navigation requires macOS.",
|
|
},
|
|
{
|
|
operation: "resolve_containing_procedure",
|
|
available: true,
|
|
reason: null,
|
|
},
|
|
],
|
|
}),
|
|
),
|
|
).toMatchObject({
|
|
available: false,
|
|
reason: "unsupported_host",
|
|
remediation: "Navigation requires macOS.",
|
|
});
|
|
});
|
|
|
|
it("lists shared missing operations once in combined remediation", () => {
|
|
const availability = entry(
|
|
"get_navigation_context",
|
|
status({ open: true, kind: "executable" }),
|
|
);
|
|
|
|
expect(availability).toMatchObject({
|
|
reason: "provider_missing",
|
|
remediation: expect.stringMatching(/configure.*provider/i),
|
|
modes: [
|
|
{
|
|
name: "current_selection",
|
|
missing_operations: [
|
|
"current_document",
|
|
"current_address",
|
|
"resolve_containing_procedure",
|
|
],
|
|
},
|
|
{
|
|
name: "explicit_document",
|
|
missing_operations: [
|
|
"current_address",
|
|
"resolve_containing_procedure",
|
|
],
|
|
},
|
|
],
|
|
});
|
|
for (const operation of [
|
|
"current_document",
|
|
"current_address",
|
|
"resolve_containing_procedure",
|
|
]) {
|
|
expect(availability.remediation?.split(operation)).toHaveLength(2);
|
|
}
|
|
});
|
|
});
|
|
|
|
it("reports host denial before target mismatch but keeps composed workflow prerequisites", () => {
|
|
const capability = {
|
|
operation: "inspect_macho",
|
|
available: false,
|
|
availability_code: "unsupported_host" as const,
|
|
reason: "Native macOS utilities require macOS.",
|
|
};
|
|
for (const target of [
|
|
{},
|
|
{ open: true, kind: "archive" as const, format: "zip" },
|
|
]) {
|
|
expect(
|
|
entry("inspect_macho", status({ ...target, capabilities: [capability] })),
|
|
).toMatchObject({
|
|
reason: "unsupported_host",
|
|
remediation: capability.reason,
|
|
});
|
|
}
|
|
expect(
|
|
entry(
|
|
"inspect_native_dispatch_metadata",
|
|
status({
|
|
capabilities: [
|
|
{ ...capability, operation: "inspect_native_dispatch_metadata" },
|
|
],
|
|
}),
|
|
).reason,
|
|
).toBe("target_required");
|
|
});
|
|
|
|
describe("binary_overview capability requirements", () => {
|
|
it("admits a native overview without requiring full inventory operations", () => {
|
|
expect(
|
|
entry(
|
|
"binary_overview",
|
|
status({
|
|
open: true,
|
|
kind: "executable",
|
|
capabilities: [
|
|
{
|
|
operation: "read_binary_inventory_summary",
|
|
available: true,
|
|
reason: null,
|
|
},
|
|
],
|
|
}),
|
|
),
|
|
).toMatchObject({ available: true, reason: "available" });
|
|
});
|
|
it("admits a provider with current_document and other mandatory capabilities", () => {
|
|
const capabilities = [
|
|
"current_document",
|
|
"list_procedures",
|
|
"list_strings",
|
|
].map((operation) => ({ operation, available: true, reason: null }));
|
|
expect(
|
|
entry(
|
|
"binary_overview",
|
|
status({ open: true, kind: "executable", capabilities }),
|
|
),
|
|
).toMatchObject({
|
|
available: true,
|
|
reason: "available",
|
|
remediation: null,
|
|
});
|
|
});
|
|
|
|
it("admits a provider with list_documents and other mandatory capabilities", () => {
|
|
const capabilities = [
|
|
"list_documents",
|
|
"list_procedures",
|
|
"list_strings",
|
|
].map((operation) => ({ operation, available: true, reason: null }));
|
|
expect(
|
|
entry(
|
|
"binary_overview",
|
|
status({ open: true, kind: "executable", capabilities }),
|
|
),
|
|
).toMatchObject({
|
|
available: true,
|
|
reason: "available",
|
|
remediation: null,
|
|
});
|
|
});
|
|
|
|
it("reports document selection remediation when neither current_document nor list_documents is declared", () => {
|
|
const capabilities = [
|
|
"list_segments",
|
|
"list_procedures",
|
|
"list_strings",
|
|
].map((operation) => ({ operation, available: true, reason: null }));
|
|
expect(
|
|
entry(
|
|
"binary_overview",
|
|
status({ open: true, kind: "executable", capabilities }),
|
|
),
|
|
).toMatchObject({
|
|
available: false,
|
|
reason: "provider_missing",
|
|
remediation: expect.stringMatching(
|
|
/configure.*provider.*current_document.*list_documents/i,
|
|
),
|
|
});
|
|
});
|
|
|
|
it("keeps binary overview available when a declared segment facet is unavailable", () => {
|
|
const capabilities = [
|
|
{
|
|
operation: "list_segments",
|
|
available: false,
|
|
reason: "This upstream profile does not expose segment enumeration.",
|
|
},
|
|
{ operation: "list_documents", available: true, reason: null },
|
|
{ operation: "list_procedures", available: true, reason: null },
|
|
{ operation: "list_strings", available: true, reason: null },
|
|
];
|
|
expect(
|
|
entry(
|
|
"binary_overview",
|
|
status({ open: true, kind: "executable", capabilities }),
|
|
),
|
|
).toMatchObject({ available: true, reason: "available" });
|
|
});
|
|
|
|
it("reports provider_unavailable when declared document selection capability is unavailable", () => {
|
|
const capabilities = [
|
|
{ operation: "list_segments", available: true, reason: null },
|
|
{
|
|
operation: "current_document",
|
|
available: false,
|
|
reason: "Document view is closed",
|
|
},
|
|
{ operation: "list_procedures", available: true, reason: null },
|
|
{ operation: "list_strings", available: true, reason: null },
|
|
];
|
|
expect(
|
|
entry(
|
|
"binary_overview",
|
|
status({ open: true, kind: "executable", capabilities }),
|
|
),
|
|
).toMatchObject({
|
|
available: false,
|
|
reason: "provider_unavailable",
|
|
remediation: "Document view is closed",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("trace_feature requirements", () => {
|
|
const capabilities = (operations: readonly string[]) =>
|
|
operations.map((operation) => ({
|
|
operation,
|
|
available: true,
|
|
reason: null,
|
|
}));
|
|
|
|
it("remains available when owner lookup is unavailable", () => {
|
|
expect(
|
|
entry(
|
|
"trace_feature",
|
|
status({
|
|
open: true,
|
|
kind: "executable",
|
|
capabilities: capabilities([
|
|
"search_strings",
|
|
"search_procedures",
|
|
"xrefs",
|
|
]),
|
|
}),
|
|
),
|
|
).toMatchObject({ available: true, reason: "available" });
|
|
});
|
|
|
|
it("needs the search operations the workflow actually calls", () => {
|
|
expect(
|
|
entry(
|
|
"trace_feature",
|
|
status({
|
|
open: true,
|
|
kind: "executable",
|
|
capabilities: capabilities([
|
|
"search_strings",
|
|
"search_procedures",
|
|
"xrefs",
|
|
"resolve_containing_procedure",
|
|
]),
|
|
}),
|
|
),
|
|
).toMatchObject({ available: true, reason: "available" });
|
|
});
|
|
|
|
it.each(["search_strings", "search_procedures"])(
|
|
"is unavailable without %s",
|
|
(missing) => {
|
|
const operations = [
|
|
"search_strings",
|
|
"search_procedures",
|
|
"xrefs",
|
|
"resolve_containing_procedure",
|
|
].filter((operation) => operation !== missing);
|
|
|
|
expect(
|
|
entry(
|
|
"trace_feature",
|
|
status({
|
|
open: true,
|
|
kind: "executable",
|
|
capabilities: capabilities(operations),
|
|
}),
|
|
),
|
|
).toMatchObject({ available: false, reason: "provider_missing" });
|
|
},
|
|
);
|
|
|
|
it("is unavailable when only the list operations exist", () => {
|
|
expect(
|
|
entry(
|
|
"trace_feature",
|
|
status({
|
|
open: true,
|
|
kind: "executable",
|
|
capabilities: capabilities([
|
|
"list_strings",
|
|
"list_procedures",
|
|
"xrefs",
|
|
"resolve_containing_procedure",
|
|
]),
|
|
}),
|
|
),
|
|
).toMatchObject({ available: false, reason: "provider_missing" });
|
|
});
|
|
});
|
|
|
|
describe("firmware operation recovery", () => {
|
|
it.each([
|
|
[false, false],
|
|
[true, false],
|
|
[false, true],
|
|
[true, true],
|
|
])(
|
|
"keeps inspection=%s and extraction=%s independent",
|
|
(inspection, extraction) => {
|
|
const inventory = buildCapabilityInventory(status(), {
|
|
processCaptureEnabled: false,
|
|
firmwareInspectionEnabled: inspection,
|
|
firmwareExtractionEnabled: extraction,
|
|
});
|
|
for (const [name, enabled, command, otherCommand] of [
|
|
[
|
|
"inspect_firmware_regions",
|
|
inspection,
|
|
"REA_BINWALK_COMMAND",
|
|
"REA_UNBLOB_COMMAND",
|
|
],
|
|
[
|
|
"extract_firmware",
|
|
extraction,
|
|
"REA_UNBLOB_COMMAND",
|
|
"REA_BINWALK_COMMAND",
|
|
],
|
|
] as const) {
|
|
const tool = inventory.find((candidate) => candidate.name === name);
|
|
expect(tool).toMatchObject({
|
|
available: enabled,
|
|
reason: enabled ? "available" : "provider_missing",
|
|
});
|
|
if (enabled) {
|
|
expect(tool?.remediation).toBeNull();
|
|
} else {
|
|
expect(tool?.remediation).toContain(command);
|
|
expect(tool?.remediation).not.toContain(otherCommand);
|
|
expect(tool?.remediation).toContain("prlimit");
|
|
expect(tool?.remediation).toContain("procps ps");
|
|
expect(tool?.remediation?.includes("external extractor")).toBe(
|
|
name === "extract_firmware",
|
|
);
|
|
}
|
|
}
|
|
const byName = (left: ToolAvailability, right: ToolAvailability) =>
|
|
left.name.localeCompare(right.name);
|
|
expect(
|
|
expandToolAvailability(compactToolAvailability(inventory)).toSorted(
|
|
byName,
|
|
),
|
|
).toEqual(inventory.toSorted(byName));
|
|
},
|
|
);
|
|
});
|
|
|
|
const HOST_RESTRICTED_TOOLS = [
|
|
["inspect_packet_capture", "packetCapture", "packetCaptureEnabled"],
|
|
["inspect_packet_fields", "packetCapture", "packetCaptureEnabled"],
|
|
["decode_signal_recording", "signalRecording", "signalRecordingEnabled"],
|
|
["inspect_signal_decoders", "signalRecording", "signalRecordingEnabled"],
|
|
["inspect_evm_interface", "evmInterface", "evmInterfaceEnabled"],
|
|
["inspect_binary_layout", "binaryLayout", "binaryLayoutEnabled"],
|
|
["inspect_recorded_crash", "recordedCrash", "recordedCrashEnabled"],
|
|
[
|
|
"inspect_firmware_regions",
|
|
"firmwareInspection",
|
|
"firmwareInspectionEnabled",
|
|
],
|
|
["extract_firmware", "firmwareExtraction", "firmwareExtractionEnabled"],
|
|
[
|
|
"recover_javascript_sources",
|
|
"javascriptRecovery",
|
|
"javascriptRecoveryEnabled",
|
|
],
|
|
] as const;
|
|
|
|
describe("host-restricted integrations", () => {
|
|
it.each(HOST_RESTRICTED_TOOLS)(
|
|
"reports %s on an unsupported host before its configuration",
|
|
(name, feature, enabledField) => {
|
|
const availability = (enabled: boolean, hostUnsupported: boolean) =>
|
|
buildCapabilityInventory(
|
|
{ open: false, capabilities: [] },
|
|
{
|
|
processCaptureEnabled: true,
|
|
...({ [enabledField]: enabled } as Record<
|
|
typeof enabledField,
|
|
boolean
|
|
>),
|
|
hostUnsupported: { [feature]: hostUnsupported },
|
|
},
|
|
).find((tool) => tool.name === name);
|
|
expect(availability(true, true)).toMatchObject({
|
|
available: false,
|
|
reason: "unsupported_host",
|
|
});
|
|
expect(availability(false, false)).toMatchObject({
|
|
available: false,
|
|
reason: "provider_missing",
|
|
});
|
|
expect(availability(true, false)).toMatchObject({
|
|
available: true,
|
|
reason: "available",
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it.each(["darwin", "linux", "win32"])(
|
|
"distinguishes pcap capture host support from its target-free client probe on %s",
|
|
(platform) => {
|
|
const tools = buildCapabilityInventory(
|
|
{ open: false, capabilities: [] },
|
|
{
|
|
processCaptureEnabled: true,
|
|
hostUnsupported: { pcapCapture: !pcapCaptureHostSupported(platform) },
|
|
},
|
|
);
|
|
expect(
|
|
tools.find(({ name }) => name === "inspect_pcap_client"),
|
|
).toMatchObject({ available: true, reason: "available" });
|
|
expect(
|
|
tools.find(({ name }) => name === "inspect_pcap_capture"),
|
|
).toMatchObject(
|
|
platform === "darwin"
|
|
? {
|
|
available: false,
|
|
reason: "unsupported_host",
|
|
remediation: expect.stringContaining("native memory bound"),
|
|
}
|
|
: { available: true, reason: "available" },
|
|
);
|
|
},
|
|
);
|
|
|
|
it("keeps independent native sessions discoverable without an unrelated binary target", () => {
|
|
const entries = buildCapabilityInventory(status(), enabledPolicy);
|
|
for (const name of [
|
|
"list_cutter_sessions",
|
|
"cutter_command",
|
|
"start_gdb_session",
|
|
"gdb_console",
|
|
"gdb_session_status",
|
|
"close_gdb_session",
|
|
"start_rizin_debug_session",
|
|
"rizin_debug_command",
|
|
"rizin_debug_session_status",
|
|
"close_rizin_debug_session",
|
|
"inspect_with_objdump",
|
|
"execute_rizin_command",
|
|
]) {
|
|
expect(
|
|
entries.find((entry) => entry.name === name),
|
|
name,
|
|
).toMatchObject({ available: true });
|
|
}
|
|
expect(entries.find((entry) => entry.name === "inspect_macho")).toMatchObject(
|
|
{ available: false, reason: "target_required" },
|
|
);
|
|
});
|
|
|
|
it("reports standalone reverse-engineering tools unavailable when launch failure is observed", () => {
|
|
const repair =
|
|
"Set REA_OBJDUMP_COMMAND to an installed executable GNU objdump command.";
|
|
const entries = buildCapabilityInventory(status(), {
|
|
...enabledPolicy,
|
|
commandProviderFailures: {
|
|
inspect_with_objdump: {
|
|
reason: "Could not launch selected objdump executable",
|
|
remediation: repair,
|
|
},
|
|
},
|
|
});
|
|
expect(
|
|
entries.find((entry) => entry.name === "inspect_with_objdump"),
|
|
).toMatchObject({
|
|
available: false,
|
|
reason: "provider_unavailable",
|
|
remediation: repair,
|
|
});
|
|
expect(
|
|
entries.find((entry) => entry.name === "execute_rizin_command"),
|
|
).toMatchObject({
|
|
available: true,
|
|
reason: "available",
|
|
remediation: null,
|
|
});
|
|
});
|
|
|
|
it.each(["darwin", "linux", "win32"] as const)(
|
|
"reports the persistent debugger start host restriction on %s",
|
|
(platform) => {
|
|
const entries = buildCapabilityInventory(status(), {
|
|
...enabledPolicy,
|
|
hostUnsupported: {
|
|
persistentDebugger: !persistentDebuggerHostSupported(platform),
|
|
},
|
|
});
|
|
for (const name of ["start_gdb_session", "start_rizin_debug_session"])
|
|
expect(entries.find((entry) => entry.name === name)).toMatchObject(
|
|
platform === "linux"
|
|
? { available: true, reason: "available" }
|
|
: {
|
|
available: false,
|
|
reason: "unsupported_host",
|
|
remediation: expect.stringContaining("Linux"),
|
|
},
|
|
);
|
|
for (const name of ["gdb_session_status", "close_rizin_debug_session"])
|
|
expect(entries.find((entry) => entry.name === name)).toMatchObject({
|
|
available: true,
|
|
});
|
|
},
|
|
);
|
|
|
|
describe("summary tool availability", () => {
|
|
it("summarizes every tool's current availability without static catalog flags", () => {
|
|
const full = buildCapabilityInventory(status(), enabledPolicy);
|
|
const withOverride = full.map((entry, index) =>
|
|
index === 1
|
|
? {
|
|
...entry,
|
|
available: false as const,
|
|
reason: "client_capability_missing" as const,
|
|
remediation: "Use an MCP client with roots support.",
|
|
client_requirements: {
|
|
required: ["roots" as const],
|
|
optional: [],
|
|
missing_required: ["roots" as const],
|
|
missing_optional: [],
|
|
},
|
|
}
|
|
: entry,
|
|
);
|
|
const complete = compactToolAvailability(withOverride);
|
|
const summary = compactToolAvailability(withOverride, "summary");
|
|
|
|
expect(toolAvailabilitySummaryInventory.parse(summary)).toEqual(summary);
|
|
expect(toolAvailabilityInventory.safeParse(summary).success).toBe(false);
|
|
expect(summary.groups.length).toBeLessThan(complete.groups.length);
|
|
expect(JSON.stringify(summary).length).toBeLessThan(
|
|
JSON.stringify(complete).length / 2,
|
|
);
|
|
for (const group of summary.groups)
|
|
for (const field of ["surface", "effects", "annotations"])
|
|
expect(Object.hasOwn(group, field)).toBe(false);
|
|
const byName = (left: { name: string }, right: { name: string }) =>
|
|
left.name.localeCompare(right.name);
|
|
expect(expandToolAvailability(summary).toSorted(byName)).toEqual(
|
|
withOverride
|
|
.map(({ surface: _s, effects: _e, annotations: _a, ...entry }) => entry)
|
|
.toSorted(byName),
|
|
);
|
|
});
|
|
});
|