Require the native node owner's content digest instead of borrowing an observation artifact digest, which can identify the containing ASAR. Keep unavailable nodes and their path/provenance/partial facts in the trace, without a false actionable target or native Evidence link. Remove the old fallback and allocate the query index only for eligible native targets. Verify real public CLI/stdio MCP workflows, known/missing companion inputs and shipped Joplin controls. Preserve reproducible scripts, source pins and compact correctness/resource/token records without archiving applications. Fixes #3926. Part of #4027. Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
224 lines
7.6 KiB
TypeScript
224 lines
7.6 KiB
TypeScript
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();
|
|
},
|
|
);
|
|
}
|