Files
rea/tests/boundary/mcp/javascriptNativeHandoffIdentity.test.ts

223 lines
7.6 KiB
TypeScript
Raw Permalink Normal View History

import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { createHash } from "node:crypto";
import { readFile, rm, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { afterAll, beforeAll, expect } from "vitest";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import {
applicationFeatureTraceResultSchema,
type ApplicationFeatureTraceResult,
} from "../../../src/domain/javascript/javascriptFeatureTraceSchemas.js";
import { createStrippedAsarAddon } from "../../fixtures/strippedAsarAddon.js";
import { cliTest } from "../../support/cli/cliFixture.js";
const client = new Client({ name: "native-handoff-identity", version: "1" });
const transport = new StdioClientTransport({
command: process.execPath,
args: [resolve("scripts/rea.mjs"), "mcp"],
cwd: process.cwd(),
env: { PATH: process.env.PATH ?? "" },
stderr: "pipe",
});
beforeAll(async () => {
await client.connect(transport);
transport.stderr?.on("data", () => undefined);
});
afterAll(async () => {
await Promise.all([client.close(), transport.close()]);
});
const sha256 = (value: Uint8Array) =>
createHash("sha256").update(value).digest("hex");
const nativeObservation = (path: string, sha256: string) =>
createEvidence(
{ path, sha256, format: "raw-binary" },
{ id: "ghidra", name: "Ghidra", version: "simulated" },
{
operation: "binary_overview",
parameters: {},
result: {
summary: "Simulated native Evidence for subject identity only.",
},
limitations: ["No native provider executed."],
},
);
interface ExpectedIdentity {
readonly available: boolean;
readonly addon: string;
readonly archiveDigest: string;
readonly nativeDigest: string;
readonly parentId: string;
readonly nativeId: string;
readonly linked: boolean;
}
const expectHandoffIdentity = (
trace: ApplicationFeatureTraceResult,
expected: ExpectedIdentity,
) => {
const node = trace.graph?.nodes.find(({ kind }) => kind === "native-addon");
expect(node).toBeDefined();
expect(trace.evidence_links).toEqual(
[
expected.parentId,
...(expected.available && expected.linked ? [expected.nativeId] : []),
].sort(),
);
if (expected.available) {
expect(node?.identity).toMatchObject({
strategy: "content-digest",
sha256: expected.nativeDigest,
});
expect(trace.native_handoffs).toHaveLength(1);
expect(trace.native_handoffs[0]).toMatchObject({
native_node_id: node?.node_id,
artifact_sha256: expected.nativeDigest,
artifact_path: expected.addon,
status: expected.linked
? "evidence-linked"
: "requires-provider-analysis",
evidence_ids: expected.linked ? [expected.nativeId] : [],
});
} else {
expect(node?.identity).toMatchObject({
strategy: "canonical-path",
artifact_sha256: expected.archiveDigest,
path: expected.addon,
});
expect(node?.observations).toContainEqual(
expect.objectContaining({
properties: expect.objectContaining({
availability: "unavailable",
hash_status: "unavailable",
inventory_artifact_id: null,
}),
}),
);
expect(trace.coverage.status).toBe("partial");
expect(trace.graph?.coverage.status).toBe("partial");
expect(trace.native_handoffs).toEqual([]);
expect(trace.summary.native_handoffs).toBe(0);
}
};
for (const available of [false, true]) {
cliTest(
`keeps native handoff byte identity ${available ? "exact when available" : "unknown for a missing ASAR companion"}`,
async ({ cli }) => {
const { root, archive, addon, original } =
await createStrippedAsarAddon();
const companion = join(`${archive}.unpacked`, addon);
if (available) await writeFile(companion, original);
else await rm(companion);
const archiveDigest = sha256(await readFile(archive));
const nativeDigest = sha256(original);
const containerEvidence = nativeObservation(archive, archiveDigest);
const nativeEvidence = nativeObservation(companion, nativeDigest);
const analysis = await cli.run({
arguments: ["analyze-javascript-application", archive, "--json"],
});
expect(analysis.exitCode, analysis.stderr).toBe(0);
const parent = toolContract(
"analyze_javascript_application",
).outputSchema.parse(analysis.json);
const summary = await client.callTool({
name: "analyze_javascript_application",
arguments: { input_path: archive, detail: "summary" },
});
expect(summary.isError).not.toBe(true);
const summaryEvidence = toolContract(
"analyze_javascript_application",
).outputSchema.parse(summary.structuredContent);
expect(
analysisViewResultSchema.parse(summaryEvidence.normalized_result)
.parent_evidence_id,
).toBe(parent.evidence_id);
const observed: {
trace: ApplicationFeatureTraceResult;
linked: boolean;
}[] = [];
for (const native_observations of [
[containerEvidence, nativeEvidence],
[],
]) {
const query = {
seed: {
kind: "string",
value: addon,
match: "exact",
case_sensitive: true,
},
direction: "outgoing",
native_observations,
};
const request = join(
root,
`trace-${String(native_observations.length)}.json`,
);
await writeFile(
request,
JSON.stringify({
...query,
application: parent,
}),
);
const cliTrace = await cli.run({
arguments: ["trace-application-feature", request, "--json"],
});
expect(
cliTrace.exitCode,
cliTrace.exitCode === 0
? undefined
: JSON.stringify(cliTrace.json ?? cliTrace.stderr),
).toBe(0);
const cliEvidence = toolContract(
"trace_application_feature",
).outputSchema.parse(cliTrace.json);
const response = await client.callTool({
name: "trace_application_feature",
arguments: {
...query,
application: {
kind: "retained-evidence",
evidence_id: parent.evidence_id,
},
},
});
expect(response.isError).not.toBe(true);
const evidence = toolContract(
"trace_application_feature",
).outputSchema.parse(response.structuredContent);
expect(evidence.evidence_id).toBe(cliEvidence.evidence_id);
for (const value of [cliEvidence, evidence])
observed.push({
trace: applicationFeatureTraceResultSchema.parse(
value.normalized_result,
),
linked: native_observations.length > 0,
});
}
// Exercise both provider-evidence selections and adapters before checking
// the missing-byte outcome; a failed baseline still observes both paths.
for (const { trace, linked } of observed)
expectHandoffIdentity(trace, {
available,
addon,
archiveDigest,
nativeDigest,
parentId: parent.evidence_id,
nativeId: nativeEvidence.evidence_id,
linked,
});
expect(sha256(await readFile(archive))).toBe(archiveDigest);
if (available)
expect(sha256(await readFile(companion))).toBe(nativeDigest);
await client.ping();
},
);
}