* feat(artifacts): compare artifacts by Evidence reference and from the CLI
compare_artifacts made agents send back the complete inventory Evidence
that REA already retains, and the CLI had no artifact comparison.
Each side of compare_artifacts now takes exactly one of: complete inline
Evidence (unchanged), {kind: "retained-evidence", evidence_id}, or
{kind: "evidence-file", path, evidence_id?}. The two references are the
sources inspect_analysis_view accepts; both tools now compose one
definition in evidenceReference.ts. A summary still resolves to its exact
retained parent, and every source form gives the same comparison Evidence.
An unresolvable retained reference is refused with a typed missing
reference error.
The application layer owns source resolution and the comparison Evidence,
so MCP and the new `rea compare-artifacts` command share one workflow.
readEvidenceFileSource reports bundle-selection refusals at the caller's
own field path.
* refactor(artifacts): resolve compare_artifacts sides like compare_functions
compare_functions, compare_wasm_interfaces and the JavaScript application
comparisons name each side with evidenceInputSchema and resolve it with
resolvePairedEvidenceRequest. compare_artifacts reached the same result
for inline and retained sides through its own dispatch.
evidenceSourceInputSchema now extends evidenceInputSchema with the
Evidence-file reference, and resolvePairedEvidenceSources sends inline and
retained sides to the same resolveEvidenceInput and file sides to the
shared Evidence file reader. compare_artifacts uses both, so the forms it
shares with compare_functions are parsed and resolved by one owner. The
advertised input schema is unchanged.
* docs(cli): point rea compare to compare-artifacts for inventory contents
rea compare (compare_bundles) compares records by Evidence identity and
explicit pairs and never reads artifact contents, so two build
inventories stay unresolved there. Its help and the CLI guide now name
compare-artifacts for that comparison.
* fix(artifacts): resolve summary parents within Evidence source bundles
* perf(evidence): reuse file-owned immutable authentication proofs
* test(evidence): inject failures at both authentication boundaries
---------
Co-authored-by: Entrpi <[email protected]>
129 lines
4.2 KiB
TypeScript
129 lines
4.2 KiB
TypeScript
import { writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
|
|
import { expect, it, onTestFinished, vi } from "vitest";
|
|
|
|
import { createApplicationMcpHarness } from "../fixtures/applicationMcpHarness.js";
|
|
import {
|
|
analysisViewCaptureEvidence,
|
|
environmentManifestCaptureFixture,
|
|
} from "../fixtures/analysisView.js";
|
|
import { createTestBinarySession } from "../fixtures/binarySession.js";
|
|
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
|
|
|
const unexpectedFailure = (
|
|
kind: "TypeError" | "Error",
|
|
source: "inline" | "evidence-file",
|
|
): Error => {
|
|
const message = `injected ${kind} during ${source} Evidence authentication`;
|
|
return kind === "TypeError" ? new TypeError(message) : new Error(message);
|
|
};
|
|
|
|
const manifestView = {
|
|
kind: "environment-manifest",
|
|
template_rules: ["opaque-token", "sibling-pair-variance"],
|
|
} as const;
|
|
|
|
const authenticationEntries = ["parseEvidence", "immutableEvidence"] as const;
|
|
|
|
const createOwnedHarness = async () => {
|
|
const harness = await createApplicationMcpHarness({
|
|
session: createTestBinarySession(() => {
|
|
throw new Error("stored Evidence views must not launch a provider");
|
|
}),
|
|
});
|
|
onTestFinished(async () => {
|
|
vi.restoreAllMocks();
|
|
await harness.close();
|
|
});
|
|
return harness;
|
|
};
|
|
|
|
it.each(
|
|
(["inline", "evidence-file"] as const).flatMap((source) =>
|
|
(["TypeError", "Error"] as const).map(
|
|
(failureKind) => [source, failureKind] as const,
|
|
),
|
|
),
|
|
)(
|
|
"preserves an unexpected parser failure from %s input (%s) without retaining a view",
|
|
async (sourceKind, failureKind) => {
|
|
const harness = await createOwnedHarness();
|
|
const parent = analysisViewCaptureEvidence(
|
|
environmentManifestCaptureFixture(),
|
|
);
|
|
expect(harness.session.recordEvidence(parent).ok).toBe(true);
|
|
const before = harness.session.exportEvidenceBundle();
|
|
|
|
let source: Record<string, unknown>;
|
|
if (sourceKind === "inline") {
|
|
source = { kind: "inline", evidence: parent };
|
|
} else {
|
|
const root = await createTestTempDirectory("rea-view-auth-failure-");
|
|
const path = join(root, "parent.json");
|
|
await writeFile(path, JSON.stringify(parent));
|
|
source = { kind: "evidence-file", path };
|
|
}
|
|
|
|
const evidenceModule = await import("../../src/domain/evidence.js");
|
|
const failure = unexpectedFailure(failureKind, sourceKind);
|
|
const authenticators = authenticationEntries.map((name) =>
|
|
vi.spyOn(evidenceModule, name).mockImplementation(() => {
|
|
throw failure;
|
|
}),
|
|
);
|
|
|
|
const response = await harness.client.callTool({
|
|
name: "inspect_analysis_view",
|
|
arguments: { source, view: manifestView },
|
|
});
|
|
|
|
expect(response).toMatchObject({
|
|
isError: true,
|
|
content: [{ type: "text", text: failure.message }],
|
|
});
|
|
expect(response.structuredContent).toBeUndefined();
|
|
expect(
|
|
authenticators.flatMap((authenticate) => authenticate.mock.calls),
|
|
).toContainEqual([
|
|
expect.objectContaining({ evidence_id: parent.evidence_id }),
|
|
]);
|
|
expect(harness.session.exportEvidenceBundle()).toEqual(before);
|
|
},
|
|
);
|
|
|
|
it("uses retained Evidence without reparsing its parent", async () => {
|
|
const harness = await createOwnedHarness();
|
|
const parent = analysisViewCaptureEvidence(
|
|
environmentManifestCaptureFixture(),
|
|
);
|
|
expect(harness.session.recordEvidence(parent).ok).toBe(true);
|
|
|
|
const evidenceModule = await import("../../src/domain/evidence.js");
|
|
for (const name of authenticationEntries) {
|
|
const originalParse = evidenceModule[name];
|
|
vi.spyOn(evidenceModule, name).mockImplementation((input) => {
|
|
if (
|
|
typeof input === "object" &&
|
|
input !== null &&
|
|
"evidence_id" in input &&
|
|
input.evidence_id === parent.evidence_id
|
|
)
|
|
throw new TypeError("retained source parent was reparsed");
|
|
return originalParse(input);
|
|
});
|
|
}
|
|
|
|
const response = await harness.client.callTool({
|
|
name: "inspect_analysis_view",
|
|
arguments: {
|
|
source: { kind: "retained-evidence", evidence_id: parent.evidence_id },
|
|
view: manifestView,
|
|
},
|
|
});
|
|
|
|
expect(response.isError).not.toBe(true);
|
|
expect(response.structuredContent).toBeDefined();
|
|
expect(harness.session.exportEvidenceBundle().records).toHaveLength(2);
|
|
});
|