rea/tests/cliOutput.test.ts

251 lines
8.0 KiB
TypeScript

import { execFile } from "node:child_process";
import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";
import { createPackageWithOptions } from "@electron/asar";
import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "./fixtures/temporaryDirectory.js";
import {
renderCliOutputArgumentError,
sanitizeCliOutput,
validateCliOutputArguments,
} from "../src/cliOutput.js";
const execFileAsync = promisify(execFile);
const CLI_INTEGRATION_TIMEOUT_MS = 60_000;
const CLI_VARIANT_TIMEOUT_MS = 120_000;
describe("CLI output boundary", () => {
it("rejects token windows that would corrupt structured output", () => {
for (const format of ["json", "jsonl", "yaml"] as const) {
const validation = validateCliOutputArguments([
"providers",
"--token-limit",
"5",
"--format",
format,
]);
expect(validation).toMatchObject({
ok: false,
format,
code: "UNSUPPORTED_OUTPUT_COMBINATION",
});
if (!validation.ok) {
const rendered = renderCliOutputArgumentError(validation);
expect(rendered).not.toContain("[truncated:");
if (format === "json" || format === "jsonl")
expect(JSON.parse(rendered)).toMatchObject({
ok: false,
error: { code: "UNSUPPORTED_OUTPUT_COMBINATION" },
});
else
expect(rendered).toMatch(
/^ok: false\nerror:\n code: UNSUPPORTED_OUTPUT_COMBINATION\n/u,
);
}
}
expect(
validateCliOutputArguments([
"providers",
"--token-limit",
"5",
"--format",
"toon",
]),
).toEqual({ ok: true });
expect(
validateCliOutputArguments(["providers", "--token-count", "--json"]),
).toEqual({ ok: true });
});
it("fails before emitting a truncated JSON document", async () => {
const execution = execFileAsync(
process.execPath,
["scripts/rea.mjs", "providers", "--token-limit", "5", "--json"],
{ cwd: process.cwd() },
);
const failure = await execution.catch((cause: unknown) => cause);
expect(failure).toMatchObject({ code: 1 });
const { stdout } = failure as { readonly stdout: string };
expect(JSON.parse(stdout)).toMatchObject({
ok: false,
error: { code: "UNSUPPORTED_OUTPUT_COMBINATION" },
});
});
it("preserves normal output and sanitizes text and JSON validation errors", () => {
expect(sanitizeCliOutput("result: ok\n")).toBe("result: ok\n");
expect(sanitizeCliOutput("result: VALIDATION_ERROR\n")).toBe(
"result: VALIDATION_ERROR\n",
);
const raw =
'code: VALIDATION_ERROR\nmessage: "raw Zod details"\nfieldErrors: SECRET\n';
expect(sanitizeCliOutput(raw)).toBe(
'code: VALIDATION_ERROR\nmessage: "REA could not read the command arguments. Run `rea --help`, correct the arguments, then try again."\n',
);
expect(
JSON.parse(
sanitizeCliOutput(
JSON.stringify({
code: "VALIDATION_ERROR",
message: "raw Zod details",
fieldErrors: [{ code: "invalid_type" }],
}),
),
),
).toEqual({
code: "VALIDATION_ERROR",
message:
"REA could not read the command arguments. Run `rea --help`, correct the arguments, then try again.",
});
expect(
JSON.parse(
sanitizeCliOutput(
JSON.stringify({
ok: false,
error: {
code: "VALIDATION_ERROR",
message: "raw Zod details",
fieldErrors: [{ code: "invalid_type" }],
},
meta: { command: "analyze" },
}),
),
),
).toEqual({
ok: false,
error: {
code: "VALIDATION_ERROR",
message:
"REA could not read the command arguments. Run `rea --help`, correct the arguments, then try again.",
},
meta: { command: "analyze" },
});
});
it(
"sanitizes a real missing-argument dispatcher failure",
async () => {
await expect(
execFileAsync(process.execPath, ["scripts/rea.mjs", "analyze"], {
cwd: process.cwd(),
}),
).rejects.toMatchObject({
stdout:
'code: VALIDATION_ERROR\nmessage: "REA could not read the command arguments. Run `rea --help`, correct the arguments, then try again."\n',
});
await expect(
execFileAsync(
process.execPath,
["scripts/rea.mjs", "--full-output", "--json", "analyze"],
{ cwd: process.cwd() },
),
).rejects.toMatchObject({
stdout: expect.not.stringContaining("fieldErrors"),
});
await expect(
execFileAsync(
process.execPath,
["scripts/rea.mjs", "--full-output", "analyze"],
{ cwd: process.cwd() },
),
).rejects.toMatchObject({
stdout:
'ok: false\nerror:\n code: VALIDATION_ERROR\n message: "REA could not read the command arguments. Run `rea --help`, correct the arguments, then try again."\n',
});
},
CLI_INTEGRATION_TIMEOUT_MS,
);
it(
"preserves artifact diagnostics in ordinary and full JSON output",
async () => {
const root = await createTestTempDirectory("rea-cli-diagnostics-");
const source = join(root, "source");
await mkdir(source);
await writeFile(join(source, "main.js"), "console.log('ok');\n");
const archive = join(root, "fixture.asar");
await createPackageWithOptions(source, archive, { unpack: "*.js" });
await writeFile(join(`${archive}.unpacked`, "main.js"), "changed();\n");
for (const flags of [["--json"], ["--full-output", "--json"]]) {
const execution = execFileAsync(process.execPath, [
"scripts/rea.mjs",
...flags,
"inventory-artifact",
archive,
]);
const failure = await execution.catch((cause: unknown) => cause);
expect(failure).toMatchObject({ code: 1 });
const { stdout } = failure as { readonly stdout: string };
const output = JSON.stringify(JSON.parse(stdout) as unknown);
expect(output).toContain('"logical_path":"main.js"');
expect(output).toMatch(/"declared_sha256":"[a-f0-9]{64}"/u);
expect(output).toMatch(/"calculated_sha256":"[a-f0-9]{64}"/u);
expect(output).toContain('"unpacked":true');
}
},
CLI_INTEGRATION_TIMEOUT_MS,
);
it(
"keeps operation failure status independent of output controls",
async () => {
const variants = [
["--format", "toon"],
["--format", "json"],
["--format", "yaml"],
["--format", "md"],
["--format", "jsonl"],
["--full-output", "--json"],
["--filter-output", "category", "--json"],
["--token-limit", "5", "--json"],
["--token-count", "--json"],
];
for (const flags of variants) {
await expect(
execFileAsync(process.execPath, [
"scripts/rea.mjs",
...flags,
"investigate-versions",
"/tmp/left",
"/tmp/right",
"/tmp/workspace.json",
]),
).rejects.toMatchObject({ code: 1, stdout: expect.any(String) });
}
},
CLI_VARIANT_TIMEOUT_MS,
);
it(
"keeps failure logs out of structured stdout",
async () => {
const execution = execFileAsync(
process.execPath,
[
"scripts/rea.mjs",
"--json",
"investigate-versions",
"/tmp/left",
"/tmp/right",
"/tmp/workspace.json",
],
{ env: { ...process.env, REA_LOG_LEVEL: "error" } },
);
const failure = await execution.catch((cause: unknown) => cause);
expect(failure).toMatchObject({
code: 1,
stderr: expect.stringContaining('"status":"error"'),
});
const { stdout } = failure as { readonly stdout: string };
expect(JSON.parse(stdout)).toMatchObject({ error: "ApprovalRequired" });
},
CLI_INTEGRATION_TIMEOUT_MS,
);
});