test: overhaul deterministic suite architecture

This commit is contained in:
morluto 2026-07-29 22:00:38 +00:00
parent 7f2348619a
commit 513d9b1555
371 changed files with 30879 additions and 24389 deletions

View File

@ -18,10 +18,84 @@
},
"overrides": [
{
"files": ["tests/**/*.ts"],
"files": ["tests/**/*.ts", "src/**/*.test.ts"],
"rules": {
"max-lines-per-function": "off",
"max-lines": "off"
"max-lines": [
"error",
{ "max": 500, "skipBlankLines": true, "skipComments": true }
],
"max-lines-per-function": [
"error",
{ "max": 150, "skipBlankLines": true, "skipComments": true }
]
}
},
{
"files": ["src/domain/**/*.test.ts"],
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
{
"group": [
"**/application/**",
"**/server/**",
"**/process/**",
"**/browser/**",
"**/ghidra/**",
"**/hopper/**",
"**/replay/**",
"**/cli.*",
"**/main.*"
],
"message": "Domain tests may import only their owning domain and inward dependencies."
}
]
}
]
}
},
{
"files": ["src/application/**/*.test.ts"],
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
{
"group": ["**/server/**", "**/cli.*", "**/main.*"],
"message": "Service tests must use application ports rather than CLI or MCP entrypoints."
}
]
}
]
}
},
{
"files": ["tests/composition/**/*.test.ts"],
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
{
"group": [
"**/server/**",
"**/cli.*",
"**/main.*",
"node:child_process",
"node:net",
"node:http",
"node:https",
"ws",
"playwright"
],
"message": "Composition tests may wire provider-neutral sessions but must not invoke CLI, MCP, process, network, or browser boundaries."
}
]
}
]
}
}
],

View File

@ -0,0 +1,35 @@
import { describe, expect, it } from "vitest";
import type {
AnalysisClient,
AnalysisProvider,
ProviderIdentity,
} from "./AnalysisProvider.js";
import { AnalysisProviderRegistry } from "./AnalysisProviderRegistry.js";
import { composeBinarySession } from "./BinarySessionComposition.js";
const auxiliaryProvider = (identity: ProviderIdentity): AnalysisProvider => ({
identity: () => identity,
capabilities: () => [],
createClient: (): AnalysisClient => {
throw new Error("The composition check must not create provider clients");
},
});
describe("composeBinarySession", () => {
it("exposes the complete configured provider set without starting clients", () => {
const session = composeBinarySession(new AnalysisProviderRegistry([]), [
auxiliaryProvider({
id: "test-auxiliary",
name: "Test auxiliary provider",
version: "1.0.0",
}),
]);
expect(session.providerIdentity()).toEqual({
id: "composite:test-auxiliary",
name: "REA composite analysis provider",
version: null,
});
});
});

View File

@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { projectBoundedCartesian } from "../src/domain/boundedCartesianProjection.js";
import { projectBoundedCartesian } from "./boundedCartesianProjection.js";
describe("bounded Cartesian projection", () => {
it("stops projection work at the output limit", () => {

View File

@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { inferJsonShape } from "../src/domain/jsonShape.js";
import { inferJsonShape } from "./jsonShape.js";
describe("inferJsonShape", () => {
it("retains paths and types without retaining JSON values", () => {

View File

@ -5,7 +5,7 @@ import {
discoverObjcClasses,
discoverObjcProtocols,
discoverSwiftClasses,
} from "../src/domain/symbolAnalysis.js";
} from "./symbolAnalysis.js";
describe("symbol analysis", () => {
it("filters Swift classes and reports the uncapped match count", () => {

View File

@ -11,7 +11,7 @@ import {
totalResourceSize,
type PackageManifest,
type DigitalSignature,
} from "../src/domain/packageAnalysis.js";
} from "../../../src/domain/packageAnalysis.js";
describe("package analysis", () => {
it("detects package format from extension", () => {

View File

@ -0,0 +1,244 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { describe, expect, it } from "vitest";
import { composeBinarySessionFromFactory } from "../../../src/application/BinarySessionComposition.js";
import type { BinarySession } from "../../../src/application/BinarySession.js";
import { createServer } from "../../../src/server/createServer.js";
import {
JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE,
JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE,
JAVASCRIPT_FEATURE_TRACE_EXAMPLE,
JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE,
SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE,
} from "../../../src/contracts/javascriptApplicationWorkflowExamples.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import { observed } from "../../fixtures/analysisExecution.js";
interface TestHarness {
readonly client: Client;
readonly session: BinarySession;
readonly resourceListChanges: () => number;
readonly close: () => Promise<void>;
}
async function createTestHarness(): Promise<TestHarness> {
const session = composeBinarySessionFromFactory(() => ({
execute: () => Promise.resolve(observed(null)),
close: () => Promise.resolve(),
}));
const server = createServer(session, session);
const client = new Client({
name: "application-workflow-test",
version: "1",
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
let resourceListChanges = 0;
client.setNotificationHandler("notifications/resources/list_changed", () => {
resourceListChanges += 1;
});
await server.connect(serverTransport);
await client.connect(clientTransport);
return {
client,
session,
resourceListChanges: () => resourceListChanges,
close: async () => {
await client.close();
await server.close();
await session.close();
},
};
}
async function runInlineEvidenceScenarios(
client: Client,
session: BinarySession,
): Promise<void> {
const traced = await client.callTool({
name: "trace_application_feature",
arguments: JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE,
});
expect(traced.isError).not.toBe(true);
expect(traced.structuredContent).toMatchObject({
result: {
schema_version: 1,
coverage: { status: expect.any(String) },
},
});
const compared = await client.callTool({
name: "compare_application_versions",
arguments: {
...JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE,
unknown_registry_approved: true,
},
});
expect(compared.isError).not.toBe(true);
expect(compared.structuredContent).toMatchObject({
result: {
schema_version: 1,
summary: { unknown: expect.any(Number) },
coverage: { status: expect.any(String) },
},
});
const sourceCompared = await client.callTool({
name: "compare_source_to_bundle",
arguments: {
reference: SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE.reference,
application: JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE.application,
},
});
expect(sourceCompared.isError).not.toBe(true);
expect(sourceCompared.structuredContent).toMatchObject({
result: {
schema_version: 1,
reference: {
root_sha256: SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE.reference.root_sha256,
},
scoring: { algorithm: "rea-source-to-bundle-signals/v1" },
},
});
expect(session.exportEvidenceBundle().records.length).toBeGreaterThan(2);
}
async function runEvidenceIdScenarios(harness: TestHarness): Promise<void> {
await new Promise<void>((resolve) => setImmediate(resolve));
const notificationsBeforeIdReuse = harness.resourceListChanges();
const tracedById = await harness.client.callTool({
name: "trace_application_feature",
arguments: {
...JAVASCRIPT_FEATURE_TRACE_EXAMPLE,
application_evidence_id:
JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE.application.evidence_id,
},
});
expect(tracedById.isError).not.toBe(true);
await new Promise<void>((resolve) => setImmediate(resolve));
expect(harness.resourceListChanges()).toBe(notificationsBeforeIdReuse);
const comparedById = await harness.client.callTool({
name: "compare_application_versions",
arguments: {
...JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE,
left_evidence_id:
JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE.left.evidence_id,
right_evidence_id:
JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE.right.evidence_id,
},
});
expect(comparedById).toMatchObject({
structuredContent: {
result: {
evidence_links: expect.arrayContaining([
JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE.left.evidence_id,
JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE.right.evidence_id,
]),
},
},
});
const sourceComparedById = await harness.client.callTool({
name: "compare_source_to_bundle",
arguments: {
...SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE,
application_evidence_id:
JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE.application.evidence_id,
},
});
expect(sourceComparedById.isError).not.toBe(true);
}
async function assertRejectedEvidenceReferences(
client: Client,
session: BinarySession,
): Promise<void> {
const missing = await client.callTool({
name: "trace_application_feature",
arguments: JAVASCRIPT_FEATURE_TRACE_EXAMPLE,
});
expect(missing).toMatchObject({
isError: true,
structuredContent: {
error: { details: { reason: "missing" } },
},
});
const wrongOperation = createEvidence(
undefined,
{ id: "fixture", name: "Fixture", version: "1" },
{ operation: "inventory_artifact", parameters: {}, result: {} },
);
const wrongPredicate = createEvidence(
undefined,
{ id: "fixture", name: "Fixture", version: "1" },
{
predicateType: "fixture.application/v1",
operation: "analyze_javascript_application",
parameters: {},
result: {},
},
);
expect(session.recordEvidence(wrongOperation).ok).toBe(true);
expect(session.recordEvidence(wrongPredicate).ok).toBe(true);
for (const [record, reason] of [
[wrongOperation, "wrong_operation"],
[wrongPredicate, "wrong_predicate"],
] as const) {
const rejected = await client.callTool({
name: "trace_application_feature",
arguments: {
...JAVASCRIPT_FEATURE_TRACE_EXAMPLE,
application_evidence_id: record.evidence_id,
},
});
expect(rejected).toMatchObject({
isError: true,
structuredContent: { error: { details: { reason } } },
});
}
}
async function assertRejectedInlineEvidence(client: Client): Promise<void> {
const duplicateNative = await client.callTool({
name: "trace_application_feature",
arguments: {
...JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE,
native_observations: [
JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE.application,
],
native_observation_evidence_ids: [
JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE.application.evidence_id,
],
},
});
expect(duplicateNative.isError).toBe(true);
const spoofed = await client.callTool({
name: "trace_application_feature",
arguments: {
...JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE,
application: {
...JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE.application,
provider: { id: "spoofed", name: "spoofed", version: "1" },
},
},
});
expect(spoofed.isError).toBe(true);
}
describe("application workflow MCP parity", () => {
it("traces and compares authenticated graph Evidence in the session", async () => {
const harness = await createTestHarness();
try {
await runInlineEvidenceScenarios(harness.client, harness.session);
await runEvidenceIdScenarios(harness);
await assertRejectedEvidenceReferences(harness.client, harness.session);
await assertRejectedInlineEvidence(harness.client);
} finally {
await harness.close();
}
});
});

View File

@ -0,0 +1,142 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { describe, expect, it } from "vitest";
import { copyFile, mkdir, rm } from "node:fs/promises";
import { join, resolve } from "node:path";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { composeBinarySessionFromFactory } from "../../../src/application/BinarySessionComposition.js";
import { createServer } from "../../../src/server/createServer.js";
import { observed } from "../../fixtures/analysisExecution.js";
import { analyzeJavaScriptApplication } from "../../../src/application/JavaScriptApplicationService.js";
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascriptApplicationAnalysis.js";
import { permissionAuthorityForRoot } from "../../fixtures/permissionAuthority.js";
describe("application workflow MCP parity", () => {
it("compares exact parser export shapes through full Evidence and session IDs", async () => {
const root = await createTestTempDirectory("rea-export-shape-mcp-");
const leftRoot = join(root, "left");
const rightRoot = join(root, "right");
await Promise.all([mkdir(leftRoot), mkdir(rightRoot)]);
await Promise.all([
copyFile(
resolve("tests/fixtures/replay/parser.mjs"),
join(leftRoot, "parser.mjs"),
),
copyFile(
resolve("tests/fixtures/replay/parser-v2.mjs"),
join(rightRoot, "parser.mjs"),
),
]);
const authority = await permissionAuthorityForRoot(
root,
["investigation_input"],
["investigation_input"],
);
const [left, right] = await Promise.all([
analyzeJavaScriptApplication(authority, {
input_path: leftRoot,
approved: true,
}),
analyzeJavaScriptApplication(authority, {
input_path: rightRoot,
approved: true,
}),
]);
if (!left.ok) throw left.error;
if (!right.ok) throw right.error;
const session = composeBinarySessionFromFactory(() => ({
execute: () => Promise.resolve(observed(null)),
close: () => Promise.resolve(),
}));
const server = createServer(session, session);
const client = new Client({ name: "export-shape-test", version: "1" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
const selectors = {
left_module_path: "parser.mjs",
left_export_name: "default",
right_module_path: "parser.mjs",
right_export_name: "default",
};
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const full = await client.callTool({
name: "compare_javascript_export_shapes",
arguments: { left: left.value, right: right.value, ...selectors },
});
expect(full.isError).not.toBe(true);
expect(full.structuredContent).toMatchObject({
result: {
summary: { added: 1, removed: 0, changed: 0, unknown: 0 },
changes: [
{
status: "added",
path: "/depth",
right: { availability: "literal", value: 1 },
},
],
},
});
expect(session.exportEvidenceBundle().records).toEqual(
expect.arrayContaining([
expect.objectContaining({
operation: "compare_javascript_export_shapes",
predicate_type: "rea.javascript-export-shape-comparison/v1",
}),
]),
);
const byId = await client.callTool({
name: "compare_javascript_export_shapes",
arguments: {
left_evidence_id: left.value.evidence_id,
right_evidence_id: right.value.evidence_id,
...selectors,
},
});
expect(byId.isError).not.toBe(true);
expect(byId.structuredContent).toMatchObject({
result: {
evidence_links: [left.value.evidence_id, right.value.evidence_id],
changes: [expect.objectContaining({ path: "/depth" })],
},
});
const analyzed = javascriptApplicationAnalysisResultSchema.parse(
left.value.normalized_result,
);
if (analyzed.schema_version !== 2)
throw new TypeError("Expected semantic application Evidence");
const seed = analyzed.semantic_graph.relations[0]?.source_node_id;
if (seed === undefined)
throw new TypeError("Expected at least one semantic relation");
const semantic = await client.callTool({
name: "trace_javascript_semantics",
arguments: {
application: left.value,
query: {
seed: { kind: "semantic-node", node_id: seed },
direction: "forward-influence",
include_ambiguous_dynamic_edges: true,
},
},
});
expect(semantic.isError).not.toBe(true);
expect(semantic.structuredContent).toMatchObject({
result: {
schema_version: 1,
source_evidence_id: left.value.evidence_id,
source_graph_id: analyzed.semantic_graph.graph_id,
summary: { retained_seed_matches: 1 },
},
});
} finally {
await client.close();
await server.close();
await session.close();
await rm(root, { recursive: true, force: true });
}
});
});

View File

@ -0,0 +1,277 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { createHash } from "node:crypto";
import { readFile, realpath } from "node:fs/promises";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";
import { composeBinarySessionFromFactory } from "../../../src/application/BinarySessionComposition.js";
import type { BinarySession } from "../../../src/application/BinarySession.js";
import { PermissionAuthority } from "../../../src/application/PermissionAuthority.js";
import { controlledReplayOutputSchema } from "../../../src/domain/javascriptReplay.js";
import { nodeCharacterizationPreparationOutputSchema } from "../../../src/domain/nodeRuntimeCharacterization.js";
import { createPermissionPolicy } from "../../../src/domain/permissionPolicy.js";
import { createServer } from "../../../src/server/createServer.js";
import { observed } from "../../fixtures/analysisExecution.js";
function createReplayPolicy(root: string) {
return {
enabled: true,
roots: [root],
nodePath: process.execPath,
bubblewrapPath: process.execPath,
systemdRunPath: process.execPath,
systemctlPath: process.execPath,
shellPath: process.execPath,
};
}
function createReplayAuthority(policy: ReturnType<typeof createReplayPolicy>) {
const ceiling = {
capability: "javascript_replay" as const,
roots: policy.roots,
executables: [
policy.nodePath,
policy.bubblewrapPath,
policy.systemdRunPath,
policy.systemctlPath,
policy.shellPath,
],
environment_names: [],
network: "none" as const,
mount: true,
};
return new PermissionAuthority(
createPermissionPolicy(
[ceiling],
[
{
...ceiling,
grant_id: "administrator:javascript_replay",
lifetime: "administrator",
operation_identity: null,
expires_at: null,
},
],
),
);
}
function createReplayScenario(root: string) {
const policy = createReplayPolicy(root);
const session = composeBinarySessionFromFactory(() => ({
execute: () => Promise.resolve(observed(null)),
close: () => Promise.resolve(),
}));
const server = createServer(session, session, {
permissionAuthority: createReplayAuthority(policy),
javascriptReplayPolicy: policy,
javascriptReplayHost: {
readSource: async (path) => ({
canonicalPath: await realpath(path),
bytes: await readFile(path),
}),
identifyExecutable: async (path) => ({
path,
version: "fixture",
sha256: "1".repeat(64),
}),
identifyWorker: async () => ({
path: "/fixture/worker.js",
version: "fixture-worker-1",
sha256: "2".repeat(64),
}),
identifyRuntimeClosure: async (path) => [
{
sourcePath: path,
destinationPath: "/runtime/node",
sha256: "1".repeat(64),
},
],
seccompDigest: () => "3".repeat(64),
probe: async () => undefined,
},
javascriptReplayRunner: {
execute: async (prepared) => ({
schema_version: 1,
plan_digest: prepared.publicPlan.plan_digest,
outcomes: [
{
case_id: "heading",
outcome: "return",
value: { type: "heading", text: "Title" },
input_sha256:
prepared.publicPlan.cases[0]?.sha256 ?? "0".repeat(64),
output_sha256: "2".repeat(64),
truncated: false,
},
],
stderr: "",
termination: "completed",
cleanup: { state: "complete", residual_resources: [] },
limitations: ["fixture runner"],
reproducer: null,
}),
},
});
const client = new Client({ name: "replay-parity-test", version: "1" });
return { client, server, session };
}
function createReplayInput(root: string) {
return {
mode: "plan",
left: {
modules: [
{
alias: "parser",
path: resolve(root, "parser.mjs"),
format: "esm",
role: "module",
dependencies: {},
},
],
entry_alias: "parser",
entry_export: "default",
},
cases: [{ case_id: "heading", arguments: ["# Title"] }],
approved: false,
};
}
async function executeReplayScenario(
client: Client,
session: BinarySession,
root: string,
) {
const input = createReplayInput(root);
const planned = await client.callTool({
name: "run_controlled_replay",
arguments: input,
});
expect(planned.isError).not.toBe(true);
const digest = controlledReplayOutputSchema.parse(planned.structuredContent)
.plan?.plan_digest;
expect(digest).toMatch(/^[a-f0-9]{64}$/u);
const executed = await client.callTool({
name: "run_controlled_replay",
arguments: {
...input,
mode: "execute",
approved: true,
plan_digest: digest,
},
});
expect(executed.isError).not.toBe(true);
expect(executed.structuredContent).toMatchObject({
phase: "execute",
evidence: {
authority: "controlled-replay",
provider: { id: "rea-javascript-replay" },
},
});
expect(session.exportEvidenceBundle().records).toEqual(
expect.arrayContaining([
expect.objectContaining({ authority: "controlled-replay" }),
]),
);
}
async function createCharacterizationInput(root: string) {
const factoryPath = resolve(root, "sanitizer.factory.txt");
const factoryBytes = await readFile(factoryPath);
const factorySha256 = createHash("sha256").update(factoryBytes).digest("hex");
return {
preparation_approved: true,
selected_alias: "bundle",
expected_effect: "pure",
instrumentation: {
artifact_path: factoryPath,
artifact_sha256: factorySha256,
selection: {
byte_start: 0,
byte_end: factoryBytes.byteLength,
selected_sha256: factorySha256,
export_name: "selected",
},
},
replay: {
mode: "plan",
left: {
modules: [
{
alias: "bundle",
path: factoryPath,
format: "commonjs-factory",
role: "module",
dependencies: {},
},
],
entry_alias: "bundle",
entry_export: "selected",
},
cases: [{ case_id: "heading", arguments: ["# Title"] }],
approved: false,
},
};
}
async function executeCharacterizationScenario(
client: Client,
session: BinarySession,
root: string,
) {
const input = await createCharacterizationInput(root);
const plan = await client.callTool({
name: "prepare_node_characterization",
arguments: input,
});
expect(plan.isError).not.toBe(true);
const approvedPlan = nodeCharacterizationPreparationOutputSchema.parse(
plan.structuredContent,
).plan;
const characterized = await client.callTool({
name: "execute_node_characterization",
arguments: {
execution_approved: true,
approved_plan_sha256: approvedPlan.plan_sha256,
preparation: input,
},
});
expect(characterized.isError).not.toBe(true);
expect(characterized.structuredContent).toMatchObject({
phase: "execution",
evidence: {
authority: "controlled-replay",
provider: { id: "rea-node-characterization" },
},
});
expect(session.exportEvidenceBundle().records).toEqual(
expect.arrayContaining([
expect.objectContaining({
predicate_type: "rea.javascript-export-transformation/v1",
}),
expect.objectContaining({
predicate_type: "rea.runtime-characterization/v1",
}),
]),
);
}
describe("application workflow MCP parity", () => {
it("plans and executes the same controlled replay contract", async () => {
const root = resolve("tests/fixtures/replay");
const { client, server, session } = createReplayScenario(root);
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
await executeReplayScenario(client, session, root);
await executeCharacterizationScenario(client, session, root);
} finally {
await client.close();
await server.close();
await session.close();
}
});
});

View File

@ -5,7 +5,7 @@ import { promisify } from "node:util";
import { afterEach, describe, expect, it } from "vitest";
import { createTestTempDirectory } from "./fixtures/temporaryDirectory.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import {
JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE,
@ -13,29 +13,28 @@ import {
JAVASCRIPT_FEATURE_TRACE_EXAMPLE,
JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE,
SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE,
} from "../src/contracts/javascriptApplicationWorkflowExamples.js";
} from "../../../src/contracts/javascriptApplicationWorkflowExamples.js";
import {
createEvidenceBundle,
serializeEvidenceBundle,
} from "../src/domain/evidenceBundle.js";
import { analyzeJavaScriptApplication } from "../src/application/JavaScriptApplicationService.js";
import { permissionAuthorityForRoot } from "./fixtures/permissionAuthority.js";
import { REPLAY_MACHINE_RUN_EXAMPLE } from "../src/contracts/replayMachineExample.js";
import { javascriptApplicationAnalysisResultSchema } from "../src/domain/javascriptApplicationAnalysis.js";
} from "../../../src/domain/evidenceBundle.js";
import { analyzeJavaScriptApplication } from "../../../src/application/JavaScriptApplicationService.js";
import { permissionAuthorityForRoot } from "../../fixtures/permissionAuthority.js";
import { REPLAY_MACHINE_RUN_EXAMPLE } from "../../../src/contracts/replayMachineExample.js";
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascriptApplicationAnalysis.js";
const execute = promisify(execFile);
const temporary: string[] = [];
afterEach(async () => {
await Promise.all(
temporary
.splice(0)
.map(async (path) => rm(path, { recursive: true, force: true })),
);
});
describe("application workflow CLI parity", () => {
const temporary: string[] = [];
afterEach(async () => {
await Promise.all(
temporary
.splice(0)
.map(async (path) => rm(path, { recursive: true, force: true })),
);
});
it("accepts inline trace JSON and file-backed comparison JSON", async () => {
const traced = await runCli([
"trace-application-feature",
@ -127,7 +126,9 @@ describe("application workflow CLI parity", () => {
},
});
}, 20_000);
});
describe("application workflow CLI Evidence resolution", () => {
it("resolves Evidence IDs from an explicitly authorized bundle", async () => {
const root = await createTestTempDirectory("rea-application-bundle-cli-");
temporary.push(root);
@ -223,8 +224,10 @@ describe("application workflow CLI parity", () => {
environment,
);
expect(denied).toMatchObject({ code: "permission_required" });
}, 20_000);
}, 30_000);
});
describe("application workflow CLI export Evidence", () => {
it("compares exact export shapes from an authorized Evidence bundle", async () => {
const root = await createTestTempDirectory("rea-export-shape-cli-");
temporary.push(root);
@ -295,7 +298,9 @@ describe("application workflow CLI parity", () => {
},
});
}, 20_000);
});
describe("application workflow CLI validation and replay", () => {
it("returns safe actionable JSON validation details", async () => {
const malformed = await runCli([
"trace-application-feature",

View File

@ -5,9 +5,9 @@ import { promisify } from "node:util";
import { afterEach, describe, expect, it } from "vitest";
import { createTestTempDirectory } from "./fixtures/temporaryDirectory.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { writeVersionedJavaScriptApplicationFixtures } from "./fixtures/javascriptArtifactApplication.js";
import { writeVersionedJavaScriptApplicationFixtures } from "../../fixtures/javascriptArtifactApplication.js";
const execute = promisify(execFile);

View File

@ -2,34 +2,34 @@ import { rm } from "node:fs/promises";
import { afterEach, describe, expect, it } from "vitest";
import { createTestTempDirectory } from "./fixtures/temporaryDirectory.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { analyzeJavaScriptApplication } from "../src/application/JavaScriptApplicationService.js";
import { compareApplicationVersionsEvidence } from "../src/application/JavaScriptApplicationWorkflowService.js";
import { createEvidence } from "../src/domain/evidence.js";
import { applicationVersionComparisonResultSchema } from "../src/domain/javascriptApplicationVersionComparisonSchemas.js";
import { compareJavaScriptApplicationVersions } from "../src/domain/javascriptApplicationVersionComparison.js";
import { createJavaScriptApplicationGraph } from "../src/domain/javascriptApplicationGraph.js";
import { traceApplicationFeature } from "../src/domain/javascriptFeatureTrace.js";
import { applicationFeatureTraceResultSchema } from "../src/domain/javascriptFeatureTraceSchemas.js";
import { analyzeJavaScriptApplication } from "../../../src/application/JavaScriptApplicationService.js";
import { compareApplicationVersionsEvidence } from "../../../src/application/JavaScriptApplicationWorkflowService.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import { applicationVersionComparisonResultSchema } from "../../../src/domain/javascriptApplicationVersionComparisonSchemas.js";
import { compareJavaScriptApplicationVersions } from "../../../src/domain/javascriptApplicationVersionComparison.js";
import { createJavaScriptApplicationGraph } from "../../../src/domain/javascriptApplicationGraph.js";
import { traceApplicationFeature } from "../../../src/domain/javascriptFeatureTrace.js";
import { applicationFeatureTraceResultSchema } from "../../../src/domain/javascriptFeatureTraceSchemas.js";
import {
APPLICATION_GRAPH_DIGESTS,
buildSyntheticJavaScriptApplicationGraph,
} from "./fixtures/javascriptApplicationGraph.js";
import { writeVersionedJavaScriptApplicationFixtures } from "./fixtures/javascriptArtifactApplication.js";
import { permissionAuthorityForRoot } from "./fixtures/permissionAuthority.js";
} from "../../fixtures/javascriptApplicationGraph.js";
import { writeVersionedJavaScriptApplicationFixtures } from "../../fixtures/javascriptArtifactApplication.js";
import { permissionAuthorityForRoot } from "../../fixtures/permissionAuthority.js";
const temporary: string[] = [];
afterEach(async () => {
await Promise.all(
temporary
.splice(0)
.map(async (path) => rm(path, { recursive: true, force: true })),
);
});
describe("JavaScript application workflows", () => {
const temporary: string[] = [];
afterEach(async () => {
await Promise.all(
temporary
.splice(0)
.map(async (path) => rm(path, { recursive: true, force: true })),
);
});
it("traces a contextBridge API through IPC into linked native evidence", () => {
const graph = buildSyntheticJavaScriptApplicationGraph();
const nativeEvidence = createEvidence(
@ -129,7 +129,9 @@ describe("JavaScript application workflows", () => {
expect(bounded.coverage.omitted_nodes).toBeGreaterThan(0);
expect(bounded.summary.native_handoffs).toBe(0);
});
});
describe("JavaScript application comparison workflows", () => {
it("matches rechunked modules by exact source and minified modules by structural fingerprint", async () => {
const root = await createTestTempDirectory("rea-application-versions-");
temporary.push(root);

View File

@ -10,11 +10,10 @@ import {
nativeLibsByArchitecture,
type MobileArtifactManifest,
type AndroidManifest,
type IosManifest,
type SigningInfo,
type NativeLibrary,
type MobileResource,
} from "../src/domain/mobileApplicationInvestigation.js";
} from "../../../src/domain/mobileApplicationInvestigation.js";
describe("mobile application investigation", () => {
it("detects mobile format from extension", () => {

View File

@ -1,19 +1,22 @@
import { readFile } from "node:fs/promises";
import { describe, expect, it } from "vitest";
import { packageHopperEnvironment } from "../scripts/verify-package-environment.mjs";
import { packageHopperEnvironment } from "../../../scripts/verify-package-environment.mjs";
describe("package installation workflows", () => {
it("runs package E2E without the retired native rebuild script", async () => {
const continuousIntegration = await readFile(
new URL("../.github/workflows/ci.yml", import.meta.url),
new URL("../../../.github/workflows/ci.yml", import.meta.url),
"utf8",
);
const realHopperLinux = await readFile(
new URL("../.github/workflows/real-hopper-linux.yml", import.meta.url),
new URL(
"../../../.github/workflows/real-hopper-linux.yml",
import.meta.url,
),
"utf8",
);
const realHopperMac = await readFile(
new URL("../.github/workflows/real-hopper.yml", import.meta.url),
new URL("../../../.github/workflows/real-hopper.yml", import.meta.url),
"utf8",
);
@ -21,7 +24,7 @@ describe("package installation workflows", () => {
expect(continuousIntegration).toContain("npm run verify:package");
expect(continuousIntegration).toContain("name: Static checks");
expect(continuousIntegration).toContain("npm run check:ci");
expect(continuousIntegration).toContain("shard: [1/2, 2/2]");
expect(continuousIntegration).toContain("shard: [1/4, 2/4, 3/4, 4/4]");
expect(continuousIntegration).toContain("name: Build package");
expect(continuousIntegration).toContain("npm run build:cached");
expect(continuousIntegration).toContain("name: rea-dist");
@ -61,11 +64,11 @@ describe("package installation workflows", () => {
it("verifies the package before publish and runs published canaries outside the checkout", async () => {
const release = await readFile(
new URL("../.github/workflows/release.yml", import.meta.url),
new URL("../../../.github/workflows/release.yml", import.meta.url),
"utf8",
);
const canary = await readFile(
new URL("../scripts/verify-published-package.mjs", import.meta.url),
new URL("../../../scripts/verify-published-package.mjs", import.meta.url),
"utf8",
);

View File

@ -0,0 +1,194 @@
import { describe, expect, it } from "vitest";
import { runSetup } from "../../../src/application/Setup.js";
import { FakeSetupHost, options } from "./setupTestSupport.js";
describe("setup workflow", () => {
it("omits an aligned managed skill from an otherwise empty plan", async () => {
const host = new FakeSetupHost();
host.hopper = "/Applications/Hopper";
host.skill = "unchanged";
const result = await runSetup(options(false), host);
expect(result.status).toBe("ready");
expect(result.plannedActions).toEqual([]);
expect(result.appliedActions).toEqual([]);
expect(host.skillInstalls).toBe(0);
});
it("installs missing Linux demo dependencies for existing Hopper", async () => {
const host = new FakeSetupHost("linux");
host.distribution = {
id: "ubuntu",
versionId: "24.04",
packageFamily: "deb",
supported: true,
};
host.hopper = "/opt/hopper/bin/Hopper";
host.linuxDemoRuntimeMissing = true;
const result = await runSetup(
{ ...options(false), structured: false },
host,
() => Promise.resolve(true),
);
expect(result.status).toBe("ready");
expect(host.hopperInstalls).toBe(1);
expect(result.plannedActions.map(({ kind }) => kind)).toContain(
"install_hopper",
);
});
it("does not reinstall Hopper for an unsupported configured path", async () => {
const host = new FakeSetupHost("linux");
host.distribution = {
id: "ubuntu",
versionId: "24.04",
packageFamily: "deb",
supported: true,
};
host.hopper = "/custom/Hopper";
host.unsupportedHopperVersion = true;
const result = await runSetup(
{ ...options(false), structured: false },
host,
() => Promise.resolve(true),
);
expect(result.status).toBe("needs_human");
expect(host.hopperInstalls).toBe(0);
expect(result.plannedActions.map(({ kind }) => kind)).not.toContain(
"install_hopper",
);
});
it("reports a failed Hopper installation without configuring clients", async () => {
const host = new FakeSetupHost();
host.hopperInstallSucceeds = false;
host.clients = [{ name: "cursor", configPath: "/cursor.json" }];
const result = await runSetup(options(true, true), host);
expect(result.status).toBe("needs_human");
expect(result.code).toBe("download_failed");
expect(result.remediation).toBe("Download failed.");
expect(host.configurations).toBe(0);
});
it("blocks malformed client configuration before Hopper or file mutation", async () => {
const host = new FakeSetupHost();
host.clients = [
{ name: "codex", displayName: "Codex", configPath: "/codex.toml" },
];
host.clientInspections.set("codex", {
status: "invalid",
remediation: "Repair the existing TOML.",
});
const result = await runSetup(options(true, true), host);
expect(result.status).toBe("needs_human");
expect(result.remediation).toBe("Codex: Repair the existing TOML.");
expect(host.hopperInstalls).toBe(0);
expect(host.configurations).toBe(0);
expect(host.skillInstalls).toBe(0);
});
it("blocks malformed configuration before unrelated writes with an existing provider", async () => {
const host = new FakeSetupHost();
host.hopper = "/Applications/Hopper";
host.clients = [
{
name: "claude_desktop",
displayName: "Claude Desktop",
configPath: "/claude.json",
},
{ name: "cursor", displayName: "Cursor", configPath: "/cursor.json" },
];
host.clientInspections.set("claude_desktop", {
status: "invalid",
remediation: "Repair the existing JSON.",
});
const result = await runSetup(options(true), host);
expect(result.status).toBe("needs_human");
expect(result.remediation).toBe(
"Claude Desktop: Repair the existing JSON.",
);
expect(host.configurations).toBe(0);
expect(host.skillInstalls).toBe(0);
});
it("plans a client update when Hopper installation changes its desired environment", async () => {
const host = new FakeSetupHost();
host.clients = [
{ name: "codex", displayName: "Codex", configPath: "/codex.toml" },
];
host.clientInspections.set("codex", { status: "already_current" });
const result = await runSetup(options(false), host);
expect(
result.plannedActions.map(({ id, operation }) => ({
id,
operation,
})),
).toContainEqual({
id: "configure_client:codex",
operation: "update",
});
expect(host.configurations).toBe(0);
});
});
describe("setup workflow action selection", () => {
it("executes only actions selected by the interactive adapter", async () => {
const host = new FakeSetupHost();
host.hopper = "/Applications/Hopper";
host.clients = [
{ name: "codex", displayName: "Codex", configPath: "/codex.toml" },
{ name: "cursor", displayName: "Cursor", configPath: "/cursor.json" },
];
const result = await runSetup(
{ ...options(false), structured: false },
host,
() =>
Promise.resolve({
approved: true,
selectedActionIds: ["configure_client:codex"],
}),
);
expect(result.plannedActions.map(({ id }) => id)).toEqual([
"configure_client:codex",
]);
expect(result.clients).toEqual({ codex: { status: "configured" } });
expect(host.configurations).toBe(1);
expect(host.skillInstalls).toBe(0);
});
it.each([
[
"path",
"Agent configuration path could not be safely verified. Check its permissions and, if it is a symbolic link, verify that the link resolves to a regular file owned by the current user, then rerun setup.",
],
[
"backup",
"Agent configuration could not be backed up, so no change was made. Check file permissions, then rerun setup.",
],
[
"write",
"Agent configuration could not be updated. Check file permissions, then rerun setup.",
],
[
"readback",
"Agent configuration could not be verified after writing. Repair the configuration file or restore its `.rea.backup`, then rerun setup.",
],
] as const)("explains %s configuration recovery", async (reason, message) => {
const host = new FakeSetupHost();
host.hopper = "/Applications/Hopper";
host.clients = [{ name: "internal_client_key", configPath: "/agent.json" }];
host.clientResults.set("internal_client_key", { status: "failed", reason });
const result = await runSetup(options(true), host);
expect(result.status).toBe("needs_human");
expect(result.remediation).toBe(message);
expect(result.remediation).not.toContain("internal_client_key");
});
});

View File

@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest";
import {
setupInstallFailure,
type HopperInstallFailureReason,
} from "../src/application/SetupInstallFailure.js";
} from "../../../src/application/SetupInstallFailure.js";
describe("setup installer failure projection", () => {
it.each([

View File

@ -11,17 +11,17 @@ import {
import { dirname, join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { createTestTempDirectory } from "./fixtures/temporaryDirectory.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import {
configureTomlClient,
detectClients,
} from "../src/application/Setup.js";
} from "../../../src/application/Setup.js";
import {
runUninstall,
systemUninstallHost,
type UninstallFileSystem,
} from "../src/application/Uninstall.js";
} from "../../../src/application/Uninstall.js";
const roots: string[] = [];
afterEach(async () => {
@ -142,7 +142,9 @@ describe("agent lifecycle", () => {
readFile(`${configPath}.rea.backup`, "utf8"),
).rejects.toThrow();
});
});
describe("agent lifecycle removal", () => {
it("uninstalls only owned entries and refuses purge symlinks", async () => {
const home = await createTestTempDirectory("rea-uninstall-");
roots.push(home);
@ -250,7 +252,9 @@ describe("agent lifecycle", () => {
);
expect(await readFile(config, "utf8")).toBe("not-json");
});
});
describe("agent lifecycle failure reporting", () => {
it.each([
[
"read",

View File

@ -0,0 +1,244 @@
import { describe, expect, it } from "vitest";
import {
registrationPermissionEnvironment,
runSetup,
type SetupProgressEvent,
} from "../../../src/application/Setup.js";
import { FakeSetupHost, options } from "./setupTestSupport.js";
describe("setup workflow", () => {
it("propagates only the explicit non-secret investigation root policy", () => {
expect(
registrationPermissionEnvironment({
REA_INVESTIGATION_INPUT_ROOTS_JSON: '["/approved/apps"]',
AUTHORIZATION: "secret",
}),
).toEqual({
REA_INVESTIGATION_INPUT_ROOTS_JSON: '["/approved/apps"]',
});
expect(registrationPermissionEnvironment({})).toEqual({});
});
it("returns a complete plan without mutation", async () => {
const host = new FakeSetupHost();
host.clients = [{ name: "cursor", configPath: "/cursor.json" }];
const result = await runSetup(options(false), host);
expect(result.status).toBe("needs_confirmation");
expect(result.plannedActions.map(({ kind }) => kind)).toEqual([
"install_hopper",
"configure_client",
"install_skill",
]);
expect(host.hopperInstalls).toBe(0);
expect(host.configurations).toBe(0);
expect(result.remediation).toBe(
"Review the setup plan, then rerun interactively or with --yes.",
);
});
it("reports ready after an accepted Linux plan installs healthy Hopper", async () => {
const host = new FakeSetupHost("linux");
host.distribution = {
id: "ubuntu",
versionId: "24.04",
packageFamily: "deb",
supported: true,
};
const result = await runSetup(
{ ...options(false), structured: false },
host,
() => Promise.resolve(true),
);
expect(result.status).toBe("ready");
expect(result.appliedActions).toEqual([
"installed_hopper",
"installed_skill",
]);
expect(host.hopperInstalls).toBe(1);
expect(result.remediation).toBeUndefined();
});
it("requires first-run activation after installing Hopper on macOS", async () => {
const host = new FakeSetupHost();
const result = await runSetup(
{ ...options(false), structured: false },
host,
() => Promise.resolve(true),
);
expect(result.status).toBe("needs_human");
expect(result.remediation).toContain("choose its demo mode");
});
it("declines an interactive plan without mutation", async () => {
const host = new FakeSetupHost();
const result = await runSetup(
{ ...options(false), structured: false },
host,
() => Promise.resolve(false),
);
expect(result.status).toBe("planned");
expect(result.appliedActions).toEqual([]);
expect(host.hopperInstalls).toBe(0);
expect(host.doctorCalls).toBe(1);
});
it("treats an empty interactive selection as cancellation", async () => {
const host = new FakeSetupHost();
host.clients = [{ name: "codex", configPath: "/codex.toml" }];
const result = await runSetup(
{ ...options(false), structured: false },
host,
() => Promise.resolve({ approved: false, selectedActionIds: [] }),
);
expect(result.status).toBe("planned");
expect(result.plannedActions).toEqual([]);
expect(result.appliedActions).toEqual([]);
expect(host.configurations).toBe(0);
expect(host.skillInstalls).toBe(0);
expect(host.hopperInstalls).toBe(0);
expect(host.doctorCalls).toBe(1);
});
it("requires the Hopper flag for unattended setup", async () => {
const host = new FakeSetupHost();
const result = await runSetup(options(true), host);
expect(result.status).toBe("needs_human");
expect(host.hopperInstalls).toBe(0);
expect(result.appliedActions).toEqual(["installed_skill"]);
expect(result.remediation).toContain("--install-hopper");
expect(result.remediation).toBe(
"Hopper is optional for non-Hopper providers. Rerun with --yes --install-hopper for deep native analysis.",
);
});
it("installs Hopper when unattended authorization is explicit", async () => {
const host = new FakeSetupHost("linux");
host.distribution = {
id: "ubuntu",
versionId: "24.04",
packageFamily: "deb",
supported: true,
};
const result = await runSetup(options(true, true), host);
expect(result.appliedActions).toEqual([
"installed_hopper",
"installed_skill",
]);
expect(host.hopperInstalls).toBe(1);
expect(result.status).toBe("ready");
expect(result.remediation).toBeUndefined();
});
it("reuses existing Hopper and configures detected clients", async () => {
const host = new FakeSetupHost();
host.hopper = "/Applications/Hopper";
host.clients = [
{ name: "claude", configPath: "/claude.json" },
{ name: "cursor", configPath: "/cursor.json" },
];
const result = await runSetup(options(true), host);
expect(result.status).toBe("ready");
expect(result.appliedActions).toEqual([
"configured_claude",
"configured_cursor",
"installed_skill",
]);
expect(host.hopperInstalls).toBe(0);
});
});
describe("setup workflow lifecycle and provider planning", () => {
it("reports append-only lifecycle progress for each selected action", async () => {
const host = new FakeSetupHost();
host.hopper = "/Applications/Hopper";
host.clients = [
{
name: "codex",
displayName: "Codex",
configPath: "/codex.toml",
},
];
const progress: SetupProgressEvent[] = [];
await runSetup(
{ ...options(true), onProgress: (event) => progress.push(event) },
host,
);
expect(progress).toEqual([
{ actionId: "configure_client:codex", label: "Codex", state: "started" },
{
actionId: "configure_client:codex",
label: "Codex",
state: "completed",
},
{
actionId: "install_skill",
label: "REA reverse-engineering skill",
state: "started",
},
{
actionId: "install_skill",
label: "REA reverse-engineering skill",
state: "completed",
},
]);
});
it("plans and propagates BYO Ghidra without installing software", async () => {
const host = new FakeSetupHost("linux");
host.distribution = {
id: "ubuntu",
versionId: "24.04",
packageFamily: "deb",
supported: true,
};
host.ghidra = "/opt/ghidra_12.1.2_PUBLIC";
host.javaHome = "/usr/lib/jvm/jdk-21";
host.doctorHealthy = true;
host.skill = "unchanged";
host.clients = [{ name: "cursor", configPath: "/cursor.json" }];
const plan = await runSetup(options(false), host);
expect(plan.plannedActions.map(({ kind }) => kind)).toEqual([
"configure_client",
]);
expect(plan.plannedActions[0]?.detail).toContain(
"GHIDRA_INSTALL_DIR=/opt/ghidra_12.1.2_PUBLIC",
);
expect(plan.plannedActions[0]?.detail).toContain(
"JAVA_HOME=/usr/lib/jvm/jdk-21",
);
expect(host.hopperInstalls).toBe(0);
expect(host.configurations).toBe(0);
const applied = await runSetup(options(true), host);
expect(applied.status).toBe("ready");
expect(host.hopperInstalls).toBe(0);
expect(host.configuredProviderEnvironments).toContainEqual({
GHIDRA_INSTALL_DIR: "/opt/ghidra_12.1.2_PUBLIC",
JAVA_HOME: "/usr/lib/jvm/jdk-21",
});
});
it("reinstalls existing Hopper when explicitly requested", async () => {
const host = new FakeSetupHost();
host.hopper = "/Applications/Hopper";
host.skill = "unchanged";
host.clients = [{ name: "codex", configPath: "/codex.toml" }];
host.clientResults.set("codex", { status: "unchanged" });
const result = await runSetup(options(true, true), host);
expect(result.plannedActions.map(({ kind }) => kind)).toEqual([
"install_hopper",
"configure_client",
]);
expect(result.appliedActions).toEqual(["installed_hopper"]);
expect(host.hopperInstalls).toBe(1);
expect(host.hopperReplaceRequests).toEqual([true]);
expect(host.configurations).toBe(0);
expect(host.checkedHopperPaths).toEqual(["/manual/Hopper"]);
});
});

View File

@ -0,0 +1,139 @@
import { describe, expect, it } from "vitest";
import { runSetup } from "../../../src/application/Setup.js";
import { FakeSetupHost, options } from "./setupTestSupport.js";
describe("setup workflow", () => {
it("records every detected client outcome after an earlier failure", async () => {
const host = new FakeSetupHost();
host.hopper = "/Applications/Hopper";
host.clients = [
{ name: "first", configPath: "/first.json" },
{ name: "second", configPath: "/second.json" },
{ name: "third", configPath: "/third.json" },
];
host.clientResults.set("first", { status: "failed", reason: "write" });
host.clientResults.set("second", { status: "configured" });
host.clientResults.set("third", { status: "failed", reason: "readback" });
const result = await runSetup(options(true), host);
expect(host.configurations).toBe(3);
expect(result.clients).toEqual({
first: { status: "failed", reason: "write" },
second: { status: "configured" },
third: { status: "failed", reason: "readback" },
});
expect(result.appliedActions).toEqual(["configured_second"]);
expect(result.status).toBe("needs_human");
expect(result.remediation).toContain("could not be updated");
});
it("explains skill installation recovery", async () => {
const host = new FakeSetupHost();
host.hopper = "/Applications/Hopper";
host.skill = "failed";
const result = await runSetup(options(true), host);
expect(result.status).toBe("needs_human");
expect(result.remediation).toBe(
"REA analysis skill could not be installed or verified. Check permissions for `~/.agents/skills`, then rerun setup.",
);
});
it("delegates remaining unhealthy checks to doctor remediation", async () => {
const host = new FakeSetupHost();
host.hopper = "/Applications/Hopper";
host.doctorHealthy = false;
const result = await runSetup(options(true), host);
expect(result.remediation).toBe(
"Run rea doctor and apply each reported remediation.",
);
});
it("uses requested readiness scope without hiding environment drift", async () => {
const host = new FakeSetupHost();
host.hopper = "/Applications/Hopper";
host.doctorHealthy = false;
host.scopedDoctorHealthy = true;
const readinessScope = {
clients: ["codex"],
providers: [] as string[],
skill: true,
};
const result = await runSetup(
{
...options(true),
proposeHopper: false,
installSkill: false,
readinessScope,
},
host,
);
expect(result).toMatchObject({
status: "ready",
doctor: { healthy: true, environment_healthy: false },
});
expect(host.doctorScopes).toEqual(expect.arrayContaining([readinessScope]));
});
it("explains unsupported Node and macOS recovery", async () => {
const platformHost = new FakeSetupHost("win32");
expect((await runSetup(options(true), platformHost)).remediation).toBe(
"REA supports Hopper on macOS and selected 64-bit Linux distributions.",
);
const nodeHost = new FakeSetupHost();
nodeHost.nodeVersion = "20.0.0";
expect((await runSetup(options(true), nodeHost)).remediation).toBe(
"Install Node.js 22.19+ or 24.11+ and rerun setup.",
);
const macHost = new FakeSetupHost();
macHost.version = "11.7";
expect((await runSetup(options(true), macHost)).remediation).toBe(
"Upgrade to macOS 12 or newer.",
);
});
it("rejects unsupported hosts before mutation", async () => {
const host = new FakeSetupHost("linux");
host.distribution = {
id: "debian",
versionId: "13",
packageFamily: "deb",
supported: false,
};
const result = await runSetup(options(true, true), host);
expect(result.status).toBe("needs_human");
expect(host.hopperInstalls).toBe(0);
expect(result.remediation).toBe(
"Automated Hopper setup supports Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux; configure an existing supported provider instead.",
);
});
it("configures BYO Ghidra on Linux outside Hopper's installer matrix", async () => {
const host = new FakeSetupHost("linux");
host.distribution = {
id: "debian",
versionId: "13",
packageFamily: "deb",
supported: false,
};
host.ghidra = "/opt/ghidra_12.1.2_PUBLIC";
host.javaHome = "/usr/lib/jvm/jdk-21";
host.doctorHealthy = true;
host.skill = "unchanged";
host.clients = [{ name: "codex", configPath: "/codex.toml" }];
const result = await runSetup(options(true), host);
expect(result.status).toBe("ready");
expect(host.hopperInstalls).toBe(0);
expect(host.configuredProviderEnvironments).toContainEqual({
GHIDRA_INSTALL_DIR: "/opt/ghidra_12.1.2_PUBLIC",
JAVA_HOME: "/usr/lib/jvm/jdk-21",
});
});
});

View File

@ -0,0 +1,170 @@
import {
type ClientConfigurationInspection,
type ClientConfigurationResult,
type SetupClient,
type SetupHost,
type SetupOptions,
type SetupHopperInstallResult,
type SetupProviderEnvironment,
} from "../../../src/application/Setup.js";
import type {
DoctorCheck,
DoctorReport,
DoctorScope,
} from "../../../src/application/Doctor.js";
import type { LinuxDistribution } from "../../../src/application/LinuxHopper.js";
export class FakeSetupHost implements SetupHost {
readonly platform: NodeJS.Platform;
nodeVersion = "25.1.0";
version: string | undefined = "14.5";
distribution: LinuxDistribution | undefined;
hopper: string | undefined;
ghidra: string | undefined;
javaHome: string | undefined;
hopperInstallSucceeds = true;
skill: "installed" | "unchanged" | "failed" = "installed";
clients: readonly SetupClient[] = [];
clientResults = new Map<string, ClientConfigurationResult>();
clientInspections = new Map<string, ClientConfigurationInspection>();
hopperInstalls = 0;
hopperReplaceRequests: boolean[] = [];
configurations = 0;
skillInstalls = 0;
doctorCalls = 0;
doctorScopes: Array<DoctorScope | undefined> = [];
checkedHopperPaths: Array<string | undefined> = [];
configuredProviderEnvironments: SetupProviderEnvironment[] = [];
doctorHealthy: boolean | undefined;
scopedDoctorHealthy: boolean | undefined;
linuxDemoRuntimeMissing = false;
unsupportedHopperVersion = false;
constructor(platform: NodeJS.Platform = "darwin") {
this.platform = platform;
}
macosVersion = (): Promise<string | undefined> =>
Promise.resolve(this.version);
linuxDistribution = (): Promise<LinuxDistribution | undefined> =>
Promise.resolve(this.distribution);
hopperPath = (): Promise<string | undefined> => Promise.resolve(this.hopper);
providerEnvironment = (): Promise<SetupProviderEnvironment> =>
Promise.resolve({
...(this.hopper === undefined
? {}
: { HOPPER_LAUNCHER_PATH: this.hopper }),
...(this.ghidra === undefined ? {} : { GHIDRA_INSTALL_DIR: this.ghidra }),
...(this.javaHome === undefined ? {} : { JAVA_HOME: this.javaHome }),
});
installHopper = (
replaceExisting: boolean,
): Promise<SetupHopperInstallResult> => {
this.hopperInstalls += 1;
this.hopperReplaceRequests.push(replaceExisting);
this.linuxDemoRuntimeMissing = false;
if (this.hopperInstallSucceeds) {
this.hopper = "/manual/Hopper";
return Promise.resolve({
status: "installed",
launcherPath: this.hopper,
});
}
return Promise.resolve({
status: "failed",
code: "download_failed",
remediation: "Download failed.",
});
};
detectedClients = (): Promise<readonly SetupClient[]> =>
Promise.resolve(this.clients);
configureClient = (
client: SetupClient,
providerEnvironment: SetupProviderEnvironment,
): Promise<ClientConfigurationResult> => {
this.configurations += 1;
this.configuredProviderEnvironments.push(providerEnvironment);
return Promise.resolve(
this.clientResults.get(client.name) ?? { status: "configured" },
);
};
clientNeedsConfigure = (
client: SetupClient,
providerEnvironment: SetupProviderEnvironment,
): Promise<boolean> => {
this.checkedHopperPaths.push(providerEnvironment.HOPPER_LAUNCHER_PATH);
return Promise.resolve(
this.clientResults.get(client.name)?.status !== "unchanged",
);
};
inspectClientConfiguration = (
client: SetupClient,
): Promise<ClientConfigurationInspection> =>
Promise.resolve(
this.clientInspections.get(client.name) ?? { status: "update" },
);
skillNeedsInstall = (): Promise<boolean> =>
Promise.resolve(this.skill !== "unchanged");
installSkill = (): Promise<"installed" | "unchanged" | "failed"> => {
this.skillInstalls += 1;
return Promise.resolve(this.skill);
};
doctor = (scope?: DoctorScope): Promise<DoctorReport> => {
this.doctorCalls += 1;
this.doctorScopes.push(scope);
const checks: DoctorCheck[] = [
...(this.linuxDemoRuntimeMissing
? ([
{
name: "hopper-demo-runtime",
ok: false,
classification: "missing_dependency",
},
] as const)
: []),
...(this.unsupportedHopperVersion
? ([
{
name: "hopper-version",
ok: false,
classification: "config_drift",
detail: this.hopper ?? "",
},
] as const)
: []),
];
const environmentHealthy =
this.doctorHealthy ??
(this.hopper !== undefined &&
!this.linuxDemoRuntimeMissing &&
!this.unsupportedHopperVersion);
const healthy =
scope === undefined
? environmentHealthy
: (this.scopedDoctorHealthy ?? environmentHealthy);
return Promise.resolve({
healthy,
environment_healthy: environmentHealthy,
scope: {
mode: scope === undefined ? "audit-wide" : "explicit",
clients: scope?.clients ?? [],
providers: scope?.providers ?? [],
skill: scope?.skill === true,
target: null,
},
scope_checks: checks,
informational_checks: [],
...(this.hopper === undefined ? {} : { hopperPath: this.hopper }),
checks,
});
};
}
export const options = (
approved: boolean,
installHopper = false,
): SetupOptions => ({
approved,
installHopper,
structured: true,
});

View File

@ -6,7 +6,7 @@ import {
type UpgradeHost,
type UpgradeOutput,
type NpmInstallationHost,
} from "../src/application/Upgrade.js";
} from "../../../src/application/Upgrade.js";
class FakeUpgradeHost implements UpgradeHost {
latest: string | undefined = "0.6.0";

View File

@ -1,582 +0,0 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { createHash } from "node:crypto";
import { describe, expect, it } from "vitest";
import { copyFile, mkdir, readFile, realpath, rm } from "node:fs/promises";
import { join, resolve } from "node:path";
import { createTestTempDirectory } from "./fixtures/temporaryDirectory.js";
import { BinarySession } from "../src/application/BinarySession.js";
import {
JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE,
JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE,
JAVASCRIPT_FEATURE_TRACE_EXAMPLE,
JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE,
SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE,
} from "../src/contracts/javascriptApplicationWorkflowExamples.js";
import { createEvidence } from "../src/domain/evidence.js";
import { createServer } from "../src/server/createServer.js";
import { observed } from "./fixtures/analysisExecution.js";
import { PermissionAuthority } from "../src/application/PermissionAuthority.js";
import { createPermissionPolicy } from "../src/domain/permissionPolicy.js";
import { controlledReplayOutputSchema } from "../src/domain/javascriptReplay.js";
import { nodeCharacterizationPreparationOutputSchema } from "../src/domain/nodeRuntimeCharacterization.js";
import { analyzeJavaScriptApplication } from "../src/application/JavaScriptApplicationService.js";
import { javascriptApplicationAnalysisResultSchema } from "../src/domain/javascriptApplicationAnalysis.js";
import { permissionAuthorityForRoot } from "./fixtures/permissionAuthority.js";
describe("application workflow MCP parity", () => {
it("compares exact parser export shapes through full Evidence and session IDs", async () => {
const root = await createTestTempDirectory("rea-export-shape-mcp-");
const leftRoot = join(root, "left");
const rightRoot = join(root, "right");
await Promise.all([mkdir(leftRoot), mkdir(rightRoot)]);
await Promise.all([
copyFile(
resolve("tests/fixtures/replay/parser.mjs"),
join(leftRoot, "parser.mjs"),
),
copyFile(
resolve("tests/fixtures/replay/parser-v2.mjs"),
join(rightRoot, "parser.mjs"),
),
]);
const authority = await permissionAuthorityForRoot(
root,
["investigation_input"],
["investigation_input"],
);
const [left, right] = await Promise.all([
analyzeJavaScriptApplication(authority, {
input_path: leftRoot,
approved: true,
}),
analyzeJavaScriptApplication(authority, {
input_path: rightRoot,
approved: true,
}),
]);
if (!left.ok) throw left.error;
if (!right.ok) throw right.error;
const session = new BinarySession(() => ({
execute: () => Promise.resolve(observed(null)),
close: () => Promise.resolve(),
}));
const server = createServer(session, session);
const client = new Client({ name: "export-shape-test", version: "1" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
const selectors = {
left_module_path: "parser.mjs",
left_export_name: "default",
right_module_path: "parser.mjs",
right_export_name: "default",
};
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const full = await client.callTool({
name: "compare_javascript_export_shapes",
arguments: { left: left.value, right: right.value, ...selectors },
});
expect(full.isError).not.toBe(true);
expect(full.structuredContent).toMatchObject({
result: {
summary: { added: 1, removed: 0, changed: 0, unknown: 0 },
changes: [
{
status: "added",
path: "/depth",
right: { availability: "literal", value: 1 },
},
],
},
});
expect(session.exportEvidenceBundle().records).toEqual(
expect.arrayContaining([
expect.objectContaining({
operation: "compare_javascript_export_shapes",
predicate_type: "rea.javascript-export-shape-comparison/v1",
}),
]),
);
const byId = await client.callTool({
name: "compare_javascript_export_shapes",
arguments: {
left_evidence_id: left.value.evidence_id,
right_evidence_id: right.value.evidence_id,
...selectors,
},
});
expect(byId.isError).not.toBe(true);
expect(byId.structuredContent).toMatchObject({
result: {
evidence_links: [left.value.evidence_id, right.value.evidence_id],
changes: [expect.objectContaining({ path: "/depth" })],
},
});
const analyzed = javascriptApplicationAnalysisResultSchema.parse(
left.value.normalized_result,
);
if (analyzed.schema_version !== 2)
throw new TypeError("Expected semantic application Evidence");
const seed = analyzed.semantic_graph.relations[0]?.source_node_id;
if (seed === undefined)
throw new TypeError("Expected at least one semantic relation");
const semantic = await client.callTool({
name: "trace_javascript_semantics",
arguments: {
application: left.value,
query: {
seed: { kind: "semantic-node", node_id: seed },
direction: "forward-influence",
include_ambiguous_dynamic_edges: true,
},
},
});
expect(semantic.isError).not.toBe(true);
expect(semantic.structuredContent).toMatchObject({
result: {
schema_version: 1,
source_evidence_id: left.value.evidence_id,
source_graph_id: analyzed.semantic_graph.graph_id,
summary: { retained_seed_matches: 1 },
},
});
} finally {
await client.close();
await server.close();
await session.close();
await rm(root, { recursive: true, force: true });
}
});
it("traces and compares authenticated graph Evidence in the session", async () => {
const session = new BinarySession(() => ({
execute: () => Promise.resolve(observed(null)),
close: () => Promise.resolve(),
}));
const server = createServer(session, session);
const client = new Client({
name: "application-workflow-test",
version: "1",
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
let resourceListChanges = 0;
client.setNotificationHandler(
"notifications/resources/list_changed",
() => {
resourceListChanges += 1;
},
);
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const traced = await client.callTool({
name: "trace_application_feature",
arguments: JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE,
});
expect(traced.isError).not.toBe(true);
expect(traced.structuredContent).toMatchObject({
result: {
schema_version: 1,
coverage: { status: expect.any(String) },
},
});
const compared = await client.callTool({
name: "compare_application_versions",
arguments: {
...JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE,
unknown_registry_approved: true,
},
});
expect(compared.isError).not.toBe(true);
expect(compared.structuredContent).toMatchObject({
result: {
schema_version: 1,
summary: { unknown: expect.any(Number) },
coverage: { status: expect.any(String) },
},
});
const sourceCompared = await client.callTool({
name: "compare_source_to_bundle",
arguments: {
reference: SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE.reference,
application:
JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE.application,
},
});
expect(sourceCompared.isError).not.toBe(true);
expect(sourceCompared.structuredContent).toMatchObject({
result: {
schema_version: 1,
reference: {
root_sha256:
SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE.reference.root_sha256,
},
scoring: { algorithm: "rea-source-to-bundle-signals/v1" },
},
});
expect(session.exportEvidenceBundle().records.length).toBeGreaterThan(2);
await new Promise<void>((resolve) => setImmediate(resolve));
const notificationsBeforeIdReuse = resourceListChanges;
const tracedById = await client.callTool({
name: "trace_application_feature",
arguments: {
...JAVASCRIPT_FEATURE_TRACE_EXAMPLE,
application_evidence_id:
JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE.application
.evidence_id,
},
});
expect(tracedById.isError).not.toBe(true);
await new Promise<void>((resolve) => setImmediate(resolve));
expect(resourceListChanges).toBe(notificationsBeforeIdReuse);
const comparedById = await client.callTool({
name: "compare_application_versions",
arguments: {
...JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE,
left_evidence_id:
JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE.left
.evidence_id,
right_evidence_id:
JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE.right
.evidence_id,
},
});
expect(comparedById).toMatchObject({
structuredContent: {
result: {
evidence_links: expect.arrayContaining([
JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE.left
.evidence_id,
JAVASCRIPT_VERSION_COMPARISON_FULL_EVIDENCE_EXAMPLE.right
.evidence_id,
]),
},
},
});
const sourceComparedById = await client.callTool({
name: "compare_source_to_bundle",
arguments: {
...SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE,
application_evidence_id:
JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE.application
.evidence_id,
},
});
expect(sourceComparedById.isError).not.toBe(true);
const missing = await client.callTool({
name: "trace_application_feature",
arguments: JAVASCRIPT_FEATURE_TRACE_EXAMPLE,
});
expect(missing).toMatchObject({
isError: true,
structuredContent: {
error: { details: { reason: "missing" } },
},
});
const wrongOperation = createEvidence(
undefined,
{ id: "fixture", name: "Fixture", version: "1" },
{ operation: "inventory_artifact", parameters: {}, result: {} },
);
const wrongPredicate = createEvidence(
undefined,
{ id: "fixture", name: "Fixture", version: "1" },
{
predicateType: "fixture.application/v1",
operation: "analyze_javascript_application",
parameters: {},
result: {},
},
);
expect(session.recordEvidence(wrongOperation).ok).toBe(true);
expect(session.recordEvidence(wrongPredicate).ok).toBe(true);
for (const [record, reason] of [
[wrongOperation, "wrong_operation"],
[wrongPredicate, "wrong_predicate"],
] as const) {
const rejected = await client.callTool({
name: "trace_application_feature",
arguments: {
...JAVASCRIPT_FEATURE_TRACE_EXAMPLE,
application_evidence_id: record.evidence_id,
},
});
expect(rejected).toMatchObject({
isError: true,
structuredContent: { error: { details: { reason } } },
});
}
const duplicateNative = await client.callTool({
name: "trace_application_feature",
arguments: {
...JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE,
native_observations: [
JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE.application,
],
native_observation_evidence_ids: [
JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE.application
.evidence_id,
],
},
});
expect(duplicateNative.isError).toBe(true);
const spoofed = await client.callTool({
name: "trace_application_feature",
arguments: {
...JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE,
application: {
...JAVASCRIPT_FEATURE_TRACE_FULL_EVIDENCE_EXAMPLE.application,
provider: { id: "spoofed", name: "spoofed", version: "1" },
},
},
});
expect(spoofed.isError).toBe(true);
} finally {
await client.close();
await server.close();
await session.close();
}
});
it("plans and executes the same controlled replay contract", async () => {
const root = resolve("tests/fixtures/replay");
const policy = {
enabled: true,
roots: [root],
nodePath: process.execPath,
bubblewrapPath: process.execPath,
systemdRunPath: process.execPath,
systemctlPath: process.execPath,
shellPath: process.execPath,
};
const ceiling = {
capability: "javascript_replay" as const,
roots: [root],
executables: [
policy.nodePath,
policy.bubblewrapPath,
policy.systemdRunPath,
policy.systemctlPath,
policy.shellPath,
],
environment_names: [],
network: "none" as const,
mount: true,
};
const authority = new PermissionAuthority(
createPermissionPolicy(
[ceiling],
[
{
...ceiling,
grant_id: "administrator:javascript_replay",
lifetime: "administrator",
operation_identity: null,
expires_at: null,
},
],
),
);
const session = new BinarySession(() => ({
execute: () => Promise.resolve(observed(null)),
close: () => Promise.resolve(),
}));
const server = createServer(session, session, {
permissionAuthority: authority,
javascriptReplayPolicy: policy,
javascriptReplayHost: {
readSource: async (path) => ({
canonicalPath: await realpath(path),
bytes: await readFile(path),
}),
identifyExecutable: async (path) => ({
path,
version: "fixture",
sha256: "1".repeat(64),
}),
identifyWorker: async () => ({
path: "/fixture/worker.js",
version: "fixture-worker-1",
sha256: "2".repeat(64),
}),
identifyRuntimeClosure: async (path) => [
{
sourcePath: path,
destinationPath: "/runtime/node",
sha256: "1".repeat(64),
},
],
seccompDigest: () => "3".repeat(64),
probe: async () => undefined,
},
javascriptReplayRunner: {
execute: async (prepared) => ({
schema_version: 1,
plan_digest: prepared.publicPlan.plan_digest,
outcomes: [
{
case_id: "heading",
outcome: "return",
value: { type: "heading", text: "Title" },
input_sha256:
prepared.publicPlan.cases[0]?.sha256 ?? "0".repeat(64),
output_sha256: "2".repeat(64),
truncated: false,
},
],
stderr: "",
termination: "completed",
cleanup: { state: "complete", residual_resources: [] },
limitations: ["fixture runner"],
reproducer: null,
}),
},
});
const client = new Client({ name: "replay-parity-test", version: "1" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
const input = {
mode: "plan",
left: {
modules: [
{
alias: "parser",
path: resolve(root, "parser.mjs"),
format: "esm",
role: "module",
dependencies: {},
},
],
entry_alias: "parser",
entry_export: "default",
},
cases: [{ case_id: "heading", arguments: ["# Title"] }],
approved: false,
};
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const planned = await client.callTool({
name: "run_controlled_replay",
arguments: input,
});
expect(planned.isError).not.toBe(true);
const digest = controlledReplayOutputSchema.parse(
planned.structuredContent,
).plan?.plan_digest;
expect(digest).toMatch(/^[a-f0-9]{64}$/u);
const executed = await client.callTool({
name: "run_controlled_replay",
arguments: {
...input,
mode: "execute",
approved: true,
plan_digest: digest,
},
});
expect(executed.isError).not.toBe(true);
expect(executed.structuredContent).toMatchObject({
phase: "execute",
evidence: {
authority: "controlled-replay",
provider: { id: "rea-javascript-replay" },
},
});
expect(session.exportEvidenceBundle().records).toEqual(
expect.arrayContaining([
expect.objectContaining({ authority: "controlled-replay" }),
]),
);
const factoryPath = resolve(root, "sanitizer.factory.txt");
const factoryBytes = await readFile(factoryPath);
const factorySha256 = createHash("sha256")
.update(factoryBytes)
.digest("hex");
const characterizationInput = {
preparation_approved: true,
selected_alias: "bundle",
expected_effect: "pure",
instrumentation: {
artifact_path: factoryPath,
artifact_sha256: factorySha256,
selection: {
byte_start: 0,
byte_end: factoryBytes.byteLength,
selected_sha256: factorySha256,
export_name: "selected",
},
},
replay: {
mode: "plan",
left: {
modules: [
{
alias: "bundle",
path: factoryPath,
format: "commonjs-factory",
role: "module",
dependencies: {},
},
],
entry_alias: "bundle",
entry_export: "selected",
},
cases: [{ case_id: "heading", arguments: ["# Title"] }],
approved: false,
},
};
const characterizationPlan = await client.callTool({
name: "prepare_node_characterization",
arguments: characterizationInput,
});
expect(characterizationPlan.isError).not.toBe(true);
const approvedPlan = nodeCharacterizationPreparationOutputSchema.parse(
characterizationPlan.structuredContent,
).plan;
const characterized = await client.callTool({
name: "execute_node_characterization",
arguments: {
execution_approved: true,
approved_plan_sha256: approvedPlan.plan_sha256,
preparation: characterizationInput,
},
});
expect(characterized.isError).not.toBe(true);
expect(characterized.structuredContent).toMatchObject({
phase: "execution",
evidence: {
authority: "controlled-replay",
provider: { id: "rea-node-characterization" },
},
});
expect(session.exportEvidenceBundle().records).toEqual(
expect.arrayContaining([
expect.objectContaining({
predicate_type: "rea.javascript-export-transformation/v1",
}),
expect.objectContaining({
predicate_type: "rea.runtime-characterization/v1",
}),
]),
);
} finally {
await client.close();
await server.close();
await session.close();
}
});
});

View File

@ -1,479 +0,0 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { createPackageWithOptions } from "@electron/asar";
import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js";
import { describe, expect, it } from "vitest";
import { z } from "zod";
import { createTestTempDirectory } from "./fixtures/temporaryDirectory.js";
import { BinarySession } from "../src/application/BinarySession.js";
import { ArtifactProvider } from "../src/artifacts/ArtifactProvider.js";
import { ARTIFACT_COMPARISON_EXAMPLE } from "../src/contracts/artifactComparisonExample.js";
import { evidenceBundleSchema } from "../src/domain/evidenceBundle.js";
import { createServer } from "../src/server/createServer.js";
import { observed } from "./fixtures/analysisExecution.js";
describe("artifact graph MCP integration", () => {
it.each([
{ unpacked: false, label: "embedded" },
{ unpacked: true, label: "unpacked" },
])(
"returns actionable local details for an $label ASAR integrity error",
async ({ unpacked }) => {
const root = await createTestTempDirectory("rea-asar-integrity-mcp-");
const source = join(root, "source");
const original = "console.log('ok');\n";
const changed = "console.log('no');\n";
await mkdir(source);
await writeFile(join(source, "main.js"), original);
const archive = join(root, "fixture.asar");
await createPackageWithOptions(
source,
archive,
unpacked ? { unpack: "*.js" } : {},
);
if (unpacked) {
await writeFile(join(`${archive}.unpacked`, "main.js"), changed);
} else {
const bytes = await readFile(archive);
const contentOffset = bytes.indexOf(original);
expect(contentOffset).toBeGreaterThanOrEqual(0);
bytes.write(changed, contentOffset, "utf8");
await writeFile(archive, bytes);
}
const session = new BinarySession(new ArtifactProvider());
const server = createServer(session, session);
const client = new Client({ name: "asar-integrity-test", version: "1" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const opened = await client.callTool({
name: "open_binary",
arguments: { path: archive },
});
expect(opened.isError).not.toBe(true);
const result = await client.callTool({
name: "inventory_artifact",
arguments: {},
});
expect(result.isError).toBe(true);
expect(result.structuredContent).toEqual({
error: {
code: "artifact_integrity_mismatch",
category: "integrity_mismatch",
message:
"Artifact is invalid or has changed. Get a fresh copy and try again.",
details: {
logical_path: "main.js",
declared_sha256: expect.stringMatching(/^[a-f0-9]{64}$/u),
calculated_sha256: expect.stringMatching(/^[a-f0-9]{64}$/u),
unpacked,
},
retryable: false,
remediation: {
action:
"Artifact is invalid or has changed. Get a fresh copy and try again.",
restart_required: false,
},
},
});
} finally {
await Promise.allSettled([
client.close(),
server.close(),
session.close(),
]);
}
},
);
it("records an approved mismatch, preserves verified siblings, and never reports equivalence", async () => {
const root = await createTestTempDirectory("rea-asar-continue-mcp-");
const source = join(root, "source");
await mkdir(source);
const original = "console.log('ok');\n";
const changed = "console.log('no');\n";
const secondOriginal = "console.log('up');\n";
const secondChanged = "console.log('dn');\n";
await writeFile(join(source, "main.js"), original);
await writeFile(join(source, "second.js"), secondOriginal);
await writeFile(join(source, "sibling.js"), "console.log('sibling');\n");
const archive = join(root, "fixture.asar");
await createPackageWithOptions(source, archive, {});
const bytes = await readFile(archive);
const contentOffset = bytes.indexOf(original);
const secondOffset = bytes.indexOf(secondOriginal);
expect(contentOffset).toBeGreaterThanOrEqual(0);
expect(secondOffset).toBeGreaterThanOrEqual(0);
bytes.write(changed, contentOffset, "utf8");
bytes.write(secondChanged, secondOffset, "utf8");
await writeFile(archive, bytes);
const session = new BinarySession(new ArtifactProvider(false, true));
const server = createServer(session, session);
const client = new Client({ name: "asar-continue-test", version: "1" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
await client.callTool({
name: "open_binary",
arguments: { path: archive },
});
const limited = await client.callTool({
name: "inventory_artifact",
arguments: {
integrity_policy: "record-and-continue",
integrity_continue_approved: true,
max_integrity_mismatches: 1,
},
});
expect(limited).toMatchObject({
isError: true,
structuredContent: { error: { code: "truncated" } },
});
const result = await client.callTool({
name: "inventory_artifact",
arguments: {
integrity_policy: "record-and-continue",
integrity_continue_approved: true,
},
});
expect(result.isError).not.toBe(true);
const compact = compactResult(result.structuredContent);
const evidence = session.evidenceById(compact.evidence_id);
if (evidence === undefined) throw new Error("missing inventory Evidence");
const inventory = z
.object({
occurrences: z.object({
items: z.array(
z.object({ logical_path: z.string(), hash_status: z.string() }),
),
}),
integrity_contradictions: z.array(
z.object({
logical_path: z.string(),
declared_sha256: z.string(),
observed_sha256: z.string(),
trust: z.literal("observed-untrusted"),
}),
),
})
.parse(compact.result);
expect(inventory.integrity_contradictions).toEqual(
expect.arrayContaining([
expect.objectContaining({
logical_path: "main.js",
trust: "observed-untrusted",
declared_sha256: expect.stringMatching(/^[a-f0-9]{64}$/u),
observed_sha256: expect.stringMatching(/^[a-f0-9]{64}$/u),
}),
expect.objectContaining({ logical_path: "second.js" }),
]),
);
expect(inventory.integrity_contradictions).toHaveLength(2);
expect(inventory.occurrences.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
logical_path: "main.js",
hash_status: "mismatched",
}),
expect.objectContaining({
logical_path: "sibling.js",
hash_status: "verified",
}),
expect.objectContaining({
logical_path: "second.js",
hash_status: "mismatched",
}),
]),
);
const inspected = await client.callTool({
name: "inspect_artifact",
arguments: {
integrity_policy: "record-and-continue",
integrity_continue_approved: true,
},
});
expect(inspected.isError).not.toBe(true);
expect(compactResult(inspected.structuredContent).result).toMatchObject({
contradictions: [
expect.objectContaining({ occurrence_id: expect.any(String) }),
expect.objectContaining({ occurrence_id: expect.any(String) }),
],
substeps: [
expect.objectContaining({
operation: "inventory_artifact",
status: "completed",
}),
],
});
const compared = await client.callTool({
name: "compare_artifacts",
arguments: {
left_evidence_ids: [evidence.evidence_id],
right_evidence_ids: [evidence.evidence_id],
},
});
expect(compared.isError).not.toBe(true);
expect(compactResult(compared.structuredContent).result).toMatchObject({
status: "contradiction",
summary: { contradiction: 2 },
changes: {
items: expect.arrayContaining([
expect.objectContaining({
logical_path: "main.js",
classification: "contradiction",
dimensions: ["integrity"],
}),
expect.objectContaining({
logical_path: "second.js",
classification: "contradiction",
}),
]),
},
});
} finally {
await Promise.allSettled([
client.close(),
server.close(),
session.close(),
]);
}
});
it("rejects altered payloads that reuse session Evidence IDs", async () => {
const session = new BinarySession(() => ({
health: () => Promise.resolve(),
execute: () => Promise.resolve(observed(null)),
close: () => Promise.resolve(),
}));
session.recordEvidence(ARTIFACT_COMPARISON_EXAMPLE.left);
session.recordEvidence(ARTIFACT_COMPARISON_EXAMPLE.right);
const server = createServer(session, session);
const client = new Client({
name: "artifact-authority-test",
version: "1",
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const result = await client.callTool({
name: "compare_artifacts",
arguments: {
...ARTIFACT_COMPARISON_EXAMPLE,
left: {
...ARTIFACT_COMPARISON_EXAMPLE.left,
limitations: ["caller altered this record"],
},
},
});
expect(result.isError).toBe(true);
expect(session.exportEvidenceBundle().records).toHaveLength(2);
} finally {
await Promise.allSettled([
client.close(),
server.close(),
session.close(),
]);
}
});
it("rejects comparison Evidence that is not owned by the session", async () => {
const session = new BinarySession(() => ({
health: () => Promise.resolve(),
execute: () => Promise.resolve(observed(null)),
close: () => Promise.resolve(),
}));
const server = createServer(session, session);
const client = new Client({
name: "artifact-ownership-test",
version: "1",
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const result = await client.callTool({
name: "compare_artifacts",
arguments: ARTIFACT_COMPARISON_EXAMPLE,
});
expect(result.isError).toBe(true);
expect(session.exportEvidenceBundle().records).toEqual([]);
} finally {
await Promise.allSettled([
client.close(),
server.close(),
session.close(),
]);
}
});
it("opens archives without Hopper, compares them, and exports linked evidence", async () => {
const directory = await createTestTempDirectory("rea-artifact-mcp-");
const archive = join(directory, "fixture.ipa");
const changedArchive = join(directory, "changed.ipa");
const writer = new ZipWriter(new Uint8ArrayWriter());
await writer.add("Payload/Fixture.app/main.js", new TextReader("main();"));
await writeFile(archive, await writer.close());
const changedWriter = new ZipWriter(new Uint8ArrayWriter());
await changedWriter.add(
"Payload/Fixture.app/main.js",
new TextReader("changed();"),
);
await writeFile(changedArchive, await changedWriter.close());
const session = new BinarySession(new ArtifactProvider());
const server = createServer(session, session);
const client = new Client({ name: "artifact-mcp-test", version: "1" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const opened = await client.callTool({
name: "open_binary",
arguments: { path: archive },
});
expect(opened.isError).not.toBe(true);
const inspected = await client.callTool({
name: "inspect_artifact",
arguments: { max_observations: 1, max_relationships: 1 },
});
expect(inspected.isError).not.toBe(true);
const inspectionResult = compactResult(inspected.structuredContent);
const inspectionEvidence = session.evidenceById(
inspectionResult.evidence_id,
);
if (inspectionEvidence === undefined)
throw new Error("missing inspection Evidence");
const inspection = z
.object({
substeps: z.array(
z.object({
evidence_id: z.string(),
status: z.literal("completed"),
}),
),
coverage: z.object({
status: z.literal("truncated"),
substeps_completed: z.literal(1),
}),
unexplored_branches: z.array(z.object({ reason: z.string() })),
})
.parse(inspectionResult.result);
expect(inspection.substeps).toHaveLength(1);
expect(
session.evidenceById(inspection.substeps[0]?.evidence_id ?? ""),
).toMatchObject({ operation: "inventory_artifact" });
expect(inspectionEvidence.evidence_links).toEqual([
inspection.substeps[0]?.evidence_id,
]);
expect(inspection.unexplored_branches).toEqual(
expect.arrayContaining([
expect.objectContaining({ reason: "page-limit" }),
]),
);
const inventory = await client.callTool({
name: "inventory_artifact",
arguments: {},
});
expect(inventory.isError).not.toBe(true);
const inventoryResult = compactResult(inventory.structuredContent);
const evidence = session.evidenceById(inventoryResult.evidence_id);
if (evidence === undefined) throw new Error("missing inventory Evidence");
expect(evidence).toMatchObject({
provider: { id: "rea-artifact-graph" },
subject: { format: "ipa" },
});
expect(inventoryResult.result).toMatchObject({
manifest: { root_format: "ipa" },
});
const openedChanged = await client.callTool({
name: "open_binary",
arguments: { path: changedArchive },
});
expect(openedChanged.isError).not.toBe(true);
const changedInventory = await client.callTool({
name: "inventory_artifact",
arguments: {},
});
const changedResult = compactResult(changedInventory.structuredContent);
const changedEvidence = session.evidenceById(changedResult.evidence_id);
if (changedEvidence === undefined)
throw new Error("missing changed Evidence");
const compared = await client.callTool({
name: "compare_artifacts",
arguments: {
left_evidence_ids: [evidence.evidence_id],
right_evidence_ids: [changedEvidence.evidence_id],
unknown_registry_approved: true,
},
});
expect(compared.isError).not.toBe(true);
expect(compactResult(compared.structuredContent)).toMatchObject({
result: { status: "changed" },
evidence_id: expect.stringMatching(/^ev_/u),
});
const unknowns = await client.callTool({
name: "list_unknowns",
arguments: { domain: "artifact-comparison" },
});
expect(unknowns.structuredContent).toMatchObject({
result: {
items: [
expect.objectContaining({
unknown: expect.objectContaining({
domain: "artifact-comparison",
}),
}),
],
},
});
const snapshotted = await client.callTool({
name: "snapshot_evidence_bundle",
arguments: {},
});
const bundleUri = z
.object({ result: z.object({ bundle_uri: z.string() }) })
.parse(snapshotted.structuredContent).result.bundle_uri;
const bundleResource = await client.readResource({ uri: bundleUri });
const bundle = evidenceBundleSchema.parse(
JSON.parse(
z.object({ text: z.string() }).parse(bundleResource.contents[0]).text,
),
);
expect(bundle.records).toHaveLength(6);
expect(bundle.artifacts).toContainEqual({
digest: { sha256: evidence.subject?.digest.sha256 },
format: "ipa",
architecture: null,
});
} finally {
await Promise.allSettled([
client.close(),
server.close(),
session.close(),
]);
}
});
});
const compactResult = (value: unknown) =>
z
.object({
result: z.unknown(),
evidence_id: z.string(),
evidence_uri: z.string(),
})
.parse(value);

View File

@ -1,523 +0,0 @@
import {
access,
mkdir,
readFile,
readdir,
symlink,
writeFile,
} from "node:fs/promises";
import { join } from "node:path";
import { createPackage, createPackageWithOptions } from "@electron/asar";
import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js";
import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "./fixtures/temporaryDirectory.js";
import { ArtifactProvider } from "../src/artifacts/ArtifactProvider.js";
import { MachOSliceArtifactReader } from "../src/artifacts/MachOSliceArtifactReader.js";
import { runProviderAnalysis } from "../src/application/DirectAnalysis.js";
import {
ArtifactPathRegistry,
normalizeArtifactPath,
} from "../src/artifacts/ArtifactPaths.js";
import {
artifactExtractionInputSchema,
artifactInspectionInputSchema,
artifactInventoryInputSchema,
} from "../src/contracts/artifactToolContracts.js";
import type { BinaryTarget } from "../src/domain/binaryTarget.js";
import { parseBinaryTarget } from "../src/domain/binaryTarget.js";
import { parseEvidence } from "../src/domain/evidence.js";
import { ok } from "../src/domain/result.js";
import type { NativeCommandRunner } from "../src/native/CommandRunner.js";
import {
artifactExtractionResultSchema,
artifactInventoryResultSchema,
} from "../src/domain/artifactGraph.js";
import { artifactInspectionResultSchema } from "../src/domain/artifactInspection.js";
describe("artifact graph provider", () => {
it("inventories app trees deterministically without following symlinks", async () => {
const root = await createTestTempDirectory("rea-artifacts-");
const app = join(root, "Fixture.app");
await mkdir(join(app, "Contents", "Resources"), { recursive: true });
await mkdir(join(app, "Contents", "MacOS"), { recursive: true });
await mkdir(join(app, "Contents", "Frameworks", "Fixture.framework"), {
recursive: true,
});
const machO = Buffer.from([0xcf, 0xfa, 0xed, 0xfe, 0x0c, 0, 0, 1]);
await writeFile(join(app, "Contents", "MacOS", "Fixture"), machO, {
mode: 0o755,
});
await writeFile(
join(app, "Contents", "Frameworks", "Fixture.framework", "Fixture"),
machO,
);
await writeFile(join(app, "Contents", "Resources", "main.js"), "main();\n");
await writeFile(join(app, "Contents", "Resources", "main.js.MAP"), "{}\n");
await symlink("/etc/passwd", join(app, "Contents", "Resources", "outside"));
const client = new ArtifactProvider().createClient(
target(app, "directory"),
);
const input = artifactInventoryInputSchema.parse({
node_limit: 500,
occurrence_limit: 500,
edge_limit: 500,
});
const first = await client.execute("inventory_artifact", input);
const second = await client.execute("inventory_artifact", input);
expect(first.ok).toBe(true);
expect(second.ok).toBe(true);
if (!first.ok || !second.ok) return;
const left = artifactInventoryResultSchema.parse(first.value.result);
const right = artifactInventoryResultSchema.parse(second.value.result);
expect(left.manifest).toEqual(right.manifest);
expect(left.nodes.items.map(({ kind }) => kind)).toEqual(
expect.arrayContaining([
"executable",
"framework",
"javascript",
"source-map",
]),
);
expect(
left.occurrences.items.find(({ logical_path: path }) =>
path.endsWith("outside"),
),
).toMatchObject({ artifact_id: null, entry_kind: "symlink" });
const script = left.occurrences.items.find(({ logical_path: path }) =>
path.endsWith("main.js"),
);
const sourceMap = left.occurrences.items.find(({ logical_path: path }) =>
path.endsWith("main.js.MAP"),
);
expect(left.edges.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
relation: "maps-source",
parent_artifact_id: script?.artifact_id,
child_artifact_id: sourceMap?.artifact_id,
}),
]),
);
});
it("streams ZIP and official ASAR inventories within shared limits", async () => {
const root = await createTestTempDirectory("rea-containers-");
const zipPath = join(root, "fixture.apk");
const zipWriter = new ZipWriter(new Uint8ArrayWriter());
await zipWriter.add("assets/index.js", new TextReader("export default 1;"));
await zipWriter.add("lib/arm64-v8a/addon.so", new TextReader("native"));
await writeFile(zipPath, await zipWriter.close());
const parsed = await parseBinaryTarget(zipPath);
expect(parsed.ok && parsed.value.format).toBe("apk");
if (!parsed.ok) return;
const zipResult = await inventory(parsed.value);
expect(zipResult.occurrences.total).toBe(3);
expect(zipResult.nodes.items.map(({ kind }) => kind)).toEqual(
expect.arrayContaining(["javascript", "dynamic-library"]),
);
const cliEvidence = parseEvidence(
await runProviderAnalysis(zipPath, "inventory_artifact", {}),
);
expect(cliEvidence).toMatchObject({
operation: "inventory_artifact",
provider: { id: "rea-artifact-graph" },
subject: { format: "apk" },
});
const inspectionEvidence = parseEvidence(
await runProviderAnalysis(zipPath, "inspect_artifact", {
max_observations: 2,
max_relationships: 1,
}),
);
const inspection = artifactInspectionResultSchema.parse(
inspectionEvidence.normalized_result,
);
expect(inspection).toMatchObject({
substeps: [
{
operation: "inventory_artifact",
status: "completed",
evidence_id: expect.stringMatching(/^ev_[a-f0-9]{64}$/u),
},
],
coverage: { status: "truncated", substeps_completed: 1 },
});
expect(inspectionEvidence.evidence_links).toEqual(
inspection.evidence_links,
);
const cancellation = new AbortController();
cancellation.abort();
await expect(
runProviderAnalysis(
zipPath,
"inventory_artifact",
{},
undefined,
cancellation.signal,
),
).resolves.toMatchObject({ code: "cancelled" });
await expect(
runProviderAnalysis(
zipPath,
"inspect_artifact",
{},
undefined,
cancellation.signal,
),
).resolves.toMatchObject({ code: "cancelled" });
const source = join(root, "asar-source");
await mkdir(source);
await writeFile(join(source, "main.js"), "console.log('ok');\n");
const asarPath = join(root, "app.asar");
await createPackage(source, asarPath);
const asarResult = await inventory(target(asarPath, "asar"));
expect(asarResult.occurrences.items).toEqual(
expect.arrayContaining([
expect.objectContaining({ logical_path: "main.js" }),
]),
);
const unpackedPath = join(root, "unpacked.asar");
await createPackageWithOptions(source, unpackedPath, { unpack: "*.js" });
const unpackedResult = await inventory(target(unpackedPath, "asar"));
expect(unpackedResult.occurrences.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
logical_path: "main.js",
hash_status: "verified",
}),
]),
);
await writeFile(
join(`${unpackedPath}.unpacked`, "main.js"),
"changed();\n",
);
const corrupted = await new ArtifactProvider()
.createClient(target(unpackedPath, "asar"))
.execute("inventory_artifact", artifactInventoryInputSchema.parse({}));
expect(corrupted).toMatchObject({
ok: false,
error: {
_tag: "ArtifactOperationError",
reason: "integrity",
artifactDetails: {
logicalPath: "main.js",
unpacked: true,
},
},
});
const failedInspection = await new ArtifactProvider()
.createClient(target(unpackedPath, "asar"))
.execute("inspect_artifact", artifactInspectionInputSchema.parse({}));
expect(failedInspection).toMatchObject({
ok: false,
error: { _tag: "ArtifactOperationError", reason: "integrity" },
});
});
it.each([
["fixture.msix", "msix"],
["fixture.appxbundle", "appx"],
] as const)(
"reuses the hardened ZIP reader for %s package inventory",
async (name, format) => {
const root = await createTestTempDirectory("rea-windows-package-");
const path = join(root, name);
const writer = new ZipWriter(new Uint8ArrayWriter());
await writer.add(
"Assets/main.js",
new TextReader("export default 'windows';"),
);
await writer.add(
"VFS/ProgramFilesX64/App/addon.node",
new TextReader("native"),
);
await writeFile(path, await writer.close());
const parsed = await parseBinaryTarget(path);
expect(parsed.ok && parsed.value).toMatchObject({
kind: "archive",
format,
});
if (!parsed.ok) return;
const result = await inventory(parsed.value);
expect(result.manifest.root_format).toBe(format);
expect(result.nodes.items.map(({ kind }) => kind)).toEqual(
expect.arrayContaining(["javascript", "native-addon"]),
);
const selected = result.occurrences.items.find(
({ logical_path: logicalPath }) => logicalPath === "Assets/main.js",
);
expect(selected).toBeDefined();
if (selected === undefined) return;
const output = join(root, "output");
const extracted = await new ArtifactProvider()
.createClient(parsed.value)
.execute(
"extract_artifact",
artifactExtractionInputSchema.parse({
approved: true,
output_root: output,
occurrence_ids: [selected.occurrence_id],
}),
);
expect(extracted.ok).toBe(true);
expect(await readFile(join(output, "Assets", "main.js"), "utf8")).toBe(
"export default 'windows';",
);
expect(
parseEvidence(
await runProviderAnalysis(path, "inventory_artifact", {
node_limit: 10,
occurrence_limit: 10,
edge_limit: 10,
}),
),
).toMatchObject({
operation: "inventory_artifact",
subject: { format },
});
},
);
it("rejects unsafe, colliding, and over-ratio entries", async () => {
expect(() => normalizeArtifactPath("../escape", limits())).toThrow(
/normalized/u,
);
const registry = new ArtifactPathRegistry();
registry.add("A.js", "file");
expect(() => registry.add("a.js", "file")).toThrow(/collision/u);
const root = await createTestTempDirectory("rea-bomb-");
const zipPath = join(root, "bomb.zip");
const writer = new ZipWriter(new Uint8ArrayWriter());
await writer.add("zeros", new TextReader("0".repeat(100_000)));
await writeFile(zipPath, await writer.close());
const client = new ArtifactProvider().createClient(target(zipPath, "zip"));
const result = await client.execute(
"inventory_artifact",
artifactInventoryInputSchema.parse({ max_compression_ratio: 2 }),
);
expect(result).toMatchObject({
ok: false,
error: { _tag: "ArtifactOperationError", reason: "limit" },
});
});
it("extracts only selected occurrences through an exclusively owned output tree", async () => {
const root = await createTestTempDirectory("rea-extract-");
const source = join(root, "source");
await mkdir(join(source, "assets"), { recursive: true });
await writeFile(join(source, "assets", "selected.js"), "selected();\n");
await writeFile(join(source, "assets", "ignored.js"), "ignored();\n");
const targetValue = target(source, "directory");
const graph = await inventory(targetValue);
const selected = graph.occurrences.items.find(
({ logical_path: path }) => path === "assets/selected.js",
);
expect(selected).toBeDefined();
if (selected === undefined) return;
const cancelledOutput = join(root, "cancelled-output");
const controller = new AbortController();
controller.abort();
const cancelled = await new ArtifactProvider()
.createClient(targetValue)
.execute(
"extract_artifact",
artifactExtractionInputSchema.parse({
approved: true,
output_root: cancelledOutput,
occurrence_ids: [selected.occurrence_id],
}),
{ signal: controller.signal },
);
expect(cancelled).toMatchObject({
ok: false,
error: { _tag: "ArtifactOperationError", reason: "cancelled" },
});
await expect(access(cancelledOutput)).rejects.toThrow();
const output = join(root, "output");
const result = await new ArtifactProvider()
.createClient(targetValue)
.execute(
"extract_artifact",
artifactExtractionInputSchema.parse({
approved: true,
output_root: output,
occurrence_ids: [selected.occurrence_id],
}),
);
expect(result.ok).toBe(true);
if (!result.ok) return;
const firstExtraction = artifactExtractionResultSchema.parse(
result.value.result,
);
expect(firstExtraction).toMatchObject({
output_root: "$OUTPUT_ROOT",
containment_verified: true,
artifacts: { total: 1 },
extraction_manifest: { output_root_alias: "$OUTPUT_ROOT" },
});
expect(await readFile(join(output, "assets", "selected.js"), "utf8")).toBe(
"selected();\n",
);
await expect(
access(join(output, "assets", "ignored.js")),
).rejects.toThrow();
const relocatedOutput = join(root, "relocated-output");
const relocated = await new ArtifactProvider()
.createClient(targetValue)
.execute(
"extract_artifact",
artifactExtractionInputSchema.parse({
approved: true,
output_root: relocatedOutput,
occurrence_ids: [selected.occurrence_id],
}),
);
expect(relocated.ok).toBe(true);
if (!relocated.ok) return;
expect(
artifactExtractionResultSchema.parse(relocated.value.result)
.extraction_manifest,
).toEqual(firstExtraction.extraction_manifest);
const second = await new ArtifactProvider()
.createClient(targetValue)
.execute(
"extract_artifact",
artifactExtractionInputSchema.parse({
approved: true,
output_root: output,
occurrence_ids: [selected.occurrence_id],
}),
);
expect(second).toMatchObject({
ok: false,
error: { _tag: "ArtifactOperationError", reason: "path" },
});
expect(await readdir(root)).not.toEqual(
expect.arrayContaining([expect.stringMatching(/^\.output\.rea-/u)]),
);
});
it("paginates wide graphs without changing manifest identity", async () => {
const root = await createTestTempDirectory("rea-wide-");
await Promise.all(
Array.from({ length: 520 }, async (_, index) =>
writeFile(
join(root, `file-${String(index).padStart(4, "0")}.txt`),
`${index}`,
),
),
);
const client = new ArtifactProvider().createClient(
target(root, "directory"),
);
const [first, secondPage] = await Promise.all([
client.execute(
"inventory_artifact",
artifactInventoryInputSchema.parse({ occurrence_limit: 500 }),
),
client.execute(
"inventory_artifact",
artifactInventoryInputSchema.parse({
occurrence_offset: 500,
occurrence_limit: 500,
}),
),
]);
expect(first.ok && secondPage.ok).toBe(true);
if (!first.ok || !secondPage.ok) return;
const left = artifactInventoryResultSchema.parse(first.value.result);
const right = artifactInventoryResultSchema.parse(secondPage.value.result);
expect(left.manifest).toEqual(right.manifest);
expect(left.occurrences).toMatchObject({ total: 521, next_offset: 500 });
expect(right.occurrences).toMatchObject({
offset: 500,
next_offset: null,
});
}, 15_000);
it("uses bounded native lipo metadata for universal slice ranges", async () => {
const root = await createTestTempDirectory("rea-slices-");
const binary = join(root, "fat");
await writeFile(binary, Buffer.from("0123456789abcdef"));
const runner: NativeCommandRunner = {
run: () =>
Promise.resolve(
ok({
tool: "lipo",
executable: "/usr/bin/lipo",
executableSha256: "1".repeat(64),
toolVersion: null,
versionReason: "fixture",
arguments: ["-detailed_info", binary],
stdout:
"architecture x86_64\n cputype 16777223\n cpusubtype 3\n offset 0\n size 8\n align 2^2\narchitecture arm64\n cputype 16777228\n cpusubtype 0\n offset 8\n size 8\n align 2^2\n",
stderr: "",
stdoutBytes: 1,
stderrBytes: 0,
stdoutTruncated: false,
stderrTruncated: false,
exitCode: 0,
signal: null,
}),
),
};
const reader = new MachOSliceArtifactReader(binary, runner);
const entries = [];
for await (const entry of reader.entries()) entries.push(entry);
expect(entries).toHaveLength(2);
const secondEntry = entries[1];
expect(secondEntry).toBeDefined();
if (secondEntry === undefined) return;
expect(secondEntry).toMatchObject({
path: "slices/arm64",
byteOffset: 8,
declaredSize: 8,
});
const chunks: Buffer[] = [];
const stream = await reader.open(secondEntry);
for await (const chunk of stream) chunks.push(Buffer.from(chunk));
expect(Buffer.concat(chunks).toString()).toBe("89abcdef");
expect(reader.provenance()).toEqual([
expect.objectContaining({ tool: "lipo", effects: ["read"] }),
]);
const provenance = reader.provenance();
if (provenance[0] !== undefined)
Reflect.set(provenance[0], "tool", "forged");
expect(reader.provenance()[0]?.tool).toBe("lipo");
});
});
const inventory = async (targetValue: BinaryTarget) => {
const result = await new ArtifactProvider().createClient(targetValue).execute(
"inventory_artifact",
artifactInventoryInputSchema.parse({
node_limit: 500,
occurrence_limit: 500,
edge_limit: 500,
}),
);
if (!result.ok) throw result.error;
return artifactInventoryResultSchema.parse(result.value.result);
};
const target = (
path: string,
format: BinaryTarget["format"] | "directory",
): BinaryTarget => ({
path,
sourcePath: path,
sha256: "0".repeat(64),
kind: format === "directory" ? "executable" : "archive",
format: format === "directory" ? "mach-o" : format,
});
const limits = () => ({ maxDepth: 20, maxPathBytes: 4_096 });

View File

@ -1,919 +0,0 @@
import { rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { createTestTempDirectory } from "./fixtures/temporaryDirectory.js";
import { BinarySession } from "../src/application/BinarySession.js";
import type { AnalysisClient } from "../src/application/AnalysisProvider.js";
import type {
AnalysisProvider,
CapabilityDescriptor,
} from "../src/application/AnalysisProvider.js";
import {
HopperStartError,
ProviderAdapterError,
} from "../src/domain/errors.js";
import { ProviderCleanupError } from "../src/domain/providerCleanupError.js";
import { err, ok as resultOk } from "../src/domain/result.js";
import { createEvidenceBundle } from "../src/domain/evidenceBundle.js";
import { createAnalysisProfile } from "../src/domain/analysisProfile.js";
import { createInvestigationWorkspace } from "../src/domain/investigationWorkspace.js";
import { observed as ok } from "./fixtures/analysisExecution.js";
let directory: string | undefined;
afterEach(async () => {
if (directory !== undefined)
await rm(directory, { recursive: true, force: true });
directory = undefined;
});
describe("binary session", () => {
it("returns detached provider, target, and workspace metadata", async () => {
const [first] = await targets();
const session = new BinarySession(cacheProvider([]));
expect((await session.open(first)).ok).toBe(true);
expect(session.status()).toMatchObject({
analysis_run: {
run_id: expect.any(String),
process_lineage: { status: "not_observed" },
},
});
expect(session.listUnknowns()).toEqual([]);
const identity = session.providerIdentity();
Reflect.set(identity, "id", "forged");
expect(session.providerIdentity().id).toBe("fixture");
const active = session.activeTarget();
expect(active).toBeDefined();
if (active !== undefined) Reflect.set(active, "path", "/tmp/forged");
expect(session.activeTarget()?.path).toBe(first);
const workspace = createInvestigationWorkspace(
"Detached workspace",
createEvidenceBundle([]),
[],
);
expect(session.retainInvestigationWorkspace(workspace)).toBe("added");
const direct = session.investigationWorkspace(workspace.workspace_id, 1);
if (direct !== undefined) Reflect.set(direct, "revision", 999);
const listed = session.investigationWorkspaces()[0];
if (listed !== undefined) Reflect.set(listed, "workspace_id", "forged");
expect(
session.investigationWorkspace(workspace.workspace_id, 1),
).toMatchObject({ revision: 1, workspace_id: workspace.workspace_id });
await session.close();
});
it("runs through a non-Hopper analysis provider", async () => {
const [first] = await targets();
const operations: string[] = [];
const provider: AnalysisProvider = {
identity: () => ({ id: "fixture", name: "Fixture", version: "1" }),
capabilities: () => [
{
provider: { id: "fixture", name: "Fixture", version: "1" },
operation: "address_name",
inputContractVersion: 1,
outputContractVersion: 1,
available: true,
reason: null,
pagination: "none",
exhaustive: true,
effects: {
mutatesArtifact: false,
launchesProcess: false,
mayShowUi: false,
mayAccessNetwork: false,
mayWriteFilesystem: false,
changesPermissions: false,
requiresRoot: false,
},
limits: {
maxResults: null,
maxPayloadBytes: null,
timeoutMs: null,
},
limitations: [],
},
],
createClient: (_target, _profile, context) => {
if (context === undefined)
throw new Error("missing analysis run context");
return {
execute: (operation) => {
operations.push(operation);
return Promise.resolve(ok(operation));
},
runtimeLineageSnapshots: () => [
{
provider: provider.identity(),
observation: {
status: "verified",
observedAt: "2026-07-22T10:00:00.000Z",
lineage: {
schemaVersion: 1,
runId: context.runId,
launcherPid: 100,
launcherParentPid: 1,
processGroupId: 100,
descendants: [
{ pid: 101, parentPid: 100, processGroupId: 100 },
],
},
},
},
],
requestActivitySnapshots: () => [
{
provider: provider.identity(),
active: {
requestId: 7,
operation: "analyze_function",
elapsedMs: 31_000,
timeoutMs: 30_000,
callerState: "timed_out",
},
queuedRequests: 2,
},
],
close: () => Promise.resolve(),
};
},
};
const session = new BinarySession(provider);
expect((await session.open(first)).ok).toBe(true);
expect(await session.execute("address_name", {})).toEqual({
ok: true,
value: {
result: "address_name",
rawResult: "address_name",
provider: {
id: "fixture",
name: "Fixture analysis provider",
version: "1",
},
limitations: [],
locations: [],
subject: null,
},
});
expect(provider.identity().id).toBe("fixture");
expect(provider.capabilities()[0]?.operation).toBe("address_name");
expect(session.status()).toMatchObject({
provider: { id: "fixture", name: "Fixture", version: "1" },
providers: [{ id: "fixture", name: "Fixture", version: "1" }],
capabilities: [
{
operation: "address_name",
available: true,
reason: null,
effects: {
mutates_artifact: false,
launches_process: false,
},
},
],
analysis_run: {
run_id: expect.stringMatching(
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u,
),
process_lineage: {
status: "snapshots",
snapshots: [
{
provider: { id: "fixture", name: "Fixture", version: "1" },
observation: {
status: "verified",
observed_at: "2026-07-22T10:00:00.000Z",
schema_version: 1,
launcher_pid: 100,
launcher_parent_pid: 1,
process_group_id: 100,
descendants: [
{ pid: 101, parent_pid: 100, process_group_id: 100 },
],
},
},
],
},
},
analysis_activity: {
status: "timed_out_busy",
providers: [
{
provider: { id: "fixture", name: "Fixture", version: "1" },
active: {
request_id: 7,
operation: "analyze_function",
elapsed_ms: 31_000,
timeout_ms: 30_000,
caller_state: "timed_out",
},
queued_requests: 2,
},
],
},
});
expect(operations).toEqual(["health", "address_name"]);
await session.close();
});
it("allocates one fresh run identity per provider client lifetime", async () => {
const [first, second] = await targets();
const provider = cacheProvider([]);
const createClient = provider.createClient.bind(provider);
const runIds: string[] = [];
provider.createClient = (target, profile, context) => {
if (context === undefined)
throw new Error("missing analysis run context");
runIds.push(context.runId);
return createClient(target, profile, context);
};
const session = new BinarySession(provider);
expect((await session.open(first)).ok).toBe(true);
expect((await session.open(first)).ok).toBe(true);
expect((await session.open(second)).ok).toBe(true);
expect(runIds).toHaveLength(2);
expect(new Set(runIds).size).toBe(2);
await session.close();
});
it("replays exact immutable calls from a matching provider-neutral snapshot", async () => {
const [first, second] = await targets();
const initialCalls: string[] = [];
const initial = new BinarySession(cacheProvider(initialCalls));
expect((await initial.open(first)).ok).toBe(true);
expect(
(
await initial.execute("address_name", {
address: "0x1000",
document: "first",
})
).ok,
).toBe(true);
const snapshot = initial.exportAnalysisSnapshot();
expect(snapshot.ok).toBe(true);
if (!snapshot.ok) return;
expect(snapshot.value.entries).toHaveLength(1);
await initial.close();
const replayCalls: string[] = [];
const replay = new BinarySession(cacheProvider(replayCalls));
expect(replay.importAnalysisSnapshot(snapshot.value)).toEqual({
ok: true,
value: 1,
});
expect((await replay.open(first)).ok).toBe(true);
const cached = await replay.execute("address_name", {
address: "0x1000",
document: "first",
});
expect(cached.ok).toBe(true);
if (cached.ok)
expect(cached.value.limitations).toContainEqual(
expect.stringContaining("local REA analysis snapshot"),
);
expect(
(
await replay.execute("set_address_name", {
address: "0x1000",
name: "renamed",
document: "first",
})
).ok,
).toBe(true);
expect(
(
await replay.execute("address_name", {
address: "0x1000",
document: "first",
})
).ok,
).toBe(true);
expect(replayCalls).toEqual(["health", "set_address_name", "address_name"]);
expect(replay.exportAnalysisSnapshot().ok).toBe(false);
expect((await replay.open(first)).ok).toBe(true);
expect(replay.exportAnalysisSnapshot()).toMatchObject({
ok: true,
value: { entries: [{ operation: "address_name" }] },
});
await replay.close();
const mismatch = new BinarySession(cacheProvider([]));
expect(mismatch.importAnalysisSnapshot(snapshot.value).ok).toBe(true);
const opened = await mismatch.open(second);
expect(opened.ok).toBe(false);
if (!opened.ok) expect(opened.error._tag).toBe("EvidenceIntegrityError");
await mismatch.close();
const profileMismatchCalls: string[] = [];
const profileMismatchProvider = cacheProvider(profileMismatchCalls);
const identity = profileMismatchProvider.identity();
profileMismatchProvider.resolveAnalysisProfile = () =>
Promise.resolve(
resultOk({
profile: createAnalysisProfile(
{
id: identity.id,
name: identity.name,
version: identity.version ?? "fixture-unresolved",
},
1,
{ fixture: "different-profile" },
),
compatibility: {},
}),
);
const profileMismatch = new BinarySession(profileMismatchProvider);
expect(profileMismatch.importAnalysisSnapshot(snapshot.value).ok).toBe(
true,
);
const profileOpened = await profileMismatch.open(first);
expect(profileOpened.ok).toBe(false);
if (!profileOpened.ok) {
expect(profileOpened.error._tag).toBe("EvidenceIntegrityError");
expect(profileOpened.error.message).toContain("profile_mismatch");
}
expect(profileMismatchCalls).toEqual([]);
await profileMismatch.close();
const activeProfileMismatch = new BinarySession(profileMismatchProvider);
expect((await activeProfileMismatch.open(first)).ok).toBe(true);
const activeImport = activeProfileMismatch.importAnalysisSnapshot(
snapshot.value,
);
expect(activeImport.ok).toBe(false);
if (!activeImport.ok)
expect(activeImport.error.message).toContain("profile_mismatch");
await activeProfileMismatch.close();
});
it("does not snapshot reads that depend on the provider cursor", async () => {
const [first] = await targets();
const calls: string[] = [];
const session = new BinarySession(cacheProvider(calls));
expect((await session.open(first)).ok).toBe(true);
expect((await session.execute("address_name", {})).ok).toBe(true);
expect((await session.execute("address_name", {})).ok).toBe(true);
expect(calls).toEqual(["health", "address_name", "address_name"]);
expect(session.exportAnalysisSnapshot()).toMatchObject({
ok: true,
value: { entries: [] },
});
await session.close();
});
it("publishes provider-health availability changes and resets on target switch", async () => {
const [first, second] = await targets();
const provider = cacheProvider([]);
provider.createClient = () => ({
execute: (operation) =>
Promise.resolve(
operation === "health"
? ok(null)
: err(new ProviderAdapterError("fixture", operation)),
),
close: () => Promise.resolve(),
});
const session = new BinarySession(provider);
let changes = 0;
session.onAvailabilityChanged(() => {
changes += 1;
});
expect((await session.open(first)).ok).toBe(true);
expect((await session.execute("address_name", {})).ok).toBe(false);
expect(session.status()).toMatchObject({
capabilities: expect.arrayContaining([
expect.objectContaining({
operation: "address_name",
available: false,
reason: "Provider became unavailable during this session.",
}),
]),
});
expect(changes).toBe(1);
expect((await session.open(second)).ok).toBe(true);
expect(session.status()).toMatchObject({
capabilities: expect.arrayContaining([
expect.objectContaining({
operation: "address_name",
available: true,
reason: null,
}),
]),
});
expect(changes).toBe(2);
});
it("isolates availability observers from execution results and session state", async () => {
const [first] = await targets();
const provider = cacheProvider([]);
let providerCalls = 0;
provider.createClient = () => ({
execute: (operation) => {
if (operation === "health") return Promise.resolve(ok(null));
providerCalls += 1;
return Promise.resolve(
providerCalls === 1
? err(new ProviderAdapterError("fixture", operation))
: ok(operation),
);
},
close: () => Promise.resolve(),
});
const session = new BinarySession(provider);
expect((await session.open(first)).ok).toBe(true);
const input = { address: "0x1000", document: "first" };
expect((await session.execute("address_name", input)).ok).toBe(false);
session.onAvailabilityChanged(() => {
throw new Error("external observer failed");
});
session.onAvailabilityChanged(() =>
Promise.reject(new Error("async external observer failed")),
);
let delivered = 0;
session.onAvailabilityChanged(() => {
delivered += 1;
});
await expect(session.execute("address_name", input)).resolves.toMatchObject(
{ ok: true },
);
expect(delivered).toBe(1);
expect(session.status()).toMatchObject({
capabilities: expect.arrayContaining([
expect.objectContaining({
operation: "address_name",
available: true,
reason: null,
}),
]),
});
expect((await session.execute("address_name", input)).ok).toBe(true);
expect(providerCalls).toBe(2);
});
it("does not replay operations with filesystem side effects", async () => {
const [first] = await targets();
const calls: string[] = [];
const session = new BinarySession(cacheProvider(calls, true));
expect((await session.open(first)).ok).toBe(true);
const input = { address: "0x1000", document: "first" };
expect((await session.execute("address_name", input)).ok).toBe(true);
expect((await session.execute("address_name", input)).ok).toBe(true);
expect(calls).toEqual(["health", "address_name", "address_name"]);
await session.close();
});
it("returns typed unavailability without dispatching a partial provider", async () => {
const [first] = await targets();
const operations: string[] = [];
const provider: AnalysisProvider = {
identity: () => ({ id: "partial", name: "Partial", version: "1" }),
capabilities: () => [
{
provider: { id: "partial", name: "Partial", version: "1" },
operation: "address_name",
inputContractVersion: 1,
outputContractVersion: 1,
available: false,
reason: "fixture intentionally omits symbol lookup",
pagination: "none",
exhaustive: false,
effects: {
mutatesArtifact: false,
launchesProcess: false,
mayShowUi: false,
mayAccessNetwork: false,
mayWriteFilesystem: false,
changesPermissions: false,
requiresRoot: false,
},
limits: {
maxResults: null,
maxPayloadBytes: null,
timeoutMs: null,
},
limitations: ["No symbol lookup implementation."],
},
],
createClient: () => ({
execute: (operation) => {
operations.push(operation);
return Promise.resolve(ok(operation));
},
close: () => Promise.resolve(),
}),
};
const session = new BinarySession(provider);
expect((await session.open(first)).ok).toBe(true);
const result = await session.execute("address_name", {});
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toMatchObject({
_tag: "AnalysisCapabilityUnavailableError",
providerId: "partial",
operation: "address_name",
reason: "fixture intentionally omits symbol lookup",
});
}
expect(operations).toEqual(["health"]);
await session.close();
});
it("requires an open binary and closes idempotently", async () => {
const session = new BinarySession(() => client());
expect((await session.execute("binary_overview", {})).ok).toBe(false);
expect(await session.close()).toEqual({ ok: true, value: null });
});
it("keeps the active client when a switch fails", async () => {
directory = await createTestTempDirectory("bb-session-");
const first = join(directory, "first.hop");
const second = join(directory, "second.hop");
await writeFile(first, "one");
await writeFile(second, "two");
let created = 0;
const session = new BinarySession(() => client(created++ === 1));
expect((await session.open(first)).ok).toBe(true);
expect((await session.open(second)).ok).toBe(false);
expect(session.status()).toMatchObject({
open: true,
sha256:
"7692c3ad3540bb803c020b3aee66cd8887123234ea0c6e7143c0add73ff431ed",
architecture: null,
});
expect(JSON.stringify(session.status())).toContain("first.hop");
expect(created).toBe(3);
});
it("serializes concurrent opens and leaves the last target active", async () => {
const [first, second] = await targets();
const clients: TestClient[] = [];
const session = new BinarySession(() => {
const value = new TestClient();
clients.push(value);
return value;
});
const one = session.open(first);
const two = session.open(second);
expect((await one).ok).toBe(true);
expect((await two).ok).toBe(true);
expect(session.status()).toMatchObject({ open: true });
expect(JSON.stringify(session.status())).toContain("second.hop");
expect(clients[0]?.closed).toBe(1);
});
it("replaces the active client when a canonical path changes contents", async () => {
directory = await createTestTempDirectory("bb-session-");
const path = join(directory, "mutable.hop");
await writeFile(path, "one");
const clients: Array<{
readonly targetSha256: string;
readonly calls: string[];
closed: number;
}> = [];
const session = new BinarySession((target) => {
const state = {
targetSha256: target.sha256,
calls: [] as string[],
closed: 0,
};
clients.push(state);
return {
execute: (operation) => {
state.calls.push(operation);
return Promise.resolve(
ok(operation === "health" ? null : target.sha256),
);
},
close: () => {
state.closed += 1;
return Promise.resolve();
},
};
});
const first = await session.open(path);
expect(first.ok).toBe(true);
await writeFile(path, "two");
const second = await session.open(path);
expect(second.ok).toBe(true);
if (!first.ok || !second.ok) return;
expect(second.value.sha256).not.toBe(first.value.sha256);
expect(session.activeTarget()?.sha256).toBe(second.value.sha256);
expect(await session.execute("binary_overview", {})).toMatchObject({
ok: true,
value: { result: second.value.sha256 },
});
expect(clients).toMatchObject([
{
targetSha256: first.value.sha256,
calls: ["health"],
closed: 1,
},
{
targetSha256: second.value.sha256,
calls: ["health", "binary_overview"],
closed: 0,
},
]);
await session.close();
});
it("waits for an active call before closing its client during a switch", async () => {
const [first, second] = await targets();
const active = deferred<ReturnType<typeof ok>>();
const clients: TestClient[] = [];
const session = new BinarySession(() => {
const value = new TestClient(
undefined,
false,
clients.length === 0 ? active.promise : undefined,
);
clients.push(value);
return value;
});
await session.open(first);
const call = session.execute("procedure_pseudo_code", {});
const switching = session.open(second);
await Promise.resolve();
expect(clients[0]?.closed).toBe(0);
active.resolve(ok(null));
await call;
await switching;
expect(clients[0]?.closed).toBe(1);
});
it("cancels an open queued behind another transition without creating a client", async () => {
const [first, second] = await targets();
const health = deferred<ReturnType<typeof ok>>();
let created = 0;
const session = new BinarySession(() => {
created += 1;
return new TestClient(created === 1 ? health.promise : undefined);
});
const opening = session.open(first);
const controller = new AbortController();
const queued = session.open(second, { signal: controller.signal });
controller.abort();
health.resolve(ok(null));
await opening;
const result = await queued;
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error._tag).toBe("AnalysisCancelledError");
expect(created).toBe(1);
});
it("cancels legacy profile resolution even when the provider ignores its signal", async () => {
const [first] = await targets();
const provider = cacheProvider([]);
let observedSignal: AbortSignal | undefined;
let created = 0;
provider.resolveAnalysisProfile = (_target, options) => {
observedSignal = options?.signal;
return new Promise<never>(() => undefined);
};
provider.createClient = () => {
created += 1;
return new TestClient();
};
const session = new BinarySession(provider);
const controller = new AbortController();
const opening = session.open(first, { signal: controller.signal });
while (observedSignal === undefined)
await new Promise<void>((resolve) => setImmediate(resolve));
expect(observedSignal).toBe(controller.signal);
controller.abort();
await expect(opening).resolves.toMatchObject({
ok: false,
error: { _tag: "AnalysisCancelledError", operation: "open_binary" },
});
expect(created).toBe(0);
});
it("cancels a call while it waits for a transition", async () => {
const [first] = await targets();
const health = deferred<ReturnType<typeof ok>>();
const session = new BinarySession(() => new TestClient(health.promise));
const opening = session.open(first);
const controller = new AbortController();
const call = session.execute(
"binary_overview",
{},
{
signal: controller.signal,
},
);
controller.abort();
const result = await call;
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error._tag).toBe("AnalysisCancelledError");
health.resolve(ok(null));
await opening;
});
it("closes a failed candidate and reopens the previous target", async () => {
const [first, second] = await targets();
const clients: TestClient[] = [];
const session = new BinarySession(() => {
const value = new TestClient(undefined, clients.length === 1);
clients.push(value);
return value;
});
await session.open(first);
await session.open(second);
expect(clients[1]?.closed).toBe(1);
expect(clients[0]?.closed).toBe(1);
expect(clients[2]?.closed).toBe(0);
expect(JSON.stringify(session.status())).toContain("first.hop");
await session.close();
await session.close();
expect(clients[2]?.closed).toBe(1);
});
it("closes the active bridge before starting a replacement", async () => {
const [first, second] = await targets();
let liveClients = 0;
let overlapped = false;
const session = new BinarySession(() => ({
execute: () => {
if (liveClients > 0) overlapped = true;
liveClients += 1;
return Promise.resolve(ok(null));
},
close: () => {
liveClients -= 1;
return Promise.resolve();
},
}));
await session.open(first);
await session.open(second);
expect(overlapped).toBe(false);
await session.close();
});
it("clears session state while preserving a typed provider cleanup failure", async () => {
const [first] = await targets();
const cleanupError = new ProviderCleanupError(
"fixture",
["fixture-document"],
{ reason: "shutdown acknowledgement missing" },
);
const session = new BinarySession(() => ({
execute: () => Promise.resolve(ok(null)),
closeWithOutcome: () => Promise.resolve(err(cleanupError)),
close: () => Promise.resolve(),
}));
expect((await session.open(first)).ok).toBe(true);
expect(await session.close()).toEqual(err(cleanupError));
expect(session.status()).toMatchObject({
open: false,
analysis_activity: { status: "not_observed", providers: [] },
});
});
it("does not start a replacement after the active provider cleanup is unconfirmed", async () => {
const [first, second] = await targets();
const cleanupError = new ProviderCleanupError(
"fixture",
["fixture-document"],
{ reason: "shutdown acknowledgement missing" },
);
let created = 0;
const session = new BinarySession(() => {
created += 1;
return {
execute: () => Promise.resolve(ok(null)),
closeWithOutcome: () => Promise.resolve(err(cleanupError)),
close: () => Promise.resolve(),
};
});
expect((await session.open(first)).ok).toBe(true);
expect(await session.open(second)).toEqual(err(cleanupError));
expect(created).toBe(1);
expect(session.status()).toMatchObject({ open: false });
});
});
const client = (fail = false): AnalysisClient => ({
execute: () => Promise.resolve(fail ? err(new HopperStartError()) : ok(null)),
close: () => Promise.resolve(),
});
const cacheProvider = (
calls: string[],
mayWriteFilesystem = false,
): AnalysisProvider => {
const identity = {
id: "fixture",
name: "Fixture analysis provider",
version: "1",
} as const;
return {
identity: () => identity,
resolveAnalysisProfile: () =>
Promise.resolve(
resultOk({
profile: createAnalysisProfile(identity, 1, { fixture: true }),
compatibility: {},
}),
),
capabilities: () => [
cacheCapability(identity, "address_name", false, mayWriteFilesystem),
cacheCapability(identity, "set_address_name", true),
],
createClient: () => ({
execute: (operation) => {
calls.push(operation);
return Promise.resolve(ok(operation));
},
close: () => Promise.resolve(),
}),
};
};
const cacheCapability = (
provider: CapabilityDescriptor["provider"],
operation: "address_name" | "set_address_name",
mutatesArtifact: boolean,
mayWriteFilesystem = false,
): CapabilityDescriptor => ({
provider,
operation,
inputContractVersion: 1,
outputContractVersion: 1,
available: true,
reason: null,
pagination: "none",
exhaustive: true,
effects: {
mutatesArtifact,
launchesProcess: false,
mayShowUi: false,
mayAccessNetwork: false,
mayWriteFilesystem,
changesPermissions: false,
requiresRoot: false,
},
limits: {
maxResults: null,
maxPayloadBytes: null,
timeoutMs: null,
},
limitations: [],
});
class TestClient implements AnalysisClient {
closed = 0;
constructor(
readonly pendingHealth?: Promise<ReturnType<typeof ok>>,
readonly failHealth = false,
readonly pendingCall?: Promise<ReturnType<typeof ok>>,
) {}
execute(name: string) {
if (name === "health")
return this.failHealth
? Promise.resolve(err(new HopperStartError()))
: (this.pendingHealth ?? Promise.resolve(ok(null)));
return this.pendingCall ?? Promise.resolve(ok(null));
}
close(): Promise<void> {
this.closed += 1;
return Promise.resolve();
}
}
const targets = async (): Promise<readonly [string, string]> => {
directory ??= await createTestTempDirectory("bb-session-");
const first = join(directory, "first.hop");
const second = join(directory, "second.hop");
await writeFile(first, "one");
await writeFile(second, "two");
return [first, second];
};
const deferred = <T>(): {
readonly promise: Promise<T>;
resolve(value: T): void;
} => {
let resolvePromise: ((value: T) => void) | undefined;
const promise = new Promise<T>((resolve) => {
resolvePromise = resolve;
});
return {
promise,
resolve(value) {
resolvePromise?.(value);
},
};
};

View File

@ -4,12 +4,12 @@ import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { createTestTempDirectory } from "./fixtures/temporaryDirectory.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import {
BrowserStartupError,
waitForBrowserDevtoolsPort,
} from "../src/browser/BrowserProcessStartup.js";
} from "../../../src/browser/BrowserProcessStartup.js";
const roots: string[] = [];

View File

@ -0,0 +1,216 @@
import { expect, it } from "vitest";
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
import {
inspectWebPageInputSchema,
listBrowserTargetsInputSchema,
} from "../../../src/domain/browserObservation.js";
import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
import { describeBrowser, trackBrowser } from "./cdpBrowserProvider.support.js";
describeBrowser("CdpBrowserProvider: discovery authorization 1", () => {
it("lists only exact-origin pages and sanitizes URLs", async () => {
const browser = await startFakeCdpBrowser();
trackBrowser(browser);
const provider = new CdpBrowserProvider();
const result = await provider.listTargets(
listBrowserTargetsInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
approved: true,
}),
);
if (!result.ok) throw result.error;
expect(result.value.targets.items).toEqual([
expect.objectContaining({
target_id: "allowed-page",
origin: browser.allowedOrigin,
url: `${browser.allowedOrigin}/app?token=%5BREDACTED%5D`,
}),
]);
expect(result.value.excluded).toEqual({
disallowed_origin: 1,
unsupported_url: 1,
non_page: 1,
});
expect(JSON.stringify(result.value)).not.toContain("forbidden");
expect(JSON.stringify(result.value)).not.toContain("page-secret");
});
it.each([
["absolute", true, ""],
["host/path", "host-path", ""],
["root-relative", "root-relative", ""],
["prefixed host/path", "prefixed", "Loading "],
] as const)(
"sanitizes %s transitional target titles",
async (_label, urlShapedAllowedTitle, prefix) => {
const browser = await startFakeCdpBrowser({ urlShapedAllowedTitle });
trackBrowser(browser);
const provider = new CdpBrowserProvider();
const input = {
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
approved: true as const,
target_id: "allowed-page",
};
const expected = `${prefix}${browser.allowedOrigin}/app?startup=%5BREDACTED%5D`;
const listed = await provider.listTargets(
listBrowserTargetsInputSchema.parse(input),
);
if (!listed.ok) throw listed.error;
expect(listed.value.targets.items[0]?.title).toBe(expected);
const inspected = await provider.inspectPage(
inspectWebPageInputSchema.parse({ ...input, observation_ms: 0 }),
);
if (!inspected.ok) throw inspected.error;
expect(inspected.value.target.title).toBe(expected);
expect(JSON.stringify({ listed, inspected })).not.toContain(
"title-secret",
);
},
);
it("uses page-scoped discovery sockets as direct target transports", async () => {
const browser = await startFakeCdpBrowser({
pageScopedVersionWebSocket: true,
omitTargetWebSocket: true,
additionalPageWithWebSocket: true,
additionalPageWithoutWebSocket: true,
});
trackBrowser(browser);
const provider = new CdpBrowserProvider();
const listed = await provider.listTargets(
listBrowserTargetsInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
approved: true,
}),
);
if (!listed.ok) throw listed.error;
expect(
listed.value.targets.items.map(({ target_id }) => target_id),
).toEqual(["allowed-page", "allowed-page-with-socket"]);
expect(listed.value.limitations).toContain(
"1 otherwise allowed page target(s) lacked a validated direct CDP WebSocket and were excluded.",
);
const inspected = await provider.inspectPage(
inspectWebPageInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
observation_ms: 0,
}),
);
if (!inspected.ok) throw inspected.error;
expect(inspected.value.target.target_id).toBe("allowed-page");
expect(inspected.value.network.requests).toHaveLength(1);
const second = await provider.inspectPage(
inspectWebPageInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page-with-socket",
approved: true,
observation_ms: 0,
}),
);
if (!second.ok) throw second.error;
expect(second.value.target.target_id).toBe("allowed-page-with-socket");
const methods = browser.commands.map(({ method }) => method);
expect(methods).not.toContain("Target.attachToTarget");
expect(methods).not.toContain("Target.detachFromTarget");
expect(
browser.commands.every(({ sessionId }) => sessionId === undefined),
).toBe(true);
});
});
describeBrowser("CdpBrowserProvider: discovery authorization 2", () => {
it("rejects empty browser attachment session identifiers", async () => {
const browser = await startFakeCdpBrowser({ invalidAttachedSession: true });
trackBrowser(browser);
const result = await new CdpBrowserProvider().inspectPage(
inspectWebPageInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
observation_ms: 0,
}),
);
expect(result).toMatchObject({
ok: false,
error: { _tag: "BrowserObservationError", reason: "protocol_error" },
});
expect(browser.commands.map(({ method }) => method)).toEqual([
"Target.attachToTarget",
]);
});
it("rejects missing and disallowed page targets before attaching", async () => {
const browser = await startFakeCdpBrowser();
trackBrowser(browser);
const provider = new CdpBrowserProvider();
for (const targetId of ["missing", "disallowed-page"]) {
const result = await provider.inspectPage(
inspectWebPageInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: targetId,
approved: true,
observation_ms: 0,
}),
);
expect(result.ok).toBe(false);
if (!result.ok)
expect(result.error).toMatchObject({
_tag: "BrowserObservationError",
reason:
targetId === "missing" ? "target_not_found" : "target_not_allowed",
});
}
expect(browser.commands).toHaveLength(0);
});
it("returns typed errors for malformed discovery and disconnects", async () => {
const malformed = await startFakeCdpBrowser({ malformedDiscovery: true });
trackBrowser(malformed);
const malformedResult = await new CdpBrowserProvider().listTargets(
listBrowserTargetsInputSchema.parse({
cdp_endpoint: malformed.endpoint,
allowed_origins: [malformed.allowedOrigin],
approved: true,
}),
);
expect(malformedResult).toMatchObject({
ok: false,
error: {
_tag: "BrowserObservationError",
reason: "invalid_endpoint_response",
},
});
const disconnecting = await startFakeCdpBrowser({
closeOnMethod: "Page.getFrameTree",
});
trackBrowser(disconnecting);
const disconnectedResult = await new CdpBrowserProvider().inspectPage(
inspectWebPageInputSchema.parse({
cdp_endpoint: disconnecting.endpoint,
allowed_origins: [disconnecting.allowedOrigin],
target_id: "allowed-page",
approved: true,
observation_ms: 0,
}),
);
expect(disconnectedResult).toMatchObject({
ok: false,
error: { _tag: "BrowserObservationError", reason: "disconnected" },
});
});
});

View File

@ -0,0 +1,441 @@
import { expect, it } from "vitest";
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
import { inspectWebPageInputSchema } from "../../../src/domain/browserObservation.js";
import { analyzeWebBundleInputSchema } from "../../../src/domain/webBundleAnalysis.js";
import { discoverWebMcpToolsInputSchema } from "../../../src/domain/webMcpDiscovery.js";
import { captureWebScreenshotInputSchema } from "../../../src/domain/webScreenshot.js";
import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
import { describeBrowser, trackBrowser } from "./cdpBrowserProvider.support.js";
describeBrowser("CdpBrowserProvider: document script 1", () => {
it("captures bounded accessibility text only after independent approval", async () => {
const browser = await startFakeCdpBrowser();
trackBrowser(browser);
const result = await new CdpBrowserProvider().inspectPage(
inspectWebPageInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
observation_ms: 0,
include_accessibility_text: true,
limits: {
max_ax_text_field_bytes: 6,
max_total_ax_text_bytes: 6,
},
}),
);
if (!result.ok) throw result.error;
expect(result.value.accessibility).toMatchObject({
text_capture: {
status: "truncated",
retained_bytes: 6,
truncated_fields: 2,
},
nodes: [expect.objectContaining({ name: "Submit" })],
});
expect(result.value.completeness.truncated_sections).toContain(
"accessibility",
);
});
it("includes allowed script source only after explicit approval", async () => {
const browser = await startFakeCdpBrowser();
trackBrowser(browser);
const provider = new CdpBrowserProvider();
const result = await provider.inspectPage(
inspectWebPageInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
observation_ms: 0,
include_script_sources: true,
}),
);
if (!result.ok) throw result.error;
expect(result.value.scripts.items[0]?.source).toEqual(
expect.objectContaining({
included: true,
artifact: expect.objectContaining({
text: "export const observed = 'source-secret';",
bytes: 40,
uri: expect.stringMatching(/^rea:\/\/web-content\/sha256\//u),
}),
}),
);
expect(browser.commands.map((command) => command.method)).toContain(
"Debugger.getScriptSource",
);
});
it("fetches and validates source maps only after separate approval", async () => {
const browser = await startFakeCdpBrowser({
sourceMapBody: JSON.stringify({
version: 3,
names: [],
sources: ["src/main.ts"],
sourcesContent: ["export const original = true;"],
mappings: "AAAA",
}),
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().analyzeBundle(
analyzeWebBundleInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
source_capture_approved: true,
observation_ms: 0,
fetch_source_maps: true,
source_map_fetch_approved: true,
}),
);
if (!result.ok) throw result.error;
expect(result.value.observations.source_maps).toMatchObject({
status: "included",
items: [
{
status: "included",
original_sources: [
expect.objectContaining({
source: `${browser.allowedOrigin}/src/main.ts`,
}),
],
mappings: [expect.any(Object)],
},
],
});
const sourceMapRequest = browser.httpRequests.find(({ url }) =>
url.startsWith("/app.js.map"),
);
expect(sourceMapRequest).toEqual({
url: "/app.js.map?token=map-secret",
authorization: undefined,
cookie: undefined,
referer: undefined,
});
});
});
describeBrowser("CdpBrowserProvider: document script 2", () => {
it("discovers untrusted WebMCP declarations without registering or invoking them", async () => {
const browser = await startFakeCdpBrowser({ webMcpTools: true });
trackBrowser(browser);
const result = await new CdpBrowserProvider().discoverWebMcpTools(
discoverWebMcpToolsInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
observation_ms: 0,
}),
);
if (!result.ok) throw result.error;
expect(result.value).toMatchObject({
status: "available",
tools: {
total: 1,
items: [
{
name: "search_orders",
description: "Search orders; authorization=[REDACTED]",
declaration_kind: "declarative",
owner_origin: browser.allowedOrigin,
annotations: {
read_only: true,
untrusted_content: true,
autosubmit: false,
},
trust: "page-declared-untrusted",
registration_source: {
url: `${browser.allowedOrigin}/app.js?token=%5BREDACTED%5D`,
line: 12,
column: 4,
},
},
],
},
});
const serialized = JSON.stringify(result.value);
for (const secret of [
"tool-secret",
"schema-secret",
"tool-source-secret",
"private-tool-secret",
])
expect(serialized).not.toContain(secret);
const methods = browser.commands.map(({ method }) => method);
expect(methods).toContain("WebMCP.enable");
expect(methods).not.toContain("WebMCP.invokeTool");
expect(methods).not.toContain("Runtime.evaluate");
});
it("reports WebMCP unavailable when the experimental domain is absent", async () => {
const browser = await startFakeCdpBrowser({
unsupportedMethods: ["WebMCP.enable"],
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().discoverWebMcpTools(
discoverWebMcpToolsInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
observation_ms: 0,
}),
);
if (!result.ok) throw result.error;
expect(result.value.status).toBe("unavailable");
expect(result.value.tools.items).toEqual([]);
expect(result.value.completeness.unavailable_sections).toContain(
"webmcp_tools",
);
});
it("rejects WebMCP evidence when the page leaves its approved origin", async () => {
const browser = await startFakeCdpBrowser({
webMcpTools: true,
frameUrlAfterFirstRead: "https://private.example.test/tools",
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().discoverWebMcpTools(
discoverWebMcpToolsInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
observation_ms: 0,
}),
);
expect(result).toMatchObject({
ok: false,
error: {
_tag: "BrowserObservationError",
operation: "discover_webmcp_tools",
reason: "target_not_allowed",
},
});
expect(browser.commands.map(({ method }) => method)).not.toContain(
"WebMCP.invokeTool",
);
});
it("bounds WebMCP registration replay and reports dropped declarations", async () => {
const browser = await startFakeCdpBrowser({
webMcpTools: true,
extraCollections: true,
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().discoverWebMcpTools(
discoverWebMcpToolsInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
observation_ms: 0,
max_tools: 1,
}),
);
if (!result.ok) throw result.error;
expect(result.value.tools).toMatchObject({
total: 2,
items: [expect.any(Object)],
});
expect(result.value.completeness).toMatchObject({
status: "truncated",
dropped_events: { webmcp_tools: 1 },
});
});
});
describeBrowser("CdpBrowserProvider: document script 3", () => {
it("removes WebMCP declarations after their child frame leaves scope", async () => {
const browser = await startFakeCdpBrowser({
webMcpTools: true,
extraCollections: true,
webMcpChildLeavesScope: true,
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().discoverWebMcpTools(
discoverWebMcpToolsInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
observation_ms: 0,
}),
);
if (!result.ok) throw result.error;
const names = result.value.tools.items.map(({ name }) => name);
expect(names).toEqual(
expect.arrayContaining(["search_orders", "update_order"]),
);
expect(names).not.toEqual(
expect.arrayContaining(["child_tool", "escaped_child_tool"]),
);
expect(result.value.completeness.policy_filtered_sections).toContain(
"webmcp_tools",
);
expect(JSON.stringify(result.value)).not.toContain(
"cross-origin-child-secret",
);
});
it("captures an explicitly approved content-addressed viewport screenshot", async () => {
const browser = await startFakeCdpBrowser();
trackBrowser(browser);
const result = await new CdpBrowserProvider().captureScreenshot(
captureWebScreenshotInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
screenshot_approved: true,
}),
);
if (!result.ok) throw result.error;
expect(result.value).toMatchObject({
viewport: { width: 1, height: 1 },
artifact: {
uri: expect.stringMatching(/^rea:\/\/web-screenshot\/sha256\//u),
bytes: 70,
media_type: "image/png",
},
});
expect(browser.commands.map(({ method }) => method)).toContain(
"Page.captureScreenshot",
);
});
it("discards screenshot pixels when the main frame navigates during capture", async () => {
const browser = await startFakeCdpBrowser({
navigateDuringScreenshotUrl: "https://private.example.test/screenshot",
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().captureScreenshot(
captureWebScreenshotInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
screenshot_approved: true,
}),
);
expect(result).toMatchObject({
ok: false,
error: {
_tag: "BrowserObservationError",
operation: "capture_web_screenshot",
reason: "target_not_allowed",
},
});
});
it("reports configured truncation without over-reading DOM or script source", async () => {
const browser = await startFakeCdpBrowser();
trackBrowser(browser);
const result = await new CdpBrowserProvider().inspectPage(
inspectWebPageInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
observation_ms: 0,
include_script_sources: true,
limits: {
max_frames: 1,
max_dom_nodes: 1,
max_ax_nodes: 1,
max_scripts: 1,
max_resources: 1,
max_workers: 1,
max_storage_keys: 1,
max_script_source_bytes: 10,
max_total_script_source_bytes: 10,
max_network_events: 1,
max_console_events: 1,
max_websocket_events: 1,
},
}),
);
if (!result.ok) throw result.error;
expect(result.value.completeness).toMatchObject({
status: "truncated",
truncated_sections: ["dom", "script_sources"],
});
expect(result.value.dom).toMatchObject({ total_nodes: 2 });
expect(result.value.dom.nodes).toHaveLength(1);
expect(result.value.scripts.items[0]?.source).toMatchObject({
included: false,
reason: "declared script length exceeds per-script limit",
});
expect(browser.commands.map(({ method }) => method)).not.toContain(
"Debugger.getScriptSource",
);
});
});
describeBrowser("CdpBrowserProvider: document script 4", () => {
it("bounds frames, resources, workers, accessibility, and storage inventories", async () => {
const browser = await startFakeCdpBrowser({ extraCollections: true });
trackBrowser(browser);
const result = await new CdpBrowserProvider().inspectPage(
inspectWebPageInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
observation_ms: 0,
include_storage_keys: true,
limits: {
max_frames: 1,
max_dom_nodes: 2,
max_ax_nodes: 1,
max_scripts: 1,
max_resources: 1,
max_workers: 1,
max_storage_keys: 1,
max_script_source_bytes: 10,
max_total_script_source_bytes: 10,
max_network_events: 1,
max_console_events: 1,
max_websocket_events: 1,
},
}),
);
if (!result.ok) throw result.error;
expect(result.value.completeness).toMatchObject({
status: "truncated",
truncated_sections: [
"accessibility",
"frames",
"resources",
"storage_keys",
"workers",
],
});
expect(result.value.frames).toHaveLength(1);
expect(result.value.resources).toHaveLength(1);
expect(result.value.workers).toHaveLength(1);
expect(result.value.accessibility).toMatchObject({ total_nodes: 2 });
expect(result.value.accessibility.nodes).toHaveLength(1);
expect(result.value.storage.local_storage_keys).toHaveLength(1);
expect(result.value.storage.session_storage_keys).toHaveLength(1);
expect(result.value.storage.indexed_db_names).toHaveLength(1);
expect(result.value.storage.cache_names).toHaveLength(1);
expect(result.value.storage.content_fingerprints).toEqual([]);
});
});

View File

@ -0,0 +1,484 @@
import { expect, it } from "vitest";
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
import {
inspectWebPageInputSchema,
listBrowserTargetsInputSchema,
} from "../../../src/domain/browserObservation.js";
import { analyzeWebBundleInputSchema } from "../../../src/domain/webBundleAnalysis.js";
import { observeWebSessionInputSchema } from "../../../src/domain/browserSession.js";
import {
startFakeCdpBrowser,
type FakeCdpBrowser,
} from "../../fixtures/fakeCdpBrowser.js";
import { describeBrowser, trackBrowser } from "./cdpBrowserProvider.support.js";
describeBrowser("CdpBrowserProvider: lifecycle failures 1", () => {
it("projects direct-session cancellation onto the requested operation", async () => {
const browser = await startFakeCdpBrowser({
pageScopedVersionWebSocket: true,
omitTargetWebSocket: true,
});
trackBrowser(browser);
const controller = new AbortController();
const result = await new CdpBrowserProvider().observeSession(
observeWebSessionInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
observation_ms: 1_000,
}),
{
signal: controller.signal,
progress: {
report(update) {
if (update.completed === 1) controller.abort();
return Promise.resolve();
},
},
},
);
expect(result).toMatchObject({
ok: false,
error: {
_tag: "AnalysisCancelledError",
operation: "observe_web_session",
},
});
const methods = browser.commands.map(({ method }) => method);
expect(methods).not.toContain("Target.attachToTarget");
expect(methods).not.toContain("Target.detachFromTarget");
expect(methods).not.toContain("Target.closeTarget");
expect(methods).not.toContain("Browser.close");
});
it("projects capture-window cancellation onto bundle analysis", async () => {
const browser = await startFakeCdpBrowser({
pageScopedVersionWebSocket: true,
omitTargetWebSocket: true,
});
trackBrowser(browser);
const controller = new AbortController();
const result = await new CdpBrowserProvider().analyzeBundle(
analyzeWebBundleInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
source_capture_approved: true,
observation_ms: 1_000,
}),
{
signal: controller.signal,
progress: {
report(update) {
if (update.completed === 2) controller.abort();
return Promise.resolve();
},
},
},
);
expect(result).toMatchObject({
ok: false,
error: {
_tag: "AnalysisCancelledError",
operation: "analyze_web_bundle",
},
});
});
it("observes user-driven reload, SPA, redirect, failure, and lifecycle events", async () => {
const browser = await startFakeCdpBrowser({
sessionTimeline: "same_origin",
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().observeSession(
observeWebSessionInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
observation_ms: 20,
}),
);
if (!result.ok) throw result.error;
expect(result.value.window.end_reason).toBe("window_elapsed");
expect(result.value.timeline.map(({ type }) => type)).toEqual([
"navigation_requested",
"same_origin_reload",
"same_document_navigation",
"redirect",
"load_failed",
"lifecycle",
]);
expect(result.value.timeline).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: "same_document_navigation",
url: `${browser.allowedOrigin}/reloaded?token=%5BREDACTED%5D`,
}),
expect.objectContaining({
type: "load_failed",
detail: "net::ERR_CONNECTION_REFUSED",
}),
]),
);
expect(result.value.completeness.status).toBe("attach_limited");
expect(JSON.stringify(result.value)).not.toContain("session-secret");
expect(JSON.stringify(result.value)).not.toContain("redirect-secret");
expect(browser.commands.map(({ method }) => method)).toContain(
"Target.detachFromTarget",
);
});
});
describeBrowser("CdpBrowserProvider: lifecycle failures 2", () => {
it("ends a browser session when its flat target session detaches", async () => {
const browser = await startFakeCdpBrowser({
sessionTimeline: "target_detached",
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().observeSession(
observeWebSessionInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
observation_ms: 1_000,
}),
);
if (!result.ok) throw result.error;
expect(result.value.window.end_reason).toBe("target_terminated");
expect(result.value.timeline.at(-1)).toMatchObject({
type: "target_terminated",
detail: "target_terminated",
});
});
it("ends a direct page session when its transport disconnects", async () => {
const browser = await startFakeCdpBrowser({
pageScopedVersionWebSocket: true,
omitTargetWebSocket: true,
closeAfterMethod: "Page.setLifecycleEventsEnabled",
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().observeSession(
observeWebSessionInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
observation_ms: 1_000,
}),
);
if (!result.ok) throw result.error;
expect(result.value.window.end_reason).toBe("target_terminated");
expect(result.value.timeline.at(-1)).toMatchObject({
type: "target_terminated",
detail: "target_terminated",
});
const methods = browser.commands.map(({ method }) => method);
expect(methods).not.toContain("Target.attachToTarget");
expect(methods).not.toContain("Target.detachFromTarget");
expect(methods).not.toContain("Target.closeTarget");
});
it("ends immediately when a redirect leaves policy without exposing its URL", async () => {
const browser = await startFakeCdpBrowser({
sessionTimeline: "outside_policy",
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().observeSession(
observeWebSessionInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
observation_ms: 1_000,
}),
);
if (!result.ok) throw result.error;
expect(result.value.window.end_reason).toBe("target_left_scope");
expect(result.value.timeline.at(-1)).toMatchObject({
type: "redirect",
url: null,
destination_scope: "outside_policy",
});
expect(result.value.completeness).toMatchObject({
status: "policy_filtered",
policy_filtered_sections: ["timeline"],
excluded: [
{
section: "timeline",
reason: "out_of_target_scope",
count: 1,
},
],
});
expect(JSON.stringify(result.value)).not.toContain("private.example.test");
});
it("fails closed when the final session frame leaves policy without an event", async () => {
const browser = await startFakeCdpBrowser({
frameUrlAfterFirstRead: "https://private.example.test/final",
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().observeSession(
observeWebSessionInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
observation_ms: 1,
}),
);
if (!result.ok) throw result.error;
expect(result.value.window.end_reason).toBe("target_left_scope");
expect(result.value.target.final_url).toBeNull();
expect(result.value.completeness).toMatchObject({
status: "policy_filtered",
policy_filtered_sections: ["timeline"],
});
expect(JSON.stringify(result.value)).not.toContain("private.example.test");
});
});
describeBrowser("CdpBrowserProvider: lifecycle failures 3", () => {
it("degrades optional domains but propagates protocol and payload failures", async () => {
const optional = await startFakeCdpBrowser({
unsupportedMethods: ["Accessibility.getFullAXTree"],
});
trackBrowser(optional);
const degraded = await new CdpBrowserProvider().inspectPage(
inspectWebPageInputSchema.parse({
cdp_endpoint: optional.endpoint,
allowed_origins: [optional.allowedOrigin],
target_id: "allowed-page",
approved: true,
observation_ms: 0,
}),
);
if (!degraded.ok) throw degraded.error;
expect(degraded.value.accessibility.nodes).toEqual([]);
expect(degraded.value.accessibility.text_capture.status).toBe(
"unavailable",
);
expect(degraded.value.completeness.unavailable_sections).toContain(
"accessibility",
);
expect(degraded.value.limitations).toContain(
"Accessibility.getFullAXTree was unavailable from this browser target.",
);
for (const [options, reason] of [
[{ oversizedDiscovery: true }, "payload_limit"],
[{ invalidBrowserWebSocket: true }, "invalid_endpoint_response"],
] as const) {
const failed = await startFakeCdpBrowser(options);
trackBrowser(failed);
const result = await new CdpBrowserProvider().listTargets(
listBrowserTargetsInputSchema.parse({
cdp_endpoint: failed.endpoint,
allowed_origins: [failed.allowedOrigin],
approved: true,
}),
);
expect(result.ok).toBe(false);
if (!result.ok)
expect(result.error).toMatchObject({
_tag: "BrowserObservationError",
reason,
});
}
const malformed = await startFakeCdpBrowser({
malformedMessageOnMethod: "Page.getFrameTree",
});
trackBrowser(malformed);
const protocol = await new CdpBrowserProvider().inspectPage(
inspectWebPageInputSchema.parse({
cdp_endpoint: malformed.endpoint,
allowed_origins: [malformed.allowedOrigin],
target_id: "allowed-page",
approved: true,
observation_ms: 0,
}),
);
expect(protocol).toMatchObject({
ok: false,
error: { _tag: "BrowserObservationError", reason: "protocol_error" },
});
const malformedEvent = await startFakeCdpBrowser({
malformedEventOnMethod: "Debugger.enable",
});
trackBrowser(malformedEvent);
const eventProtocol = await new CdpBrowserProvider().inspectPage(
inspectWebPageInputSchema.parse({
cdp_endpoint: malformedEvent.endpoint,
allowed_origins: [malformedEvent.allowedOrigin],
target_id: "allowed-page",
approved: true,
observation_ms: 0,
}),
);
expect(eventProtocol).toMatchObject({
ok: false,
error: { _tag: "BrowserObservationError", reason: "protocol_error" },
});
const malformedEventShape = await startFakeCdpBrowser({
malformedEventShapeOnMethod: "Debugger.enable",
});
trackBrowser(malformedEventShape);
const eventShapeProtocol = await new CdpBrowserProvider().inspectPage(
inspectWebPageInputSchema.parse({
cdp_endpoint: malformedEventShape.endpoint,
allowed_origins: [malformedEventShape.allowedOrigin],
target_id: "allowed-page",
approved: true,
observation_ms: 0,
}),
);
expect(eventShapeProtocol).toMatchObject({
ok: false,
error: { _tag: "BrowserObservationError", reason: "protocol_error" },
});
});
it("cancels observation and still detaches without closing the page", async () => {
const browser = await startFakeCdpBrowser();
trackBrowser(browser);
const controller = new AbortController();
const pending = new CdpBrowserProvider().inspectPage(
inspectWebPageInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
observation_ms: 1_000,
}),
{ signal: controller.signal },
);
// Wait until the attach response has been consumed and capture has begun.
// Seeing the attach request alone races its response under worker load.
await waitForCommand(browser, "Page.getFrameTree");
controller.abort();
const result = await pending;
expect(result).toMatchObject({
ok: false,
error: {
_tag: "AnalysisCancelledError",
operation: "inspect_web_page",
},
});
const methods = browser.commands.map(({ method }) => method);
expect(methods).toContain("Target.detachFromTarget");
expect(methods).not.toContain("Target.closeTarget");
expect(methods).not.toContain("Browser.close");
});
});
describeBrowser("CdpBrowserProvider: lifecycle failures 4", () => {
it("waits for navigation commit and rechecks the attached main-frame origin", async () => {
const transitioning = await startFakeCdpBrowser({
transitionalFrameReads: 2,
});
trackBrowser(transitioning);
const input = (browser: FakeCdpBrowser) =>
inspectWebPageInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
observation_ms: 0,
});
const committed = await new CdpBrowserProvider().inspectPage(
input(transitioning),
);
if (!committed.ok) throw committed.error;
expect(committed.value.target.origin).toBe(transitioning.allowedOrigin);
expect(
transitioning.commands.filter(
({ method }) => method === "Page.getFrameTree",
),
).toHaveLength(5);
const navigated = await startFakeCdpBrowser({
attachedFrameUrl: "https://unapproved.example.test/after-attach",
});
trackBrowser(navigated);
const denied = await new CdpBrowserProvider().inspectPage(input(navigated));
expect(denied).toMatchObject({
ok: false,
error: { _tag: "BrowserObservationError", reason: "target_not_allowed" },
});
const methods = navigated.commands.map(({ method }) => method);
expect(methods).toContain("Target.detachFromTarget");
expect(methods).not.toContain("DOMSnapshot.captureSnapshot");
expect(methods).not.toContain("Storage.getUsageAndQuota");
const duringObservation = await startFakeCdpBrowser({
navigateDuringObservationUrl:
"https://unapproved.example.test/during-observation",
});
trackBrowser(duringObservation);
const interrupted = await new CdpBrowserProvider().inspectPage(
input(duringObservation),
);
expect(interrupted).toMatchObject({
ok: false,
error: { _tag: "BrowserObservationError", reason: "target_not_allowed" },
});
expect(
duringObservation.commands.map(({ method }) => method),
).not.toContain("Accessibility.getFullAXTree");
const captureOptions: { navigateDuringCaptureUrl?: string } = {};
const duringCapture = await startFakeCdpBrowser(captureOptions);
captureOptions.navigateDuringCaptureUrl = `${duringCapture.allowedOrigin}/changed`;
trackBrowser(duringCapture);
const discarded = await new CdpBrowserProvider().inspectPage(
input(duringCapture),
);
expect(discarded).toMatchObject({
ok: false,
error: { _tag: "BrowserObservationError", reason: "target_changed" },
});
expect(duringCapture.commands.map(({ method }) => method)).toContain(
"Target.detachFromTarget",
);
const crossOriginCapture = await startFakeCdpBrowser({
navigateDuringCaptureUrl:
"https://unapproved.example.test/during-capture",
});
trackBrowser(crossOriginCapture);
const rejectedCapture = await new CdpBrowserProvider().inspectPage(
input(crossOriginCapture),
);
expect(rejectedCapture).toMatchObject({
ok: false,
error: { _tag: "BrowserObservationError", reason: "target_not_allowed" },
});
});
});
const waitForCommand = async (
browser: FakeCdpBrowser,
method: string,
): Promise<void> => {
for (let attempt = 0; attempt < 200; attempt += 1) {
if (browser.commands.some((command) => command.method === method)) return;
await new Promise<void>((resolve) => setTimeout(resolve, 5));
}
throw new Error(`Timed out waiting for fake CDP command ${method}`);
};

View File

@ -0,0 +1,27 @@
import { expect, it } from "vitest";
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
import { inspectWebPageInputSchema } from "../../../src/domain/browserObservation.js";
import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
import { describeBrowser, trackBrowser } from "./cdpBrowserProvider.support.js";
describeBrowser("CdpBrowserProvider: network 1", () => {
it("drops network evidence when a request redirects outside the approved origin", async () => {
const browser = await startFakeCdpBrowser({
redirectToDisallowedOrigin: true,
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().inspectPage(
inspectWebPageInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
observation_ms: 0,
}),
);
if (!result.ok) throw result.error;
expect(result.value.network.requests).toEqual([]);
});
});

View File

@ -0,0 +1,387 @@
import { expect, it } from "vitest";
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
import {
inspectWebPageInputSchema,
webPageInspectionSchema,
} from "../../../src/domain/browserObservation.js";
import {
startFakeCdpBrowser,
type FakeCdpBrowser,
} from "../../fixtures/fakeCdpBrowser.js";
import { describeBrowser, trackBrowser } from "./cdpBrowserProvider.support.js";
describeBrowser("CdpBrowserProvider: sensitive data 1", () => {
it("captures bounded passive evidence without retaining sensitive values", async () => {
const browser = await startFakeCdpBrowser({
binaryWebSocketEvent: true,
foreignSessionEvents: true,
unrelatedWorker: true,
});
trackBrowser(browser);
const provider = new CdpBrowserProvider();
const result = await provider.inspectPage(
inspectWebPageInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
observation_ms: 0,
include_storage_keys: true,
include_storage_fingerprints: true,
}),
);
if (!result.ok) throw result.error;
expect(() => webPageInspectionSchema.parse(result.value)).not.toThrow();
expect(result.value.frames).toHaveLength(1);
expect(result.value.dom.nodes).toHaveLength(2);
expect(result.value.dom.nodes[1]?.attribute_names).toEqual([
"token",
"href",
"rel",
]);
expect(result.value.accessibility).toMatchObject({
text_capture: {
status: "not_approved",
retained_bytes: 0,
},
nodes: [expect.objectContaining({ name: null, description: null })],
});
expect(result.value.scripts.items).toHaveLength(1);
expect(result.value.scripts.items[0]?.script_key).toMatch(
/^scr_[a-f0-9]{64}$/u,
);
expect(result.value.scripts.items[0]?.frame_id).toBe("frame-main");
expect(result.value.scripts.items[0]?.source_map_url).toBe(
`${browser.allowedOrigin}/app.js.map?token=%5BREDACTED%5D`,
);
expect(result.value.scripts.items[0]?.resource_reconciliation).toEqual({
status: "exact",
resource_key: result.value.resources[0]?.resource_key,
});
expect(result.value.scripts.items[0]?.source).toEqual({
included: false,
reason: "source capture was not approved",
});
expect(result.value.resources).toHaveLength(1);
expect(result.value.network.requests).toHaveLength(1);
expect(result.value.network.requests[0]?.initiator).toEqual({
type: "script",
url: `${browser.allowedOrigin}/app.js?caller=%5BREDACTED%5D`,
line: 3,
column: 5,
});
expect(result.value.network.websocket_events).toEqual([
{
request_id: "websocket-1",
direction: "sent",
opcode: 1,
payload_bytes: Buffer.byteLength("websocket-secret"),
payload_shape: null,
},
{
request_id: "websocket-1",
direction: "received",
opcode: 2,
payload_bytes: 3,
payload_shape: null,
},
]);
expect(result.value.console.events[0]?.argument_types).toEqual(["string"]);
expect(result.value.workers).toHaveLength(1);
expect(result.value.workers[0]).toMatchObject({
opener_target_id: "allowed-page",
parent_frame_id: null,
});
expect(result.value.metadata).toMatchObject({
headers_allowlisted: true,
responses: [
{
content_length: 321,
content_encoding: "br",
csp: { nonce_count: 1, hash_count: 1 },
policies: {
coop: "same-origin",
coep: "require-corp",
permissions_policy_features: ["camera", "geolocation"],
},
},
],
dom_urls: [
{
attribute: "href",
url: `${browser.allowedOrigin}/agent?token=%5BREDACTED%5D`,
destination_scope: "approved",
},
],
agent_hints: expect.arrayContaining([
expect.objectContaining({
mechanism: "link_rel",
declaration: "mcp service-desc",
}),
expect.objectContaining({
mechanism: "dom_link_rel",
declaration: "mcp",
}),
expect.objectContaining({
mechanism: "response_header",
declaration: "x-model-context",
}),
]),
});
expect(result.value.storage).toEqual(
expect.objectContaining({
local_storage_keys: ["public-key"],
session_storage_keys: ["public-key"],
indexed_db_names: ["app-db"],
cache_names: ["assets-v1"],
content_fingerprints: expect.arrayContaining([
expect.objectContaining({ scope: "cookie", complete: true }),
expect.objectContaining({ scope: "local_storage", complete: true }),
expect.objectContaining({ scope: "session_storage", complete: true }),
expect.objectContaining({
scope: "indexed_db_schema",
complete: true,
}),
expect.objectContaining({
scope: "indexed_db_record",
complete: true,
}),
expect.objectContaining({ scope: "cache_entry", complete: true }),
]),
fingerprint_algorithm: "sha256",
fingerprints_complete: true,
values_redacted: true,
}),
);
expectSensitiveValuesAbsent(result.value, browser);
});
});
describeBrowser("CdpBrowserProvider: sensitive data 2", () => {
it("captures only approved redacted console text and value-free payload shapes", async () => {
const browser = await startFakeCdpBrowser({
sensitiveShapes: true,
binaryWebSocketEvent: true,
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().inspectPage(
inspectWebPageInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
observation_ms: 0,
include_console_text: true,
console_text_approved: true,
include_json_body_shapes: true,
json_body_schema_approved: true,
include_websocket_shapes: true,
websocket_shape_approved: true,
}),
);
if (!result.ok) throw result.error;
expect(result.value.console.events[0]?.text_capture).toEqual({
status: "included",
values: [
{ argument_index: 0, type: "string", text: "authorization=[REDACTED]" },
{ argument_index: 1, type: "number", text: "42" },
],
retained_bytes: 26,
truncated_values: 0,
});
expect(result.value.network.requests[0]?.body_shapes).toMatchObject({
status: "included",
request: {
root_type: "object",
properties: expect.arrayContaining([
expect.objectContaining({ path: "/token", types: ["string"] }),
expect.objectContaining({
path: "/filters/active",
types: ["boolean"],
}),
]),
},
response: {
root_type: "object",
properties: expect.arrayContaining([
expect.objectContaining({
path: "/result/token",
types: ["string"],
}),
expect.objectContaining({ path: "/items/*/id", types: ["number"] }),
]),
},
});
expect(result.value.network.websocket_events).toEqual([
expect.objectContaining({
opcode: 1,
payload_shape: expect.objectContaining({
format: "json",
json_shape: expect.objectContaining({
properties: expect.arrayContaining([
expect.objectContaining({ path: "/token", types: ["string"] }),
]),
}),
}),
}),
expect.objectContaining({
opcode: 2,
payload_shape: {
format: "binary",
json_shape: null,
truncated: false,
},
}),
]);
const serialized = JSON.stringify(result.value);
for (const secret of [
"request-body-secret",
"response-body-secret",
"websocket-secret",
"console-secret",
"object-secret",
])
expect(serialized).not.toContain(secret);
const methods = browser.commands.map(({ method }) => method);
expect(methods).toContain("Network.getResponseBody");
expect(methods).not.toContain("Runtime.getProperties");
expect(methods).not.toContain("Runtime.callFunctionOn");
});
});
describeBrowser("CdpBrowserProvider: sensitive data 3", () => {
it("reports independent sensitive-capture truncation at aggregate byte limits", async () => {
const browser = await startFakeCdpBrowser({ sensitiveShapes: true });
trackBrowser(browser);
const result = await new CdpBrowserProvider().inspectPage(
inspectWebPageInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
observation_ms: 0,
include_console_text: true,
console_text_approved: true,
include_json_body_shapes: true,
json_body_schema_approved: true,
include_websocket_shapes: true,
websocket_shape_approved: true,
limits: {
max_console_text_field_bytes: 5,
max_total_console_text_bytes: 5,
max_json_body_bytes: 10,
max_total_json_body_bytes: 10,
max_websocket_shape_bytes: 5,
max_total_websocket_shape_bytes: 5,
},
}),
);
if (!result.ok) throw result.error;
expect(result.value.console.events[0]?.text_capture).toEqual({
status: "truncated",
values: [{ argument_index: 0, type: "string", text: "autho" }],
retained_bytes: 5,
truncated_values: 2,
});
expect(result.value.network.requests[0]?.body_shapes.status).toBe(
"truncated",
);
expect(result.value.network.websocket_events[0]?.payload_shape).toEqual({
format: "text",
json_shape: null,
truncated: true,
});
expect(result.value.completeness.truncated_sections).toEqual(
expect.arrayContaining([
"console_text",
"json_body_shapes",
"websocket_shapes",
]),
);
});
it("fails closed on malformed approved response and binary payload encodings", async () => {
const browser = await startFakeCdpBrowser({
invalidResponseBodyBase64: true,
binaryWebSocketEvent: true,
invalidBinaryWebSocketEvent: true,
});
trackBrowser(browser);
const result = await new CdpBrowserProvider().inspectPage(
inspectWebPageInputSchema.parse({
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
observation_ms: 0,
include_json_body_shapes: true,
json_body_schema_approved: true,
include_websocket_shapes: true,
websocket_shape_approved: true,
}),
);
if (!result.ok) throw result.error;
expect(result.value.network.requests[0]?.body_shapes).toMatchObject({
status: "partial",
request: expect.any(Object),
response: null,
});
expect(result.value.network.websocket_events[1]).toMatchObject({
opcode: 2,
payload_bytes: 0,
payload_shape: { format: "binary", json_shape: null },
});
expect(result.value.completeness.unavailable_sections).toEqual(
expect.arrayContaining(["json_body_shapes", "websocket_frames"]),
);
});
});
const expectSensitiveValuesAbsent = (
inspection: ReturnType<typeof webPageInspectionSchema.parse>,
browser: FakeCdpBrowser,
): void => {
const serialized = JSON.stringify(inspection);
for (const secret of [
"page-secret",
"frame-secret",
"dom-secret",
"forbidden",
"resource-secret",
"script-secret",
"inline-secret",
"map-secret",
"network-secret",
"request-secret",
"request-body-secret",
"response-secret",
"response-body-secret",
"websocket-secret",
"websocket-url-secret",
"console-secret",
"unknown-origin-console-secret",
"unknown-console-value-secret",
"storage-secret",
"cookie-secret",
"indexed-db-secret",
"cache-body-secret",
"secret-id",
"dom-url-secret",
"link-secret",
"csp-secret",
"hash-secret",
"header-secret",
])
expect(serialized).not.toContain(secret);
const methods = browser.commands.map((command) => command.method);
expect(methods).toContain("Target.detachFromTarget");
expect(methods).not.toContain("Browser.close");
expect(methods).not.toContain("Target.closeTarget");
expect(methods).not.toContain("Runtime.evaluate");
expect(methods).not.toContain("Network.getResponseBody");
};

View File

@ -0,0 +1,15 @@
import { afterEach, describe } from "vitest";
import type { FakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
const browsers: FakeCdpBrowser[] = [];
afterEach(async () => {
await Promise.all(browsers.splice(0).map(async (browser) => browser.close()));
});
export const trackBrowser = (browser: FakeCdpBrowser): void => {
browsers.push(browser);
};
export const describeBrowser = describe;

View File

@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { CdpCaptureCompleteness } from "../src/browser/CdpCaptureCompleteness.js";
import { CdpCaptureCompleteness } from "../../../src/browser/CdpCaptureCompleteness.js";
describe("CdpCaptureCompleteness", () => {
it("reports attach-window limitations without claiming a complete history", () => {

View File

@ -5,8 +5,8 @@ import {
captureDom,
captureFrames,
captureResources,
} from "../src/browser/CdpCaptureDocuments.js";
import { inspectWebPageInputSchema } from "../src/domain/browserObservation.js";
} from "../../../src/browser/CdpCaptureDocuments.js";
import { inspectWebPageInputSchema } from "../../../src/domain/browserObservation.js";
const origin = "https://app.example.test";

View File

@ -1,10 +1,10 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { CdpConnection } from "../src/browser/CdpConnection.js";
import { CdpConnection } from "../../../src/browser/CdpConnection.js";
import {
startFakeCdpBrowser,
type FakeCdpBrowser,
} from "./fixtures/fakeCdpBrowser.js";
} from "../../fixtures/fakeCdpBrowser.js";
describe("CDP connection", () => {
const browsers: FakeCdpBrowser[] = [];

View File

@ -4,33 +4,31 @@ import { pathToFileURL } from "node:url";
import { afterEach, describe, expect, it } from "vitest";
import { createTestTempDirectory } from "./fixtures/temporaryDirectory.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { CdpElectronProvider } from "../src/browser/CdpElectronProvider.js";
import { CdpElectronProvider } from "../../../src/browser/CdpElectronProvider.js";
import {
inspectElectronPageInputSchema,
listElectronTargetsInputSchema,
} from "../src/domain/electronObservation.js";
} from "../../../src/domain/electronObservation.js";
import {
startFakeCdpBrowser,
type FakeCdpBrowser,
} from "./fixtures/fakeCdpBrowser.js";
} from "../../fixtures/fakeCdpBrowser.js";
describe("CdpElectronProvider", () => {
const browsers: FakeCdpBrowser[] = [];
const temporary: string[] = [];
const browsers: FakeCdpBrowser[] = [];
const temporary: string[] = [];
afterEach(async () => {
await Promise.all(
browsers.splice(0).map(async (browser) => browser.close()),
);
await Promise.all(
temporary
.splice(0)
.map(async (path) => rm(path, { recursive: true, force: true })),
);
});
afterEach(async () => {
await Promise.all(browsers.splice(0).map(async (browser) => browser.close()));
await Promise.all(
temporary
.splice(0)
.map(async (path) => rm(path, { recursive: true, force: true })),
);
});
describe("CdpElectronProvider target access", () => {
it("lists and inspects only canonical file targets beneath approved roots", async () => {
const root = await electronFixture();
const index = join(root, "index.html");
@ -164,7 +162,9 @@ describe("CdpElectronProvider", () => {
},
});
});
});
describe("CdpElectronProvider capture", () => {
it("captures Electron script source only after separate approval", async () => {
const root = await electronFixture();
const browser = await startFakeCdpBrowser({
@ -250,13 +250,13 @@ describe("CdpElectronProvider", () => {
},
});
});
const electronFixture = async (): Promise<string> => {
const root = await createTestTempDirectory("rea-electron-provider-");
temporary.push(root);
await writeFile(join(root, "index.html"), "<script src='app.js'></script>");
await writeFile(join(root, "app.js"), "export const app = true;");
await writeFile(join(root, "child.html"), "<div>child</div>");
return root;
};
});
const electronFixture = async (): Promise<string> => {
const root = await createTestTempDirectory("rea-electron-provider-");
temporary.push(root);
await writeFile(join(root, "index.html"), "<script src='app.js'></script>");
await writeFile(join(root, "app.js"), "export const app = true;");
await writeFile(join(root, "child.html"), "<div>child</div>");
return root;
};

View File

@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { safeResponseMetadata } from "../src/browser/CdpSafeMetadata.js";
import { safeResponseMetadata } from "../../../src/browser/CdpSafeMetadata.js";
const origin = "https://app.example.test";

View File

@ -15,7 +15,7 @@ import {
type TokenLifecycleEvent,
type CustomProtocolCapture,
type ProtocolFrame,
} from "../src/domain/customProtocolCapture.js";
} from "../../../src/domain/customProtocolCapture.js";
const sampleFrames: ProtocolFrame[] = [
{

View File

@ -1,38 +1,30 @@
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { afterEach, describe, expect, test } from "vitest";
import { describe, expect, test } from "vitest";
import { createJavaScriptRuntimeObservationEvidence } from "../src/application/JavaScriptRuntimeObservationEvidence.js";
import { createJavaScriptRuntimeObservationEvidence } from "../../../src/application/JavaScriptRuntimeObservationEvidence.js";
import {
listJavaScriptRuntimeTargets,
observeJavaScriptRuntime,
} from "../src/application/JavaScriptRuntimeObservationService.js";
import { createPermissionAuthority } from "../src/application/PermissionAuthority.js";
import { reconcileJavaScriptRuntimeEvidence } from "../src/application/JavaScriptRuntimeReconciliationService.js";
import { V8_INSPECTOR_PROVIDER_IDENTITY } from "../src/browser/V8InspectorProvider.js";
import { V8InspectorProvider } from "../src/browser/V8InspectorProvider.js";
import { JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE } from "../src/contracts/javascriptRuntimeReconciliationExample.js";
} from "../../../src/application/JavaScriptRuntimeObservationService.js";
import { createPermissionAuthority } from "../../../src/application/PermissionAuthority.js";
import { reconcileJavaScriptRuntimeEvidence } from "../../../src/application/JavaScriptRuntimeReconciliationService.js";
import { V8_INSPECTOR_PROVIDER_IDENTITY } from "../../../src/browser/V8InspectorProvider.js";
import { V8InspectorProvider } from "../../../src/browser/V8InspectorProvider.js";
import { JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE } from "../../../src/contracts/javascriptRuntimeReconciliationExample.js";
import type {
JavaScriptRuntimeObservation,
ObserveJavaScriptRuntimeInput,
} from "../src/domain/javascriptRuntimeObservation.js";
import { javascriptRuntimeReconciliationResultSchema } from "../src/domain/javascriptRuntimeReconciliationSchemas.js";
} from "../../../src/domain/javascriptRuntimeObservation.js";
import { javascriptRuntimeReconciliationResultSchema } from "../../../src/domain/javascriptRuntimeReconciliationSchemas.js";
import type {
PermissionCeiling,
PermissionGrant,
} from "../src/domain/permissionPolicy.js";
import { startFakeV8Inspector } from "./fixtures/fakeV8Inspector.js";
const temporaryRoots: string[] = [];
afterEach(async () => {
await Promise.all(
temporaryRoots.splice(0).map((path) => rm(path, { recursive: true })),
);
});
} from "../../../src/domain/permissionPolicy.js";
import { startFakeV8Inspector } from "../../fixtures/fakeV8Inspector.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
describe("passive V8 Inspector provider", () => {
test("filters target locations and never retains the excluded path", async () => {
@ -124,7 +116,9 @@ describe("passive V8 Inspector provider", () => {
}
},
);
});
describe("passive V8 Inspector evidence", () => {
test("marks capture truncation instead of claiming complete coverage", async () => {
const fixture = await runtimeFixture();
const second = join(fixture.root, "second.js");
@ -246,16 +240,14 @@ describe("passive V8 Inspector provider", () => {
});
const runtimeFixture = async () => {
const root = await mkdtemp(join(tmpdir(), "rea-v8-runtime-"));
temporaryRoots.push(root);
const root = await createTestTempDirectory("rea-v8-runtime-");
const entry = join(root, "entry.js");
await writeFile(entry, "export const value = 1;\n");
return { root, entry };
};
const temporaryFile = async (name: string): Promise<string> => {
const root = await mkdtemp(join(tmpdir(), "rea-v8-outside-"));
temporaryRoots.push(root);
const root = await createTestTempDirectory("rea-v8-outside-");
const path = join(root, name);
await writeFile(path, "export {};\n");
return path;

View File

@ -1,19 +1,19 @@
import { describe, expect, it } from "vitest";
import type { BrowserScenarioSessionPort } from "../src/browser/BrowserScenarioSessionPort.js";
import { PlaywrightBrowserScenarioProvider } from "../src/browser/PlaywrightBrowserScenarioProvider.js";
import { BrowserScenarioCaptureBudget } from "../src/browser/PlaywrightScenarioArtifacts.js";
import { sanitizeBrowserUrl } from "../src/domain/browserObservation.js";
import type { BrowserScenarioSessionPort } from "../../../src/browser/BrowserScenarioSessionPort.js";
import { PlaywrightBrowserScenarioProvider } from "../../../src/browser/PlaywrightBrowserScenarioProvider.js";
import { BrowserScenarioCaptureBudget } from "../../../src/browser/PlaywrightScenarioArtifacts.js";
import { sanitizeBrowserUrl } from "../../../src/domain/browserObservation.js";
import {
browserScenarioSchema,
type BrowserScenario,
type BrowserScenarioAction,
} from "../src/domain/browserScenario.js";
} from "../../../src/domain/browserScenario.js";
import {
browserStepArtifactsSchema,
type BrowserScenarioEvent,
type BrowserStepArtifacts,
} from "../src/domain/browserScenarioCapture.js";
} from "../../../src/domain/browserScenarioCapture.js";
const scenario = (
options: {

View File

@ -1,10 +1,10 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { withPlaywrightExecutionBoundary } from "../src/browser/PlaywrightExecutionBoundary.js";
import { withPlaywrightExecutionBoundary } from "../../../src/browser/PlaywrightExecutionBoundary.js";
import {
AnalysisCancelledError,
AnalysisTimeoutError,
} from "../src/domain/errors.js";
} from "../../../src/domain/errors.js";
describe("withPlaywrightExecutionBoundary", () => {
afterEach(() => vi.useRealTimers());

View File

@ -1,10 +1,10 @@
import { describe, expect, it } from "vitest";
import type { Page } from "playwright-core";
import { BrowserScenarioSecrets } from "../src/browser/BrowserScenarioSecrets.js";
import { BrowserScenarioCaptureBudget } from "../src/browser/PlaywrightScenarioArtifacts.js";
import { PlaywrightScenarioEvents } from "../src/browser/PlaywrightScenarioEvents.js";
import { browserScenarioSchema } from "../src/domain/browserScenario.js";
import { BrowserScenarioSecrets } from "../../../src/browser/BrowserScenarioSecrets.js";
import { BrowserScenarioCaptureBudget } from "../../../src/browser/PlaywrightScenarioArtifacts.js";
import { PlaywrightScenarioEvents } from "../../../src/browser/PlaywrightScenarioEvents.js";
import { browserScenarioSchema } from "../../../src/domain/browserScenario.js";
describe("PlaywrightScenarioEvents", () => {
it("bounds oversized page errors before validating captured events", () => {

View File

@ -10,7 +10,7 @@ import {
protocolCaptureSchema,
type ProtocolMessage,
type ProtocolCapture,
} from "../src/domain/protocolCapture.js";
} from "../../../src/domain/protocolCapture.js";
describe("protocol capture", () => {
it("classifies gRPC from content type", () => {

View File

@ -1,7 +1,7 @@
import { describe, expect, it } from "vitest";
import { fetchWebSourceMaps } from "../src/browser/WebSourceMapFetcher.js";
import { analyzeWebBundleInputSchema } from "../src/domain/webBundleAnalysis.js";
import { fetchWebSourceMaps } from "../../../src/browser/WebSourceMapFetcher.js";
import { analyzeWebBundleInputSchema } from "../../../src/domain/webBundleAnalysis.js";
const origin = "https://app.example.test";
const request = {
@ -10,7 +10,7 @@ const request = {
fetchUrl: `${origin}/assets/app.js.map?token=secret`,
};
describe("web source-map fetcher", () => {
describe("web source-map fetching and validation", () => {
it("fetches without credentials and derives mappings and original modules", async () => {
const calls: Array<{ url: string; init: RequestInit | undefined }> = [];
const map = JSON.stringify({
@ -157,7 +157,9 @@ describe("web source-map fetcher", () => {
);
}
});
});
describe("web source-map aggregate limits", () => {
it("bounds aggregate map count and reports omitted requests", async () => {
const calls: string[] = [];
const requests = [0, 1, 2].map((index) => ({

View File

@ -6,7 +6,7 @@ import { afterEach, describe, expect, it } from "vitest";
import {
startFakeCdpBrowser,
type FakeCdpBrowser,
} from "./fixtures/fakeCdpBrowser.js";
} from "../../fixtures/fakeCdpBrowser.js";
const execute = promisify(execFile);
const browsers: FakeCdpBrowser[] = [];
@ -117,6 +117,29 @@ describe("browser CLI parity", () => {
alignment: { status: "aligned", aligned_steps: 2 },
},
});
},
INTEGRATION_TEST_TIMEOUT_MS,
);
});
describe("browser CLI capture parity", () => {
it(
"returns the same analysis, session, WebMCP, screenshot, and policy contracts",
async () => {
const browser = await startFakeCdpBrowser({
sessionTimeline: "same_origin",
webMcpTools: true,
sensitiveShapes: true,
});
browsers.push(browser);
const environment = {
...process.env,
REA_BROWSER_OBSERVE_ENABLED: "true",
REA_BROWSER_CDP_ENDPOINTS_JSON: JSON.stringify([browser.endpoint]),
REA_BROWSER_ALLOWED_ORIGINS_JSON: JSON.stringify([
browser.allowedOrigin,
]),
};
const analyzed = await runCli(
[
"analyze-web-bundle",
@ -226,7 +249,9 @@ describe("browser CLI parity", () => {
},
INTEGRATION_TEST_TIMEOUT_MS,
);
});
describe("browser CLI approval", () => {
it(
"requires explicit approval before opening the CDP endpoint",
async () => {

View File

@ -1,7 +1,10 @@
import { afterEach, describe, expect, it } from "vitest";
import { isCliOperationFailure, logCliCommand } from "../src/cliLogging.js";
import { silentLogger } from "../src/logger.js";
import {
isCliOperationFailure,
logCliCommand,
} from "../../../src/cliLogging.js";
import { silentLogger } from "../../../src/logger.js";
const originalExitCode = process.exitCode;

View File

@ -1,24 +1,21 @@
import { execFile } from "node:child_process";
import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";
import { createPackageWithOptions } from "@electron/asar";
import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "./fixtures/temporaryDirectory.js";
import { fixtureTest } from "../../support/fixtures.js";
import {
renderCliOutputArgumentError,
sanitizeCliOutput,
validateCliOutputArguments,
} from "../src/cliOutput.js";
} from "../../../src/cliOutput.js";
const execFileAsync = promisify(execFile);
const CLI_INTEGRATION_TIMEOUT_MS = 60_000;
const CLI_VARIANT_TIMEOUT_MS = 120_000;
describe("CLI output boundary", () => {
describe("CLI output argument and sanitization boundary", () => {
it("rejects token windows that would corrupt structured output", () => {
for (const format of ["json", "jsonl", "yaml"] as const) {
const validation = validateCliOutputArguments([
@ -61,20 +58,20 @@ describe("CLI output boundary", () => {
).toEqual({ ok: true });
});
it("fails before emitting a truncated JSON document", async () => {
const execution = execFileAsync(
process.execPath,
["scripts/rea.mjs", "providers", "--token-limit", "5", "--json"],
{ cwd: process.cwd() },
);
const failure = await execution.catch((cause: unknown) => cause);
expect(failure).toMatchObject({ code: 1 });
const { stdout } = failure as { readonly stdout: string };
expect(JSON.parse(stdout)).toMatchObject({
ok: false,
error: { code: "UNSUPPORTED_OUTPUT_COMBINATION" },
});
});
fixtureTest(
"fails before emitting a truncated JSON document",
async ({ cli }) => {
const result = await cli.run({
arguments: ["providers", "--token-limit", "5", "--json"],
});
expect(result.exitCode).toBe(1);
expect(result.json).toMatchObject({
ok: false,
error: { code: "UNSUPPORTED_OUTPUT_COMBINATION" },
});
},
CLI_INTEGRATION_TIMEOUT_MS,
);
it("preserves normal output and sanitizes text and JSON validation errors", () => {
expect(sanitizeCliOutput("result: ok\n")).toBe("result: ok\n");
@ -125,34 +122,30 @@ describe("CLI output boundary", () => {
meta: { command: "analyze" },
});
});
});
it(
describe("compiled CLI output boundary", () => {
fixtureTest(
"sanitizes a real missing-argument dispatcher failure",
async () => {
await expect(
execFileAsync(process.execPath, ["scripts/rea.mjs", "analyze"], {
cwd: process.cwd(),
}),
).rejects.toMatchObject({
async ({ cli }) => {
const ordinary = await cli.run({ arguments: ["analyze"] });
expect(ordinary).toMatchObject({
exitCode: 1,
stdout:
'code: VALIDATION_ERROR\nmessage: "REA could not read the command arguments. Run `rea --help`, correct the arguments, then try again."\n',
});
await expect(
execFileAsync(
process.execPath,
["scripts/rea.mjs", "--full-output", "--json", "analyze"],
{ cwd: process.cwd() },
),
).rejects.toMatchObject({
const json = await cli.run({
arguments: ["--full-output", "--json", "analyze"],
});
expect(json).toMatchObject({
exitCode: 1,
stdout: expect.not.stringContaining("fieldErrors"),
});
await expect(
execFileAsync(
process.execPath,
["scripts/rea.mjs", "--full-output", "analyze"],
{ cwd: process.cwd() },
),
).rejects.toMatchObject({
const yaml = await cli.run({
arguments: ["--full-output", "analyze"],
});
expect(yaml).toMatchObject({
exitCode: 1,
stdout:
'ok: false\nerror:\n code: VALIDATION_ERROR\n message: "REA could not read the command arguments. Run `rea --help`, correct the arguments, then try again."\n',
});
@ -160,28 +153,22 @@ describe("CLI output boundary", () => {
CLI_INTEGRATION_TIMEOUT_MS,
);
it(
fixtureTest(
"preserves artifact diagnostics in ordinary and full JSON output",
async () => {
const root = await createTestTempDirectory("rea-cli-diagnostics-");
const source = join(root, "source");
async ({ cli, workspace }) => {
const source = workspace.path("source");
await mkdir(source);
await writeFile(join(source, "main.js"), "console.log('ok');\n");
const archive = join(root, "fixture.asar");
const archive = workspace.path("fixture.asar");
await createPackageWithOptions(source, archive, { unpack: "*.js" });
await writeFile(join(`${archive}.unpacked`, "main.js"), "changed();\n");
for (const flags of [["--json"], ["--full-output", "--json"]]) {
const execution = execFileAsync(process.execPath, [
"scripts/rea.mjs",
...flags,
"inventory-artifact",
archive,
]);
const failure = await execution.catch((cause: unknown) => cause);
expect(failure).toMatchObject({ code: 1 });
const { stdout } = failure as { readonly stdout: string };
const output = JSON.stringify(JSON.parse(stdout) as unknown);
const result = await cli.run({
arguments: [...flags, "inventory-artifact", archive],
});
expect(result.exitCode).toBe(1);
const output = JSON.stringify(result.json);
expect(output).toContain('"logical_path":"main.js"');
expect(output).toMatch(/"declared_sha256":"[a-f0-9]{64}"/u);
expect(output).toMatch(/"calculated_sha256":"[a-f0-9]{64}"/u);
@ -191,9 +178,9 @@ describe("CLI output boundary", () => {
CLI_INTEGRATION_TIMEOUT_MS,
);
it(
fixtureTest(
"keeps operation failure status independent of output controls",
async () => {
async ({ cli }) => {
const variants = [
["--format", "toon"],
["--format", "json"],
@ -207,43 +194,42 @@ describe("CLI output boundary", () => {
];
for (const flags of variants) {
await expect(
execFileAsync(process.execPath, [
"scripts/rea.mjs",
const result = await cli.run({
arguments: [
...flags,
"investigate-versions",
"/tmp/left",
"/tmp/right",
"/tmp/workspace.json",
]),
).rejects.toMatchObject({ code: 1, stdout: expect.any(String) });
],
});
expect(result).toMatchObject({
exitCode: 1,
stdout: expect.any(String),
});
}
},
CLI_VARIANT_TIMEOUT_MS,
);
it(
fixtureTest(
"keeps failure logs out of structured stdout",
async () => {
const execution = execFileAsync(
process.execPath,
[
"scripts/rea.mjs",
async ({ cli }) => {
const result = await cli.run({
arguments: [
"--json",
"investigate-versions",
"/tmp/left",
"/tmp/right",
"/tmp/workspace.json",
],
{ env: { ...process.env, REA_LOG_LEVEL: "error" } },
);
const failure = await execution.catch((cause: unknown) => cause);
expect(failure).toMatchObject({
code: 1,
environment: { REA_LOG_LEVEL: "error" },
});
expect(result).toMatchObject({
exitCode: 1,
stderr: expect.stringContaining('"status":"error"'),
});
const { stdout } = failure as { readonly stdout: string };
expect(JSON.parse(stdout)).toMatchObject({ error: "ApprovalRequired" });
expect(result.json).toMatchObject({ error: "ApprovalRequired" });
},
CLI_INTEGRATION_TIMEOUT_MS,
);

View File

@ -5,13 +5,13 @@ import { promisify } from "node:util";
import { afterEach, describe, expect, it, vi } from "vitest";
import { createTestTempDirectory } from "./fixtures/temporaryDirectory.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import {
readProjectPermissionStore,
writeProjectPermissionStore,
} from "../src/application/ProjectPermissionStore.js";
import { approvePolicyRevocation } from "../src/cliPolicyCommands.js";
} from "../../../src/application/ProjectPermissionStore.js";
import { approvePolicyRevocation } from "../../../src/cliPolicyCommands.js";
const execute = promisify(execFile);
const roots: string[] = [];

View File

@ -5,7 +5,7 @@ import { promisify } from "node:util";
import { spawn } from "@lydell/node-pty";
import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "./fixtures/temporaryDirectory.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
const execute = promisify(execFile);
const decisionMarker = "REA_SETUP_DECISION:";
@ -136,7 +136,9 @@ describe("interactive setup journey", () => {
expect(result.output).toContain("Cursor (detected)");
expect(result.decision.approved).toBe(false);
});
});
describe("interactive setup selection and confirmation", () => {
it("retains the bundled skill even when all agents are deselected (#399)", async () => {
const result = await runJourney([
step("What should REA set up?", "\r"),
@ -241,7 +243,9 @@ describe("interactive setup journey", () => {
expect(result.output).toContain("Nothing selected. No changes were made.");
expect(result.decision.selectedActionIds).toEqual([]);
});
});
describe("interactive setup completion", () => {
it("frames completion around the capabilities that are now ready", async () => {
const { stderr } = await execute(
process.execPath,

View File

@ -0,0 +1,103 @@
import { copyFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect } from "vitest";
import { fixtureTest } from "../../support/fixtures.js";
describe("executable dispatcher", () => {
fixtureTest(
"reserves production MCP routing for bare mode arguments",
async ({ processes, workspace }) => {
const scripts = await workspace.mkdir("scripts");
await workspace.mkdir("dist");
await Promise.all([
copyFile("scripts/rea.mjs", join(scripts, "rea.mjs")),
copyFile(
"scripts/package-runner-bootstrap.mjs",
join(scripts, "package-runner-bootstrap.mjs"),
),
workspace.write(
"package.json",
JSON.stringify({ type: "module", version: "1.0.0" }),
),
workspace.write(
"dist/main.js",
'export const runEntrypoint = () => process.stdout.write(JSON.stringify({ adapter: "mcp" }));\n',
),
workspace.write(
"dist/cli.js",
'export const createCli = () => ({ serve: (args) => process.stdout.write(JSON.stringify({ adapter: "cli", args })) });\n',
),
workspace.write(
"dist/cliOutput.js",
"export const sanitizeCliOutput = (output) => output; export const validateCliOutputArguments = () => ({ ok: true }); export const renderCliOutputArgumentError = () => '';\n",
),
workspace.write(
"dist/mcpDoctor.js",
'export const runProductionMcpDoctorCli = (args) => Promise.resolve({ output: JSON.stringify({ adapter: "mcp-doctor", args }), exitCode: 0 });\n',
),
]);
const invoke = async (args: readonly string[]): Promise<unknown> => {
const { exitCode, stdout } = await processes.run(
process.execPath,
[join(scripts, "rea.mjs"), ...args],
{ cwd: workspace.root },
);
expect(exitCode).toBe(0);
return JSON.parse(stdout) as unknown;
};
await expect(invoke(["mcp"])).resolves.toEqual({ adapter: "mcp" });
await expect(invoke(["--mcp"])).resolves.toEqual({ adapter: "mcp" });
await expect(invoke(["mcp", "doctor", "--json"])).resolves.toEqual({
adapter: "mcp-doctor",
args: ["--json"],
});
await expect(invoke(["mcp", "add"])).resolves.toEqual({
adapter: "cli",
args: ["mcp", "add"],
});
await expect(invoke(["--mcp", "extra"])).resolves.toEqual({
adapter: "cli",
args: ["--mcp", "extra"],
});
},
);
fixtureTest(
"explains how to restore a missing compiled runtime",
async ({ processes, workspace }) => {
const scripts = await workspace.mkdir("scripts");
await Promise.all([
copyFile("scripts/rea.mjs", join(scripts, "rea.mjs")),
copyFile(
"scripts/package-runner-bootstrap.mjs",
join(scripts, "package-runner-bootstrap.mjs"),
),
workspace.write(
"package.json",
JSON.stringify({ type: "module", version: "1.0.0" }),
),
]);
for (const args of [["mcp"], ["mcp", "doctor"], ["--help"]]) {
const result = await processes.run(
process.execPath,
[join(scripts, "rea.mjs"), ...args],
{ cwd: workspace.root },
);
expect(result).toMatchObject({
exitCode: 1,
stderr: expect.stringContaining(
"REA's compiled runtime is missing. Run `npm ci` in",
),
});
expect(result).toMatchObject({
stderr: expect.not.stringContaining("ERR_MODULE_NOT_FOUND"),
});
}
},
);
});

View File

@ -1,7 +1,7 @@
import { describe, expect, it } from "vitest";
import { runDoctor, type DoctorHost } from "../src/application/Doctor.js";
import { CATALOG_IDENTITY } from "../src/catalogIdentity.js";
import { PRODUCT_IDENTITY } from "../src/identity.js";
import { runDoctor, type DoctorHost } from "../../../src/application/Doctor.js";
import { CATALOG_IDENTITY } from "../../../src/catalogIdentity.js";
import { PRODUCT_IDENTITY } from "../../../src/identity.js";
const host = (overrides: Partial<DoctorHost> = {}): DoctorHost => ({
platform: "darwin",
@ -167,7 +167,9 @@ describe("doctor", () => {
"Unset or update HOPPER_LAUNCHER_PATH to an executable Hopper launcher.",
});
});
});
describe("doctor installation identity", () => {
it("distinguishes stale skills, path shadowing, and unobservable live state", async () => {
const result = await runDoctor(
undefined,
@ -242,7 +244,9 @@ describe("doctor", () => {
}),
);
});
});
describe("doctor scoped readiness", () => {
it("scopes effective readiness to selected clients without hiding drift", async () => {
const registrations = [
{
@ -322,6 +326,9 @@ describe("doctor", () => {
});
expect(selectedGhidra.healthy).toBe(true);
});
});
describe("doctor Linux and Hopper discovery", () => {
it("accepts an officially supported Linux distribution", async () => {
const path = "/home/user/.local/share/rea/hopper/bin/Hopper";
const result = await runDoctor(
@ -445,64 +452,4 @@ describe("doctor", () => {
expect(result.hopperPath).toBeUndefined();
expect(result.checks.find(({ name }) => name === "hopper")?.ok).toBe(false);
});
it("reports a broken shadowed Node candidate without hiding the healthy launcher", async () => {
const result = await runDoctor(
undefined,
host({
runtimeExecutables: () =>
Promise.resolve({
launcher_node: "/healthy/node",
candidates: [
{
tool: "node",
lexical_path: "/healthy/node",
canonical_path: "/healthy/node",
path_index: null,
selection: "rea-launcher",
version: "v24.18.0",
healthy: true,
failure: null,
},
{
tool: "node",
lexical_path: "/opt/homebrew/bin/node",
canonical_path: "/opt/homebrew/Cellar/node/25.2.1/bin/node",
path_index: 2,
selection: "path-shadowed",
version: null,
healthy: false,
failure: {
code: "runtime_dynamic_library_missing",
exit_code: 134,
signal: null,
dependency:
"/opt/homebrew/opt/simdjson/lib/libsimdjson.29.dylib",
stderr: "dyld: Library not loaded",
},
},
],
}),
}),
);
expect(result.healthy).toBe(false);
expect(result.identity?.runtime_executables).toMatchObject({
launcher_node: "/healthy/node",
candidates: [
{ healthy: true, selection: "rea-launcher" },
{
healthy: false,
failure: { code: "runtime_dynamic_library_missing" },
},
],
});
expect(
result.checks.find(({ name }) => name === "node-toolchains"),
).toMatchObject({
ok: false,
classification: "missing_dependency",
remediation: expect.stringContaining("Do not create"),
});
});
});

View File

@ -0,0 +1,93 @@
import { describe, expect, it } from "vitest";
import { runDoctor, type DoctorHost } from "../../../src/application/Doctor.js";
import { CATALOG_IDENTITY } from "../../../src/catalogIdentity.js";
import { PRODUCT_IDENTITY } from "../../../src/identity.js";
const host = (overrides: Partial<DoctorHost> = {}): DoctorHost => ({
platform: "darwin",
architecture: "x64",
nodeVersion: "24.18.0",
macosVersion: () => Promise.resolve("12.0"),
linuxDistribution: () => Promise.resolve(undefined),
validTarget: (path) => Promise.resolve(path.includes("Hopper")),
executable: (path) => Promise.resolve(path.includes("Hopper")),
supportedLinuxHopper: () => Promise.resolve(true),
linuxDemoRuntimeCheck: () =>
Promise.resolve({
name: "hopper-demo-runtime",
ok: true,
classification: "healthy",
}),
brewHopperPath: () => Promise.resolve(undefined),
manualHopperPaths: () => Promise.resolve([]),
installedSkillIdentity: () =>
Promise.resolve({
version: PRODUCT_IDENTITY.skillVersion,
toolCount: CATALOG_IDENTITY.counts.mcp_tools,
catalogDigest: CATALOG_IDENTITY.digests.combined_sha256,
}),
...overrides,
});
describe("doctor runtime executable diagnostics", () => {
it("reports a broken shadowed Node candidate without hiding the healthy launcher", async () => {
const result = await runDoctor(
undefined,
host({
runtimeExecutables: () =>
Promise.resolve({
launcher_node: "/healthy/node",
candidates: [
{
tool: "node",
lexical_path: "/healthy/node",
canonical_path: "/healthy/node",
path_index: null,
selection: "rea-launcher",
version: "v24.18.0",
healthy: true,
failure: null,
},
{
tool: "node",
lexical_path: "/opt/homebrew/bin/node",
canonical_path: "/opt/homebrew/Cellar/node/25.2.1/bin/node",
path_index: 2,
selection: "path-shadowed",
version: null,
healthy: false,
failure: {
code: "runtime_dynamic_library_missing",
exit_code: 134,
signal: null,
dependency:
"/opt/homebrew/opt/simdjson/lib/libsimdjson.29.dylib",
stderr: "dyld: Library not loaded",
},
},
],
}),
}),
);
expect(result.healthy).toBe(false);
expect(result.identity?.runtime_executables).toMatchObject({
launcher_node: "/healthy/node",
candidates: [
{ healthy: true, selection: "rea-launcher" },
{
healthy: false,
failure: { code: "runtime_dynamic_library_missing" },
},
],
});
expect(
result.checks.find(({ name }) => name === "node-toolchains"),
).toMatchObject({
ok: false,
classification: "missing_dependency",
remediation: expect.stringContaining("Do not create"),
});
});
});

View File

@ -6,33 +6,30 @@ import { promisify } from "node:util";
import { afterEach, describe, expect, it } from "vitest";
import { createTestTempDirectory } from "./fixtures/temporaryDirectory.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import {
startFakeCdpBrowser,
type FakeCdpBrowser,
} from "./fixtures/fakeCdpBrowser.js";
import { writeElectronBoundaryFixture } from "./fixtures/electronBoundaryApplication.js";
import { JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE } from "../src/contracts/javascriptRuntimeReconciliationExample.js";
} from "../../fixtures/fakeCdpBrowser.js";
import { writeElectronBoundaryFixture } from "../../fixtures/electronBoundaryApplication.js";
import { JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE } from "../../../src/contracts/javascriptRuntimeReconciliationExample.js";
const execute = promisify(execFile);
const INTEGRATION_TEST_TIMEOUT_MS = 20_000;
const browsers: FakeCdpBrowser[] = [];
const temporary: string[] = [];
afterEach(async () => {
await Promise.all(browsers.splice(0).map(async (browser) => browser.close()));
await Promise.all(
temporary
.splice(0)
.map(async (path) => rm(path, { recursive: true, force: true })),
);
});
describe("Electron CLI parity", () => {
const browsers: FakeCdpBrowser[] = [];
const temporary: string[] = [];
afterEach(async () => {
await Promise.all(
browsers.splice(0).map(async (browser) => browser.close()),
);
await Promise.all(
temporary
.splice(0)
.map(async (path) => rm(path, { recursive: true, force: true })),
);
});
it(
"returns the same root-confined Electron Evidence contracts",
async () => {
@ -117,7 +114,9 @@ describe("Electron CLI parity", () => {
},
INTEGRATION_TEST_TIMEOUT_MS,
);
});
describe("Electron CLI artifact behavior", () => {
it(
"keeps artifact format separate from structured output format",
async () => {
@ -211,7 +210,9 @@ describe("Electron CLI parity", () => {
},
INTEGRATION_TEST_TIMEOUT_MS,
);
});
describe("Electron CLI runtime reconciliation", () => {
it(
"reads a file and returns the derived static/runtime reconciliation contract",
async () => {

View File

@ -6,12 +6,12 @@ import { promisify } from "node:util";
import { afterEach, describe, expect, test } from "vitest";
import { loadConfiguredPermissionAuthority } from "../src/application/PermissionConfiguration.js";
import { observeJavaScriptRuntime } from "../src/application/JavaScriptRuntimeObservationService.js";
import { V8InspectorProvider } from "../src/browser/V8InspectorProvider.js";
import { parseConfig } from "../src/config.js";
import { startFakeV8Inspector } from "./fixtures/fakeV8Inspector.js";
import { createTestTempDirectory } from "./fixtures/temporaryDirectory.js";
import { loadConfiguredPermissionAuthority } from "../../../src/application/PermissionConfiguration.js";
import { observeJavaScriptRuntime } from "../../../src/application/JavaScriptRuntimeObservationService.js";
import { V8InspectorProvider } from "../../../src/browser/V8InspectorProvider.js";
import { parseConfig } from "../../../src/config.js";
import { startFakeV8Inspector } from "../../fixtures/fakeV8Inspector.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
const execute = promisify(execFile);
const TIMEOUT_MS = 20_000;

View File

@ -6,8 +6,8 @@ import { describe, expect, it } from "vitest";
import {
packageRunnerSetupPlan,
runPackageRunnerSetupBootstrap,
} from "../scripts/package-runner-bootstrap.mjs";
import { createTestTempDirectory } from "./fixtures/temporaryDirectory.js";
} from "../../../scripts/package-runner-bootstrap.mjs";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
describe("package-runner setup bootstrap", () => {
it("reruns bare setup through the current release when npm selected a local package", () => {

View File

@ -6,15 +6,15 @@ import { promisify } from "node:util";
import { afterEach, describe, expect, it } from "vitest";
import { createTestTempDirectory } from "./fixtures/temporaryDirectory.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import {
captureProcessScenarioFile,
compareProcessEvidenceFiles,
projectProcessCliError,
} from "../src/application/ProcessCli.js";
import { createEvidence } from "../src/domain/evidence.js";
import { PROCESS_PROVIDER } from "../src/application/ProcessEvidence.js";
} from "../../../src/application/ProcessCli.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import { PROCESS_PROVIDER } from "../../../src/application/ProcessEvidence.js";
const roots: string[] = [];
const execFileAsync = promisify(execFile);

View File

@ -5,9 +5,9 @@ import { fileURLToPath } from "node:url";
import { afterEach, describe, expect, it } from "vitest";
import { z } from "zod";
import { createTestTempDirectory } from "./fixtures/temporaryDirectory.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { SUPPORTED_CLIENT_DEFINITIONS } from "../src/application/SupportedClients.js";
import { SUPPORTED_CLIENT_DEFINITIONS } from "../../../src/application/SupportedClients.js";
import {
ARTIFACT_GRAPH_PROVIDER,
JAVASCRIPT_APPLICATION_PROVIDER,
@ -15,30 +15,30 @@ import {
JAVASCRIPT_RUNTIME_RECONCILIATION_PROVIDER,
MANAGED_STATIC_PROVIDER,
MANAGED_WORKFLOW_PROVIDER,
} from "../src/application/InvestigationProviders.js";
import { CDP_BROWSER_PROVIDER_IDENTITY } from "../src/browser/CdpBrowserProvider.js";
import { CDP_ELECTRON_PROVIDER_IDENTITY } from "../src/browser/CdpElectronProvider.js";
import { PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY } from "../src/browser/PlaywrightBrowserScenarioProvider.js";
import { V8_INSPECTOR_PROVIDER_IDENTITY } from "../src/browser/V8InspectorProvider.js";
import { CLI_COMMAND_NAMES } from "../src/cliCommandNames.js";
import { createCli } from "../src/cli.js";
import { TOOL_CONTRACTS } from "../src/contracts/toolContracts.js";
import { analysisSnapshotSchema } from "../src/domain/analysisSnapshot.js";
import { investigationWorkspaceSchema } from "../src/domain/investigationWorkspace.js";
} from "../../../src/application/InvestigationProviders.js";
import { CDP_BROWSER_PROVIDER_IDENTITY } from "../../../src/browser/CdpBrowserProvider.js";
import { CDP_ELECTRON_PROVIDER_IDENTITY } from "../../../src/browser/CdpElectronProvider.js";
import { PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY } from "../../../src/browser/PlaywrightBrowserScenarioProvider.js";
import { V8_INSPECTOR_PROVIDER_IDENTITY } from "../../../src/browser/V8InspectorProvider.js";
import { CLI_COMMAND_NAMES } from "../../../src/cliCommandNames.js";
import { createCli } from "../../../src/cli.js";
import { TOOL_CONTRACTS } from "../../../src/contracts/toolContracts.js";
import { analysisSnapshotSchema } from "../../../src/domain/analysisSnapshot.js";
import { investigationWorkspaceSchema } from "../../../src/domain/investigationWorkspace.js";
import {
HOPPER_PROVIDER_IDENTITY,
HOPPER_PROVIDER_TOOL_CONTRACTS,
} from "../src/hopper/HopperProvider.js";
} from "../../../src/hopper/HopperProvider.js";
import {
GHIDRA_PROVIDER_IDENTITY,
GHIDRA_PROVIDER_TOOL_CONTRACTS,
} from "../src/ghidra/GhidraProvider.js";
import { NATIVE_MACOS_PROVIDER_IDENTITY } from "../src/native/NativeMacOSProvider.js";
} from "../../../src/ghidra/GhidraProvider.js";
import { NATIVE_MACOS_PROVIDER_IDENTITY } from "../../../src/native/NativeMacOSProvider.js";
import {
assertDocumentationFacts,
documentationFactIssues,
} from "../scripts/lib/docs-facts.mjs";
import { ensureGeneratedFile } from "../scripts/lib/generated-file.mjs";
} from "../../../scripts/lib/docs-facts.mjs";
import { ensureGeneratedFile } from "../../../scripts/lib/generated-file.mjs";
import {
createCliInventory,
cliCommandDescriptionIssues,
@ -46,9 +46,9 @@ import {
createProductCatalog,
providerCatalogDigest,
serializeProductCatalog,
} from "../scripts/lib/product-catalog.mjs";
} from "../../../scripts/lib/product-catalog.mjs";
const root = fileURLToPath(new URL("..", import.meta.url));
const root = fileURLToPath(new URL("../../..", import.meta.url));
const temporaryRoots: string[] = [];
afterEach(async () => {
@ -166,15 +166,17 @@ describe("canonical product catalog", () => {
assertDocumentationFacts(root, catalog),
).resolves.toBeUndefined();
await expect(documentationFactIssues(root, catalog)).resolves.toEqual([]);
}, 15_000);
}, 60_000);
});
describe("canonical CLI catalog", () => {
it("uses the same primary command names as the actual Incur router", () => {
const inventory = createCliInventory(createCli());
expect(inventory.primary).toEqual([...CLI_COMMAND_NAMES].sort());
expect(inventory.aliases).toEqual([
{ name: "compare-bundles", target: "compare" },
]);
});
}, 30_000);
it("describes every primary command argument and option", () => {
expect(cliCommandDescriptionIssues(createCli())).toEqual([]);
@ -195,7 +197,9 @@ describe("canonical product catalog", () => {
expect(cliCommandOptionNames(cli, name)).toContain("provider");
}
});
});
describe("canonical product catalog drift", () => {
it("reports tool-family, setup-client, and schema fact drift", async () => {
const catalog = await createProductCatalog(root);
const firstFamily = catalog.tools.families[0];

View File

@ -3,7 +3,7 @@ import { promisify } from "node:util";
import { describe, expect, it } from "vitest";
import { createEvidenceBundle } from "../src/domain/evidenceBundle.js";
import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
const execute = promisify(execFile);

View File

@ -3,7 +3,7 @@ import { promisify } from "node:util";
import { describe, expect, it } from "vitest";
import { RECONSTRUCTION_READINESS_EXAMPLE } from "../src/contracts/reconstructionReadinessExample.js";
import { RECONSTRUCTION_READINESS_EXAMPLE } from "../../../src/contracts/reconstructionReadinessExample.js";
const execute = promisify(execFile);

View File

@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { parseConfig } from "../../src/config.js";
import { parseConfig } from "../../../src/config.js";
describe("configuration parsing integration", () => {
it("returns defaults when no environment variables are set", () => {

View File

@ -4,7 +4,7 @@ import { spawnSync } from "node:child_process";
import { afterEach, describe, expect, it } from "vitest";
import { createTestTempDirectory } from "./fixtures/temporaryDirectory.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
const roots: string[] = [];
const script = join(process.cwd(), "scripts", "check-dependency-install.mjs");

View File

@ -3,19 +3,19 @@ import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { createTestTempDirectory } from "./fixtures/temporaryDirectory.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import {
readEvidenceBundle,
writeEvidenceBundle,
} from "../src/application/EvidenceBundleFiles.js";
import { compareEvidenceBundlesCommand } from "../src/application/EvidenceBundleCommands.js";
import { createEvidence } from "../src/domain/evidence.js";
} from "../../../src/application/EvidenceBundleFiles.js";
import { compareEvidenceBundlesCommand } from "../../../src/application/EvidenceBundleCommands.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import {
createEvidenceBundle,
serializeEvidenceBundle,
type EvidenceFilePolicy,
} from "../src/domain/evidenceBundle.js";
} from "../../../src/domain/evidenceBundle.js";
let directory: string | undefined;

View File

@ -13,7 +13,7 @@ import { promisify } from "node:util";
import { afterEach, describe, expect, it } from "vitest";
import { createTestTempDirectory } from "./fixtures/temporaryDirectory.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
const execFileAsync = promisify(execFile);
const roots: string[] = [];

View File

@ -5,18 +5,18 @@ import { join } from "node:path";
import { add, commit, init } from "isomorphic-git";
import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "./fixtures/temporaryDirectory.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import {
importReferenceSource,
normalizeHistoricalSourceParseFailures,
} from "../src/application/ReferenceSourceImport.js";
import { projectReferenceSourceEntryFailure } from "../src/application/ReferenceSourceImportEntries.js";
} from "../../../src/application/ReferenceSourceImport.js";
import { projectReferenceSourceEntryFailure } from "../../../src/application/ReferenceSourceImportEntries.js";
import {
projectReferenceSourceImportError,
type ReferenceSourceImportError,
} from "../src/application/ReferenceSourceImportTypes.js";
import { createHistoricalSourceManifest } from "../src/domain/referenceSourceGraph.js";
} from "../../../src/application/ReferenceSourceImportTypes.js";
import { createHistoricalSourceManifest } from "../../../src/domain/referenceSourceGraph.js";
const limits = {
maxBytes: 1024 * 1024,
@ -55,7 +55,7 @@ const importTree = (
...(signal === undefined ? {} : { signal }),
});
describe("reference source import", () => {
describe("reference source import error projection", () => {
it("deduplicates exact parse failures without collapsing distinct reasons", () => {
const malformed = {
path: "src/main.ts",
@ -122,7 +122,9 @@ describe("reference source import", () => {
);
}
});
});
describe("reference source import behavior", () => {
it("is relocation-stable, resolves imports, and excludes secrets before capture", async () => {
const parent = await createTestTempDirectory("rea-reference-import-");
try {

View File

@ -3,9 +3,9 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "./fixtures/temporaryDirectory.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { readReferenceSource } from "../src/reference/ReferenceSourceReader.js";
import { readReferenceSource } from "../../../src/reference/ReferenceSourceReader.js";
const limits = {
maxBytes: 1_024,
@ -14,7 +14,7 @@ const limits = {
maxPathBytes: 100,
} as const;
describe("readReferenceSource", () => {
describe("readReferenceSource entries", () => {
it("returns explicit entries in canonical code-point path order", async () => {
const root = await createTestTempDirectory("rea-reference-");
await mkdir(join(root, "nested"));
@ -148,7 +148,9 @@ describe("readReferenceSource", () => {
expect(result.value.bytesRead).toBe(1);
expect(result.value.truncated).toBe(true);
});
});
describe("readReferenceSource failures and exclusions", () => {
it("applies exclusions to normalized paths before reading entries", async () => {
const root = await createTestTempDirectory("rea-reference-");
await mkdir(join(root, "ignored"));

View File

@ -5,9 +5,9 @@ import { Readable } from "node:stream";
import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "./fixtures/temporaryDirectory.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { SafeOutputTree } from "../src/artifacts/SafeOutputTree.js";
import { SafeOutputTree } from "../../../src/artifacts/SafeOutputTree.js";
const LIMITS = {
maxEntries: 10,

View File

@ -4,7 +4,7 @@ import { dirname, join } from "node:path";
import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "./fixtures/temporaryDirectory.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
describe.sequential("test temporary directory ownership", () => {
let completedDirectory: string | undefined;

View File

@ -0,0 +1,467 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { createPackageWithOptions } from "@electron/asar";
import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js";
import { expect, it } from "vitest";
import { z } from "zod";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import {
composeBinarySessionFromFactory,
composeBinarySessionFromProvider,
} from "../../../src/application/BinarySessionComposition.js";
import { ArtifactProvider } from "../../../src/artifacts/ArtifactProvider.js";
import { ARTIFACT_COMPARISON_EXAMPLE } from "../../../src/contracts/artifactComparisonExample.js";
import { evidenceBundleSchema } from "../../../src/domain/evidenceBundle.js";
import { createServer } from "../../../src/server/createServer.js";
import { observed } from "../../fixtures/analysisExecution.js";
it.each([
{ unpacked: false, label: "embedded" },
{ unpacked: true, label: "unpacked" },
])(
"returns actionable local details for an $label ASAR integrity error",
async ({ unpacked }) => {
const root = await createTestTempDirectory("rea-asar-integrity-mcp-");
const source = join(root, "source");
const original = "console.log('ok');\n";
const changed = "console.log('no');\n";
await mkdir(source);
await writeFile(join(source, "main.js"), original);
const archive = join(root, "fixture.asar");
await createPackageWithOptions(
source,
archive,
unpacked ? { unpack: "*.js" } : {},
);
if (unpacked) {
await writeFile(join(`${archive}.unpacked`, "main.js"), changed);
} else {
const bytes = await readFile(archive);
const contentOffset = bytes.indexOf(original);
expect(contentOffset).toBeGreaterThanOrEqual(0);
bytes.write(changed, contentOffset, "utf8");
await writeFile(archive, bytes);
}
const session = composeBinarySessionFromProvider(new ArtifactProvider());
const server = createServer(session, session);
const client = new Client({ name: "asar-integrity-test", version: "1" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const opened = await client.callTool({
name: "open_binary",
arguments: { path: archive },
});
expect(opened.isError).not.toBe(true);
const result = await client.callTool({
name: "inventory_artifact",
arguments: {},
});
expect(result.isError).toBe(true);
expect(result.structuredContent).toEqual({
error: {
code: "artifact_integrity_mismatch",
category: "integrity_mismatch",
message:
"Artifact is invalid or has changed. Get a fresh copy and try again.",
details: {
logical_path: "main.js",
declared_sha256: expect.stringMatching(/^[a-f0-9]{64}$/u),
calculated_sha256: expect.stringMatching(/^[a-f0-9]{64}$/u),
unpacked,
},
retryable: false,
remediation: {
action:
"Artifact is invalid or has changed. Get a fresh copy and try again.",
restart_required: false,
},
},
});
} finally {
await Promise.allSettled([
client.close(),
server.close(),
session.close(),
]);
}
},
20_000,
);
const artifactInventoryResultSchema = z.object({
occurrences: z.object({
items: z.array(
z.object({ logical_path: z.string(), hash_status: z.string() }),
),
}),
integrity_contradictions: z.array(
z.object({
logical_path: z.string(),
declared_sha256: z.string(),
observed_sha256: z.string(),
trust: z.literal("observed-untrusted"),
}),
),
});
it("records an approved mismatch, preserves verified siblings, and never reports equivalence", async () => {
const root = await createTestTempDirectory("rea-asar-continue-mcp-");
const source = join(root, "source");
await mkdir(source);
const original = "console.log('ok');\n",
changed = "console.log('no');\n",
secondOriginal = "console.log('up');\n",
secondChanged = "console.log('dn');\n";
await writeFile(join(source, "main.js"), original);
await writeFile(join(source, "second.js"), secondOriginal);
await writeFile(join(source, "sibling.js"), "console.log('sibling');\n");
const archive = join(root, "fixture.asar");
await createPackageWithOptions(source, archive, {});
const bytes = await readFile(archive);
const contentOffset = bytes.indexOf(original);
const secondOffset = bytes.indexOf(secondOriginal);
expect(contentOffset).toBeGreaterThanOrEqual(0);
expect(secondOffset).toBeGreaterThanOrEqual(0);
bytes.write(changed, contentOffset, "utf8");
bytes.write(secondChanged, secondOffset, "utf8");
await writeFile(archive, bytes);
const session = composeBinarySessionFromProvider(
new ArtifactProvider(false, true),
);
const server = createServer(session, session);
const client = new Client({ name: "asar-continue-test", version: "1" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
await client.callTool({
name: "open_binary",
arguments: { path: archive },
});
const limited = await client.callTool({
name: "inventory_artifact",
arguments: {
integrity_policy: "record-and-continue",
integrity_continue_approved: true,
max_integrity_mismatches: 1,
},
});
expect(limited).toMatchObject({
isError: true,
structuredContent: { error: { code: "truncated" } },
});
const result = await client.callTool({
name: "inventory_artifact",
arguments: {
integrity_policy: "record-and-continue",
integrity_continue_approved: true,
},
});
expect(result.isError).not.toBe(true);
const compact = compactResult(result.structuredContent);
const evidence = session.evidenceById(compact.evidence_id);
if (evidence === undefined) throw new Error("missing inventory Evidence");
const inventory = artifactInventoryResultSchema.parse(compact.result);
expect(inventory.integrity_contradictions).toEqual(
expect.arrayContaining([
expect.objectContaining({
logical_path: "main.js",
trust: "observed-untrusted",
declared_sha256: expect.stringMatching(/^[a-f0-9]{64}$/u),
observed_sha256: expect.stringMatching(/^[a-f0-9]{64}$/u),
}),
expect.objectContaining({ logical_path: "second.js" }),
]),
);
expect(inventory.integrity_contradictions).toHaveLength(2);
expect(inventory.occurrences.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
logical_path: "main.js",
hash_status: "mismatched",
}),
expect.objectContaining({
logical_path: "sibling.js",
hash_status: "verified",
}),
expect.objectContaining({
logical_path: "second.js",
hash_status: "mismatched",
}),
]),
);
const inspected = await client.callTool({
name: "inspect_artifact",
arguments: {
integrity_policy: "record-and-continue",
integrity_continue_approved: true,
},
});
expect(inspected.isError).not.toBe(true);
expect(compactResult(inspected.structuredContent).result).toMatchObject({
contradictions: [
expect.objectContaining({ occurrence_id: expect.any(String) }),
expect.objectContaining({ occurrence_id: expect.any(String) }),
],
substeps: [
expect.objectContaining({
operation: "inventory_artifact",
status: "completed",
}),
],
});
const compared = await client.callTool({
name: "compare_artifacts",
arguments: {
left_evidence_ids: [evidence.evidence_id],
right_evidence_ids: [evidence.evidence_id],
},
});
expect(compared.isError).not.toBe(true);
expect(compactResult(compared.structuredContent).result).toMatchObject({
status: "contradiction",
summary: { contradiction: 2 },
changes: {
items: expect.arrayContaining([
expect.objectContaining({
logical_path: "main.js",
classification: "contradiction",
dimensions: ["integrity"],
}),
expect.objectContaining({
logical_path: "second.js",
classification: "contradiction",
}),
]),
},
});
} finally {
await Promise.allSettled([client.close(), server.close(), session.close()]);
}
});
it("rejects altered payloads that reuse session Evidence IDs", async () => {
const session = composeBinarySessionFromFactory(() => ({
health: () => Promise.resolve(),
execute: () => Promise.resolve(observed(null)),
close: () => Promise.resolve(),
}));
session.recordEvidence(ARTIFACT_COMPARISON_EXAMPLE.left);
session.recordEvidence(ARTIFACT_COMPARISON_EXAMPLE.right);
const server = createServer(session, session);
const client = new Client({
name: "artifact-authority-test",
version: "1",
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const result = await client.callTool({
name: "compare_artifacts",
arguments: {
...ARTIFACT_COMPARISON_EXAMPLE,
left: {
...ARTIFACT_COMPARISON_EXAMPLE.left,
limitations: ["caller altered this record"],
},
},
});
expect(result.isError).toBe(true);
expect(session.exportEvidenceBundle().records).toHaveLength(2);
} finally {
await Promise.allSettled([client.close(), server.close(), session.close()]);
}
});
it("rejects comparison Evidence that is not owned by the session", async () => {
const session = composeBinarySessionFromFactory(() => ({
health: () => Promise.resolve(),
execute: () => Promise.resolve(observed(null)),
close: () => Promise.resolve(),
}));
const server = createServer(session, session);
const client = new Client({
name: "artifact-ownership-test",
version: "1",
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const result = await client.callTool({
name: "compare_artifacts",
arguments: ARTIFACT_COMPARISON_EXAMPLE,
});
expect(result.isError).toBe(true);
expect(session.exportEvidenceBundle().records).toEqual([]);
} finally {
await Promise.allSettled([client.close(), server.close(), session.close()]);
}
});
it("opens archives without Hopper, compares them, and exports linked evidence", async () => {
const directory = await createTestTempDirectory("rea-artifact-mcp-");
const archive = join(directory, "fixture.ipa");
const changedArchive = join(directory, "changed.ipa");
const writer = new ZipWriter(new Uint8ArrayWriter());
await writer.add("Payload/Fixture.app/main.js", new TextReader("main();"));
await writeFile(archive, await writer.close());
const changedWriter = new ZipWriter(new Uint8ArrayWriter());
await changedWriter.add(
"Payload/Fixture.app/main.js",
new TextReader("changed();"),
);
await writeFile(changedArchive, await changedWriter.close());
const session = composeBinarySessionFromProvider(new ArtifactProvider());
const server = createServer(session, session);
const client = new Client({ name: "artifact-mcp-test", version: "1" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const opened = await client.callTool({
name: "open_binary",
arguments: { path: archive },
});
expect(opened.isError).not.toBe(true);
const inspected = await client.callTool({
name: "inspect_artifact",
arguments: { max_observations: 1, max_relationships: 1 },
});
expect(inspected.isError).not.toBe(true);
const inspectionResult = compactResult(inspected.structuredContent);
const inspectionEvidence = session.evidenceById(
inspectionResult.evidence_id,
);
if (inspectionEvidence === undefined)
throw new Error("missing inspection Evidence");
const inspection = z
.object({
substeps: z.array(
z.object({
evidence_id: z.string(),
status: z.literal("completed"),
}),
),
coverage: z.object({
status: z.literal("truncated"),
substeps_completed: z.literal(1),
}),
unexplored_branches: z.array(z.object({ reason: z.string() })),
})
.parse(inspectionResult.result);
expect(inspection.substeps).toHaveLength(1);
expect(
session.evidenceById(inspection.substeps[0]?.evidence_id ?? ""),
).toMatchObject({ operation: "inventory_artifact" });
expect(inspectionEvidence.evidence_links).toEqual([
inspection.substeps[0]?.evidence_id,
]);
expect(inspection.unexplored_branches).toEqual(
expect.arrayContaining([
expect.objectContaining({ reason: "page-limit" }),
]),
);
const inventory = await client.callTool({
name: "inventory_artifact",
arguments: {},
});
expect(inventory.isError).not.toBe(true);
const inventoryResult = compactResult(inventory.structuredContent);
const evidence = session.evidenceById(inventoryResult.evidence_id);
if (evidence === undefined) throw new Error("missing inventory Evidence");
expect(evidence).toMatchObject({
provider: { id: "rea-artifact-graph" },
subject: { format: "ipa" },
});
expect(inventoryResult.result).toMatchObject({
manifest: { root_format: "ipa" },
});
const openedChanged = await client.callTool({
name: "open_binary",
arguments: { path: changedArchive },
});
expect(openedChanged.isError).not.toBe(true);
const changedInventory = await client.callTool({
name: "inventory_artifact",
arguments: {},
});
const changedResult = compactResult(changedInventory.structuredContent);
const changedEvidence = session.evidenceById(changedResult.evidence_id);
if (changedEvidence === undefined)
throw new Error("missing changed Evidence");
const compared = await client.callTool({
name: "compare_artifacts",
arguments: {
left_evidence_ids: [evidence.evidence_id],
right_evidence_ids: [changedEvidence.evidence_id],
unknown_registry_approved: true,
},
});
expect(compared.isError).not.toBe(true);
expect(compactResult(compared.structuredContent)).toMatchObject({
result: { status: "changed" },
evidence_id: expect.stringMatching(/^ev_/u),
});
const unknowns = await client.callTool({
name: "list_unknowns",
arguments: { domain: "artifact-comparison" },
});
expect(unknowns.structuredContent).toMatchObject({
result: {
items: [
expect.objectContaining({
unknown: expect.objectContaining({
domain: "artifact-comparison",
}),
}),
],
},
});
const snapshotted = await client.callTool({
name: "snapshot_evidence_bundle",
arguments: {},
});
const bundleUri = z
.object({ result: z.object({ bundle_uri: z.string() }) })
.parse(snapshotted.structuredContent).result.bundle_uri;
const bundleResource = await client.readResource({ uri: bundleUri });
const bundle = evidenceBundleSchema.parse(
JSON.parse(
z.object({ text: z.string() }).parse(bundleResource.contents[0]).text,
),
);
expect(bundle.records).toHaveLength(6);
expect(bundle.artifacts).toContainEqual({
digest: { sha256: evidence.subject?.digest.sha256 },
format: "ipa",
architecture: null,
});
} finally {
await Promise.allSettled([client.close(), server.close(), session.close()]);
}
});
const compactResult = (value: unknown) =>
z
.object({
result: z.unknown(),
evidence_id: z.string(),
evidence_uri: z.string(),
})
.parse(value);

View File

@ -0,0 +1,330 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { afterEach, expect, it } from "vitest";
import { composeBinarySessionFromFactory } from "../../../src/application/BinarySessionComposition.js";
import type { BinarySession } from "../../../src/application/BinarySession.js";
import { loadConfiguredPermissionAuthority } from "../../../src/application/PermissionConfiguration.js";
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
import { parseConfig } from "../../../src/config.js";
import { JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE } from "../../../src/contracts/javascriptRuntimeReconciliationExample.js";
import { TOOL_CONTRACTS } from "../../../src/contracts/toolContracts.js";
import { createServer } from "../../../src/server/createServer.js";
import { observed } from "../../fixtures/analysisExecution.js";
import {
startFakeCdpBrowser,
type FakeCdpBrowser,
} from "../../fixtures/fakeCdpBrowser.js";
const resources: Array<{ close(): Promise<unknown> }> = [];
const browsers: FakeCdpBrowser[] = [];
const INTEGRATION_TEST_TIMEOUT_MS = 20_000;
afterEach(async () => {
await Promise.all(
resources.splice(0).map(async (resource) => resource.close()),
);
await Promise.all(browsers.splice(0).map(async (browser) => browser.close()));
});
it(
"exposes CLI-equivalent Evidence v2 and session resources",
async () => {
const browser = await startFakeCdpBrowser({
sessionTimeline: "same_origin",
webMcpTools: true,
sensitiveShapes: true,
});
browsers.push(browser);
const connected = await connectBrowser(browser);
const tools = await connected.client.listTools();
expect(tools.tools.length).toBeLessThan(TOOL_CONTRACTS.length);
expect(tools.tools.map(({ name }) => name)).toEqual(
expect.arrayContaining([
"list_browser_targets",
"inspect_web_page",
"analyze_web_bundle",
"observe_web_session",
"discover_webmcp_tools",
"compare_web_captures",
"capture_web_screenshot",
"compare_web_screenshots",
]),
);
const status = await connected.client.callTool({
name: "binary_session",
arguments: { detail: "full" },
});
expect(status.structuredContent).toMatchObject({
result: {
tool_availability: expect.arrayContaining([
expect.objectContaining({
name: "inspect_web_page",
available: true,
reason: "available",
}),
]),
},
});
const listed = await connected.client.callTool({
name: "list_browser_targets",
arguments: {
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
approved: true,
},
});
expect(listed.isError).not.toBe(true);
expect(listed.structuredContent).toMatchObject({
result: {
targets: { items: [{ target_id: "allowed-page" }] },
},
});
const inspected = await connected.client.callTool({
name: "inspect_web_page",
arguments: {
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
observation_ms: 0,
include_console_text: true,
console_text_approved: true,
include_json_body_shapes: true,
json_body_schema_approved: true,
include_websocket_shapes: true,
websocket_shape_approved: true,
include_script_sources: true,
},
});
expect(inspected.isError).not.toBe(true);
expect(inspected.structuredContent).toMatchObject({
result: {
target: { target_id: "allowed-page" },
console: { prior_activity_available: false },
network: {
prior_activity_available: false,
requests: [
expect.objectContaining({
body_shapes: expect.objectContaining({ status: "included" }),
}),
],
},
},
});
const reconciled = await connected.client.callTool({
name: "reconcile_javascript_runtime",
arguments: {
static_layers: JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE.static_layers,
runtime_observations: [
evidenceFor(connected.session, inspected.structuredContent),
],
},
});
expect(reconciled.isError).not.toBe(true);
expect(reconciled.structuredContent).toMatchObject({
result: { summary: { runtime_scripts: 1 } },
});
expect(JSON.stringify(reconciled.structuredContent)).not.toContain(
"source-secret",
);
const analyzed = await connected.client.callTool({
name: "analyze_web_bundle",
arguments: {
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
source_capture_approved: true,
observation_ms: 0,
},
});
expect(analyzed.isError).not.toBe(true);
expect(analyzed.structuredContent).toMatchObject({
result: {
capture: { scripts_analyzed: 1 },
observations: { source_maps: { status: "not_requested" } },
},
});
await verifySessionAndComparisonTools(connected, browser, inspected);
},
INTEGRATION_TEST_TIMEOUT_MS,
);
const verifySessionAndComparisonTools = async (
connected: Awaited<ReturnType<typeof connectBrowser>>,
browser: FakeCdpBrowser,
inspected: Awaited<ReturnType<Client["callTool"]>>,
): Promise<void> => {
const observedSession = await connected.client.callTool({
name: "observe_web_session",
arguments: {
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
observation_ms: 5,
},
});
expect(observedSession.isError).not.toBe(true);
expect(observedSession.structuredContent).toMatchObject({
result: {
window: { end_reason: "window_elapsed" },
timeline: expect.arrayContaining([
expect.objectContaining({ type: "same_origin_reload" }),
]),
},
});
const webMcp = await connected.client.callTool({
name: "discover_webmcp_tools",
arguments: {
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
observation_ms: 0,
},
});
expect(webMcp.isError).not.toBe(true);
expect(webMcp.structuredContent).toMatchObject({
result: {
tools: {
items: [expect.objectContaining({ name: "search_orders" })],
},
},
});
const capture = normalizedResultOf(inspected.structuredContent);
const compared = await connected.client.callTool({
name: "compare_web_captures",
arguments: {
before: { inspection: capture },
after: { inspection: capture },
},
});
expect(compared.isError).not.toBe(true);
expect(compared.structuredContent).toMatchObject({
result: { overall_status: "unknown" },
});
const screenshot = await connected.client.callTool({
name: "capture_web_screenshot",
arguments: {
cdp_endpoint: browser.endpoint,
allowed_origins: [browser.allowedOrigin],
target_id: "allowed-page",
approved: true,
screenshot_approved: true,
},
});
expect(screenshot.isError).not.toBe(true);
const screenshotArtifact = artifactOf(screenshot.structuredContent);
const visual = await connected.client.callTool({
name: "compare_web_screenshots",
arguments: {
before: screenshotArtifact,
after: screenshotArtifact,
},
});
expect(visual.isError).not.toBe(true);
expect(visual.structuredContent).toMatchObject({
result: { status: "identical", changed_pixels: 0 },
});
const evidenceId = evidenceIdOf(inspected.structuredContent);
const evidence = await connected.client.readResource({
uri: `rea://evidence/${evidenceId}`,
});
expect(evidence.contents[0]).toEqual(
expect.objectContaining({ text: expect.stringContaining(evidenceId) }),
);
};
it("denies origins outside the administrator ceiling before CDP attach", async () => {
const browser = await startFakeCdpBrowser();
browsers.push(browser);
const connected = await connectBrowser(browser);
const denied = await connected.client.callTool({
name: "inspect_web_page",
arguments: {
cdp_endpoint: browser.endpoint,
allowed_origins: ["https://unapproved.example.test"],
target_id: "allowed-page",
approved: true,
observation_ms: 0,
},
});
expect(denied.isError).toBe(true);
expect(denied.structuredContent).toMatchObject({
error: {
code: "permission_required",
details: {
missing: { origins: ["https://unapproved.example.test"] },
},
},
});
expect(browser.commands).toHaveLength(0);
});
const connectBrowser = async (browser: FakeCdpBrowser) => {
const config = parseConfig({
REA_BROWSER_OBSERVE_ENABLED: "true",
REA_BROWSER_CDP_ENDPOINTS_JSON: JSON.stringify([browser.endpoint]),
REA_BROWSER_ALLOWED_ORIGINS_JSON: JSON.stringify([browser.allowedOrigin]),
});
if (!config.ok) throw config.error;
const authority = await loadConfiguredPermissionAuthority(config.value);
if (!authority.ok) throw authority.error;
const session = composeBinarySessionFromFactory(() => ({
execute: () => Promise.resolve(observed(null)),
close: () => Promise.resolve(),
}));
const server = createServer(session, session, {
browserObservation: new CdpBrowserProvider(),
permissionAuthority: authority.value,
availabilityPolicy: () => ({
processCaptureEnabled: false,
evidenceFileRoots: 0,
investigationInputRoots: 0,
browserObservationEnabled: true,
}),
});
const client = new Client({ name: "browser-mcp-test", version: "1" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
resources.push(client, server, session);
await server.connect(serverTransport);
await client.connect(clientTransport);
return { client, session };
};
const evidenceIdOf = (value: unknown): string => {
if (
typeof value !== "object" ||
value === null ||
!("evidence_id" in value) ||
typeof value.evidence_id !== "string"
)
throw new TypeError("Missing browser evidence ID");
return value.evidence_id;
};
const normalizedResultOf = (value: unknown): unknown => {
if (typeof value !== "object" || value === null || !("result" in value))
throw new TypeError("Missing normalized browser result");
return value.result;
};
const evidenceFor = (session: BinarySession, value: unknown) => {
const evidence = session.evidenceById(evidenceIdOf(value));
if (evidence === undefined) throw new TypeError("Missing session Evidence");
return evidence;
};
const artifactOf = (value: unknown): unknown => {
const normalized = normalizedResultOf(value);
if (
typeof normalized !== "object" ||
normalized === null ||
!("artifact" in normalized)
)
throw new TypeError("Missing screenshot artifact");
return normalized.artifact;
};

View File

@ -2,25 +2,25 @@ import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { dirname } from "node:path";
import { afterEach, describe, expect, test } from "vitest";
import type { BrowserScenarioCapturePort } from "../src/application/BrowserScenarioCapturePort.js";
import { BinarySession } from "../src/application/BinarySession.js";
import { loadConfiguredPermissionAuthority } from "../src/application/PermissionConfiguration.js";
import type { BrowserScenarioCapturePort } from "../../../src/application/BrowserScenarioCapturePort.js";
import { composeBinarySessionFromFactory } from "../../../src/application/BinarySessionComposition.js";
import { loadConfiguredPermissionAuthority } from "../../../src/application/PermissionConfiguration.js";
import type {
ExecutionOptions,
ProviderIdentity,
} from "../src/application/AnalysisProvider.js";
import { CdpBrowserProvider } from "../src/browser/CdpBrowserProvider.js";
import { parseConfig } from "../src/config.js";
import { sanitizeBrowserUrl } from "../src/domain/browserObservation.js";
import type { BrowserScenario } from "../src/domain/browserScenario.js";
} from "../../../src/application/AnalysisProvider.js";
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
import { parseConfig } from "../../../src/config.js";
import { sanitizeBrowserUrl } from "../../../src/domain/browserObservation.js";
import type { BrowserScenario } from "../../../src/domain/browserScenario.js";
import {
browserScenarioCaptureSchema,
type BrowserScenarioCapture,
} from "../src/domain/browserScenarioCapture.js";
import type { AnalysisError } from "../src/domain/errors.js";
import { ok, type Result } from "../src/domain/result.js";
import { createServer } from "../src/server/createServer.js";
import { observed } from "./fixtures/analysisExecution.js";
} from "../../../src/domain/browserScenarioCapture.js";
import type { AnalysisError } from "../../../src/domain/errors.js";
import { ok, type Result } from "../../../src/domain/result.js";
import { createServer } from "../../../src/server/createServer.js";
import { observed } from "../../fixtures/analysisExecution.js";
const providerIdentity: ProviderIdentity = {
id: "test-browser-scenario",
@ -189,7 +189,7 @@ describe("browser scenario MCP tool", () => {
const authority = await loadConfiguredPermissionAuthority(configured.value);
if (!authority.ok) throw authority.error;
const provider = new FakeBrowserScenarioProvider();
const session = new BinarySession(() => ({
const session = composeBinarySessionFromFactory(() => ({
execute: () => Promise.resolve(observed(null)),
close: () => Promise.resolve(),
}));

View File

@ -4,16 +4,16 @@ import { join } from "node:path";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { afterEach, describe, expect, it } from "vitest";
import { createTestTempDirectory } from "./fixtures/temporaryDirectory.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { BinarySession } from "../src/application/BinarySession.js";
import { createEvidence } from "../src/domain/evidence.js";
import { composeBinarySessionFromFactory } from "../../../src/application/BinarySessionComposition.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import {
createEvidenceBundle,
serializeEvidenceBundle,
} from "../src/domain/evidenceBundle.js";
import { createServer } from "../src/server/createServer.js";
import { observed } from "./fixtures/analysisExecution.js";
} from "../../../src/domain/evidenceBundle.js";
import { createServer } from "../../../src/server/createServer.js";
import { observed } from "../../fixtures/analysisExecution.js";
const roots: string[] = [];
afterEach(async () => {
@ -137,7 +137,7 @@ describe("bundle comparison MCP integration", () => {
});
const connect = async (root: string, maxBytes = 1024 * 1024) => {
const session = new BinarySession(() => ({
const session = composeBinarySessionFromFactory(() => ({
health: () => Promise.resolve(),
execute: () => Promise.resolve(observed(null)),
close: () => Promise.resolve(),

View File

@ -1,8 +1,8 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { describe, expect, it } from "vitest";
import { observed as ok } from "./fixtures/analysisExecution.js";
import { createServer } from "../src/server/createServer.js";
import { observed as ok } from "../../fixtures/analysisExecution.js";
import { mcpTest } from "../../support/mcp/mcpFixture.js";
import { createServer } from "../../../src/server/createServer.js";
const analysis = {
execute: () => Promise.resolve(ok(null)),
@ -13,18 +13,15 @@ describe("MCP server composition", () => {
expect(createServer(analysis)).not.toBe(createServer(analysis));
});
it("does not advertise unavailable session tools", async () => {
mcpTest("does not advertise unavailable session tools", async ({ mcp }) => {
const server = createServer(analysis);
const client = new Client({ name: "composition-test", version: "1.0.0" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await server.connect(serverTransport);
await client.connect(clientTransport);
const client = await mcp.connect(server, {
name: "composition-test",
version: "1.0.0",
});
expect(client.getInstructions()).not.toContain("open_binary");
const names = (await client.listTools()).tools.map(({ name }) => name);
expect(names).not.toContain("compare_managed_members");
expect(names).not.toContain("plan_managed_runtime_correlation");
await client.close();
await server.close();
});
});

View File

@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { runDerivedOperation } from "../src/server/runDerivedOperation.js";
import { runDerivedOperation } from "../../../src/server/runDerivedOperation.js";
const context = (
controller: AbortController,

View File

@ -0,0 +1,351 @@
import { mkdir, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { afterEach, expect, it } from "vitest";
import { z } from "zod";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { composeBinarySessionFromFactory } from "../../../src/application/BinarySessionComposition.js";
import type { BinarySession } from "../../../src/application/BinarySession.js";
import { loadConfiguredPermissionAuthority } from "../../../src/application/PermissionConfiguration.js";
import { CdpElectronProvider } from "../../../src/browser/CdpElectronProvider.js";
import { parseConfig } from "../../../src/config.js";
import { createServer } from "../../../src/server/createServer.js";
import { observed } from "../../fixtures/analysisExecution.js";
import {
startFakeCdpBrowser,
type FakeCdpBrowser,
} from "../../fixtures/fakeCdpBrowser.js";
import { writeElectronBoundaryFixture } from "../../fixtures/electronBoundaryApplication.js";
const browsers: FakeCdpBrowser[] = [];
const resources: Array<{ close(): Promise<unknown> }> = [];
const temporary: string[] = [];
afterEach(async () => {
await Promise.all(resources.splice(0).map(async (item) => item.close()));
await Promise.all(browsers.splice(0).map(async (browser) => browser.close()));
await Promise.all(
temporary
.splice(0)
.map(async (path) => rm(path, { recursive: true, force: true })),
);
});
it("exposes root-confined Electron discovery and inspection as Evidence v2", async () => {
const root = await createTestTempDirectory("rea-electron-mcp-");
temporary.push(root);
await writeFile(join(root, "index.html"), "<script src='app.js'></script>");
await writeFile(
join(root, "app.js"),
"export const observed = 'source-secret';",
);
await writeFile(
join(root, "package.json"),
JSON.stringify({ name: "runtime-fixture", renderer: "index.html" }),
);
await writeFile(join(root, "worker.js"), "self.onmessage = () => {};\n");
const browser = await startFakeCdpBrowser({
electronFileUrl: pathToFileURL(join(root, "index.html")).href,
});
browsers.push(browser);
const config = parseConfig({
REA_ELECTRON_OBSERVE_ENABLED: "true",
REA_ELECTRON_CDP_ENDPOINTS_JSON: JSON.stringify([browser.endpoint]),
REA_ELECTRON_FILE_ROOTS_JSON: JSON.stringify([root]),
REA_INVESTIGATION_INPUT_ROOTS_JSON: JSON.stringify([root]),
});
if (!config.ok) throw config.error;
const authority = await loadConfiguredPermissionAuthority(config.value);
if (!authority.ok) throw authority.error;
const session = composeBinarySessionFromFactory(() => ({
execute: () => Promise.resolve(observed(null)),
close: () => Promise.resolve(),
}));
const server = createServer(session, session, {
electronObservation: new CdpElectronProvider(),
permissionAuthority: authority.value,
availabilityPolicy: () => ({
processCaptureEnabled: false,
evidenceFileRoots: 0,
investigationInputRoots: 1,
electronObservationEnabled: true,
}),
});
const client = new Client({ name: "electron-mcp-test", version: "1" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
resources.push(client, server, session);
await server.connect(serverTransport);
await client.connect(clientTransport);
const listed = await client.callTool({
name: "list_electron_targets",
arguments: {
cdp_endpoint: browser.endpoint,
allowed_file_roots: [root],
approved: true,
},
});
expect(listed.isError).not.toBe(true);
expect(listed.structuredContent).toMatchObject({
result: {
targets: { items: [{ target_id: "electron-page" }] },
},
});
const inspected = await client.callTool({
name: "inspect_electron_page",
arguments: {
cdp_endpoint: browser.endpoint,
allowed_file_roots: [root],
target_id: "electron-page",
approved: true,
observation_ms: 0,
include_script_sources: true,
source_capture_approved: true,
},
});
const analyzed = await client.callTool({
name: "analyze_javascript_application",
arguments: { input_path: root, approved: true, detail: "full" },
});
expect(analyzed.isError).not.toBe(true);
expect(analyzed.structuredContent).toMatchObject({
result: { graph: { nodes: expect.any(Array), edges: expect.any(Array) } },
});
const analysisText = analyzed.content.find(({ type }) => type === "text");
expect(analysisText).toBeDefined();
if (analysisText?.type !== "text")
throw new TypeError("Missing JavaScript analysis text projection");
expect(analysisText.text).not.toContain('"nodes":[');
const reconciled = await client.callTool({
name: "reconcile_javascript_runtime",
arguments: {
static_layers: [
{
role: "application",
analysis: evidenceFor(session, analyzed.structuredContent),
},
],
runtime_observations: [evidenceFor(session, inspected.structuredContent)],
},
});
expect(reconciled.isError).not.toBe(true);
expect(reconciled.structuredContent).toMatchObject({
result: {
summary: { runtime_scripts: 1, matched: expect.any(Number) },
source_map_authority: { used_for_primary_matching: false },
},
});
expect(inspected.isError).not.toBe(true);
expect(inspected.structuredContent).toMatchObject({
result: {
target: { file_path: expect.stringMatching(/\/index\.html$/u) },
scripts: {
items: [
expect.objectContaining({
frame_id: "frame-main",
file_path: expect.stringMatching(/\/app\.js$/u),
}),
],
},
workers: [
expect.objectContaining({
target_id: "electron-worker",
opener_target_id: "electron-page",
file_path: expect.stringMatching(/\/worker\.js$/u),
}),
],
},
});
const outside = join(root, "..", `outside-${Date.now().toString(16)}`);
await mkdir(outside);
temporary.push(outside);
const commandsBeforeDenial = browser.commands.length;
const denied = await client.callTool({
name: "list_electron_targets",
arguments: {
cdp_endpoint: browser.endpoint,
allowed_file_roots: [outside],
approved: true,
},
});
expect(denied.isError).toBe(true);
expect(denied.structuredContent).toMatchObject({
error: {
code: "permission_required",
details: { missing: { roots: [expect.stringMatching(/outside-/u)] } },
},
});
expect(browser.commands).toHaveLength(commandsBeforeDenial);
}, 20_000);
it("exposes the target-free static JavaScript application workflow", async () => {
const root = await createTestTempDirectory("rea-electron-static-mcp-");
temporary.push(root);
await writeElectronBoundaryFixture(root);
const config = parseConfig({
REA_INVESTIGATION_INPUT_ROOTS_JSON: JSON.stringify([root]),
});
if (!config.ok) throw config.error;
const authority = await loadConfiguredPermissionAuthority(config.value);
if (!authority.ok) throw authority.error;
const session = composeBinarySessionFromFactory(() => ({
execute: () => Promise.resolve(observed(null)),
close: () => Promise.resolve(),
}));
const server = createServer(session, session, {
permissionAuthority: authority.value,
availabilityPolicy: () => ({
processCaptureEnabled: false,
evidenceFileRoots: 0,
investigationInputRoots: 1,
}),
});
const client = new Client({
name: "electron-static-mcp-test",
version: "1",
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
resources.push(client, server, session);
await server.connect(serverTransport);
await client.connect(clientTransport);
const analyzed = await client.callTool({
name: "analyze_javascript_application",
arguments: { input_path: root, approved: true },
});
expect(analyzed.isError).not.toBe(true);
const projected = z
.object({
evidence_id: z.string(),
result: z.object({
schema_version: z.literal(2),
input_path: z.string(),
unknowns: z.array(z.string()),
summary: z.object({
browser_windows: z.number(),
context_bridge_apis: z.number(),
ipc: z.object({ paired_renderer_transmissions: z.number() }),
}),
graph: z.object({
graph_id: z.string(),
node_count: z.number(),
edge_count: z.number(),
top_findings: z.array(z.unknown()),
pages: z.object({ nodes: z.string(), edges: z.string() }),
}),
semantic_graph: z.object({
graph_id: z.string(),
nodes: z.number(),
relations: z.number(),
unknown_frontiers: z.number(),
query_tool: z.literal("trace_javascript_semantics"),
}),
}),
})
.parse(analyzed.structuredContent);
expect(projected.result).toMatchObject({
input_path: expect.stringMatching(/rea-electron-static-mcp-/u),
summary: {
browser_windows: 3,
context_bridge_apis: 2,
ipc: { paired_renderer_transmissions: 4 },
},
});
expect(projected.result.graph.node_count).toBeGreaterThan(0);
expect(projected.result.semantic_graph.nodes).toBeGreaterThan(0);
expect(JSON.stringify(analyzed.structuredContent)).not.toContain('"nodes":[');
const nodePage = await client.readResource({
uri: projected.result.graph.pages.nodes,
});
const nodeContent = nodePage.contents[0];
if (nodeContent === undefined || !("text" in nodeContent))
throw new TypeError("Missing JavaScript application graph node page");
expect(JSON.parse(nodeContent.text)).toMatchObject({
evidence_id: projected.evidence_id,
graph_id: projected.result.graph.graph_id,
collection: "nodes",
items: expect.any(Array),
offset: 0,
limit: 100,
});
});
it("reports an unconfigured investigation ceiling before static analysis", async () => {
const root = await createTestTempDirectory("rea-electron-denied-mcp-");
temporary.push(root);
await writeElectronBoundaryFixture(root);
const config = parseConfig({});
if (!config.ok) throw config.error;
const authority = await loadConfiguredPermissionAuthority(config.value);
if (!authority.ok) throw authority.error;
const session = composeBinarySessionFromFactory(() => ({
execute: () => Promise.resolve(observed(null)),
close: () => Promise.resolve(),
}));
const server = createServer(session, session, {
permissionAuthority: authority.value,
});
const client = new Client({
name: "electron-denied-mcp-test",
version: "1",
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
resources.push(client, server, session);
await server.connect(serverTransport);
await client.connect(clientTransport);
const status = await client.callTool({
name: "binary_session",
arguments: {
detail: "capabilities",
capability_family: "electron-provider",
limit: 100,
},
});
expect(status.structuredContent).toMatchObject({
result: {
capabilities: {
items: expect.arrayContaining([
expect.objectContaining({
name: "analyze_javascript_application",
available: false,
reason: "policy_disabled",
}),
expect.objectContaining({
name: "reconcile_javascript_runtime",
available: true,
reason: "available",
}),
]),
},
},
});
expect(
(await client.listTools()).tools.some(
({ name }) => name === "analyze_javascript_application",
),
).toBe(false);
});
const evidenceFor = (session: BinarySession, value: unknown) => {
if (
typeof value !== "object" ||
value === null ||
!("evidence_id" in value) ||
typeof value.evidence_id !== "string"
)
throw new TypeError("Missing compact Evidence ID");
const evidence = session.evidenceById(value.evidence_id);
if (evidence === undefined) throw new TypeError("Missing session Evidence");
return evidence;
};

View File

@ -0,0 +1,315 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import type { CallToolResult } from "@modelcontextprotocol/server";
import { afterEach, describe, expect, it } from "vitest";
import type { AnalysisOperationPort } from "../../../src/application/AnalysisProvider.js";
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "../../../src/contracts/managedWorkflowToolContracts.js";
import {
ENHANCED_TOOL_CONTRACTS,
SESSION_TOOL_CONTRACTS,
TOOL_CONTRACTS,
} from "../../../src/contracts/toolContracts.js";
import {
jsonValueSchema,
type JsonValue,
} from "../../../src/domain/jsonValue.js";
import { createServer } from "../../../src/server/createServer.js";
import { observed as ok } from "../../fixtures/analysisExecution.js";
const PROCEDURES = {
"0x1": "_TtC7Fixture5Class",
"0x2": "_TtV7Fixture6Struct",
"0x3": "_TtP7Fixture8Protocol",
"0x4": "_TtO7Fixture4Enum",
"0x5": "_TtE7Fixture9Extension",
"0x6": "prefix_TtOther",
};
const page = (values: Readonly<Record<string, string>>) => ({
items: Object.entries(values).map(([address, value]) => ({ address, value })),
offset: 0,
limit: 100,
total: Object.keys(values).length,
next_offset: null,
has_more: false,
});
const fixturePort = (): AnalysisOperationPort => ({
execute: (name, arguments_) => {
switch (name) {
case "list_procedures":
return Promise.resolve(ok(page(PROCEDURES)));
case "list_names":
return Promise.resolve(
ok(
page({
"0x10": "_OBJC_CLASS_$_Fixture",
"0x11": "_OBJC_CLASS_$_Fixture",
"0x12": "_OBJC_PROTOCOL_$_FixtureDelegate",
"0x13": "entry",
}),
),
);
case "procedure_pseudo_code": {
const procedure = arguments_.procedure;
return Promise.resolve(
ok(typeof procedure === "string" ? `pseudo:${procedure}` : "invalid"),
);
}
case "procedure_callees": {
const procedure = arguments_.procedure;
return Promise.resolve(
ok(
procedure === "0x1"
? ["0x2", "0x3"]
: procedure === "0x2"
? ["0x1"]
: [],
),
);
}
case "procedure_callers":
return Promise.resolve(ok(["0x9"]));
case "address_name":
return Promise.resolve(ok(arguments_.address ?? null));
case "xrefs":
return Promise.resolve(ok(["0x20", "0x21"]));
case "resolve_containing_procedure":
return Promise.resolve(
ok({
query_address:
typeof arguments_.address === "string"
? arguments_.address
: "0x0",
found: false,
procedure: null,
reason: "not_in_procedure",
}),
);
case "list_segments":
return Promise.resolve(
ok([{ name: "__TEXT", start: "0x1000", end: "0x2000" }]),
);
case "list_documents":
return Promise.resolve(ok(["fixture"]));
case "list_strings":
return Promise.resolve(ok(page({ "0x30": "hello" })));
case "analyze_function":
return Promise.resolve(
ok({
procedure: {
address: "0x1",
name: "entry",
signature: null,
locals: [],
},
pseudocode: {
text: "return 0;",
total_chars: 9,
returned_chars: 9,
truncated: false,
next_offset: null,
},
assembly: {
items: [],
total: 0,
returned: 0,
truncated: false,
next_offset: null,
},
comments: emptyBounded(),
callers: emptyBounded(),
callees: emptyBounded(),
incoming_references: emptyBounded(),
outgoing_references: emptyBounded(),
referenced_strings: emptyBounded(),
referenced_names: emptyBounded(),
basic_blocks: emptyBounded(),
instruction_scan: { scanned: 0, truncated: false },
}),
);
default:
return Promise.resolve(ok(null));
}
},
});
const emptyBounded = () => ({
items: [],
total: 0,
returned: 0,
truncated: false,
next_offset: null,
});
const resources: Array<{ close(): Promise<void> }> = [];
afterEach(async () => {
await Promise.all(
resources.splice(0).map(async (resource) => resource.close()),
);
});
const connect = async (analysis: AnalysisOperationPort = fixturePort()) => {
const server = createServer(analysis);
const client = new Client({ name: "enhanced-test", version: "1.0.0" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
resources.push(client, server);
await server.connect(serverTransport);
await client.connect(clientTransport);
return client;
};
const jsonResult = (result: CallToolResult): JsonValue => {
if (result.structuredContent === undefined)
throw new Error("Tool result omitted structured content");
const structured = jsonValueSchema.safeParse(result.structuredContent);
if (!structured.success)
throw new Error("Tool structured result was not JSON");
if (
typeof structured.data === "object" &&
structured.data !== null &&
!Array.isArray(structured.data) &&
"normalized_result" in structured.data
) {
return structured.data.normalized_result ?? null;
}
if (
typeof structured.data === "object" &&
structured.data !== null &&
!Array.isArray(structured.data) &&
"evidence_id" in structured.data &&
"result" in structured.data
)
return structured.data.result ?? null;
const text = result.content.find((item) => item.type === "text");
if (text?.type !== "text")
throw new Error("Tool result omitted text content");
const decoded: unknown = JSON.parse(text.text);
const parsed = jsonValueSchema.safeParse(decoded);
if (!parsed.success) throw new Error("Tool result was not JSON");
return parsed.data;
};
describe("enhanced MCP tools", () => {
it("lists the complete target-open analysis surface", async () => {
const client = await connect();
const listed = await client.listTools();
expect(listed.tools).toHaveLength(
TOOL_CONTRACTS.length -
SESSION_TOOL_CONTRACTS.length -
MANAGED_WORKFLOW_TOOL_CONTRACTS.length,
);
expect(
listed.tools
.map(({ name }) => name)
.filter((name) =>
ENHANCED_TOOL_CONTRACTS.some((tool) => tool.name === name),
)
.sort(),
).toEqual(ENHANCED_TOOL_CONTRACTS.map(({ name }) => name).sort());
});
it("executes all thirteen tools through production registration", async () => {
const client = await connect();
const calls = [
["swift_classes", { pattern: "Fixture" }],
["get_objc_classes", { pattern: "Fixture" }],
["get_objc_protocols", {}],
["batch_decompile", { addresses: ["0x1", "0x2"] }],
["get_call_graph", { address: "0x1", direction: "forward", depth: 2 }],
["analyze_swift_types", {}],
["find_xrefs_to_name", { name: "entry" }],
["binary_overview", {}],
["analyze_function", { procedure: "0x1" }],
["inspect_native_api", { procedure: "0x1" }],
["trace_feature", { query: "hello", max_operations: 10 }],
["find_code_for_string", { query: "hello", max_operations: 10 }],
["trace_call_path", { start: "0x1", goal: "0x2", max_operations: 10 }],
] as const;
const results = await Promise.all(
calls.map(async ([name, arguments_]) =>
jsonResult(await client.callTool({ name, arguments: arguments_ })),
),
);
expect(results[0]).toMatchObject({ count: 1 });
expect(results[1]).toMatchObject({ count: 1 });
expect(results[2]).toMatchObject({ count: 1 });
expect(results[3]).toEqual({
items: [
{ address: "0x1", status: "ok", pseudocode: "pseudo:0x1" },
{ address: "0x2", status: "ok", pseudocode: "pseudo:0x2" },
],
total: 2,
succeeded: 2,
failed: 0,
});
expect(results[4]).toEqual({
"0": [{ address: "0x1", status: "ok", calls: ["0x2", "0x3"] }],
"1": [
{ address: "0x2", status: "ok", calls: ["0x1"] },
{ address: "0x3", status: "ok", calls: [] },
],
});
expect(results[5]).toMatchObject({ total: 6 });
expect(results[6]).toEqual({
status: "resolved",
name: "entry",
address: "0x13",
xrefs: ["0x20", "0x21"],
});
expect(results[7]).toMatchObject({
document: "fixture",
segment_count: 1,
procedure_count: 6,
string_count: 1,
});
expect(results[8]).toMatchObject({
procedure: { address: "0x1", name: "entry" },
pseudocode: { text: "return 0;" },
});
expect(results[9]).toMatchObject({
procedure: { address: "0x1", name: "entry" },
boundary: { available: false },
unsupported_branches: [
"structured-boundary-types",
"jump-table-data-mapping",
],
residual_unknowns: expect.arrayContaining([
expect.stringContaining("boundary types"),
]),
substeps: [
{ operation: "analyze_function", status: "completed" },
{ operation: "project_native_api_boundary", status: "unsupported" },
{ operation: "preserve_residual_unknowns", status: "completed" },
],
});
expect(results[10]).toMatchObject({
query: "hello",
search_mode: "literal",
truncated: false,
references: [
{ target_address: "0x30", source_address: "0x20" },
{ target_address: "0x30", source_address: "0x21" },
],
});
expect(results[11]).toMatchObject({
query: "hello",
matches: [{ type: "string", address: "0x30", value: "hello" }],
truncated: false,
});
expect(results[12]).toMatchObject({
start: "0x1",
goal: "0x2",
direction: "forward",
goal_status: "reached",
traversal_path: ["0x1", "0x2"],
nodes: [
{ address: "0x1", depth: 0 },
{ address: "0x2", depth: 1 },
],
truncated: false,
});
});
});

View File

@ -0,0 +1,262 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { afterEach, describe, expect, it } from "vitest";
import type { AnalysisOperationPort } from "../../../src/application/AnalysisProvider.js";
import { EnhancedTools } from "../../../src/application/EnhancedTools.js";
import { AnalysisOutputError } from "../../../src/domain/errors.js";
import { err } from "../../../src/domain/result.js";
import { createServer } from "../../../src/server/createServer.js";
import { observed as ok } from "../../fixtures/analysisExecution.js";
const PROCEDURES = {
"0x1": "_TtC7Fixture5Class",
"0x2": "_TtV7Fixture6Struct",
"0x3": "_TtP7Fixture8Protocol",
"0x4": "_TtO7Fixture4Enum",
"0x5": "_TtE7Fixture9Extension",
"0x6": "prefix_TtOther",
};
const page = (values: Readonly<Record<string, string>>) => ({
items: Object.entries(values).map(([address, value]) => ({ address, value })),
offset: 0,
limit: 100,
total: Object.keys(values).length,
next_offset: null,
has_more: false,
});
const fixturePort = (): AnalysisOperationPort => ({
execute: (name, arguments_) => {
switch (name) {
case "list_procedures":
return Promise.resolve(ok(page(PROCEDURES)));
case "list_names":
return Promise.resolve(
ok(
page({
"0x10": "_OBJC_CLASS_$_Fixture",
"0x11": "_OBJC_CLASS_$_Fixture",
"0x12": "_OBJC_PROTOCOL_$_FixtureDelegate",
"0x13": "entry",
}),
),
);
case "procedure_pseudo_code": {
const procedure = arguments_.procedure;
return Promise.resolve(
ok(typeof procedure === "string" ? `pseudo:${procedure}` : "invalid"),
);
}
case "procedure_callees": {
const procedure = arguments_.procedure;
return Promise.resolve(
ok(
procedure === "0x1"
? ["0x2", "0x3"]
: procedure === "0x2"
? ["0x1"]
: [],
),
);
}
case "procedure_callers":
return Promise.resolve(ok(["0x9"]));
case "address_name":
return Promise.resolve(ok(arguments_.address ?? null));
case "xrefs":
return Promise.resolve(ok(["0x20", "0x21"]));
case "resolve_containing_procedure":
return Promise.resolve(
ok({
query_address:
typeof arguments_.address === "string"
? arguments_.address
: "0x0",
found: false,
procedure: null,
reason: "not_in_procedure",
}),
);
case "list_segments":
return Promise.resolve(
ok([{ name: "__TEXT", start: "0x1000", end: "0x2000" }]),
);
case "list_documents":
return Promise.resolve(ok(["fixture"]));
case "list_strings":
return Promise.resolve(ok(page({ "0x30": "hello" })));
case "analyze_function":
return Promise.resolve(
ok({
procedure: {
address: "0x1",
name: "entry",
signature: null,
locals: [],
},
pseudocode: {
text: "return 0;",
total_chars: 9,
returned_chars: 9,
truncated: false,
next_offset: null,
},
assembly: {
items: [],
total: 0,
returned: 0,
truncated: false,
next_offset: null,
},
comments: emptyBounded(),
callers: emptyBounded(),
callees: emptyBounded(),
incoming_references: emptyBounded(),
outgoing_references: emptyBounded(),
referenced_strings: emptyBounded(),
referenced_names: emptyBounded(),
basic_blocks: emptyBounded(),
instruction_scan: { scanned: 0, truncated: false },
}),
);
default:
return Promise.resolve(ok(null));
}
},
});
const emptyBounded = () => ({
items: [],
total: 0,
returned: 0,
truncated: false,
next_offset: null,
});
const resources: Array<{ close(): Promise<void> }> = [];
afterEach(async () => {
await Promise.all(
resources.splice(0).map(async (resource) => resource.close()),
);
});
const connect = async (analysis: AnalysisOperationPort = fixturePort()) => {
const server = createServer(analysis);
const client = new Client({ name: "enhanced-test", version: "1.0.0" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
resources.push(client, server);
await server.connect(serverTransport);
await client.connect(clientTransport);
return client;
};
describe("enhanced MCP tools", () => {
it("returns Evidence IDs and explicit call-path frontiers at limits", async () => {
const client = await connect();
const result = await client.callTool({
name: "trace_call_path",
arguments: {
start: "0x1",
goal: "0x3",
max_nodes: 1,
max_operations: 10,
},
});
expect(result.isError).not.toBe(true);
expect(result.structuredContent).toMatchObject({
evidence_id: expect.stringMatching(/^ev_[a-f0-9]{64}$/u),
result: {
goal_status: "not_reached",
nodes: [{ address: "0x1", depth: 0 }],
frontier: ["0x2"],
truncated: true,
residual_unknowns: expect.arrayContaining([
expect.stringContaining("reached the node limit"),
expect.stringContaining("reached the edge limit"),
]),
limits: {
max_nodes: 1,
nodes_visited: 1,
operations_used: 1,
},
},
});
});
it("bounds batch concurrency at the parsed maximum of 20", async () => {
let active = 0;
let maximum = 0;
const analysis: AnalysisOperationPort = {
execute: async (name) => {
if (name !== "procedure_pseudo_code") return ok(null);
active += 1;
maximum = Math.max(maximum, active);
await new Promise((resolve) => setTimeout(resolve, 2));
active -= 1;
return ok("pseudo");
},
};
const client = await connect(analysis);
const result = await client.callTool({
name: "batch_decompile",
arguments: {
addresses: Array.from(
{ length: 20 },
(_, index) => `0x${String(index)}`,
),
},
});
expect(result.isError).not.toBe(true);
expect(maximum).toBe(20);
});
it("returns ordered typed batch failures and zero counts for empty input", async () => {
const tools = new EnhancedTools({
execute: (_name, arguments_) =>
arguments_.procedure === "0x2"
? Promise.resolve(err(new AnalysisOutputError("decompile", "failed")))
: Promise.resolve(ok("pseudo")),
});
const result = await tools.execute("batch_decompile", {
addresses: ["0x1", "0x2"],
});
const empty = await tools.execute("batch_decompile", { addresses: [] });
expect(result).toEqual({
ok: true,
value: {
items: [
{ address: "0x1", status: "ok", pseudocode: "pseudo" },
{
address: "0x2",
status: "error",
error: {
code: "unreadable_output",
category: "execution_failure",
message:
"Analysis returned an unreadable result. Retry once; if it continues, run `rea doctor`.",
retryable: false,
remediation: {
action:
"Analysis returned an unreadable result. Retry once; if it continues, run `rea doctor`.",
restart_required: false,
},
},
},
],
total: 2,
succeeded: 1,
failed: 1,
},
});
expect(empty).toEqual({
ok: true,
value: { items: [], total: 0, succeeded: 0, failed: 0 },
});
});
});

View File

@ -0,0 +1,297 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import type { CallToolResult } from "@modelcontextprotocol/server";
import { afterEach, describe, expect, it } from "vitest";
import type { AnalysisOperationPort } from "../../../src/application/AnalysisProvider.js";
import { EnhancedTools } from "../../../src/application/EnhancedTools.js";
import { AnalysisOutputError } from "../../../src/domain/errors.js";
import {
jsonValueSchema,
type JsonValue,
} from "../../../src/domain/jsonValue.js";
import { err } from "../../../src/domain/result.js";
import { createServer } from "../../../src/server/createServer.js";
import { observed as ok } from "../../fixtures/analysisExecution.js";
const PROCEDURES = {
"0x1": "_TtC7Fixture5Class",
"0x2": "_TtV7Fixture6Struct",
"0x3": "_TtP7Fixture8Protocol",
"0x4": "_TtO7Fixture4Enum",
"0x5": "_TtE7Fixture9Extension",
"0x6": "prefix_TtOther",
};
const page = (values: Readonly<Record<string, string>>) => ({
items: Object.entries(values).map(([address, value]) => ({ address, value })),
offset: 0,
limit: 100,
total: Object.keys(values).length,
next_offset: null,
has_more: false,
});
const fixturePort = (): AnalysisOperationPort => ({
execute: (name, arguments_) => {
switch (name) {
case "list_procedures":
return Promise.resolve(ok(page(PROCEDURES)));
case "list_names":
return Promise.resolve(
ok(
page({
"0x10": "_OBJC_CLASS_$_Fixture",
"0x11": "_OBJC_CLASS_$_Fixture",
"0x12": "_OBJC_PROTOCOL_$_FixtureDelegate",
"0x13": "entry",
}),
),
);
case "procedure_pseudo_code": {
const procedure = arguments_.procedure;
return Promise.resolve(
ok(typeof procedure === "string" ? `pseudo:${procedure}` : "invalid"),
);
}
case "procedure_callees": {
const procedure = arguments_.procedure;
return Promise.resolve(
ok(
procedure === "0x1"
? ["0x2", "0x3"]
: procedure === "0x2"
? ["0x1"]
: [],
),
);
}
case "procedure_callers":
return Promise.resolve(ok(["0x9"]));
case "address_name":
return Promise.resolve(ok(arguments_.address ?? null));
case "xrefs":
return Promise.resolve(ok(["0x20", "0x21"]));
case "resolve_containing_procedure":
return Promise.resolve(
ok({
query_address:
typeof arguments_.address === "string"
? arguments_.address
: "0x0",
found: false,
procedure: null,
reason: "not_in_procedure",
}),
);
case "list_segments":
return Promise.resolve(
ok([{ name: "__TEXT", start: "0x1000", end: "0x2000" }]),
);
case "list_documents":
return Promise.resolve(ok(["fixture"]));
case "list_strings":
return Promise.resolve(ok(page({ "0x30": "hello" })));
case "analyze_function":
return Promise.resolve(
ok({
procedure: {
address: "0x1",
name: "entry",
signature: null,
locals: [],
},
pseudocode: {
text: "return 0;",
total_chars: 9,
returned_chars: 9,
truncated: false,
next_offset: null,
},
assembly: {
items: [],
total: 0,
returned: 0,
truncated: false,
next_offset: null,
},
comments: emptyBounded(),
callers: emptyBounded(),
callees: emptyBounded(),
incoming_references: emptyBounded(),
outgoing_references: emptyBounded(),
referenced_strings: emptyBounded(),
referenced_names: emptyBounded(),
basic_blocks: emptyBounded(),
instruction_scan: { scanned: 0, truncated: false },
}),
);
default:
return Promise.resolve(ok(null));
}
},
});
const emptyBounded = () => ({
items: [],
total: 0,
returned: 0,
truncated: false,
next_offset: null,
});
const resources: Array<{ close(): Promise<void> }> = [];
afterEach(async () => {
await Promise.all(
resources.splice(0).map(async (resource) => resource.close()),
);
});
const connect = async (analysis: AnalysisOperationPort = fixturePort()) => {
const server = createServer(analysis);
const client = new Client({ name: "enhanced-test", version: "1.0.0" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
resources.push(client, server);
await server.connect(serverTransport);
await client.connect(clientTransport);
return client;
};
const jsonResult = (result: CallToolResult): JsonValue => {
if (result.structuredContent === undefined)
throw new Error("Tool result omitted structured content");
const structured = jsonValueSchema.safeParse(result.structuredContent);
if (!structured.success)
throw new Error("Tool structured result was not JSON");
if (
typeof structured.data === "object" &&
structured.data !== null &&
!Array.isArray(structured.data) &&
"normalized_result" in structured.data
) {
return structured.data.normalized_result ?? null;
}
if (
typeof structured.data === "object" &&
structured.data !== null &&
!Array.isArray(structured.data) &&
"evidence_id" in structured.data &&
"result" in structured.data
)
return structured.data.result ?? null;
const text = result.content.find((item) => item.type === "text");
if (text?.type !== "text")
throw new Error("Tool result omitted text content");
const decoded: unknown = JSON.parse(text.text);
const parsed = jsonValueSchema.safeParse(decoded);
if (!parsed.success) throw new Error("Tool result was not JSON");
return parsed.data;
};
describe("enhanced MCP tools", () => {
it("returns typed graph failures and stable unresolved-name results", async () => {
const tools = new EnhancedTools({
execute: (name) =>
name === "procedure_callees"
? Promise.resolve(err(new AnalysisOutputError(name, "failed")))
: Promise.resolve(ok(page({}))),
});
const graph = await tools.execute("get_call_graph", {
address: "0x1",
direction: "forward",
depth: 1,
});
const unresolved = await tools.execute("find_xrefs_to_name", {
name: "missing",
});
expect(graph).toMatchObject({
ok: true,
value: {
"0": [
{
address: "0x1",
status: "error",
error: {
category: "execution_failure",
message:
"Analysis returned an unreadable result. Retry once; if it continues, run `rea doctor`.",
},
},
],
},
});
expect(unresolved).toEqual({
ok: true,
value: {
status: "unresolved",
name: "missing",
reason: "name_not_found",
},
});
});
it("follows procedure pagination for whole-binary workflows", async () => {
const offsets: number[] = [];
const client = await connect({
execute: (_name, arguments_) => {
const offset = arguments_.offset;
offsets.push(typeof offset === "number" ? offset : 0);
return Promise.resolve(
ok({
items: [
{
address: offset === 500 ? "0x2" : "0x1",
value: offset === 500 ? "_TtC4Last" : "_TtC5First",
},
],
offset: typeof offset === "number" ? offset : 0,
limit: 500,
total: 2,
next_offset: offset === 500 ? null : 500,
has_more: offset !== 500,
}),
);
},
});
const result = jsonResult(
await client.callTool({ name: "swift_classes", arguments: {} }),
);
expect(offsets).toEqual([0, 500]);
expect(result).toMatchObject({ count: 2 });
});
it("does not start another page after cancellation", async () => {
const controller = new AbortController();
let calls = 0;
const analysis: AnalysisOperationPort = {
execute: () => {
calls += 1;
controller.abort();
return Promise.resolve(
ok({
items: [{ address: "0x1", value: "_TtC5First" }],
offset: 0,
limit: 500,
total: 2,
next_offset: 500,
has_more: true,
}),
);
},
};
const result = await new EnhancedTools(analysis).execute(
"swift_classes",
{},
controller.signal,
);
expect(calls).toBe(1);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("expected cancellation");
expect(result.error._tag).toBe("AnalysisCancelledError");
});
});

View File

@ -0,0 +1,316 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import type { CallToolResult } from "@modelcontextprotocol/server";
import { afterEach, describe, expect, it } from "vitest";
import type { AnalysisOperationPort } from "../../../src/application/AnalysisProvider.js";
import {
jsonValueSchema,
type JsonValue,
} from "../../../src/domain/jsonValue.js";
import { createServer } from "../../../src/server/createServer.js";
import { observed as ok } from "../../fixtures/analysisExecution.js";
const PROCEDURES = {
"0x1": "_TtC7Fixture5Class",
"0x2": "_TtV7Fixture6Struct",
"0x3": "_TtP7Fixture8Protocol",
"0x4": "_TtO7Fixture4Enum",
"0x5": "_TtE7Fixture9Extension",
"0x6": "prefix_TtOther",
};
const page = (values: Readonly<Record<string, string>>) => ({
items: Object.entries(values).map(([address, value]) => ({ address, value })),
offset: 0,
limit: 100,
total: Object.keys(values).length,
next_offset: null,
has_more: false,
});
const fixturePort = (): AnalysisOperationPort => ({
execute: (name, arguments_) => {
switch (name) {
case "list_procedures":
return Promise.resolve(ok(page(PROCEDURES)));
case "list_names":
return Promise.resolve(
ok(
page({
"0x10": "_OBJC_CLASS_$_Fixture",
"0x11": "_OBJC_CLASS_$_Fixture",
"0x12": "_OBJC_PROTOCOL_$_FixtureDelegate",
"0x13": "entry",
}),
),
);
case "procedure_pseudo_code": {
const procedure = arguments_.procedure;
return Promise.resolve(
ok(typeof procedure === "string" ? `pseudo:${procedure}` : "invalid"),
);
}
case "procedure_callees": {
const procedure = arguments_.procedure;
return Promise.resolve(
ok(
procedure === "0x1"
? ["0x2", "0x3"]
: procedure === "0x2"
? ["0x1"]
: [],
),
);
}
case "procedure_callers":
return Promise.resolve(ok(["0x9"]));
case "address_name":
return Promise.resolve(ok(arguments_.address ?? null));
case "xrefs":
return Promise.resolve(ok(["0x20", "0x21"]));
case "resolve_containing_procedure":
return Promise.resolve(
ok({
query_address:
typeof arguments_.address === "string"
? arguments_.address
: "0x0",
found: false,
procedure: null,
reason: "not_in_procedure",
}),
);
case "list_segments":
return Promise.resolve(
ok([{ name: "__TEXT", start: "0x1000", end: "0x2000" }]),
);
case "list_documents":
return Promise.resolve(ok(["fixture"]));
case "list_strings":
return Promise.resolve(ok(page({ "0x30": "hello" })));
case "analyze_function":
return Promise.resolve(
ok({
procedure: {
address: "0x1",
name: "entry",
signature: null,
locals: [],
},
pseudocode: {
text: "return 0;",
total_chars: 9,
returned_chars: 9,
truncated: false,
next_offset: null,
},
assembly: {
items: [],
total: 0,
returned: 0,
truncated: false,
next_offset: null,
},
comments: emptyBounded(),
callers: emptyBounded(),
callees: emptyBounded(),
incoming_references: emptyBounded(),
outgoing_references: emptyBounded(),
referenced_strings: emptyBounded(),
referenced_names: emptyBounded(),
basic_blocks: emptyBounded(),
instruction_scan: { scanned: 0, truncated: false },
}),
);
default:
return Promise.resolve(ok(null));
}
},
});
const emptyBounded = () => ({
items: [],
total: 0,
returned: 0,
truncated: false,
next_offset: null,
});
const resources: Array<{ close(): Promise<void> }> = [];
afterEach(async () => {
await Promise.all(
resources.splice(0).map(async (resource) => resource.close()),
);
});
const connect = async (analysis: AnalysisOperationPort = fixturePort()) => {
const server = createServer(analysis);
const client = new Client({ name: "enhanced-test", version: "1.0.0" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
resources.push(client, server);
await server.connect(serverTransport);
await client.connect(clientTransport);
return client;
};
const jsonResult = (result: CallToolResult): JsonValue => {
if (result.structuredContent === undefined)
throw new Error("Tool result omitted structured content");
const structured = jsonValueSchema.safeParse(result.structuredContent);
if (!structured.success)
throw new Error("Tool structured result was not JSON");
if (
typeof structured.data === "object" &&
structured.data !== null &&
!Array.isArray(structured.data) &&
"normalized_result" in structured.data
) {
return structured.data.normalized_result ?? null;
}
if (
typeof structured.data === "object" &&
structured.data !== null &&
!Array.isArray(structured.data) &&
"evidence_id" in structured.data &&
"result" in structured.data
)
return structured.data.result ?? null;
const text = result.content.find((item) => item.type === "text");
if (text?.type !== "text")
throw new Error("Tool result omitted text content");
const decoded: unknown = JSON.parse(text.text);
const parsed = jsonValueSchema.safeParse(decoded);
if (!parsed.success) throw new Error("Tool result was not JSON");
return parsed.data;
};
describe("enhanced MCP tools", () => {
it("follows name pagination for exhaustive Objective-C discovery", async () => {
const offsets: number[] = [];
const client = await connect({
execute: (name, arguments_) => {
expect(name).toBe("list_names");
const offset =
typeof arguments_.offset === "number" ? arguments_.offset : 0;
offsets.push(offset);
return Promise.resolve(
ok({
items: [
{
address: offset === 500 ? "0x2" : "0x1",
value:
offset === 500 ? "_OBJC_CLASS_$_Last" : "_OBJC_CLASS_$_First",
},
],
offset,
limit: 500,
total: 2,
next_offset: offset === 500 ? null : 500,
has_more: offset !== 500,
}),
);
},
});
const result = jsonResult(
await client.callTool({ name: "get_objc_classes", arguments: {} }),
);
expect(offsets).toEqual([0, 500]);
expect(result).toMatchObject({ count: 2 });
});
it("honors overview detail and limit while reporting exhaustive totals", async () => {
const procedureOffsets: number[] = [];
const client = await connect({
execute: (name, arguments_) => {
switch (name) {
case "list_segments":
return Promise.resolve(
ok([
{ name: "__TEXT", start: "0x1000", end: "0x1800" },
{ name: "__DATA", start: "0x1800", end: "0x2000" },
]),
);
case "list_documents":
return Promise.resolve(ok(["fixture"]));
case "list_strings":
return Promise.resolve(
ok({
items: [{ address: "0x30", value: "first page only" }],
offset: 0,
limit: 100,
total: 700,
next_offset: 100,
has_more: true,
}),
);
case "list_procedures": {
const offset =
typeof arguments_.offset === "number" ? arguments_.offset : 0;
procedureOffsets.push(offset);
return Promise.resolve(
ok({
items: [
{
address: offset === 500 ? "0x2" : "0x1",
value: offset === 500 ? "last" : "first",
},
],
offset,
limit: 500,
total: 2,
next_offset: offset === 500 ? null : 500,
has_more: offset !== 500,
}),
);
}
default:
return Promise.resolve(ok(null));
}
},
});
const result = jsonResult(
await client.callTool({
name: "binary_overview",
arguments: { detail: "detailed", limit: 1 },
}),
);
expect(procedureOffsets).toEqual([0]);
expect(result).toEqual({
document: "fixture",
detail: "detailed",
segments: [
{ name: "__TEXT", start: "0x1000", end: "0x1800", length: 2048 },
],
segment_count: 2,
procedure_count: 2,
string_count: 700,
});
});
it("rejects non-advancing pagination metadata", async () => {
const client = await connect({
execute: () =>
Promise.resolve(
ok({
items: [{ address: "0x1", value: "_TtC5First" }],
offset: 0,
limit: 500,
total: 2,
next_offset: 0,
has_more: true,
}),
),
});
const result = await client.callTool({
name: "swift_classes",
arguments: {},
});
expect(result.isError).toBe(true);
const text = result.content.find((item) => item.type === "text");
expect(text?.type === "text" ? text.text : "").toBe(
JSON.stringify(result.structuredContent),
);
});
});

View File

@ -0,0 +1,304 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import type { CallToolResult } from "@modelcontextprotocol/server";
import { afterEach, describe, expect, it } from "vitest";
import type { AnalysisOperationPort } from "../../../src/application/AnalysisProvider.js";
import {
jsonValueSchema,
type JsonValue,
} from "../../../src/domain/jsonValue.js";
import { createServer } from "../../../src/server/createServer.js";
import { observed as ok } from "../../fixtures/analysisExecution.js";
const PROCEDURES = {
"0x1": "_TtC7Fixture5Class",
"0x2": "_TtV7Fixture6Struct",
"0x3": "_TtP7Fixture8Protocol",
"0x4": "_TtO7Fixture4Enum",
"0x5": "_TtE7Fixture9Extension",
"0x6": "prefix_TtOther",
};
const page = (values: Readonly<Record<string, string>>) => ({
items: Object.entries(values).map(([address, value]) => ({ address, value })),
offset: 0,
limit: 100,
total: Object.keys(values).length,
next_offset: null,
has_more: false,
});
const fixturePort = (): AnalysisOperationPort => ({
execute: (name, arguments_) => {
switch (name) {
case "list_procedures":
return Promise.resolve(ok(page(PROCEDURES)));
case "list_names":
return Promise.resolve(
ok(
page({
"0x10": "_OBJC_CLASS_$_Fixture",
"0x11": "_OBJC_CLASS_$_Fixture",
"0x12": "_OBJC_PROTOCOL_$_FixtureDelegate",
"0x13": "entry",
}),
),
);
case "procedure_pseudo_code": {
const procedure = arguments_.procedure;
return Promise.resolve(
ok(typeof procedure === "string" ? `pseudo:${procedure}` : "invalid"),
);
}
case "procedure_callees": {
const procedure = arguments_.procedure;
return Promise.resolve(
ok(
procedure === "0x1"
? ["0x2", "0x3"]
: procedure === "0x2"
? ["0x1"]
: [],
),
);
}
case "procedure_callers":
return Promise.resolve(ok(["0x9"]));
case "address_name":
return Promise.resolve(ok(arguments_.address ?? null));
case "xrefs":
return Promise.resolve(ok(["0x20", "0x21"]));
case "resolve_containing_procedure":
return Promise.resolve(
ok({
query_address:
typeof arguments_.address === "string"
? arguments_.address
: "0x0",
found: false,
procedure: null,
reason: "not_in_procedure",
}),
);
case "list_segments":
return Promise.resolve(
ok([{ name: "__TEXT", start: "0x1000", end: "0x2000" }]),
);
case "list_documents":
return Promise.resolve(ok(["fixture"]));
case "list_strings":
return Promise.resolve(ok(page({ "0x30": "hello" })));
case "analyze_function":
return Promise.resolve(
ok({
procedure: {
address: "0x1",
name: "entry",
signature: null,
locals: [],
},
pseudocode: {
text: "return 0;",
total_chars: 9,
returned_chars: 9,
truncated: false,
next_offset: null,
},
assembly: {
items: [],
total: 0,
returned: 0,
truncated: false,
next_offset: null,
},
comments: emptyBounded(),
callers: emptyBounded(),
callees: emptyBounded(),
incoming_references: emptyBounded(),
outgoing_references: emptyBounded(),
referenced_strings: emptyBounded(),
referenced_names: emptyBounded(),
basic_blocks: emptyBounded(),
instruction_scan: { scanned: 0, truncated: false },
}),
);
default:
return Promise.resolve(ok(null));
}
},
});
const emptyBounded = () => ({
items: [],
total: 0,
returned: 0,
truncated: false,
next_offset: null,
});
const resources: Array<{ close(): Promise<void> }> = [];
afterEach(async () => {
await Promise.all(
resources.splice(0).map(async (resource) => resource.close()),
);
});
const connect = async (analysis: AnalysisOperationPort = fixturePort()) => {
const server = createServer(analysis);
const client = new Client({ name: "enhanced-test", version: "1.0.0" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
resources.push(client, server);
await server.connect(serverTransport);
await client.connect(clientTransport);
return client;
};
const jsonResult = (result: CallToolResult): JsonValue => {
if (result.structuredContent === undefined)
throw new Error("Tool result omitted structured content");
const structured = jsonValueSchema.safeParse(result.structuredContent);
if (!structured.success)
throw new Error("Tool structured result was not JSON");
if (
typeof structured.data === "object" &&
structured.data !== null &&
!Array.isArray(structured.data) &&
"normalized_result" in structured.data
) {
return structured.data.normalized_result ?? null;
}
if (
typeof structured.data === "object" &&
structured.data !== null &&
!Array.isArray(structured.data) &&
"evidence_id" in structured.data &&
"result" in structured.data
)
return structured.data.result ?? null;
const text = result.content.find((item) => item.type === "text");
if (text?.type !== "text")
throw new Error("Tool result omitted text content");
const decoded: unknown = JSON.parse(text.text);
const parsed = jsonValueSchema.safeParse(decoded);
if (!parsed.success) throw new Error("Tool result was not JSON");
return parsed.data;
};
describe("enhanced MCP tools", () => {
it("stops trace_feature at its explicit operation budget", async () => {
let operations = 0;
const client = await connect({
execute: () => {
operations += 1;
return Promise.resolve(
ok({
items: [{ address: "0x1", value: "needle" }],
offset: 0,
limit: 500,
total: 2,
next_offset: 1,
has_more: true,
}),
);
},
});
const result = jsonResult(
await client.callTool({
name: "trace_feature",
arguments: { query: "needle", max_operations: 1 },
}),
);
expect(operations).toBe(1);
expect(result).toMatchObject({
operations_used: 1,
operation_budget: 1,
truncated: true,
});
expect(JSON.stringify(result)).toContain("operation budget");
});
it("returns correction-only structured validation issues", async () => {
const client = await connect();
const valid = await client.callTool({
name: "trace_feature",
arguments: { query: "needle", max_operations: 1 },
});
expect(valid.isError).not.toBe(true);
const misspelled = await client.callTool({
name: "trace_feature",
arguments: { query: "needle", max_operatons: 1 },
});
expect(misspelled).toMatchObject({
isError: true,
structuredContent: {
error: {
code: "invalid_request",
retryable: true,
details: {
issues: [{ path: ["max_operatons"], reason: "unknown_argument" }],
},
},
},
});
const nestedMisspelled = await client.callTool({
name: "analyze_function",
arguments: {
procedure: "0x1",
collection_offset: { commentz: 1 },
},
});
expect(nestedMisspelled).toMatchObject({
isError: true,
structuredContent: {
error: {
details: {
issues: [
{
path: ["collection_offset", "commentz"],
reason: "unknown_argument",
},
],
},
},
},
});
const bounded = await client.callTool({
name: "trace_feature",
arguments: { query: "needle", max_operations: 101 },
});
expect(bounded.structuredContent).toMatchObject({
error: {
details: {
issues: [
{
path: ["max_operations"],
reason: "out_of_range",
maximum: 100,
},
],
},
},
});
expect(bounded.content[0]).toEqual({
type: "text",
text: JSON.stringify(bounded.structuredContent),
});
});
it("returns a typed tool error for malformed Hopper boundary values", async () => {
const client = await connect({
execute: () => Promise.resolve(ok(["not", "a", "procedure", "map"])),
});
const result = await client.callTool({
name: "swift_classes",
arguments: {},
});
expect(result.isError).toBe(true);
const text = result.content.find((item) => item.type === "text");
expect(text?.type === "text" ? text.text : "").toBe(
JSON.stringify(result.structuredContent),
);
});
});

View File

@ -0,0 +1,242 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { afterEach, describe, expect, it } from "vitest";
import type { AnalysisOperationPort } from "../../../src/application/AnalysisProvider.js";
import { createServer } from "../../../src/server/createServer.js";
import { observed as ok } from "../../fixtures/analysisExecution.js";
const PROCEDURES = {
"0x1": "_TtC7Fixture5Class",
"0x2": "_TtV7Fixture6Struct",
"0x3": "_TtP7Fixture8Protocol",
"0x4": "_TtO7Fixture4Enum",
"0x5": "_TtE7Fixture9Extension",
"0x6": "prefix_TtOther",
};
const page = (values: Readonly<Record<string, string>>) => ({
items: Object.entries(values).map(([address, value]) => ({ address, value })),
offset: 0,
limit: 100,
total: Object.keys(values).length,
next_offset: null,
has_more: false,
});
const fixturePort = (): AnalysisOperationPort => ({
execute: (name, arguments_) => {
switch (name) {
case "list_procedures":
return Promise.resolve(ok(page(PROCEDURES)));
case "list_names":
return Promise.resolve(
ok(
page({
"0x10": "_OBJC_CLASS_$_Fixture",
"0x11": "_OBJC_CLASS_$_Fixture",
"0x12": "_OBJC_PROTOCOL_$_FixtureDelegate",
"0x13": "entry",
}),
),
);
case "procedure_pseudo_code": {
const procedure = arguments_.procedure;
return Promise.resolve(
ok(typeof procedure === "string" ? `pseudo:${procedure}` : "invalid"),
);
}
case "procedure_callees": {
const procedure = arguments_.procedure;
return Promise.resolve(
ok(
procedure === "0x1"
? ["0x2", "0x3"]
: procedure === "0x2"
? ["0x1"]
: [],
),
);
}
case "procedure_callers":
return Promise.resolve(ok(["0x9"]));
case "address_name":
return Promise.resolve(ok(arguments_.address ?? null));
case "xrefs":
return Promise.resolve(ok(["0x20", "0x21"]));
case "resolve_containing_procedure":
return Promise.resolve(
ok({
query_address:
typeof arguments_.address === "string"
? arguments_.address
: "0x0",
found: false,
procedure: null,
reason: "not_in_procedure",
}),
);
case "list_segments":
return Promise.resolve(
ok([{ name: "__TEXT", start: "0x1000", end: "0x2000" }]),
);
case "list_documents":
return Promise.resolve(ok(["fixture"]));
case "list_strings":
return Promise.resolve(ok(page({ "0x30": "hello" })));
case "analyze_function":
return Promise.resolve(
ok({
procedure: {
address: "0x1",
name: "entry",
signature: null,
locals: [],
},
pseudocode: {
text: "return 0;",
total_chars: 9,
returned_chars: 9,
truncated: false,
next_offset: null,
},
assembly: {
items: [],
total: 0,
returned: 0,
truncated: false,
next_offset: null,
},
comments: emptyBounded(),
callers: emptyBounded(),
callees: emptyBounded(),
incoming_references: emptyBounded(),
outgoing_references: emptyBounded(),
referenced_strings: emptyBounded(),
referenced_names: emptyBounded(),
basic_blocks: emptyBounded(),
instruction_scan: { scanned: 0, truncated: false },
}),
);
default:
return Promise.resolve(ok(null));
}
},
});
const emptyBounded = () => ({
items: [],
total: 0,
returned: 0,
truncated: false,
next_offset: null,
});
const resources: Array<{ close(): Promise<void> }> = [];
afterEach(async () => {
await Promise.all(
resources.splice(0).map(async (resource) => resource.close()),
);
});
const connect = async (analysis: AnalysisOperationPort = fixturePort()) => {
const server = createServer(analysis);
const client = new Client({ name: "enhanced-test", version: "1.0.0" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
resources.push(client, server);
await server.connect(serverTransport);
await client.connect(clientTransport);
return client;
};
describe("enhanced MCP tools", () => {
it("rejects incomplete function dossiers at the application boundary", async () => {
const client = await connect({
execute: () =>
Promise.resolve(
ok({
procedure: { address: "0x1", name: "entry" },
pseudocode: { text: "plausible but incomplete" },
}),
),
});
const result = await client.callTool({
name: "analyze_function",
arguments: { procedure: "0x1" },
});
expect(result.isError).toBe(true);
const text = result.content.find((item) => item.type === "text");
expect(text?.type === "text" ? text.text : "").toBe(
JSON.stringify(result.structuredContent),
);
});
it("rejects deceptive function dossier collection metadata", async () => {
const malformedPort = fixturePort();
const client = await connect({
execute: async (name, arguments_, options) => {
const result = await malformedPort.execute(name, arguments_, options);
if (!result.ok || name !== "analyze_function") return result;
const dossier = result.value.result;
if (
typeof dossier !== "object" ||
dossier === null ||
Array.isArray(dossier)
)
return result;
return ok({
...dossier,
comments: {
items: [],
total: 0,
returned: 1,
truncated: false,
next_offset: null,
},
});
},
});
const result = await client.callTool({
name: "analyze_function",
arguments: { procedure: "0x1" },
});
expect(result.isError).toBe(true);
const text = result.content.find((item) => item.type === "text");
expect(text?.type === "text" ? text.text : "").toBe(
JSON.stringify(result.structuredContent),
);
});
it("accepts a final dossier page whose total exceeds returned", async () => {
const malformedPort = fixturePort();
const client = await connect({
execute: async (name, arguments_, options) => {
const result = await malformedPort.execute(name, arguments_, options);
if (!result.ok || name !== "analyze_function") return result;
const dossier = result.value.result;
if (
typeof dossier !== "object" ||
dossier === null ||
Array.isArray(dossier)
)
return result;
return ok({
...dossier,
callers: {
items: [],
total: 1,
returned: 0,
truncated: false,
next_offset: null,
},
});
},
});
const result = await client.callTool({
name: "analyze_function",
arguments: { procedure: "0x1" },
});
expect(result.isError).not.toBe(true);
});
});

View File

@ -0,0 +1,269 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { describe, expect, it } from "vitest";
import { z } from "zod";
import { composeBinarySessionFromFactory } from "../../../src/application/BinarySessionComposition.js";
import type { BinarySession } from "../../../src/application/BinarySession.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import { observed } from "../../fixtures/analysisExecution.js";
import { createServer } from "../../../src/server/createServer.js";
import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
import { createInvestigationWorkspace } from "../../../src/domain/investigationWorkspace.js";
const provider = { id: "fixture", name: "Fixture", version: "1" };
interface ResourceFixture {
readonly evidence: ReturnType<typeof createEvidence>;
readonly capture: ReturnType<typeof createEvidence>;
readonly workspace: ReturnType<typeof createInvestigationWorkspace>;
readonly unknown: ReturnType<BinarySession["recordUnknown"]>;
}
function seedResourceFixture(session: BinarySession): ResourceFixture {
const evidence = createEvidence(undefined, provider, {
operation: "resource_probe",
parameters: {},
result: { observed: true },
});
session.recordEvidence(evidence);
const capture = createEvidence(undefined, provider, {
operation: "capture_process_scenario",
parameters: {},
result: {
frames: [{ sequence: 0, at_ms: 0, data: "ready" }],
protocol_events: [],
replay_transitions: [],
},
});
session.recordEvidence(capture);
const workspace = createInvestigationWorkspace(
"resource test",
createEvidenceBundle([]),
[],
);
session.retainInvestigationWorkspace(workspace);
const unknown = session.recordUnknown({
approved: true,
question: "Was every branch observed?",
severity: "medium",
domain: "control-flow",
supporting_evidence_ids: [evidence.evidence_id],
contradicting_evidence_ids: [],
required_authority: "controlled-replay",
required_confidence: "observed",
required_environment: null,
recommended_probes: [],
relationships: [],
});
expect(unknown.ok).toBe(true);
return { evidence, capture, workspace, unknown };
}
async function verifyResourceProtocol(
client: Client,
fixture: ResourceFixture,
): Promise<void> {
const { evidence, capture, workspace, unknown } = fixture;
const templates = await client.listResourceTemplates();
expect(templates.resourceTemplates).toContainEqual(
expect.objectContaining({ uriTemplate: "rea://evidence/{evidenceId}" }),
);
expect(
templates.resourceTemplates.map(({ uriTemplate }) => uriTemplate),
).toEqual(
expect.arrayContaining([
"rea://artifact/{manifestId}/{collection}",
"rea://function/{targetSha256}/{address}",
"rea://evidence-bundle/{bundleDigest}",
"rea://workspace/{workspaceId}/revision/{revision}",
]),
);
expect(templates.resourceTemplates).toContainEqual(
expect.objectContaining({
uriTemplate: "rea://evidence/{evidenceId}/section/{section}",
}),
);
const listed = await client.listResources();
expect(listed.resources).toContainEqual(
expect.objectContaining({ uri: "rea://snapshot/current" }),
);
expect(listed.resources).not.toContainEqual(
expect.objectContaining({
uri: `rea://evidence/${evidence.evidence_id}`,
}),
);
const read = await client.readResource({
uri: `rea://evidence/${evidence.evidence_id}`,
});
expect(read.contents).toEqual([
expect.objectContaining({
uri: `rea://evidence/${evidence.evidence_id}`,
mimeType: "application/json",
text: expect.stringContaining(evidence.evidence_id),
}),
]);
const firstSnapshot = await client.callTool({
name: "snapshot_evidence_bundle",
arguments: {},
});
const secondSnapshot = await client.callTool({
name: "snapshot_evidence_bundle",
arguments: {},
});
expect(secondSnapshot.structuredContent).toEqual(
firstSnapshot.structuredContent,
);
expect(firstSnapshot.content).toContainEqual(
expect.objectContaining({
type: "text",
text: expect.stringContaining("Copy the opaque URI exactly"),
}),
);
const bundleUri = z
.object({ result: z.object({ bundle_uri: z.string() }) })
.parse(firstSnapshot.structuredContent).result.bundle_uri;
const retainedBundle = await client.readResource({ uri: bundleUri });
expect(retainedBundle.contents[0]).toEqual(
expect.objectContaining({
uri: bundleUri,
text: expect.stringContaining(evidence.evidence_id),
}),
);
const terminal = await client.readResource({
uri: `rea://evidence/${capture.evidence_id}/section/terminal`,
});
const workspaceResource = await client.readResource({
uri: `rea://workspace/${workspace.workspace_id}/revision/${String(workspace.revision)}`,
});
expect(workspaceResource.contents[0]).toEqual(
expect.objectContaining({
text: expect.stringContaining(workspace.revision_digest),
}),
);
expect(terminal.contents[0]).toEqual(
expect.objectContaining({ text: expect.stringContaining("ready") }),
);
if (unknown.ok) {
const unknownResource = await client.readResource({
uri: `rea://unknown/${unknown.value.unknown_id}`,
});
expect(unknownResource.contents[0]).toEqual(
expect.objectContaining({
text: expect.stringContaining(unknown.value.revision_digest),
}),
);
}
await expect(
client.readResource({ uri: `rea://evidence/ev_${"0".repeat(64)}` }),
).rejects.toThrow(/not found/iu);
}
describe("evidence MCP resources", () => {
it("publishes mutable snapshot changes for every Evidence mutation", () => {
const session = composeBinarySessionFromFactory(() => ({
health: () => Promise.resolve(),
execute: () => Promise.resolve(observed(null)),
close: () => Promise.resolve(),
}));
let updates = 0;
session.onAnalysisSnapshotChanged(() => {
updates += 1;
});
const first = createEvidence(undefined, provider, {
operation: "notification_probe",
parameters: {},
result: 1,
});
expect(session.recordEvidence(first)).toEqual({ ok: true, value: "added" });
expect(session.recordEvidence(first)).toEqual({
ok: true,
value: "duplicate",
});
const imported = createEvidence(undefined, provider, {
operation: "import_notification_probe",
parameters: {},
result: 2,
});
expect(
session.importEvidenceBundle(createEvidenceBundle([imported])),
).toEqual({ ok: true, value: 1 });
expect(
session.recordUnknown({
approved: true,
question: "Does mutation publish a snapshot update?",
severity: "low",
domain: "notifications",
supporting_evidence_ids: [first.evidence_id],
contradicting_evidence_ids: [],
required_authority: "controlled-replay",
required_confidence: "observed",
required_environment: null,
recommended_probes: [],
relationships: [],
}).ok,
).toBe(true);
expect(updates).toBe(3);
});
it("retains bundles without eviction and rejects a seventeenth digest", () => {
const session = composeBinarySessionFromFactory(() => ({
health: () => Promise.resolve(),
execute: () => Promise.resolve(observed(null)),
close: () => Promise.resolve(),
}));
const first = session.snapshotEvidenceBundle();
expect(first.ok).toBe(true);
for (let index = 0; index < 15; index += 1) {
session.recordEvidence(
createEvidence(undefined, provider, {
operation: `retention_probe_${String(index)}`,
parameters: {},
result: index,
}),
);
expect(session.snapshotEvidenceBundle().ok).toBe(true);
}
session.recordEvidence(
createEvidence(undefined, provider, {
operation: "retention_probe_overflow",
parameters: {},
result: true,
}),
);
expect(session.snapshotEvidenceBundle()).toMatchObject({
ok: false,
error: { _tag: "EvidenceLimitError", limit: "records", maximum: 16 },
});
if (first.ok)
expect(session.retainedEvidenceBundle(first.value.bundleDigest)).toEqual(
expect.any(String),
);
});
it("lists and reads only evidence owned by the current session", async () => {
const session = composeBinarySessionFromFactory(() => ({
health: () => Promise.resolve(),
execute: () => Promise.resolve(observed(null)),
close: () => Promise.resolve(),
}));
const fixture = seedResourceFixture(session);
const server = createServer(session, session);
const client = new Client({
name: "evidence-resource-test",
version: "1",
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
await verifyResourceProtocol(client, fixture);
} finally {
await Promise.allSettled([
client.close(),
server.close(),
session.close(),
]);
}
}, 10_000);
});

View File

@ -1,15 +1,15 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { describe, expect, it } from "vitest";
import { BinarySession } from "../src/application/BinarySession.js";
import { FUNCTION_COMPARISON_EXAMPLE } from "../src/contracts/functionComparisonExample.js";
import { createEvidence } from "../src/domain/evidence.js";
import { createServer } from "../src/server/createServer.js";
import { observed } from "./fixtures/analysisExecution.js";
import { composeBinarySessionFromFactory } from "../../../src/application/BinarySessionComposition.js";
import { FUNCTION_COMPARISON_EXAMPLE } from "../../../src/contracts/functionComparisonExample.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import { createServer } from "../../../src/server/createServer.js";
import { observed } from "../../fixtures/analysisExecution.js";
describe("function comparison MCP integration", () => {
it("records linked comparison Evidence and an approved residual unknown", async () => {
const session = new BinarySession(() => ({
const session = composeBinarySessionFromFactory(() => ({
health: () => Promise.resolve(),
execute: () => Promise.resolve(observed(null)),
close: () => Promise.resolve(),
@ -75,7 +75,7 @@ describe("function comparison MCP integration", () => {
});
it("rejects dangling Evidence links before recording output", async () => {
const session = new BinarySession(() => ({
const session = composeBinarySessionFromFactory(() => ({
health: () => Promise.resolve(),
execute: () => Promise.resolve(observed(null)),
close: () => Promise.resolve(),
@ -106,7 +106,7 @@ describe("function comparison MCP integration", () => {
});
it("rejects Evidence IDs with the wrong operation", async () => {
const session = new BinarySession(() => ({
const session = composeBinarySessionFromFactory(() => ({
health: () => Promise.resolve(),
execute: () => Promise.resolve(observed(null)),
close: () => Promise.resolve(),

View File

@ -0,0 +1,150 @@
import { describe, expect, it } from "vitest";
import { EnhancedTools } from "../../../src/application/EnhancedTools.js";
import { connectGhidraMcp, sessionEvidence } from "./ghidraMcpHarness.js";
describe("Ghidra MCP evidence parity", () => {
it("preserves provider evidence, composed parity, and capability routing", async () => {
const harness = await connectGhidraMcp("ghidra-parity");
const { mcp, session } = harness;
try {
const listed = sessionEvidence(
session,
(
await mcp.callTool({
name: "list_procedures",
arguments: {},
})
).structuredContent,
);
expect(listed).toMatchObject({
operation: "list_procedures",
provider: { id: "ghidra", name: "Ghidra", version: "12.1.2" },
analysis_profile: {
provider: { id: "ghidra", version: "12.1.2" },
parameters: {
import_mode: "ephemeral-read-only",
analyzer_preset: "ghidra-default",
},
},
normalized_result: {
items: [
{
address: "0x401000",
value: "fixture_main",
procedure: {
external: false,
thunk: false,
thunk_target: null,
},
},
],
total: 1,
},
raw_result: {
items: [{ value_truncated: false }],
},
});
const mcpOverview = sessionEvidence(
session,
(
await mcp.callTool({
name: "binary_overview",
arguments: { detail: "detailed", limit: 5 },
})
).structuredContent,
);
const directOverview = await new EnhancedTools(session).execute(
"binary_overview",
{ detail: "detailed", limit: 5 },
);
expect(directOverview.ok).toBe(true);
if (!directOverview.ok) return;
expect(mcpOverview.normalized_result).toEqual(directOverview.value);
expect(mcpOverview).toMatchObject({
provider: { id: "rea-workflow" },
confidence: "derived",
normalized_result: {
document: "fixture",
segment_count: 1,
procedure_count: 1,
string_count: 2,
segments: [{ name: ".text", length: 256 }],
},
});
const pseudocode = sessionEvidence(
session,
(
await mcp.callTool({
name: "procedure_pseudo_code",
arguments: { procedure: "fixture_main" },
})
).structuredContent,
);
expect(pseudocode).toMatchObject({
operation: "procedure_pseudo_code",
provider: { id: "ghidra", version: "12.1.2" },
normalized_result: expect.stringContaining("return 42"),
limitations: expect.arrayContaining([
expect.stringContaining("not text-equivalent to Hopper"),
expect.stringContaining("30-second native deadline"),
]),
});
const analyzed = sessionEvidence(
session,
(
await mcp.callTool({
name: "analyze_function",
arguments: { procedure: "fixture_main", include_assembly: true },
})
).structuredContent,
);
expect(analyzed).toMatchObject({
operation: "analyze_function",
provider: { id: "rea-workflow", version: "1" },
normalized_result: {
procedure: {
address: "0x401000",
name: "fixture_main",
classification: {
external: false,
thunk: false,
provenance: "ghidra-function-manager",
},
},
pseudocode: { truncated: false },
outgoing_references: {
items: [
{
kind: {
available: true,
provenance: "ghidra-reference-manager",
},
},
],
},
limitations: expect.arrayContaining([
expect.stringContaining("indirect flows without target addresses"),
expect.stringContaining(
"not original source or Hopper-equivalent text",
),
]),
},
limitations: [
"Derived by an REA workflow from one or more provider observations.",
],
});
const directAnalyzed = await new EnhancedTools(session).execute(
"analyze_function",
{ procedure: "fixture_main", include_assembly: true },
);
expect(directAnalyzed.ok).toBe(true);
if (!directAnalyzed.ok) return;
expect(analyzed.normalized_result).toEqual(directAnalyzed.value);
} finally {
await harness.close();
}
}, 30_000);
});

View File

@ -0,0 +1,299 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { composeBinarySession } from "../../../src/application/BinarySessionComposition.js";
import type { BinarySession } from "../../../src/application/BinarySession.js";
import { AnalysisProviderRegistry } from "../../../src/application/AnalysisProviderRegistry.js";
import { SessionProviderRouter } from "../../../src/application/SessionProviderRouter.js";
import { parseConfig } from "../../../src/config.js";
import type { JsonValue } from "../../../src/domain/jsonValue.js";
import { ok } from "../../../src/domain/result.js";
import type { GhidraInstallationHost } from "../../../src/ghidra/GhidraInstallation.js";
import type { GhidraOperation } from "../../../src/ghidra/GhidraClient.js";
import {
GhidraProvider,
type GhidraProviderClientFactory,
} from "../../../src/ghidra/GhidraProvider.js";
import { GHIDRA_SESSION_CAPABILITIES } from "../../../src/ghidra/GhidraSessionValues.js";
import { silentLogger } from "../../../src/logger.js";
import { createServer } from "../../../src/server/createServer.js";
import {
ghidraBounded,
ghidraFunctionClassification,
ghidraFunctionDossier,
ghidraFunctionIdentity,
ghidraReferenceEdge,
} from "../../fixtures/ghidraFunction.js";
const INSTALL = "/opt/ghidra_12.1.2_PUBLIC";
export const connectGhidraMcp = async (name: string) => {
const calls: GhidraOperation[] = [];
const factory: GhidraProviderClientFactory = (options) => ({
start: () =>
Promise.resolve(
ok(sessionInfo(options.profileDigest, options.targetSha256)),
),
callTool: (operation, input) => {
calls.push(operation);
return Promise.resolve(ok(resultFor(operation, input)));
},
close: () => Promise.resolve(),
});
const config = parseConfig({ GHIDRA_INSTALL_DIR: INSTALL });
if (!config.ok) throw config.error;
const provider = new GhidraProvider(
config.value,
silentLogger,
installationHost(),
factory,
);
const session = composeBinarySession(
SessionProviderRouter.selectable(
new AnalysisProviderRegistry([provider]),
[],
),
);
const server = createServer(session, session, { logger: silentLogger });
const mcp = new Client({ name, version: "1.0.0" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
const close = async () => {
await Promise.allSettled([mcp.close(), server.close()]);
await session.close();
};
try {
await server.connect(serverTransport);
await mcp.connect(clientTransport);
const opened = await mcp.callTool({
name: "open_binary",
arguments: { path: process.execPath },
});
if (opened.isError === true)
throw new Error("The Ghidra MCP harness could not open its target");
} catch (cause: unknown) {
await close();
throw cause;
}
return {
calls,
mcp,
session,
close,
};
};
export const sessionEvidence = (session: BinarySession, value: unknown) => {
if (
typeof value !== "object" ||
value === null ||
!("evidence_id" in value) ||
typeof value.evidence_id !== "string"
)
throw new TypeError(
`Missing compact Evidence ID: ${JSON.stringify(value)}`,
);
const evidence = session.evidenceById(value.evidence_id);
if (evidence === undefined) throw new TypeError("Missing session Evidence");
return evidence;
};
const installationHost = (): GhidraInstallationHost => ({
platform: "linux",
architecture: "x64",
readText: () => "application.version=12.1.2\n",
executable: () => true,
probeJava: () => ({
version: "21.0.11",
major: 21,
home: "/usr/lib/jvm/jdk-21",
bits: 64,
jdk: true,
}),
});
const sessionInfo = (profileDigest: string, targetSha256: string) => ({
name: "REA Ghidra bridge" as const,
bridge_version: 6 as const,
run_id: "11111111-1111-4111-8111-111111111111",
profile_digest: profileDigest,
provider: { id: "ghidra" as const, version: "12.1.2" },
read_only: true as const,
analysis_complete: true,
analysis_timed_out: false,
capabilities: [...GHIDRA_SESSION_CAPABILITIES],
target: {
name: "fixture",
language_id: "x86:LE:64:default",
compiler_spec_id: "gcc",
image_base: "0x400000",
default_address_space: "ram",
sha256: targetSha256,
},
});
type GhidraResultBuilder = (
input: Readonly<Record<string, JsonValue>>,
limit: number,
) => JsonValue;
const resultBuilders = new Map<GhidraOperation, GhidraResultBuilder>([
["list_documents", () => ["fixture"]],
[
"list_procedures",
(_input, limit) =>
page(
[
{
address: "0x401000",
value: "fixture_main",
value_truncated: false,
procedure: {
external: false,
thunk: false,
thunk_target: null,
},
},
],
limit,
),
],
[
"list_strings",
(_input, limit) =>
page(
[
stringItem("0x402000", "inventory fixture"),
stringItem("0x402020", "external fixture"),
],
limit,
),
],
[
"list_segments",
() => [
{
name: ".text",
start: "0x401000",
end: "0x401100",
readable: true,
writable: false,
executable: true,
permissions: { available: true, source: "ghidra-memory-block" },
provenance: "ghidra-memory-block",
address_space: "ram",
image_base: "0x400000",
initialized: true,
overlay: false,
sections: [],
},
],
],
[
"list_names",
(_input, limit) =>
page(
[
{
address: "0x401000",
value: "fixture_main",
value_truncated: false,
symbol: {
primary: true,
dynamic: false,
external: false,
type: "function",
source: "user_defined",
},
},
],
limit,
),
],
["search_procedures", (_input, limit) => page([], limit)],
["search_strings", (_input, limit) => page([], limit)],
["address_name", () => "fixture_main"],
["procedure_address", () => "0x401000"],
[
"resolve_containing_procedure",
() => ({
query_address: "0x401001",
found: true,
procedure: ghidraFunctionIdentity(),
}),
],
["procedure_assembly", () => "0x401000: CALL 0x401020\n0x401005: RET"],
["procedure_callees", () => []],
["procedure_callers", () => []],
[
"procedure_info",
() => ({
name: "fixture_main",
entrypoint: "0x401000",
basicblock_count: 1,
length: 6,
signature: "int fixture_main(void)",
locals: [],
classification: ghidraFunctionClassification(),
}),
],
["procedure_pseudo_code", () => "int fixture_main(void) { return 42; }"],
[
"read_function_instructions",
() => ({
procedure: ghidraFunctionIdentity(),
instructions: ghidraBounded(["0x401000: CALL 0x401020", "0x401005: RET"]),
instructions_scanned: 2,
instruction_scan_truncated: false,
limitations: ["Ghidra-specific instruction text."],
}),
],
[
"procedure_references",
(input, _limit) => ({
procedure: ghidraFunctionIdentity(),
direction: input.direction ?? "outgoing",
references: ghidraBounded([ghidraReferenceEdge()]),
instructions_scanned: 2,
instruction_scan_truncated: false,
}),
],
["xrefs", () => ["0x401001"]],
[
"analyze_function",
(input, _limit) =>
ghidraFunctionDossier(
input.include_assembly === true,
input.procedure === "fixture_truncated",
),
],
]);
const resultFor = (
operation: GhidraOperation,
input: Readonly<Record<string, JsonValue>>,
): JsonValue => {
const limit = typeof input.limit === "number" ? input.limit : 100;
const builder = resultBuilders.get(operation);
if (builder === undefined)
throw new TypeError(`Unexpected Ghidra operation: ${operation}`);
return builder(input, limit);
};
const page = (items: readonly JsonValue[], limit: number): JsonValue => ({
items: items.slice(0, limit),
offset: 0,
limit,
total: items.length,
next_offset: items.length > limit ? limit : null,
has_more: items.length > limit,
});
const stringItem = (address: string, value: string): JsonValue => ({
address,
value,
value_truncated: false,
string: {
encoding: "UTF-8",
termination: "present_or_not_required",
byte_length: value.length + 1,
},
});

View File

@ -0,0 +1,88 @@
import { describe, expect, it } from "vitest";
import { connectGhidraMcp, sessionEvidence } from "./ghidraMcpHarness.js";
describe("Ghidra MCP native API evidence", () => {
it("records approved native API truncation once and links its evidence", async () => {
const harness = await connectGhidraMcp("ghidra-native-api");
const { mcp, session } = harness;
try {
const unapprovedInspection = sessionEvidence(
session,
(
await mcp.callTool({
name: "inspect_native_api",
arguments: { procedure: "fixture_truncated" },
})
).structuredContent,
);
expect(unapprovedInspection.normalized_result).toMatchObject({
boundary: {
available: true,
jump_tables: [{ mappings_truncated: true }],
},
residual_unknowns: [
expect.stringContaining("additional data sources or targets"),
],
});
expect(
(
await mcp.callTool({
name: "list_unknowns",
arguments: {},
})
).structuredContent,
).toMatchObject({ result: { items: [] } });
const approvedInspection = sessionEvidence(
session,
(
await mcp.callTool({
name: "inspect_native_api",
arguments: {
procedure: "fixture_truncated",
unknown_registry_approved: true,
},
})
).structuredContent,
);
await mcp.callTool({
name: "inspect_native_api",
arguments: {
procedure: "fixture_truncated",
unknown_registry_approved: true,
},
});
expect(
(
await mcp.callTool({
name: "list_unknowns",
arguments: {},
})
).structuredContent,
).toMatchObject({
result: {
items: [
{
unknown: {
status: "open",
domain: "native-api",
question: expect.stringContaining(
"additional data sources or targets",
),
supporting_evidence_ids: [approvedInspection.evidence_id],
recommended_probes: [
{
operation: "inspect_native_api",
rationale: expect.stringContaining("ABI probe"),
},
],
},
},
],
},
});
} finally {
await harness.close();
}
}, 30_000);
});

View File

@ -0,0 +1,75 @@
import { describe, expect, it } from "vitest";
import { connectGhidraMcp } from "./ghidraMcpHarness.js";
describe("Ghidra MCP capability routing", () => {
it("routes composed and capability tools", async () => {
const harness = await connectGhidraMcp("ghidra-routing");
const { calls, mcp } = harness;
try {
const batch = await mcp.callTool({
name: "batch_decompile",
arguments: { addresses: ["fixture_main"] },
});
expect(batch.isError).not.toBe(true);
expect(batch.structuredContent).toMatchObject({
result: {
succeeded: 1,
failed: 0,
items: [{ status: "ok" }],
},
});
const availableNames = (await mcp.listTools()).tools.map(
({ name }) => name,
);
expect(availableNames).not.toContain("set_comment");
expect(calls).not.toContain("set_comment");
const status = await mcp.callTool({
name: "binary_session",
arguments: { detail: "full" },
});
expect(status.structuredContent).toMatchObject({
result: {
open: true,
analysis_provider_binding: {
provider: { id: "ghidra", version: "12.1.2" },
},
tool_availability: expect.arrayContaining([
expect.objectContaining({
name: "binary_overview",
available: true,
}),
expect.objectContaining({ name: "swift_classes", available: true }),
expect.objectContaining({
name: "get_objc_classes",
available: true,
}),
expect.objectContaining({
name: "batch_decompile",
available: true,
}),
expect.objectContaining({
name: "trace_feature",
available: true,
}),
expect.objectContaining({
name: "get_call_graph",
available: true,
}),
expect.objectContaining({
name: "find_xrefs_to_name",
available: true,
}),
expect.objectContaining({
name: "set_comment",
available: false,
}),
]),
},
});
} finally {
await harness.close();
}
}, 30_000);
});

View File

@ -0,0 +1,348 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { expect, it } from "vitest";
import { z } from "zod";
import { composeBinarySessionFromFactory } from "../../../src/application/BinarySessionComposition.js";
import type { BinarySession } from "../../../src/application/BinarySession.js";
import { FUNCTION_COMPARISON_EXAMPLE } from "../../../src/contracts/functionComparisonExample.js";
import { INVESTIGATION_EXAMPLES } from "../../../src/contracts/investigationExamples.js";
import {
FUNCTION_COMPARISON_EVIDENCE,
PROCESS_CAPTURE_RECONSTRUCTION,
PROCESS_CAPTURE_REFERENCE,
PROCESS_COMPARISON_EVIDENCE,
} from "../../../src/contracts/investigationExamples.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import {
jsonObjectSchema,
jsonValueSchema,
} from "../../../src/domain/jsonValue.js";
import { createServer } from "../../../src/server/createServer.js";
import { observed } from "../../fixtures/analysisExecution.js";
import type { EvidenceFilePolicy } from "../../../src/domain/evidenceBundle.js";
import type { PermissionAuthority } from "../../../src/application/PermissionAuthority.js";
it("aggregates comparison Evidence and records an approved runtime gap", async () => {
const { session, server, client } = await connected();
const comparison =
INVESTIGATION_EXAMPLES.find_changed_behavior.comparisons[0];
if (comparison === undefined) throw new Error("missing comparison fixture");
for (const evidence of [
FUNCTION_COMPARISON_EXAMPLE.left,
FUNCTION_COMPARISON_EXAMPLE.right,
comparison,
])
expect(session.recordEvidence(evidence).ok).toBe(true);
try {
const response = await client.callTool({
name: "find_changed_behavior",
arguments: {
comparisons: [comparison],
unknown_registry_approved: true,
},
});
expect(response.isError, JSON.stringify(response)).not.toBe(true);
const evidence = sessionEvidence(session, response.structuredContent);
expect(evidence).toMatchObject({
provider: { id: "rea-changed-behavior" },
normalized_result: { behavior_status: "unknown" },
});
expect(session.listUnknowns({ domain: "changed-behavior" })).toMatchObject([
{
question:
"Did both versions behave the same under a complete controlled replay?",
},
]);
} finally {
await close(session, server, client);
}
}, 10_000);
it("builds a zero-hop exact-address path with dossier citations", async () => {
const { session, server, client } = await connected();
expect(session.recordEvidence(FUNCTION_COMPARISON_EXAMPLE.left).ok).toBe(
true,
);
try {
const response = await client.callTool({
name: "build_call_path",
arguments: INVESTIGATION_EXAMPLES.build_call_path,
});
expect(response.isError).not.toBe(true);
const evidence = sessionEvidence(session, response.structuredContent);
expect(evidence).toMatchObject({
provider: { id: "rea-call-path" },
normalized_result: {
status: "found",
shortest_hops: 0,
paths: { total: 1 },
},
evidence_links: [FUNCTION_COMPARISON_EXAMPLE.left.evidence_id],
});
} finally {
await close(session, server, client);
}
});
it("records a safe unresolved call-path question", async () => {
const { session, server, client } = await connected();
const base = FUNCTION_COMPARISON_EXAMPLE.left;
if (base.subject === null) throw new Error("missing dossier subject");
const dossier = createEvidence(
{
path: base.subject.local_path,
sha256: base.subject.digest.sha256,
format: base.subject.format,
...(base.subject.architecture === null
? {}
: { architecture: base.subject.architecture }),
},
base.provider,
{
predicateType: base.predicate_type,
operation: base.operation,
parameters: base.parameters,
result: jsonValueSchema.parse({
...jsonObjectSchema.parse(base.normalized_result),
callees: {
items: [{ address: "0x2000", name: "next" }],
total: 1,
returned: 1,
truncated: false,
next_offset: null,
},
}),
rawResult: base.raw_result,
confidence: base.confidence,
authority: base.authority,
limitations: base.limitations,
locations: base.locations,
evidenceLinks: base.evidence_links,
},
);
expect(session.recordEvidence(dossier).ok).toBe(true);
try {
const response = await client.callTool({
name: "build_call_path",
arguments: {
...INVESTIGATION_EXAMPLES.build_call_path,
functions: [dossier],
start: { address: "0x1000" },
goal: { address: "0x3000" },
unknown_registry_approved: true,
},
});
expect(response.isError, JSON.stringify(response)).not.toBe(true);
expect(session.listUnknowns({ domain: "call-path" })).toMatchObject([
{
question:
"Can the requested call path be established from complete analysis?",
},
]);
} finally {
await close(session, server, client);
}
});
it("records an explicit non-causal static/runtime hypothesis", async () => {
const { session, server, client } = await connected();
for (const evidence of [
FUNCTION_COMPARISON_EXAMPLE.left,
FUNCTION_COMPARISON_EXAMPLE.right,
FUNCTION_COMPARISON_EVIDENCE,
PROCESS_CAPTURE_REFERENCE,
PROCESS_CAPTURE_RECONSTRUCTION,
PROCESS_COMPARISON_EVIDENCE,
])
expect(session.recordEvidence(evidence).ok).toBe(true);
try {
const response = await client.callTool({
name: "correlate_static_and_runtime",
arguments: INVESTIGATION_EXAMPLES.correlate_static_and_runtime,
});
expect(response.isError).not.toBe(true);
const evidence = sessionEvidence(session, response.structuredContent);
expect(evidence).toMatchObject({
provider: { id: "rea-static-runtime-correlation" },
confidence: "inferred",
normalized_result: {
status: "correlated",
summary: { hypotheses: 1 },
},
});
} finally {
await close(session, server, client);
}
});
it("records a safe unresolved static/runtime question", async () => {
const { session, server, client } = await connected();
for (const evidence of [
FUNCTION_COMPARISON_EXAMPLE.left,
FUNCTION_COMPARISON_EXAMPLE.right,
FUNCTION_COMPARISON_EVIDENCE,
PROCESS_CAPTURE_REFERENCE,
PROCESS_CAPTURE_RECONSTRUCTION,
PROCESS_COMPARISON_EVIDENCE,
])
expect(session.recordEvidence(evidence).ok).toBe(true);
const mapping =
INVESTIGATION_EXAMPLES.correlate_static_and_runtime.mappings[0];
if (mapping === undefined) throw new Error("missing correlation fixture");
try {
const response = await client.callTool({
name: "correlate_static_and_runtime",
arguments: {
...INVESTIGATION_EXAMPLES.correlate_static_and_runtime,
mappings: [
{
...mapping,
static: {
...mapping.static,
selector: { kind: "function", dimension: "assembly" },
},
},
],
unknown_registry_approved: true,
},
});
expect(response.isError).not.toBe(true);
expect(
session.listUnknowns({ domain: "static-runtime-correlation" }),
).toMatchObject([
{
question: "Does runtime behavior match the available static analysis?",
},
]);
} finally {
await close(session, server, client);
}
});
it("passes only the finite declared reconstruction specification", async () => {
const { session, server, client } = await connected();
for (const evidence of [
PROCESS_CAPTURE_REFERENCE,
PROCESS_CAPTURE_RECONSTRUCTION,
PROCESS_COMPARISON_EVIDENCE,
])
expect(session.recordEvidence(evidence).ok).toBe(true);
try {
const response = await client.callTool({
name: "verify_reconstruction",
arguments: {
...INVESTIGATION_EXAMPLES.verify_reconstruction,
unknown_registry_approved: true,
},
});
expect(response.isError).not.toBe(true);
const evidence = sessionEvidence(session, response.structuredContent);
expect(evidence).toMatchObject({
provider: { id: "rea-reconstruction-verifier" },
normalized_result: { status: "pass", summary: { passed: 1 } },
});
expect(evidence.limitations).toContain(
"Pass means every declared claim passed; it does not establish global implementation equivalence.",
);
} finally {
await close(session, server, client);
}
});
it("cannot omit a session-owned active unknown from reconstruction input", async () => {
const { session, server, client } = await connected();
for (const evidence of [
PROCESS_CAPTURE_REFERENCE,
PROCESS_CAPTURE_RECONSTRUCTION,
PROCESS_COMPARISON_EVIDENCE,
])
expect(session.recordEvidence(evidence).ok).toBe(true);
expect(
session.recordUnknown({
approved: true,
question: "Was terminal equivalence reproduced independently?",
severity: "high",
domain: "reconstruction-verification",
supporting_evidence_ids: [PROCESS_COMPARISON_EVIDENCE.evidence_id],
contradicting_evidence_ids: [],
required_authority: "controlled-replay",
required_confidence: "observed",
required_environment: null,
recommended_probes: [
{
operation: "capture_process_scenario",
rationale: "Repeat both sides under one controlled environment.",
},
],
relationships: [],
}).ok,
).toBe(true);
try {
const response = await client.callTool({
name: "verify_reconstruction",
arguments: {
...INVESTIGATION_EXAMPLES.verify_reconstruction,
unknown_registry_approved: true,
},
});
expect(response.isError).not.toBe(true);
const evidence = sessionEvidence(session, response.structuredContent);
expect(evidence.normalized_result).toMatchObject({
status: "unknown",
summary: { unknown: 1 },
});
expect(
session.listUnknowns({ domain: "reconstruction-verification" }),
).toContainEqual(
expect.objectContaining({
question: "Does the reconstruction satisfy every declared claim?",
}),
);
} finally {
await close(session, server, client);
}
});
const connected = async (
evidenceFilePolicy?: EvidenceFilePolicy,
investigationInputRoots: readonly string[] = evidenceFilePolicy?.roots ?? [],
permissionAuthority?: PermissionAuthority,
) => {
const session = composeBinarySessionFromFactory(() => ({
health: () => Promise.resolve(),
execute: () => Promise.resolve(observed(null)),
close: () => Promise.resolve(),
}));
const server = createServer(session, session, {
...(evidenceFilePolicy === undefined
? {}
: {
evidenceFilePolicy,
investigationInputRoots,
}),
...(permissionAuthority === undefined ? {} : { permissionAuthority }),
});
const client = new Client({ name: "investigation-test", version: "1" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await server.connect(serverTransport);
await client.connect(clientTransport);
return { session, server, client };
};
const sessionEvidence = (session: BinarySession, value: unknown) => {
const parsed = z
.object({ evidence_id: z.string().regex(/^ev_[a-f0-9]{64}$/u) })
.parse(value);
const evidence = session.evidenceById(parsed.evidence_id);
if (evidence === undefined) throw new TypeError("Missing session Evidence");
return evidence;
};
const close = async (
session: BinarySession,
server: Awaited<ReturnType<typeof createServer>>,
client: Client,
) => {
await Promise.allSettled([client.close(), server.close(), session.close()]);
};

View File

@ -0,0 +1,395 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { mkdir, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import { z } from "zod";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { composeBinarySessionFromFactory } from "../../../src/application/BinarySessionComposition.js";
import type { BinarySession } from "../../../src/application/BinarySession.js";
import { FUNCTION_COMPARISON_EXAMPLE } from "../../../src/contracts/functionComparisonExample.js";
import { INVESTIGATION_EXAMPLES } from "../../../src/contracts/investigationExamples.js";
import {
FUNCTION_COMPARISON_EVIDENCE,
PROCESS_CAPTURE_RECONSTRUCTION,
PROCESS_CAPTURE_REFERENCE,
PROCESS_COMPARISON_EVIDENCE,
} from "../../../src/contracts/investigationExamples.js";
import { changedBehaviorResultSchema } from "../../../src/domain/changedBehavior.js";
import { createServer } from "../../../src/server/createServer.js";
import { observed } from "../../fixtures/analysisExecution.js";
import { readInvestigationWorkspace } from "../../../src/application/InvestigationWorkspaceStore.js";
import type { EvidenceFilePolicy } from "../../../src/domain/evidenceBundle.js";
import type { PermissionAuthority } from "../../../src/application/PermissionAuthority.js";
import { permissionAuthorityForRoot } from "../../fixtures/permissionAuthority.js";
it("never retains derived evidence after request cancellation", async () => {
const { session, server, client } = await connected();
const inputs = [
{
name: "find_changed_behavior",
arguments: INVESTIGATION_EXAMPLES.find_changed_behavior,
},
{
name: "build_call_path",
arguments: INVESTIGATION_EXAMPLES.build_call_path,
},
{
name: "correlate_static_and_runtime",
arguments: INVESTIGATION_EXAMPLES.correlate_static_and_runtime,
},
] as const;
for (const evidence of [
FUNCTION_COMPARISON_EXAMPLE.left,
FUNCTION_COMPARISON_EXAMPLE.right,
FUNCTION_COMPARISON_EVIDENCE,
PROCESS_CAPTURE_REFERENCE,
PROCESS_CAPTURE_RECONSTRUCTION,
PROCESS_COMPARISON_EVIDENCE,
])
expect(session.recordEvidence(evidence).ok).toBe(true);
const initialCount = session.exportEvidenceBundle().records.length;
try {
for (const input of inputs) {
const controller = new AbortController();
const request = client.callTool(input, {
signal: controller.signal,
onprogress: () => controller.abort(),
});
await expect(request).rejects.toThrow(/abort/iu);
}
await expect
.poll(() => session.exportEvidenceBundle().records.length)
.toBe(initialCount);
} finally {
await close(session, server, client);
}
}, 10_000);
it("runs and reuses a persistent cross-version workspace", async () => {
const directory = await createTestTempDirectory("rea-investigation-mcp-");
const left = join(directory, "left");
const right = join(directory, "right");
const workspace = join(directory, "workspace.json");
await Promise.all([mkdir(left), mkdir(right)]);
await Promise.all([
writeFile(join(left, "feature.txt"), "old\n"),
writeFile(join(right, "feature.txt"), "new\n"),
]);
const filePolicy = evidencePolicy(directory);
const { session, server, client } = await connected(filePolicy);
try {
const arguments_ = {
investigation_run: {
approved: true,
workspace_path: workspace,
workspace_name: "mcp-release-diff",
left_path: left,
right_path: right,
options: { page_size: 500, change_limit: 100 },
},
};
const first = await client.callTool({
name: "find_changed_behavior",
arguments: arguments_,
});
expect(first.isError).not.toBe(true);
const firstEvidence = sessionEvidence(session, first.structuredContent);
expect(firstEvidence.normalized_result).toMatchObject({
behavior_status: "unknown",
summary: { static_candidates: 2 },
investigation_run: { inventory_evidence_count: 2 },
});
expect(session.hasEvidence(firstEvidence.evidence_id)).toBe(true);
const loaded = await readInvestigationWorkspace(workspace, filePolicy);
expect(loaded).toMatchObject({
ok: true,
value: { revision: 3, runs: [{ status: "complete" }] },
});
expect(first.content).toContainEqual(
expect.objectContaining({
type: "resource_link",
uri: expect.stringMatching(
/^rea:\/\/workspace\/ws_[a-f0-9]{64}\/revision\/3$/u,
),
}),
);
const retainedWorkspace = session.investigationWorkspaces()[0];
expect(retainedWorkspace).toBeDefined();
if (retainedWorkspace !== undefined) {
const resource = await client.readResource({
uri: `rea://workspace/${retainedWorkspace.workspace_id}/revision/${String(retainedWorkspace.revision)}`,
});
expect(resource.contents[0]).toEqual(
expect.objectContaining({
text: expect.stringContaining(retainedWorkspace.revision_digest),
}),
);
}
const second = await client.callTool({
name: "find_changed_behavior",
arguments: arguments_,
});
expect(second.isError, JSON.stringify(second)).not.toBe(true);
const secondEvidence = sessionEvidence(session, second.structuredContent);
expect(secondEvidence.evidence_id).toBe(firstEvidence.evidence_id);
expect(
await readInvestigationWorkspace(workspace, filePolicy),
).toMatchObject({ ok: true, value: { revision: 3 } });
} finally {
await close(session, server, client);
await rm(directory, { recursive: true, force: true });
}
});
it("replays a complete workspace without write authority", async () => {
const directory = await createTestTempDirectory("rea-investigation-read-");
const left = join(directory, "left");
const right = join(directory, "right");
const workspace = join(directory, "workspace.json");
await Promise.all([mkdir(left), mkdir(right)]);
await Promise.all([
writeFile(join(left, "feature.txt"), "old\n"),
writeFile(join(right, "feature.txt"), "new\n"),
]);
const filePolicy = evidencePolicy(directory);
const arguments_ = {
investigation_run: {
approved: true,
workspace_path: workspace,
workspace_name: "read-only-replay",
left_path: left,
right_path: right,
options: { page_size: 500, change_limit: 100 },
},
};
const writer = await investigationAuthority(directory, true);
const initial = await connected(filePolicy, [directory], writer);
try {
const created = await initial.client.callTool({
name: "find_changed_behavior",
arguments: arguments_,
});
expect(created.isError, JSON.stringify(created)).not.toBe(true);
} finally {
await close(initial.session, initial.server, initial.client);
}
const reader = await investigationAuthority(directory, false);
const replay = await connected(filePolicy, [directory], reader);
try {
const cached = await replay.client.callTool({
name: "find_changed_behavior",
arguments: arguments_,
});
expect(cached.isError, JSON.stringify(cached)).not.toBe(true);
expect(
await readInvestigationWorkspace(workspace, filePolicy),
).toMatchObject({ ok: true, value: { revision: 3 } });
await writeFile(join(right, "feature.txt"), "changed again\n");
const denied = await replay.client.callTool({
name: "find_changed_behavior",
arguments: arguments_,
});
expect(denied).toMatchObject({
isError: true,
structuredContent: {
error: {
code: "permission_required",
details: { capability: "investigation_workspace_write" },
},
},
});
} finally {
await close(replay.session, replay.server, replay.client);
await rm(directory, { recursive: true, force: true });
}
});
it("defers input permission for an explicit replay with deleted inputs", async () => {
const directory = await createTestTempDirectory("rea-investigation-replay-");
const left = join(directory, "left");
const right = join(directory, "right");
const workspace = join(directory, "workspace.json");
await Promise.all([mkdir(left), mkdir(right)]);
await Promise.all([
writeFile(join(left, "feature.txt"), "old\n"),
writeFile(join(right, "feature.txt"), "new\n"),
]);
const filePolicy = evidencePolicy(directory);
const investigationRun = {
approved: true,
workspace_path: workspace,
workspace_name: "selected-replay",
left_path: left,
right_path: right,
options: { page_size: 500, change_limit: 100 },
};
const writer = await investigationAuthority(directory, true);
const initial = await connected(filePolicy, [directory], writer);
let runId: string | undefined;
try {
const created = await initial.client.callTool({
name: "find_changed_behavior",
arguments: { investigation_run: investigationRun },
});
expect(created.isError, JSON.stringify(created)).not.toBe(true);
const evidence = sessionEvidence(
initial.session,
created.structuredContent,
);
const result = changedBehaviorResultSchema.parse(
evidence.normalized_result,
);
runId = result.investigation_run?.run_id;
} finally {
await close(initial.session, initial.server, initial.client);
}
if (runId === undefined) throw new Error("missing completed run ID");
await Promise.all([
rm(left, { recursive: true, force: true }),
rm(right, { recursive: true, force: true }),
]);
const workspaceOnly = await permissionAuthorityForRoot(
directory,
[
"investigation_workspace_read",
"investigation_workspace_write",
"investigation_input",
],
["investigation_workspace_read"],
);
const replay = await connected(filePolicy, [], workspaceOnly);
try {
const cached = await replay.client.callTool({
name: "find_changed_behavior",
arguments: {
investigation_run: {
...investigationRun,
replay_run_id: runId,
},
},
});
expect(cached.isError, JSON.stringify(cached)).not.toBe(true);
expect(
await readInvestigationWorkspace(workspace, filePolicy),
).toMatchObject({ ok: true, value: { revision: 3 } });
} finally {
await close(replay.session, replay.server, replay.client);
await rm(directory, { recursive: true, force: true });
}
});
it("refuses automatic artifact reads outside operator-approved roots", async () => {
const directory = await createTestTempDirectory("rea-investigation-mcp-");
const approvedInputs = join(directory, "approved-inputs");
const outsideInputs = join(directory, "outside-inputs");
const evidenceRoot = join(directory, "evidence");
await Promise.all([
mkdir(approvedInputs),
mkdir(outsideInputs),
mkdir(evidenceRoot),
]);
const left = join(outsideInputs, "left");
const right = join(outsideInputs, "right");
await Promise.all([mkdir(left), mkdir(right)]);
const { session, server, client } = await connected(
evidencePolicy(evidenceRoot),
[approvedInputs],
);
try {
const response = await client.callTool({
name: "find_changed_behavior",
arguments: {
investigation_run: {
approved: true,
workspace_path: join(evidenceRoot, "workspace.json"),
left_path: left,
right_path: right,
},
},
});
expect(response.isError).toBe(true);
expect(response.content[0]).toEqual({
type: "text",
text: JSON.stringify(response.structuredContent),
});
} finally {
await close(session, server, client);
await rm(directory, { recursive: true, force: true });
}
});
const connected = async (
evidenceFilePolicy?: EvidenceFilePolicy,
investigationInputRoots: readonly string[] = evidenceFilePolicy?.roots ?? [],
permissionAuthority?: PermissionAuthority,
) => {
const session = composeBinarySessionFromFactory(() => ({
health: () => Promise.resolve(),
execute: () => Promise.resolve(observed(null)),
close: () => Promise.resolve(),
}));
const server = createServer(session, session, {
...(evidenceFilePolicy === undefined
? {}
: {
evidenceFilePolicy,
investigationInputRoots,
}),
...(permissionAuthority === undefined ? {} : { permissionAuthority }),
});
const client = new Client({ name: "investigation-test", version: "1" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await server.connect(serverTransport);
await client.connect(clientTransport);
return { session, server, client };
};
const evidencePolicy = (root: string): EvidenceFilePolicy => ({
roots: [root],
maxBytes: 64 * 1024 * 1024,
maxDepth: 64,
maxStringLength: 1024 * 1024,
maxNodes: 1_000_000,
});
const sessionEvidence = (session: BinarySession, value: unknown) => {
const parsed = z
.object({ evidence_id: z.string().regex(/^ev_[a-f0-9]{64}$/u) })
.parse(value);
const evidence = session.evidenceById(parsed.evidence_id);
if (evidence === undefined) throw new TypeError("Missing session Evidence");
return evidence;
};
const investigationAuthority = async (
root: string,
includeWrite: boolean,
): Promise<PermissionAuthority> =>
permissionAuthorityForRoot(
root,
[
"investigation_workspace_read",
"investigation_workspace_write",
"investigation_input",
],
[
"investigation_workspace_read",
"investigation_input",
...(includeWrite ? (["investigation_workspace_write"] as const) : []),
],
);
const close = async (
session: BinarySession,
server: Awaited<ReturnType<typeof createServer>>,
client: Client,
) => {
await Promise.allSettled([client.close(), server.close(), session.close()]);
};

View File

@ -5,15 +5,15 @@ import { pathToFileURL } from "node:url";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { afterEach, describe, expect, test } from "vitest";
import { BinarySession } from "../src/application/BinarySession.js";
import { loadConfiguredPermissionAuthority } from "../src/application/PermissionConfiguration.js";
import { observeJavaScriptRuntime } from "../src/application/JavaScriptRuntimeObservationService.js";
import { V8InspectorProvider } from "../src/browser/V8InspectorProvider.js";
import { parseConfig } from "../src/config.js";
import { createServer } from "../src/server/createServer.js";
import { observed } from "./fixtures/analysisExecution.js";
import { startFakeV8Inspector } from "./fixtures/fakeV8Inspector.js";
import { createTestTempDirectory } from "./fixtures/temporaryDirectory.js";
import { composeBinarySessionFromFactory } from "../../../src/application/BinarySessionComposition.js";
import { loadConfiguredPermissionAuthority } from "../../../src/application/PermissionConfiguration.js";
import { observeJavaScriptRuntime } from "../../../src/application/JavaScriptRuntimeObservationService.js";
import { V8InspectorProvider } from "../../../src/browser/V8InspectorProvider.js";
import { parseConfig } from "../../../src/config.js";
import { createServer } from "../../../src/server/createServer.js";
import { observed } from "../../fixtures/analysisExecution.js";
import { startFakeV8Inspector } from "../../fixtures/fakeV8Inspector.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
describe("JavaScript runtime observation MCP tools", () => {
const resources: Array<{ close(): Promise<unknown> }> = [];
@ -45,7 +45,7 @@ describe("JavaScript runtime observation MCP tools", () => {
if (!config.ok) throw config.error;
const authority = await loadConfiguredPermissionAuthority(config.value);
if (!authority.ok) throw authority.error;
const session = new BinarySession(() => ({
const session = composeBinarySessionFromFactory(() => ({
execute: () => Promise.resolve(observed(null)),
close: () => Promise.resolve(),
}));

View File

@ -1,7 +1,7 @@
import type { ServeStdioOptions } from "@modelcontextprotocol/server/stdio";
import { describe, expect, it } from "vitest";
import { run, runEntrypoint } from "../src/main.js";
import { run, runEntrypoint } from "../../../src/main.js";
type RuntimeDependencies = NonNullable<Parameters<typeof run>[0]>;

View File

@ -2,26 +2,25 @@ import { createHash } from "node:crypto";
import { mkdir, rm } from "node:fs/promises";
import { join } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { afterEach, describe, expect, it } from "vitest";
import { createTestTempDirectory } from "./fixtures/temporaryDirectory.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import type { PermissionAuthority } from "../src/application/PermissionAuthority.js";
import type { ProjectPermissionStore } from "../src/application/ProjectPermissionStore.js";
import type { PermissionGrant } from "../src/domain/permissionPolicy.js";
import { ok } from "../src/domain/result.js";
import { run } from "../src/main.js";
import type { PermissionAuthority } from "../../../src/application/PermissionAuthority.js";
import type { ProjectPermissionStore } from "../../../src/application/ProjectPermissionStore.js";
import type { PermissionGrant } from "../../../src/domain/permissionPolicy.js";
import { ok } from "../../../src/domain/result.js";
import { run } from "../../../src/main.js";
import {
createServer,
type CreateServerOptions,
} from "../src/server/createServer.js";
} from "../../../src/server/createServer.js";
type RuntimeDependencies = NonNullable<Parameters<typeof run>[0]>;
const directories: string[] = [];
afterEach(async () => {
vi.restoreAllMocks();
await Promise.all(
directories
.splice(0)
@ -198,12 +197,12 @@ const startRuntime = async (
createServer: (analysis, session, received) => {
options = received;
const server = createServer(analysis, session, received);
vi.spyOn(server, "sendToolListChanged").mockImplementation(() => {
server.sendToolListChanged = () => {
notificationCount += 1;
notification.resolve(undefined);
notification = deferred<void>();
waiters.get(notificationCount)?.resolve(undefined);
});
};
return server;
},
});

View File

@ -0,0 +1,400 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import type { CallToolResult } from "@modelcontextprotocol/server";
import { expect, it } from "vitest";
import { z } from "zod";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { AnalysisProviderRegistry } from "../../../src/application/AnalysisProviderRegistry.js";
import { composeBinarySession } from "../../../src/application/BinarySessionComposition.js";
import type { BinarySession } from "../../../src/application/BinarySession.js";
import { createPermissionAuthority } from "../../../src/application/PermissionAuthority.js";
import { SessionProviderRouter } from "../../../src/application/SessionProviderRouter.js";
import { MANAGED_NATIVE_VERIFICATION_EXAMPLE } from "../../../src/contracts/managedWorkflowExamples.js";
import type { PermissionCeiling } from "../../../src/domain/permissionPolicy.js";
import { ManagedStaticProvider } from "../../../src/dotnet/ManagedStaticProvider.js";
import { createServer } from "../../../src/server/createServer.js";
import { buildManagedPeFixture } from "../../fixtures/managedPe.js";
it("opens a managed PE and executes the managed static provider through MCP", async () => {
const directory = await createTestTempDirectory("rea-managed-mcp-");
const path = join(directory, "fixture.exe");
const rightPath = join(directory, "fixture-renamed.exe");
const runtimePath = join(directory, "dotnet");
await writeFile(path, buildManagedPeFixture());
await writeFile(runtimePath, "#!/bin/sh\n");
await writeFile(rightPath, buildManagedPeFixture({ methodName: "Renamed" }));
const session = composeBinarySession(
SessionProviderRouter.selectable(new AnalysisProviderRegistry([]), [
new ManagedStaticProvider(),
]),
);
const runtimeCeiling: PermissionCeiling = {
capability: "managed_runtime",
roots: [directory],
executables: [runtimePath],
environment_names: [],
network: "none",
mount: false,
};
const authority = await createPermissionAuthority(
[runtimeCeiling],
[
{
...runtimeCeiling,
grant_id: "administrator:managed_runtime",
lifetime: "administrator",
operation_identity: null,
expires_at: null,
},
],
);
if (!authority.ok) throw authority.error;
const server = createServer(session, session, {
permissionAuthority: authority.value,
managedRuntimePolicy: {
enabled: true,
roots: [directory],
executablePath: runtimePath,
},
availabilityPolicy: () => ({
processCaptureEnabled: false,
evidenceFileRoots: 0,
investigationInputRoots: 0,
browserObservationEnabled: false,
electronObservationEnabled: false,
javascriptReplayEnabled: false,
managedRuntimeEnabled: true,
}),
});
const client = new Client({ name: "managed-mcp-test", version: "1.0.0" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
await verifyManagedCatalogAndNativeWorkflow(client, session);
const members = await inspectManagedStaticWorkflow(client, session, path);
await verifyManagedComparisonAndReconstruction(
client,
session,
members,
rightPath,
);
} finally {
await Promise.all([client.close(), server.close()]);
await session.close();
}
}, 30_000);
const verifyManagedCatalogAndNativeWorkflow = async (
client: Client,
session: BinarySession,
): Promise<void> => {
const names = (await client.listTools()).tools.map(({ name }) => name);
expect(names).toEqual(
expect.arrayContaining([
"inspect_managed_artifact",
"inspect_managed_members",
"inspect_managed_native_boundaries",
"compare_managed_members",
"import_managed_reconstruction",
"verify_managed_native_boundaries",
"plan_managed_runtime_correlation",
]),
);
for (const evidence of [
MANAGED_NATIVE_VERIFICATION_EXAMPLE.managed_boundaries,
...MANAGED_NATIVE_VERIFICATION_EXAMPLE.native_observations,
])
expect(session.recordEvidence(evidence)).toMatchObject({ ok: true });
const verified = structured(
await client.callTool({
name: "verify_managed_native_boundaries",
arguments: {
managed_boundaries_evidence_id:
MANAGED_NATIVE_VERIFICATION_EXAMPLE.managed_boundaries.evidence_id,
native_observation_evidence_ids:
MANAGED_NATIVE_VERIFICATION_EXAMPLE.native_observations.map(
({ evidence_id: evidenceId }) => evidenceId,
),
limits: MANAGED_NATIVE_VERIFICATION_EXAMPLE.limits,
},
}),
);
expect(verified).toMatchObject({
evidence_id: expect.stringMatching(/^ev_[a-f0-9]{64}$/u),
evidence_uri: expect.stringMatching(/^rea:\/\/evidence\/ev_/u),
result: {
summary: { verified: 1 },
algorithm: { token_to_address_mapping: "not-inferred" },
},
});
const native = MANAGED_NATIVE_VERIFICATION_EXAMPLE.native_observations[0];
if (native === undefined) throw new Error("missing native Evidence");
const wrong = await client.callTool({
name: "verify_managed_native_boundaries",
arguments: {
managed_boundaries_evidence_id: native.evidence_id,
native_observation_evidence_ids: [
MANAGED_NATIVE_VERIFICATION_EXAMPLE.managed_boundaries.evidence_id,
],
},
});
expect(wrong).toMatchObject({
isError: true,
structuredContent: {
error: {
code: "evidence_integrity_mismatch",
details: { reason: "wrong_operation", actual: "inspect_macho" },
},
},
});
};
const inspectManagedStaticWorkflow = async (
client: Client,
session: BinarySession,
path: string,
): Promise<Record<string, unknown>> => {
const inspected = structured(
await client.callTool({
name: "inspect_managed_artifact",
arguments: { path, reference_limit: 1 },
}),
);
expect(inspected).toMatchObject({
result: {
classification: { status: "managed", runtime_family: "modern-dotnet" },
references: { limit: 1 },
},
});
const members = sessionEvidence(
session,
structured(
await client.callTool({
name: "inspect_managed_members",
arguments: { method_limit: 1 },
}),
),
);
expect(members).toMatchObject({
operation: "inspect_managed_members",
provider: { id: "rea-dotnet-static" },
subject: { local_path: path, format: "pe" },
normalized_result: {
identity_scope: { token_identity: "build-local" },
methods: { total: 1, returned: 1 },
call_edges: { total: 1 },
field_accesses: { total: 1 },
},
});
const boundaries = structured(
await client.callTool({
name: "inspect_managed_native_boundaries",
arguments: { import_limit: 1 },
}),
);
expect(boundaries).toMatchObject({
result: {
identity_scope: { token_identity: "build-local" },
pinvoke_imports: { total: 0, limit: 1 },
native_implementations: { total: 0 },
},
});
return members;
};
const methodFrom = (members: Record<string, unknown>) =>
z
.object({
items: z.array(
z.object({
token: z.string(),
signature: z.object({ raw_sha256: z.string() }),
body: z.object({ normalized_il_sha256: z.string().nullable() }),
}),
),
})
.parse(
z.record(z.string(), z.unknown()).parse(members.normalized_result)
.methods,
).items[0];
const verifyManagedComparisonAndReconstruction = async (
client: Client,
session: BinarySession,
members: Record<string, unknown>,
rightPath: string,
): Promise<void> => {
await client.callTool({
name: "open_binary",
arguments: { path: rightPath },
});
const right = sessionEvidence(
session,
structured(
await client.callTool({
name: "inspect_managed_members",
arguments: { method_limit: 1 },
}),
),
);
const compared = sessionEvidence(
session,
structured(
await client.callTool({
name: "compare_managed_members",
arguments: {
left_evidence_id: members.evidence_id,
right_evidence_id: right.evidence_id,
limits: {
max_method_matches: 100,
max_field_matches: 100,
max_candidates: 10,
},
},
}),
),
);
expect(compared).toMatchObject({
operation: "compare_managed_members",
provider: { id: "rea-dotnet-workflows" },
confidence: "inferred",
normalized_result: {
algorithm: { name_matching: "not-used" },
matching: { exact_il_signature: 1 },
},
});
const method = methodFrom(members);
expect(method).toBeDefined();
if (method === undefined) return;
await verifyImport(client, session, members, method);
await verifyRuntimePlan(client, session, members, method);
};
type ManagedMethod = NonNullable<ReturnType<typeof methodFrom>>;
const verifyImport = async (
client: Client,
session: BinarySession,
members: Record<string, unknown>,
method: ManagedMethod,
): Promise<void> => {
const imported = sessionEvidence(
session,
structured(
await client.callTool({
name: "import_managed_reconstruction",
arguments: {
static_members_evidence_id: members.evidence_id,
decompiler: {
name: "ilspycmd",
version: "9.1.0.7988",
family: "ilspy",
executable_sha256: null,
options: ["--type", "Example.Program"],
},
methods: [
{
token: method.token,
signature_sha256: method.signature.raw_sha256,
normalized_il_sha256: method.body.normalized_il_sha256,
reconstruction: {
kind: "decompiled-csharp",
language: "csharp",
text: "internal static void Main() { }",
},
},
],
notes: ["synthetic MCP import"],
},
}),
),
);
expect(imported).toMatchObject({
operation: "import_managed_reconstruction",
provider: { id: "rea-dotnet-workflows" },
confidence: "inferred",
normalized_result: {
executed: false,
summary: { imported_methods: 1, decompiled_csharp_methods: 1 },
methods: [
{
token: method.token,
validation: { canonical_observation: false },
},
],
},
});
};
const verifyRuntimePlan = async (
client: Client,
session: BinarySession,
members: Record<string, unknown>,
method: ManagedMethod,
): Promise<void> => {
const planned = sessionEvidence(
session,
structured(
await client.callTool({
name: "plan_managed_runtime_correlation",
arguments: {
static_members_evidence_id: members.evidence_id,
method: {
token: method.token,
signature_sha256: method.signature.raw_sha256,
normalized_il_sha256: method.body.normalized_il_sha256,
},
requested_effect: "attach",
host: {
os: "linux",
clr_family: "dotnet",
architecture: "x86_64",
},
bounds: {
timeout_ms: 5000,
max_threads: 32,
max_output_bytes: 65536,
allow_network: false,
allow_ui: false,
},
},
}),
),
);
expect(planned).toMatchObject({
operation: "plan_managed_runtime_correlation",
provider: { id: "rea-dotnet-workflows" },
confidence: "derived",
normalized_result: {
executed: false,
authority_model: { capability: "managed_runtime" },
requested_runtime: { effect: "attach", network: "none" },
effect_taxonomy: { attaches_process: true },
},
});
};
const structured = (result: CallToolResult): Record<string, unknown> => {
if (
typeof result.structuredContent !== "object" ||
result.structuredContent === null
)
throw new Error("missing structured result");
return z.record(z.string(), z.unknown()).parse(result.structuredContent);
};
const sessionEvidence = (
session: BinarySession,
result: Readonly<Record<string, unknown>>,
): Record<string, unknown> => {
const evidenceId = z.string().parse(result.evidence_id);
const evidence = session.evidenceById(evidenceId);
if (evidence === undefined) throw new Error("missing session Evidence");
return z.record(z.string(), z.unknown()).parse(evidence);
};

View File

@ -3,10 +3,13 @@ import { join, resolve } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { createTestTempDirectory } from "./fixtures/temporaryDirectory.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { compareInventory, runProductionMcpDoctor } from "../src/mcpDoctor.js";
import { CATALOG_IDENTITY } from "../src/catalogIdentity.js";
import {
compareInventory,
runProductionMcpDoctor,
} from "../../../src/mcpDoctor.js";
import { CATALOG_IDENTITY } from "../../../src/catalogIdentity.js";
const temporary: string[] = [];

View File

@ -1,10 +1,10 @@
import { describe, expect, it } from "vitest";
import { MCP_STARTUP_POLICY } from "../src/mcpStartupPolicy.js";
import { MCP_STARTUP_POLICY } from "../../../src/mcpStartupPolicy.js";
import {
createEsmModuleProfileCollector,
parseLinuxVmRssBytes,
} from "../scripts/lib/mcp-startup-probe.mjs";
} from "../../../scripts/lib/mcp-startup-probe.mjs";
describe("MCP startup policy", () => {
it("preserves headroom beneath the configured Codex timeout", () => {

View File

@ -1,13 +1,13 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { afterEach, describe, expect, it } from "vitest";
import type { AnalysisOperationPort } from "../src/application/AnalysisProvider.js";
import { OFFICIAL_TOOL_CONTRACTS } from "../src/contracts/toolContracts.js";
import { HopperRemoteError } from "../src/domain/errors.js";
import { err } from "../src/domain/result.js";
import { observed as ok } from "./fixtures/analysisExecution.js";
import type { JsonValue } from "../src/domain/jsonValue.js";
import { createServer } from "../src/server/createServer.js";
import type { AnalysisOperationPort } from "../../../src/application/AnalysisProvider.js";
import { OFFICIAL_TOOL_CONTRACTS } from "../../../src/contracts/toolContracts.js";
import { HopperRemoteError } from "../../../src/domain/errors.js";
import { err } from "../../../src/domain/result.js";
import { observed as ok } from "../../fixtures/analysisExecution.js";
import type { JsonValue } from "../../../src/domain/jsonValue.js";
import { createServer } from "../../../src/server/createServer.js";
const VALID_INPUTS: Readonly<
Record<string, Readonly<Record<string, JsonValue>>>

View File

@ -3,10 +3,10 @@ import { realpath } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import type { AnalysisOperationPort } from "../src/application/AnalysisProvider.js";
import { PermissionAuthority } from "../src/application/PermissionAuthority.js";
import { createPermissionPolicy } from "../src/domain/permissionPolicy.js";
import { createServer } from "../src/server/createServer.js";
import type { AnalysisOperationPort } from "../../../src/application/AnalysisProvider.js";
import { PermissionAuthority } from "../../../src/application/PermissionAuthority.js";
import { createPermissionPolicy } from "../../../src/domain/permissionPolicy.js";
import { createServer } from "../../../src/server/createServer.js";
describe("MCP permission preflight", () => {
it("denies extraction before dispatch and returns exact typed remediation", async () => {

View File

@ -0,0 +1,89 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { serveStdio } from "@modelcontextprotocol/server/stdio";
import { rm } from "node:fs/promises";
import { dirname } from "node:path";
import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { composeBinarySessionFromFactory } from "../../../src/application/BinarySessionComposition.js";
import { PermissionAuthority } from "../../../src/application/PermissionAuthority.js";
import { createPermissionPolicy } from "../../../src/domain/permissionPolicy.js";
import { silentLogger } from "../../../src/logger.js";
import { createServer } from "../../../src/server/createServer.js";
import { observed } from "../../fixtures/analysisExecution.js";
import { PROCESS_CAPTURE_ELICITATION_POLICY } from "../../../src/server/ProcessCaptureElicitation.js";
describe("process-capture MCP elicitation handshake", () => {
it("completes signed consent through the modern MCP client and server", async () => {
const root = await createTestTempDirectory("rea-elicit-mcp-");
const authority = new PermissionAuthority(
createPermissionPolicy([
{
capability: "process_capture",
roots: [root],
executables: [process.execPath],
environment_names: [],
network: "external",
mount: false,
},
]),
);
const session = composeBinarySessionFromFactory(() => ({
execute: () => Promise.resolve(observed(null)),
close: () => Promise.resolve(),
}));
const createTestServer = () =>
createServer(session, session, {
logger: silentLogger,
permissionAuthority: authority,
processPolicy: {
enabled: true,
executableRoots: [dirname(process.execPath)],
workingRoots: [root],
allowedEnvironment: [],
allowExternalNetwork: true,
},
});
const client = new Client(
{ name: "process-elicit", version: "1" },
{
capabilities: { elicitation: { form: {} } },
versionNegotiation: {
mode: {
pin: PROCESS_CAPTURE_ELICITATION_POLICY.protocolVersions[0],
},
},
inputRequired: { autoFulfill: true, maxRounds: 3 },
},
);
let prompts = 0;
client.setRequestHandler("elicitation/create", () => {
prompts += 1;
return Promise.resolve({
action: "accept" as const,
content: { lifetime: "session" },
});
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
const server = serveStdio(createTestServer, {
transport: serverTransport,
legacy: "reject",
});
try {
await client.connect(clientTransport);
const captured = await client.callTool({
name: "capture_process_scenario",
arguments: {
approved: true,
executable: process.execPath,
arguments: ["-e", "process.exit(0)"],
working_directory: root,
},
});
expect(captured.isError).not.toBe(true);
expect(prompts).toBe(1);
} finally {
await Promise.allSettled([client.close(), server.close()]);
await rm(root, { recursive: true, force: true });
}
}, 20000);
});

View File

@ -0,0 +1,149 @@
import { isInputRequiredResult } from "@modelcontextprotocol/server";
import { describe, expect, it } from "vitest";
import { PermissionAuthority } from "../../../src/application/PermissionAuthority.js";
import { createPermissionPolicy } from "../../../src/domain/permissionPolicy.js";
import {
authorizeProcessCaptureWithElicitation,
type ProcessCaptureElicitationState,
} from "../../../src/server/ProcessCaptureElicitation.js";
const request = {
capability: "process_capture" as const,
roots: ["/tmp"],
executables: [process.execPath],
environment_names: ["PATH"],
network: "external" as const,
mount: false,
operation_identity: "capture:test",
};
const now = Date.parse("2026-07-18T00:00:00.000Z");
const ceilingOnlyAuthority = (
origins?: readonly string[],
): PermissionAuthority =>
new PermissionAuthority(
createPermissionPolicy([
{
capability: "process_capture",
roots: request.roots,
executables: request.executables,
environment_names: request.environment_names,
...(origins === undefined ? {} : { origins }),
network: request.network,
mount: request.mount,
},
]),
);
const requestContext = (
state?: ProcessCaptureElicitationState,
response?: Record<string, unknown>,
) => ({
mcpReq: {
requestState: () => state,
...(response === undefined
? {}
: { inputResponses: { process_capture_grant: response } }),
},
});
const verifiedState = (
state: ProcessCaptureElicitationState | undefined,
): Promise<ProcessCaptureElicitationState> => {
if (state === undefined) throw new Error("state was not minted");
return Promise.resolve(state);
};
describe("process-capture MCP elicitation grants", () => {
it("grants and reuses a signed session scope on the modern protocol", async () => {
const authority = ceilingOnlyAuthority();
let state: ProcessCaptureElicitationState | undefined;
const elicitation = {
stateCodec: {
mint: (value: ProcessCaptureElicitationState) => {
state = value;
return Promise.resolve("signed-state");
},
verify: () => verifiedState(state),
},
supported: () => true,
now: () => now,
consumedNonces: new Map<string, number>(),
};
const first = await authorizeProcessCaptureWithElicitation(
authority,
request,
requestContext(),
elicitation,
);
expect(isInputRequiredResult(first)).toBe(true);
expect(first).toMatchObject({ requestState: "signed-state" });
expect(first).toMatchObject({
inputRequests: {
process_capture_grant: {
params: {
message: expect.stringContaining("Mount access: no"),
},
},
},
});
expect(state?.request).toEqual(request);
const accepted = await authorizeProcessCaptureWithElicitation(
authority,
request,
requestContext(state, {
action: "accept",
content: { lifetime: "session" },
}),
elicitation,
);
expect(isInputRequiredResult(accepted)).toBe(false);
if (isInputRequiredResult(accepted)) return;
expect(accepted.ok).toBe(true);
const reused = await authorizeProcessCaptureWithElicitation(
authority,
request,
requestContext(),
elicitation,
);
expect(isInputRequiredResult(reused)).toBe(false);
if (!isInputRequiredResult(reused)) expect(reused.ok).toBe(true);
});
it("fails closed for legacy clients and altered continuations", async () => {
const authority = ceilingOnlyAuthority();
let state: ProcessCaptureElicitationState | undefined;
const elicitation = {
stateCodec: {
mint: (value: ProcessCaptureElicitationState) => {
state = value;
return Promise.resolve("signed-state");
},
verify: () => verifiedState(state),
},
supported: () => false,
now: () => now,
consumedNonces: new Map<string, number>(),
};
const legacy = await authorizeProcessCaptureWithElicitation(
authority,
request,
requestContext(),
elicitation,
);
expect(isInputRequiredResult(legacy)).toBe(false);
if (!isInputRequiredResult(legacy)) expect(legacy.ok).toBe(false);
const modern = { ...elicitation, supported: () => true };
await authorizeProcessCaptureWithElicitation(
authority,
request,
requestContext(),
modern,
);
const altered = await authorizeProcessCaptureWithElicitation(
authority,
{ ...request, operation_identity: "capture:altered" },
requestContext(state, {
action: "accept",
content: { lifetime: "session" },
}),
modern,
);
expect(isInputRequiredResult(altered)).toBe(false);
if (!isInputRequiredResult(altered)) expect(altered.ok).toBe(false);
});
});

View File

@ -0,0 +1,167 @@
import { isInputRequiredResult } from "@modelcontextprotocol/server";
import { describe, expect, it } from "vitest";
import { PermissionAuthority } from "../../../src/application/PermissionAuthority.js";
import { createPermissionPolicy } from "../../../src/domain/permissionPolicy.js";
import {
authorizeProcessCaptureWithElicitation,
type ProcessCaptureElicitationState,
} from "../../../src/server/ProcessCaptureElicitation.js";
const request = {
capability: "process_capture" as const,
roots: ["/tmp"],
executables: [process.execPath],
environment_names: ["PATH"],
network: "external" as const,
mount: false,
operation_identity: "capture:test",
};
const now = Date.parse("2026-07-18T00:00:00.000Z");
const ceilingOnlyAuthority = (
origins?: readonly string[],
): PermissionAuthority =>
new PermissionAuthority(
createPermissionPolicy([
{
capability: "process_capture",
roots: request.roots,
executables: request.executables,
environment_names: request.environment_names,
...(origins === undefined ? {} : { origins }),
network: request.network,
mount: request.mount,
},
]),
);
const requestContext = (
state?: ProcessCaptureElicitationState,
response?: Record<string, unknown>,
) => ({
mcpReq: {
requestState: () => state,
...(response === undefined
? {}
: { inputResponses: { process_capture_grant: response } }),
},
});
const verifiedState = (
state: ProcessCaptureElicitationState | undefined,
): Promise<ProcessCaptureElicitationState> => {
if (state === undefined) throw new Error("state was not minted");
return Promise.resolve(state);
};
describe("process-capture MCP elicitation grants", () => {
it("binds signed continuation state to exact origins", async () => {
const authority = ceilingOnlyAuthority([
"https://example.test",
"https://other.test",
]);
let state: ProcessCaptureElicitationState | undefined;
const elicitation = {
stateCodec: {
mint: (value: ProcessCaptureElicitationState) => {
state = value;
return Promise.resolve("signed-state");
},
verify: () => verifiedState(state),
},
supported: () => true,
now: () => now,
consumedNonces: new Map<string, number>(),
};
const scoped = { ...request, origins: ["https://example.test"] };
const prompted = await authorizeProcessCaptureWithElicitation(
authority,
scoped,
requestContext(),
elicitation,
);
expect(prompted).toMatchObject({
inputRequests: {
process_capture_grant: {
params: {
message: expect.stringContaining("Origins: https://example.test"),
},
},
},
});
const altered = await authorizeProcessCaptureWithElicitation(
authority,
{ ...scoped, origins: ["https://other.test"] },
requestContext(state, {
action: "accept",
content: { lifetime: "session" },
}),
elicitation,
);
expect(isInputRequiredResult(altered)).toBe(false);
if (!isInputRequiredResult(altered)) expect(altered.ok).toBe(false);
});
it("consumes once grants and rejects continuation replay", async () => {
const authority = ceilingOnlyAuthority();
let state: ProcessCaptureElicitationState | undefined;
const elicitation = {
stateCodec: {
mint: (value: ProcessCaptureElicitationState) => {
state = value;
return Promise.resolve("signed-state");
},
verify: () => verifiedState(state),
},
supported: () => true,
now: () => now,
consumedNonces: new Map<string, number>(),
};
await authorizeProcessCaptureWithElicitation(
authority,
request,
requestContext(),
elicitation,
);
const response = {
action: "accept",
content: { lifetime: "once" },
};
const accepted = await authorizeProcessCaptureWithElicitation(
authority,
request,
requestContext(state, response),
elicitation,
);
if (isInputRequiredResult(accepted)) throw new Error("grant was not used");
expect(accepted.ok).toBe(true);
const replayed = await authorizeProcessCaptureWithElicitation(
authority,
request,
requestContext(state, response),
elicitation,
);
expect(isInputRequiredResult(replayed)).toBe(false);
if (!isInputRequiredResult(replayed)) expect(replayed.ok).toBe(false);
const next = await authorizeProcessCaptureWithElicitation(
authority,
request,
requestContext(),
elicitation,
);
expect(isInputRequiredResult(next)).toBe(true);
const declined = await authorizeProcessCaptureWithElicitation(
authority,
request,
requestContext(state, { action: "decline" }),
elicitation,
);
expect(isInputRequiredResult(declined)).toBe(false);
const acceptedAfterDecline = await authorizeProcessCaptureWithElicitation(
authority,
request,
requestContext(state, {
action: "accept",
content: { lifetime: "session" },
}),
elicitation,
);
expect(isInputRequiredResult(acceptedAfterDecline)).toBe(false);
if (!isInputRequiredResult(acceptedAfterDecline))
expect(acceptedAfterDecline.ok).toBe(false);
});
});

View File

@ -0,0 +1,182 @@
import { isInputRequiredResult } from "@modelcontextprotocol/server";
import { mkdir, rm } from "node:fs/promises";
import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { PermissionAuthority } from "../../../src/application/PermissionAuthority.js";
import { createPermissionPolicy } from "../../../src/domain/permissionPolicy.js";
import {
authorizeProcessCaptureWithElicitation,
type ProcessCaptureElicitationState,
} from "../../../src/server/ProcessCaptureElicitation.js";
const request = {
capability: "process_capture" as const,
roots: ["/tmp"],
executables: [process.execPath],
environment_names: ["PATH"],
network: "external" as const,
mount: false,
operation_identity: "capture:test",
};
const now = Date.parse("2026-07-18T00:00:00.000Z");
const ceilingOnlyAuthority = (
origins?: readonly string[],
): PermissionAuthority =>
new PermissionAuthority(
createPermissionPolicy([
{
capability: "process_capture",
roots: request.roots,
executables: request.executables,
environment_names: request.environment_names,
...(origins === undefined ? {} : { origins }),
network: request.network,
mount: request.mount,
},
]),
);
const requestContext = (
state?: ProcessCaptureElicitationState,
response?: Record<string, unknown>,
) => ({
mcpReq: {
requestState: () => state,
...(response === undefined
? {}
: { inputResponses: { process_capture_grant: response } }),
},
});
const verifiedState = (
state: ProcessCaptureElicitationState | undefined,
): Promise<ProcessCaptureElicitationState> => {
if (state === undefined) throw new Error("state was not minted");
return Promise.resolve(state);
};
describe("process-capture MCP elicitation grants", () => {
it("bounds replay state and prunes entries at the signed-state TTL", async () => {
const authority = ceilingOnlyAuthority();
let state: ProcessCaptureElicitationState | undefined;
const consumedNonces = new Map<string, number>();
const elicitation = {
stateCodec: {
mint: (value: ProcessCaptureElicitationState) => {
state = value;
return Promise.resolve("signed-state");
},
verify: () => verifiedState(state),
},
supported: () => true,
now: () => now,
consumedNonces,
};
await authorizeProcessCaptureWithElicitation(
authority,
request,
requestContext(),
elicitation,
);
const future = now + 600000;
for (let index = 0; index < 4096; index += 1)
consumedNonces.set(`occupied-${String(index)}`, future);
const rejectedAtCapacity = await authorizeProcessCaptureWithElicitation(
authority,
request,
requestContext(state, {
action: "accept",
content: { lifetime: "session" },
}),
elicitation,
);
expect(isInputRequiredResult(rejectedAtCapacity)).toBe(false);
if (!isInputRequiredResult(rejectedAtCapacity))
expect(rejectedAtCapacity.ok).toBe(false);
consumedNonces.set("occupied-0", now - 1);
const acceptedAfterPrune = await authorizeProcessCaptureWithElicitation(
authority,
request,
requestContext(state, {
action: "accept",
content: { lifetime: "session" },
}),
elicitation,
);
expect(isInputRequiredResult(acceptedAfterPrune)).toBe(false);
if (!isInputRequiredResult(acceptedAfterPrune))
expect(acceptedAfterPrune.ok).toBe(true);
expect(consumedNonces.has("occupied-0")).toBe(false);
expect(consumedNonces.size).toBe(4096);
});
it.each(["result", "throw"] as const)(
"revokes an elicited grant when final authorization ends with %s",
async (failure) => {
const root = await createTestTempDirectory("rea-elicit-rollback-");
const scoped = { ...request, roots: [root] };
const policy = createPermissionPolicy([
{
capability: "process_capture",
roots: scoped.roots,
executables: scoped.executables,
environment_names: scoped.environment_names,
network: scoped.network,
mount: scoped.mount,
},
]);
class FailingFinalAuthority extends PermissionAuthority {
private authorizationCount = 0;
override async authorize(
...arguments_: Parameters<PermissionAuthority["authorize"]>
): ReturnType<PermissionAuthority["authorize"]> {
this.authorizationCount += 1;
const result = await super.authorize(...arguments_);
if (failure === "result" && this.authorizationCount === 2)
await rm(root, { recursive: true, force: true });
if (failure === "throw" && this.authorizationCount === 3)
throw new Error("synthetic final authorization rejection");
return result;
}
}
const authority = new FailingFinalAuthority(policy);
let state: ProcessCaptureElicitationState | undefined;
const elicitation = {
stateCodec: {
mint: (value: ProcessCaptureElicitationState) => {
state = value;
return Promise.resolve("signed-state");
},
verify: () => verifiedState(state),
},
supported: () => true,
now: () => now,
consumedNonces: new Map<string, number>(),
};
await authorizeProcessCaptureWithElicitation(
authority,
scoped,
requestContext(),
elicitation,
);
const finalAuthorization = authorizeProcessCaptureWithElicitation(
authority,
scoped,
requestContext(state, {
action: "accept",
content: { lifetime: "session" },
}),
elicitation,
);
if (failure === "throw")
await expect(finalAuthorization).rejects.toThrow(
"synthetic final authorization rejection",
);
else {
const failed = await finalAuthorization;
expect(isInputRequiredResult(failed)).toBe(false);
if (!isInputRequiredResult(failed)) expect(failed.ok).toBe(false);
await mkdir(root);
}
const retry = await authority.authorize(scoped, "read", {
elicitationSupported: true,
});
expect(retry.ok).toBe(false);
},
);
});

Some files were not shown because too many files have changed in this diff Show More