Files
reverse-engineering-anything/tests/acceptance/jsonLinesCli.test.ts
T
Kaoru0822andClaude Opus 5.5 e06be06530 fix(cli): report JSON Lines selection refusals as caller errors (#7124)
* fix(cli): report JSON Lines selection refusals as caller errors

A --format jsonl refusal because the selected value is not a record
collection is the caller's output selection, not unreadable analysis
output. Report it as invalid_request/invalid_input that names the
selection and its observed shape, keep the --format json /
--filter-output remediation, and drop the rea doctor advice.

AnalysisInputError gains an optional producer remediation, mirroring
the other refusal classes. The JSON Lines owner examines its documented
boundaries once for both admission and refusal facts.

docs/cli.md now states that columnar results such as
inspect-sqlite-database --table rows are not record collections.

Fixes #7110

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cli): carry JSON Lines refusal facts as structured observations

JSON Lines refusals now put the location and shape of each refused value
in details.issues[].observed, and the message is built from those facts.
Tests assert the classification, issue path, observed facts, and the
shared remediation constant instead of fixed sentences.

The filtered analysis-view export check now uses a non-empty request
and compares the emitted rows with the unfiltered export.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 05:26:13 -07:00

187 lines
6.2 KiB
TypeScript

import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { DatabaseSync } from "node:sqlite";
import { expect } from "vitest";
import { JSON_LINES_SELECTION_REMEDIATION } from "../../src/cli/jsonLines.js";
import { parseEvidence } from "../../src/domain/evidence.js";
import { analysisViewJavaScriptEvidence } from "../fixtures/analysisView.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { cliTest } from "../support/cli/cliFixture.js";
cliTest(
"exports authenticated analysis-view rows, preserving every Evidence fact and empty/partial coverage",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-json-lines-view-");
const parent = analysisViewJavaScriptEvidence();
const evidencePath = join(root, "parent.json");
const requestPath = join(root, "request.json");
await writeFile(evidencePath, JSON.stringify(parent));
const request = {
source: { kind: "evidence-file", path: evidencePath },
view: { kind: "javascript-facts", property_keys: [], limit: 1 },
};
await writeFile(requestPath, JSON.stringify(request));
const invalid = await cli.run({
arguments: ["inspect-analysis-view", "--format", "jsonl"],
});
expect(invalid.exitCode).toBe(1);
expect(JSON.parse(invalid.stdout)).toMatchObject({
$rea: "jsonl/v1",
type: "error",
result: { code: "VALIDATION_ERROR" },
});
const badOutputValue = await cli.run({
arguments: ["--format", "jsonl", "--token-limit", "invalid"],
});
expect(badOutputValue.exitCode).toBe(1);
expect(JSON.parse(badOutputValue.stdout)).toMatchObject({
$rea: "jsonl/v1",
type: "error",
});
for (const where of [undefined, { paths: [] }]) {
await writeFile(
requestPath,
JSON.stringify({
...request,
view: { ...request.view, ...(where === undefined ? {} : { where }) },
}),
);
const json = await cli.run({
arguments: ["inspect-analysis-view", requestPath, "--json"],
});
expect(json.exitCode, json.stderr).toBe(0);
const expected = parseEvidence(json.json);
const result = await cli.run({
arguments: ["inspect-analysis-view", requestPath, "--format", "jsonl"],
});
expect(result.exitCode, result.stderr).toBe(0);
const records = result.stdout
.trimEnd()
.split("\n")
.map((line) => JSON.parse(line));
const final = records.pop();
expect(final).toMatchObject({
$rea: "jsonl/v1",
type: "metadata",
records: {
path: ["normalized_result", "rows"],
emitted: records.length,
source: records.length,
},
context: {
evidence_links: [parent.evidence_id],
normalized_result: { parent_evidence_id: parent.evidence_id },
},
});
const reconstructed = {
...final.context,
normalized_result: {
...final.context.normalized_result,
rows: records,
},
};
// Authenticate the reconstructed Evidence, rather than checking only a few
// convenient fields from a shortened view-shaped object.
expect(parseEvidence(reconstructed)).toEqual(expected);
expect(records.length).toBe(where === undefined ? 1 : 0);
// The documented disambiguating selection exports the same view rows.
const filter = "normalized_result.rows";
const selected = await cli.run({
arguments: [
"inspect-analysis-view",
requestPath,
"--format",
"jsonl",
"--filter-output",
filter,
],
});
expect(selected.exitCode, selected.stderr).toBe(0);
const selectedRecords = selected.stdout
.trimEnd()
.split("\n")
.map((line) => JSON.parse(line));
expect(selectedRecords.pop()).toMatchObject({
type: "metadata",
records: {
path: ["normalized_result", "rows"],
emitted: records.length,
source: records.length,
filter,
},
});
expect(selectedRecords).toEqual(records);
}
},
);
cliTest(
"refuses non-record SQLite selections as caller output errors while the analysis succeeds",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-json-lines-sqlite-");
const path = join(root, "config.sqlite");
const database = new DatabaseSync(path);
try {
database.exec(
"CREATE TABLE configInts(name TEXT PRIMARY KEY, value INTEGER); INSERT INTO configInts VALUES ('a', 1), ('b', 2);",
);
} finally {
database.close();
}
const table = ["--table", "configInts", "--row-limit", "3"];
const json = await cli.run({
arguments: ["inspect-sqlite-database", path, ...table, "--json"],
});
expect(json.exitCode, json.stderr).toBe(0);
const sqliteJsonLines = async (selection: readonly string[]) => {
const result = await cli.run({
arguments: [
"inspect-sqlite-database",
path,
...selection,
"--format",
"jsonl",
],
});
expect(result.exitCode).toBe(1);
expect(result.stdout.trimEnd().split("\n")).toHaveLength(1);
return result.json;
};
for (const [selection, option, observed] of [
[[], "format", [{ path: ["normalized_result", "rows"], shape: "null" }]],
[
[...table, "--filter-output", "normalized_result.rows"],
"filter-output",
[{ path: ["normalized_result", "rows"], shape: "object" }],
],
[
["--filter-output", "normalized_result.schema.tables"],
"filter-output",
[{ path: ["normalized_result", "schema", "tables"], shape: "array" }],
],
] as const) {
expect(await sqliteJsonLines(selection)).toMatchObject({
$rea: "jsonl/v1",
type: "error",
ok: false,
result: {
code: "invalid_request",
category: "invalid_input",
retryable: false,
remediation: { action: JSON_LINES_SELECTION_REMEDIATION },
details: {
operation: "inspect-sqlite-database",
issues: [{ path: [option], reason: "invalid_value", observed }],
},
},
});
}
},
);