fix(cli): load Evidence record pairs from JSON files (#5129)

* fix(cli): read explicit Evidence record pairs from JSON files

* test(cli): isolate record-pair file regression

* style(cli): format record-pair file regression

* test(cli): reuse comparison fixture for record-pair file input

* style(cli): format reused record-pair test invocation

* test(cli): keep paired-input coverage within lint function limit
This commit is contained in:
Jay-Alexander Elliot authored and GitHub committed 2026-11-06 15:38:30 +08:00
1 parent fe9797b487
commit 1902fcb13d
3 files changed
+38 -44

No files matched your search

+6 -4
View File
@@ -744,10 +744,12 @@ not read artifact contents: the inventories of two builds are unresolved
records, or one opaque change when you pair them. Use `compare-artifacts`,
described at the end of this section, to compare their contents.
`--record_pairs` supplies an explicit JSON array of observation pairs, matching
MCP `compare_bundles.record_pairs`. Pair IDs must exist on their named sides and
be one-to-one. Without pairs, one-sided membership remains unknown; REA does
not infer correspondence from position or display names.
`--record_pairs` accepts an inline JSON array or the path to a local JSON
file containing that array, matching MCP `compare_bundles.record_pairs`. For a
file, use `rea compare left.json right.json --record_pairs pairs.json --json`.
Pair IDs must exist on their named sides and be one-to-one. Without pairs,
one-sided membership remains unknown; REA does not infer correspondence from
position or display names.
Exports preserve an existing destination unless `--overwrite` is explicit.
+8 -18
View File
@@ -1,6 +1,5 @@
import { withCommandCancellation } from "./cli/commandCancellation.js";
import { safeParseJson } from "./domain/safeJson.js";
import { AnalysisInputError } from "./domain/analysisErrorCore.js";
import { parseCliJsonInput } from "./cliJsonInput.js";
import { analysisInputErrorFromIssues } from "./domain/inputIssueProjection.js";
import { bundleComparisonInputSchema } from "./domain/bundleComparison.js";
import { Cli, z } from "incur";
@@ -72,7 +71,7 @@ export const registerEvidenceCommands = (
.string()
.optional()
.describe(
"JSON array of explicit left_evidence_id/right_evidence_id observation pairs",
"Inline JSON array or local JSON file of left_evidence_id/right_evidence_id pairs",
),
}),
run: ({ args, options }) =>
@@ -81,21 +80,12 @@ export const registerEvidenceCommands = (
const decoded =
options.record_pairs === undefined
? { ok: true as const, value: [] }
: safeParseJson(options.record_pairs);
if (!decoded.ok)
return cliError(
new AnalysisInputError(
"compare_bundles",
{ cause: decoded.cause },
[
{
path: ["record_pairs"],
reason: "invalid_format",
expected: "JSON array",
},
],
),
);
: await parseCliJsonInput(
options.record_pairs,
"compare_bundles",
signal,
);
if (!decoded.ok) return decoded.error;
const input = { record_pairs: decoded.value };
const selected = bundleComparisonInputSchema
.pick({ record_pairs: true })
+24 -22
View File
@@ -123,16 +123,10 @@ cliTest(
right_evidence_id: different.records[0]!.evidence_id,
},
];
const paired = await cli.run({
arguments: [
"compare",
leftPath,
changedPath,
"--record_pairs",
JSON.stringify(pairs),
"--json",
],
});
const pairArgs = ["compare", leftPath, changedPath, "--record_pairs"];
const compareWithPairs = (value: string) =>
cli.run({ arguments: [...pairArgs, value, "--json"] });
const paired = await compareWithPairs(JSON.stringify(pairs));
expect(paired.exitCode).toBe(0);
const pairedResult = bundleComparisonResultSchema.parse(paired.json);
expect(pairedResult).toMatchObject({
@@ -142,6 +136,21 @@ cliTest(
{ classification: "changed", conclusion_kind: "observed_change" },
],
});
const pairFile = join(root, "pairs.json");
await writeFile(pairFile, JSON.stringify(pairs));
const fromFile = await compareWithPairs(pairFile);
expect(fromFile.exitCode).toBe(0);
expect(bundleComparisonResultSchema.parse(fromFile.json)).toEqual(
pairedResult,
);
const missingFile = join(root, "missing-pairs.json");
const absentFile = await compareWithPairs(missingFile);
expect(absentFile.exitCode).toBe(1);
expect(absentFile.json).toMatchObject({
code: "invalid_request",
input_path: missingFile,
input_reason: "read-failed",
});
const call = await cli.sharedMcpCall?.();
if (call === undefined) throw new Error("Expected shared MCP session");
const mcp = await call("compare_bundles", {
@@ -155,18 +164,11 @@ cliTest(
parseEvidence(mcp.structuredContent).normalized_result,
),
).toEqual(pairedResult);
const absentPair = await cli.run({
arguments: [
"compare",
leftPath,
changedPath,
"--record_pairs",
JSON.stringify([
{ ...pairs[0], left_evidence_id: `ev_${"f".repeat(64)}` },
]),
"--json",
],
});
const absentPair = await compareWithPairs(
JSON.stringify([
{ ...pairs[0], left_evidence_id: `ev_${"f".repeat(64)}` },
]),
);
expect(absentPair.exitCode).toBe(1);
expect(absentPair.json).toMatchObject({
code: "invalid_request",