mirror of
https://github.com/morluto/rea.git
synced 2026-11-14 10:43:30 +00:00
* test(mcp): open stdio clients through the shared local-tools fixture Twelve files built their own StdioClientTransport and Client for `rea mcp`. Those clients never listed tools, so the SDK did not validate structured results against advertised output schemas, and each file repeated transport setup, stderr draining and close ordering. Use connectLocalToolsMcp, which lists tools, validates replies, silences logging, leaves deep providers unconfigured and owns cleanup. Per-test clients register test-scoped cleanup; module-level clients collect cleanup for afterAll. * test(mcp): keep failed file-scoped MCP cleanup owned The three module-level clients removed every cleanup callback before running them, so a failed cleanup lost its only handle. Add fileLocalToolsMcp, which connects before the file's tests and removes each cleanup only after it succeeds, and use it in those files.
209 lines
7.1 KiB
TypeScript
209 lines
7.1 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { readFile, rm, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { 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 { fileLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
|
|
import { createStrippedAsarAddon } from "../../fixtures/strippedAsarAddon.js";
|
|
import { cliTest } from "../../support/cli/cliFixture.js";
|
|
|
|
const mcp = fileLocalToolsMcp();
|
|
|
|
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 mcp.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 mcp.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 mcp.client.ping();
|
|
},
|
|
);
|
|
}
|