Files
rea/tests/boundary/cli/cliOutput.test.ts
T
morluto 4f7b5fb602 fix(cli): route update flag through verified updater
Normalize the unambiguous root --update alias before output handling and
through direct createCli().serve calls. Reuse the existing output-value scan
and canonical update command, including selected environment, installation
ownership, Windows-aware launch, verification, maintenance and failure facts.
Retain option data and help/check precedence. Reject remaining Incur install
requests: update:false does not disable its explicit installer in0.6.1.
Remove the superseded output-only normalizer name and update its consumers.

Validation:123focused checks/7files pass, including compiled CLI with an owned
rejecting fake npm and direct serve across5formats. Original argument owner
fails6new controls/7pass; isolated baseline fails8workflow checks/2controls
pass. Static/docs/discovery1561 pass. Both scoped source reviews are clear.
Six completed equivalent current-version workflows compare baseline canonical
update with the alias; complete replies identical. Native Windows upgrade,
real package-manager installation and full install costs remain unverified.

The first baseline escaped a Vitest-only builtin mock and installed REA in the
pinned Node global prefix. Removed the newly added package and verified original
Node/npm/Corepack tools. Re-ran using the real builtin spy with synchronized
ESM exports, an independently rejecting PATH shim and owned npm prefix/cache.
Keep heavy tests/profiling stopped. Independent updater output bounds remain
tracked in #4888 rather than changing collection policy in this routing fix.

Fixes #4883
2026-10-18 05:33:21 +08:00

596 lines
18 KiB
TypeScript

import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { createPackageWithOptions } from "@electron/asar";
import { describe, expect, it } from "vitest";
import { workspaceCliTest } from "../../support/cli/workspaceCliFixture.js";
import {
normalizeCliArguments,
renderCliOutputArgumentError,
renderEmptyFilteredCliOutput,
validateCliOutputArguments,
} from "../../../src/cliOutput.js";
const CLI_INTEGRATION_TIMEOUT_MS = 60_000;
describe("CLI output argument 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 });
});
});
describe("CLI output flag value forms", () => {
it.each(["--filter-output", "--format", "--token-limit", "--token-offset"])(
"preserves a flag-shaped spaced value owned by %s",
(flag) => {
expect(
normalizeCliArguments([flag, "--format=json", "providers", "--json"]),
).toEqual([flag, "--format=json", "providers", "--json"]);
},
);
workspaceCliTest(
"keeps a flag-shaped filter value equivalent in spaced and equals forms",
async ({ cli }) => {
for (const filter of [
["--filter-output", "--format=json"],
["--filter-output=--format=json"],
]) {
const result = await cli.run({
arguments: [...filter, "providers", "--json"],
timeoutMs: 10_000,
});
expect(result.exitCode).toBe(0);
expect(JSON.parse(result.stdout)).toEqual({});
expect(result.stderr).toBe("");
}
},
CLI_INTEGRATION_TIMEOUT_MS,
);
it("splits only the equals forms of valued output flags", () => {
expect(
normalizeCliArguments([
"inspect-sqlite-database",
"--table=a=b",
"--format=json",
"--filter-output=a.b,c[0,1]",
"--token-limit=5",
"--token-offset=",
"--json",
"--format-hint=x",
]),
).toEqual([
"inspect-sqlite-database",
"--table=a=b",
"--format",
"json",
"--filter-output",
"a.b,c[0,1]",
"--token-limit",
"5",
"--token-offset",
"",
"--json",
"--format-hint=x",
]);
});
it("reports a missing value only for a flag the parser did not consume", () => {
expect(
validateCliOutputArguments([
"capabilities",
"--json",
"--filter-output",
"--format",
]),
).toEqual({ ok: true });
expect(
validateCliOutputArguments(["capabilities", "--json", "--format"]),
).toMatchObject({ ok: false, code: "MISSING_OUTPUT_VALUE" });
});
});
describe("unclosed CLI output filter slices", () => {
it.each([
"providers[",
"providers[0,1].name[",
"name,providers[",
"][,]",
"providers],name[0,1]",
])("rejects %s before the filter parser runs", (filter) => {
expect(
validateCliOutputArguments([
"providers",
"--json",
"--filter-output",
filter,
]),
).toMatchObject({ ok: false, code: "INVALID_OUTPUT_FILTER" });
});
it("preserves effective-filter and earlier flag-error precedence", () => {
for (const arguments_ of [
[
"--filter-output",
"providers[",
"--filter-output",
"capabilities[0,1].provider.id,open",
],
["--filter-output", "capabilities[-1,].provider.id"],
["--filter-output", "capabilities[[0,1]"],
["--format", "invalid", "--filter-output", "providers["],
["--token-limit", "invalid", "--filter-output", "providers["],
])
expect(validateCliOutputArguments(arguments_)).toEqual({ ok: true });
});
workspaceCliTest(
"fails promptly across output formats without invoking the command",
async ({ cli }) => {
for (const format of ["json", "jsonl", "yaml", "toon", "md"]) {
const result = await cli.run({
arguments: [
"analyze",
"/absent/target",
"--format",
format,
"--filter-output",
"result[0,1].name[",
],
timeoutMs: 3000,
});
expect(result.exitCode).toBe(1);
expect(result.stdout).toContain("INVALID_OUTPUT_FILTER");
expect(result.stdout).toContain("closing ]");
expect(result.stderr).toBe("");
}
const splitPath = await cli.run({
arguments: ["providers", "--json", "--filter-output", "][,]"],
timeoutMs: 3000,
});
expect(splitPath.exitCode).toBe(1);
expect(splitPath.json).toMatchObject({
ok: false,
error: { code: "INVALID_OUTPUT_FILTER" },
});
},
CLI_INTEGRATION_TIMEOUT_MS,
);
workspaceCliTest(
"retains closed slices and multiple filter paths",
async ({ cli }) => {
const result = await cli.run({
arguments: [
"providers",
"--json",
"--filter-output",
"capabilities[0,1].provider.id,open",
],
timeoutMs: 3000,
});
expect(result.exitCode).toBe(0);
expect(result.json).toEqual({
capabilities: [{ provider: { id: expect.any(String) } }],
open: false,
});
},
CLI_INTEGRATION_TIMEOUT_MS,
);
});
describe("CLI output valued-flag parsing", () => {
workspaceCliTest(
"preserves complete JSON errors when native builtin flags follow unusual arguments",
async ({ cli }) => {
for (const arguments_ of [
["providers", "--", "--json", "--token-limit", "5"],
["providers", "--format=json", "--token-limit=5"],
]) {
const result = await cli.run({ arguments: arguments_ });
expect(result.exitCode).toBe(1);
expect(result.stdout).not.toContain("[truncated:");
expect(JSON.parse(result.stdout)).toMatchObject({
ok: false,
error: { code: "UNSUPPORTED_OUTPUT_COMBINATION" },
});
}
},
CLI_INTEGRATION_TIMEOUT_MS,
);
workspaceCliTest(
"fails before emitting a truncated JSON document",
async ({ cli }) => {
const result = await cli.run({
arguments: ["providers", "--token-limit", "5", "--json"],
});
expect(result.exitCode).toBe(1);
expect(result.json).toMatchObject({
ok: false,
error: { code: "UNSUPPORTED_OUTPUT_COMBINATION" },
});
},
CLI_INTEGRATION_TIMEOUT_MS,
);
workspaceCliTest(
"names missing output values and honors equals-style output values",
async ({ cli }) => {
for (const [arguments_, flag] of [
[["capabilities", "--json", "--format", ""], "--format"],
[["capabilities", "--json", "--filter-output", ""], "--filter-output"],
[["capabilities", "--json", "--token-limit", ""], "--token-limit"],
[["capabilities", "--json", "--token-offset="], "--token-offset"],
[["capabilities", "--format"], "--format"],
] as const) {
const result = await cli.run({ arguments: arguments_ });
expect(result.exitCode).toBe(1);
expect(result.stdout).not.toContain("[truncated:");
expect(result.json).toMatchObject({
ok: false,
error: {
code: "MISSING_OUTPUT_VALUE",
message: expect.stringContaining(`${flag} requires a value`),
},
});
}
const equalsForms = await cli.run({
arguments: [
"capabilities",
"--format=json",
"--filter-output=capabilities[0,1]",
],
});
expect(equalsForms.exitCode).toBe(0);
expect(equalsForms.json).toMatchObject({
capabilities: [expect.any(Object)],
});
const equalsTokenWindow = await cli.run({
arguments: ["capabilities", "--json", "--token-limit=5"],
});
expect(equalsTokenWindow.exitCode).toBe(1);
expect(equalsTokenWindow.json).toMatchObject({
ok: false,
error: { code: "UNSUPPORTED_OUTPUT_COMBINATION" },
});
const validTokenWindow = await cli.run({
arguments: ["capabilities", "--json", "--token-limit", "5"],
});
expect(validTokenWindow.exitCode).toBe(1);
expect(validTokenWindow.json).toMatchObject({
ok: false,
error: { code: "UNSUPPORTED_OUTPUT_COMBINATION" },
});
const emptyFormatWithTokenWindow = await cli.run({
arguments: [
"capabilities",
"--json",
"--format",
"",
"--token-limit",
"5",
],
});
expect(emptyFormatWithTokenWindow.exitCode).toBe(1);
expect(emptyFormatWithTokenWindow.stdout).not.toContain("[truncated:");
expect(emptyFormatWithTokenWindow.json).toMatchObject({
ok: false,
error: { code: "UNSUPPORTED_OUTPUT_COMBINATION" },
});
for (const arguments_ of [
["capabilities", "--json", "--token-limit", "not-a-number"],
[
"capabilities",
"--format",
"unsupported-format",
"--json",
"--token-limit",
"5",
],
]) {
const result = await cli.run({ arguments: arguments_ });
expect(result.exitCode).toBe(1);
expect(result.stdout).not.toContain("UNSUPPORTED_OUTPUT_COMBINATION");
expect(result.stdout).not.toContain("[truncated:");
}
},
CLI_INTEGRATION_TIMEOUT_MS,
);
});
describe("CLI output argument boundary", () => {
it("renders an explicit empty projection for structured filtered output", () => {
for (const format of ["json", "jsonl", "yaml"])
expect(
renderEmptyFilteredCliOutput([
"providers",
"--format",
format,
"--filter-output",
"missing",
]),
).toBe("{}\n");
expect(
renderEmptyFilteredCliOutput(["providers", "--filter-output", "missing"]),
).toBeUndefined();
expect(
renderEmptyFilteredCliOutput(["providers", "--format", "json"]),
).toBeUndefined();
});
});
describe("compiled CLI output boundary", () => {
workspaceCliTest(
"emits valid JSON when an output filter misses the top-level result",
async ({ cli }) => {
const result = await cli.run({
arguments: [
"analyze-javascript-application",
"tests/conformance/readiness/javascript-cli",
"--format",
"json",
"--filter-output",
"summary",
],
});
expect(result).toMatchObject({
exitCode: 0,
stdout: "{}\n",
stderr: expect.stringContaining('"rea_progress":'),
json: {},
});
const nested = await cli.run({
arguments: [
"analyze-javascript-application",
"tests/conformance/readiness/javascript-cli",
"--format",
"json",
"--filter-output",
"normalized_result.not_a_field",
],
});
expect(nested).toMatchObject({
exitCode: 0,
stderr: expect.stringContaining('"rea_progress":'),
json: { normalized_result: {} },
});
const selected = await cli.run({
arguments: [
"analyze-javascript-application",
"tests/conformance/readiness/javascript-cli",
"--format",
"json",
"--filter-output",
"normalized_result.summary",
],
});
expect(selected).toMatchObject({
exitCode: 0,
stderr: expect.stringContaining('"rea_progress":'),
json: {
normalized_result: {
summary: expect.objectContaining({ browser_windows: 0 }),
},
},
});
},
CLI_INTEGRATION_TIMEOUT_MS,
);
workspaceCliTest(
"preserves missing-argument coordinates through every dispatcher formatter",
async ({ cli }) => {
const json = await cli.run({ arguments: ["--json", "analyze"] });
expect(json).toMatchObject({
exitCode: 1,
json: {
code: "VALIDATION_ERROR",
message: expect.stringContaining("expected string"),
fieldErrors: [
{
path: "path",
code: "invalid_type",
missing: true,
expected: "string",
},
],
},
});
for (const arguments_ of [
["analyze"],
["--format", "yaml", "analyze"],
["--full-output", "analyze"],
]) {
const result = await cli.run({ arguments: arguments_ });
expect(result.exitCode).toBe(1);
expect(result.stdout).toContain("VALIDATION_ERROR");
expect(result.stdout).toContain("fieldErrors");
expect(result.stdout).toContain("path");
expect(result.stdout).toContain("expected string");
}
},
CLI_INTEGRATION_TIMEOUT_MS,
);
workspaceCliTest(
"preserves invalid option names and enum constraints in structured diagnostics",
async ({ cli }) => {
const result = await cli.run({
arguments: [
"search",
"/caller/local/input",
"needle",
"--kind",
"unrecognized",
"--json",
],
});
expect(result).toMatchObject({
exitCode: 1,
json: {
code: "VALIDATION_ERROR",
fieldErrors: [
{
path: "kind",
code: "invalid_value",
missing: false,
message: expect.stringContaining('"strings"|"procedures"'),
},
],
},
});
},
CLI_INTEGRATION_TIMEOUT_MS,
);
workspaceCliTest(
"prints field-specific missing-argument recovery in terminal mode",
async ({ processes }) => {
const result = await processes.run(
process.execPath,
[
"--input-type=module",
"--eval",
[
'import { createCli } from "./dist/cli.js";',
'Object.defineProperty(process.stdout, "isTTY", { value: true });',
'await createCli({}).serve(["analyze"], { exit: (code) => { process.exitCode = code; } });',
].join("\n"),
],
{ cwd: process.cwd() },
);
expect(result.exitCode).toBe(1);
expect(result.stdout).toContain(
"Error: missing required argument <path>",
);
expect(result.stdout).toContain("See below for usage.");
},
CLI_INTEGRATION_TIMEOUT_MS,
);
});
describe("compiled CLI artifact diagnostics", () => {
workspaceCliTest(
"preserves artifact diagnostics in JSON output",
async ({ cli, workspace }) => {
const source = workspace.path("source");
await mkdir(source);
await writeFile(join(source, "main.js"), "console.log('ok');\n");
const archive = workspace.path("fixture.asar");
await createPackageWithOptions(source, archive, { unpack: "*.js" });
await writeFile(join(`${archive}.unpacked`, "main.js"), "changed();\n");
const result = await cli.run({
arguments: ["--json", "inspect-artifact", archive],
});
expect(result.exitCode).toBe(1);
const output = JSON.stringify(result.json);
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,
);
});
describe("CLI update root alias", () => {
it.each([
[["--update"], ["update"]],
[
["--json", "--update", "--filter-output=status"],
["update", "--json", "--filter-output", "status"],
],
[
["--format=yaml", "--update", "--full-output"],
["update", "--format", "yaml", "--full-output"],
],
[
["--update", "--token-limit=100", "--token-count"],
["update", "--token-limit", "100", "--token-count"],
],
[["--update", "--update"], ["update"]],
[
["--filter-output", "--update"],
["--filter-output", "--update"],
],
[
["--filter-output=--update", "--update"],
["update", "--filter-output", "--update"],
],
[
["--update", "--help"],
["--update", "--help"],
],
[
["--incur-update-check", "--update"],
["--incur-update-check", "--update"],
],
[
["providers", "--update"],
["providers", "--update"],
],
[
["--", "--update"],
["--", "--update"],
],
[
["--config", "--update"],
["--config", "--update"],
],
[
["update", "--json"],
["update", "--json"],
],
])(
"routes only an unambiguous root update while retaining option data: %j",
(input, expected) => {
expect(normalizeCliArguments(input)).toEqual(expected);
expect(normalizeCliArguments(expected)).toEqual(expected);
},
);
});