fix(cli): preserve failure semantics across full-output formats
This commit is contained in:
parent
221c856481
commit
570c40cde5
|
|
@ -41,6 +41,7 @@
|
|||
"smol-toml": "1.9.0",
|
||||
"stream-chain": "4.2.6",
|
||||
"stream-json": "3.7.0",
|
||||
"tokenx": "1.3.0",
|
||||
"write-file-atomic": "7.0.0",
|
||||
"ws": "8.21.0",
|
||||
"yaml": "2.9.0",
|
||||
|
|
|
|||
|
|
@ -320,6 +320,7 @@
|
|||
"smol-toml": "1.9.0",
|
||||
"stream-chain": "4.2.6",
|
||||
"stream-json": "3.7.0",
|
||||
"tokenx": "1.3.0",
|
||||
"write-file-atomic": "7.0.0",
|
||||
"ws": "8.21.0",
|
||||
"yaml": "2.9.0",
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export const bundleRuntime = async (root) => {
|
|||
{
|
||||
cli: "cli.js",
|
||||
cliOutput: "cliOutput.js",
|
||||
streamedJsonOutput: "cli/streamedJsonOutput.js",
|
||||
cliResultOutput: "cli/cliResultOutput.js",
|
||||
mcpDoctor: "mcpDoctor.js",
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ if (await pathExists(join(root, "src"))) {
|
|||
"dist/mcp/main.js",
|
||||
"dist/cli-runtime/cli.js",
|
||||
"dist/cli-runtime/cliOutput.js",
|
||||
"dist/cli-runtime/streamedJsonOutput.js",
|
||||
"dist/cli-runtime/cliResultOutput.js",
|
||||
"dist/cli-runtime/mcpDoctor.js",
|
||||
"dist/mcpDoctor.js",
|
||||
"dist/cli.js",
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ const runtimeFiles = isMcpMode
|
|||
: [
|
||||
"dist/cli-runtime/cli.js",
|
||||
"dist/cli-runtime/cliOutput.js",
|
||||
"dist/cli-runtime/streamedJsonOutput.js",
|
||||
"dist/cli-runtime/cliResultOutput.js",
|
||||
];
|
||||
|
||||
if (!(await compiledRuntimeExists(runtimeFiles))) {
|
||||
|
|
@ -60,8 +60,8 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
|
|||
} else {
|
||||
const { cliConfigurationError, createCli } =
|
||||
await import("../dist/cli-runtime/cli.js");
|
||||
const { createStreamedCliJsonOutput } =
|
||||
await import("../dist/cli-runtime/streamedJsonOutput.js");
|
||||
const { createCliResultOutput } =
|
||||
await import("../dist/cli-runtime/cliResultOutput.js");
|
||||
const {
|
||||
renderCliOutputArgumentError,
|
||||
renderEmptyFilteredCliOutput,
|
||||
|
|
@ -77,7 +77,7 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
|
|||
process.exitCode = 1;
|
||||
} else {
|
||||
let wroteOutput = false;
|
||||
const resultOutput = createStreamedCliJsonOutput(args, process.stdout);
|
||||
const resultOutput = createCliResultOutput(args, process.stdout);
|
||||
await createCli(process.env, resultOutput).serve(args, {
|
||||
stdout: (output) => {
|
||||
if (resultOutput?.handled) {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { finished } from "node:stream/promises";
|
|||
|
||||
import { Cli } from "incur";
|
||||
|
||||
import { createStreamedCliJsonOutput } from "../../../dist/cli/streamedJsonOutput.js";
|
||||
import { createCliResultOutput } from "../../../dist/cli/cliResultOutput.js";
|
||||
import { createEvidence } from "../../../dist/domain/evidence.js";
|
||||
|
||||
// This opt-in format-boundary check writes one temporary file larger than the
|
||||
|
|
@ -79,7 +79,7 @@ try {
|
|||
|
||||
destination = createWriteStream(path, { flags: "wx", mode: 0o600 });
|
||||
const argv = [metadata.command, "--format", format, "--full-output"];
|
||||
const output = createStreamedCliJsonOutput(argv, destination);
|
||||
const output = createCliResultOutput(argv, destination);
|
||||
assert.ok(output);
|
||||
let unexpectedOutputBytes = 0;
|
||||
let exitCode = 0;
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ import type { CliInstance } from "./cli/types.js";
|
|||
import {
|
||||
streamCliCommandResults,
|
||||
type CliResultOutput,
|
||||
} from "./cli/streamedJsonOutput.js";
|
||||
} from "./cli/cliResultOutput.js";
|
||||
|
||||
/** Describe CLI settings that are invalid before any command runs, as MCP does. */
|
||||
export const cliConfigurationError = (
|
||||
|
|
|
|||
|
|
@ -1,15 +1,24 @@
|
|||
import { constants } from "node:buffer";
|
||||
import type { Writable } from "node:stream";
|
||||
|
||||
import { Cli, Filter } from "incur";
|
||||
import { Cli, Filter, Formatter } from "incur";
|
||||
import { estimateTokenCount, sliceByTokens } from "tokenx";
|
||||
|
||||
import { parseCliOutputArguments } from "../cliOutput.js";
|
||||
import {
|
||||
parseCliOutputArguments,
|
||||
validateCliOutputArguments,
|
||||
} from "../cliOutput.js";
|
||||
import { cliOperationFailurePayload } from "../cliLogging.js";
|
||||
import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js";
|
||||
import {
|
||||
projectAnalysisError,
|
||||
type AnalysisErrorProjection,
|
||||
} from "../domain/analysisErrorProjection.js";
|
||||
import { jsonOutputParts, writeJsonOutput } from "./jsonOutput.js";
|
||||
import {
|
||||
jsonOutputParts,
|
||||
writeJsonOutput,
|
||||
writeOutputText,
|
||||
} from "./jsonOutput.js";
|
||||
import type { CliInstance } from "./types.js";
|
||||
|
||||
/** Result metadata supplied by the command that actually ran. */
|
||||
|
|
@ -100,19 +109,15 @@ const wrapCommand = (
|
|||
};
|
||||
};
|
||||
|
||||
/** Preserve Incur's JSON controls while streaming the complete result. */
|
||||
export const createStreamedCliJsonOutput = (
|
||||
/** Preserve Incur output controls and stream JSON while framing failures. */
|
||||
export const createCliResultOutput = (
|
||||
arguments_: readonly string[],
|
||||
destination: Writable,
|
||||
): CliResultOutput | undefined => {
|
||||
const options = parseCliOutputArguments(arguments_);
|
||||
if (
|
||||
options.parseError ||
|
||||
options.tokenWindow ||
|
||||
(options.format !== "json" && options.format !== "jsonl")
|
||||
)
|
||||
if (options.parseError || !validateCliOutputArguments(arguments_).ok)
|
||||
return undefined;
|
||||
const format = options.format;
|
||||
const format = options.format as "json" | "jsonl" | "yaml" | "toon" | "md";
|
||||
let handled = false;
|
||||
let failed = false;
|
||||
let error: AnalysisErrorProjection | undefined;
|
||||
|
|
@ -128,27 +133,53 @@ export const createStreamedCliJsonOutput = (
|
|||
},
|
||||
async write(value, metadata) {
|
||||
if (metadata.format !== format) return false;
|
||||
const failure = options.fullOutput
|
||||
? cliOperationFailurePayload(value)
|
||||
: undefined;
|
||||
const filtered: unknown = options.filterOutput
|
||||
? Filter.apply(value, Filter.parse(options.filterOutput))
|
||||
: value;
|
||||
const meta = {
|
||||
command: metadata.command,
|
||||
duration: metadata.duration,
|
||||
};
|
||||
// Leave successful YAML and bare YAML rendering to Incur. The custom
|
||||
// boundary is needed only for a full failure envelope, whose error
|
||||
// sentinel otherwise drops category, remediation, and details.
|
||||
if (format !== "json" && format !== "jsonl" && failure === undefined)
|
||||
return false;
|
||||
if (options.tokenCount) {
|
||||
// Incur/tokenx materialize the formatted value. Measure its UTF-16
|
||||
// expansion before allowing that allocation, independently of UTF-8 bytes.
|
||||
// Incur/tokenx materialize JSON. Measure its UTF-16 expansion before
|
||||
// allowing that allocation, independently of UTF-8 bytes. Other formats
|
||||
// retain Incur's materializing behavior.
|
||||
const tokenValue = failure ?? filtered;
|
||||
let characters = 0;
|
||||
if (
|
||||
filtered !== undefined &&
|
||||
filtered !== null &&
|
||||
typeof filtered !== "function" &&
|
||||
typeof filtered !== "symbol"
|
||||
)
|
||||
for (const part of jsonOutputParts(filtered, format))
|
||||
characters += part.length;
|
||||
characters = Math.max(0, characters - 1);
|
||||
if (characters <= constants.MAX_STRING_LENGTH) return false;
|
||||
tokenValue !== undefined &&
|
||||
tokenValue !== null &&
|
||||
typeof tokenValue !== "function" &&
|
||||
typeof tokenValue !== "symbol"
|
||||
) {
|
||||
if (format === "json" || format === "jsonl") {
|
||||
for (const part of jsonOutputParts(
|
||||
tokenValue,
|
||||
format,
|
||||
format === "json",
|
||||
))
|
||||
characters += part.length;
|
||||
characters = Math.max(0, characters - 1);
|
||||
}
|
||||
}
|
||||
if (characters <= constants.MAX_STRING_LENGTH) {
|
||||
if (failure === undefined) return false;
|
||||
handled = true;
|
||||
try {
|
||||
await writeOutputText(
|
||||
destination,
|
||||
`${estimateTokenCount(Formatter.format(failure, format))}\n`,
|
||||
);
|
||||
return true;
|
||||
} catch (cause: unknown) {
|
||||
failed = true;
|
||||
throw cause;
|
||||
}
|
||||
}
|
||||
error = projectAnalysisError(
|
||||
new AnalysisResourceConstraintError(
|
||||
metadata.command,
|
||||
|
|
@ -165,10 +196,26 @@ export const createStreamedCliJsonOutput = (
|
|||
),
|
||||
);
|
||||
}
|
||||
const meta = {
|
||||
command: metadata.command,
|
||||
duration: metadata.duration,
|
||||
};
|
||||
const errorDocument =
|
||||
options.fullOutput && failure !== undefined
|
||||
? fullFailureEnvelope(
|
||||
failure,
|
||||
meta,
|
||||
format,
|
||||
options.tokenLimit,
|
||||
options.tokenOffset,
|
||||
)
|
||||
: undefined;
|
||||
const document = options.fullOutput
|
||||
? filtered === undefined
|
||||
? { ok: true, meta }
|
||||
: { ok: true, data: filtered, meta }
|
||||
? failure !== undefined
|
||||
? errorDocument
|
||||
: filtered === undefined
|
||||
? { ok: true, meta }
|
||||
: { ok: true, data: filtered, meta }
|
||||
: filtered === undefined ||
|
||||
filtered === null ||
|
||||
typeof filtered === "function" ||
|
||||
|
|
@ -179,15 +226,24 @@ export const createStreamedCliJsonOutput = (
|
|||
// document from Incur's error formatter.
|
||||
handled = true;
|
||||
try {
|
||||
await writeJsonOutput(
|
||||
error === undefined
|
||||
? document
|
||||
: options.fullOutput
|
||||
? { ok: false, error, meta }
|
||||
: error,
|
||||
destination,
|
||||
format,
|
||||
);
|
||||
if (failure !== undefined && format !== "json" && format !== "jsonl") {
|
||||
// Incur's error sentinel only carries code/message/retryable. Render
|
||||
// the full caller-visible failure at this output boundary.
|
||||
await writeOutputText(
|
||||
destination,
|
||||
`${Formatter.format(document, format)}\n`,
|
||||
);
|
||||
} else {
|
||||
await writeJsonOutput(
|
||||
error === undefined
|
||||
? document
|
||||
: options.fullOutput
|
||||
? { ok: false, error, meta }
|
||||
: error,
|
||||
destination,
|
||||
format as "json" | "jsonl",
|
||||
);
|
||||
}
|
||||
} catch (cause: unknown) {
|
||||
failed = true;
|
||||
throw cause;
|
||||
|
|
@ -196,3 +252,26 @@ export const createStreamedCliJsonOutput = (
|
|||
},
|
||||
};
|
||||
};
|
||||
|
||||
const fullFailureEnvelope = (
|
||||
failure: unknown,
|
||||
meta: Readonly<Record<string, unknown>>,
|
||||
format: "json" | "jsonl" | "yaml" | "toon" | "md",
|
||||
tokenLimit: number | undefined,
|
||||
tokenOffset: number | undefined,
|
||||
): Readonly<Record<string, unknown>> => {
|
||||
if (tokenLimit === undefined && tokenOffset === undefined)
|
||||
return { ok: false, error: failure, meta };
|
||||
const formatted = Formatter.format(failure, format);
|
||||
const total = estimateTokenCount(formatted);
|
||||
const offset = tokenOffset ?? 0;
|
||||
const end = tokenLimit === undefined ? total : offset + tokenLimit;
|
||||
if (offset === 0 && end >= total) return { ok: false, error: failure, meta };
|
||||
const actualEnd = Math.min(end, total);
|
||||
const nextOffset = actualEnd < total ? actualEnd : undefined;
|
||||
return {
|
||||
ok: false,
|
||||
error: `${sliceByTokens(formatted, offset, end)}\n[truncated: showing tokens ${offset}–${actualEnd} of ${total}]`,
|
||||
meta: { ...meta, ...(nextOffset === undefined ? {} : { nextOffset }) },
|
||||
};
|
||||
};
|
||||
|
|
@ -19,13 +19,16 @@ export const writeJsonOutput = async (
|
|||
for (const part of bufferedJsonParts(
|
||||
jsonOutputParts(value, format, isTerminal(destination)),
|
||||
))
|
||||
await writePart(destination, part);
|
||||
await writeOutputText(destination, part);
|
||||
};
|
||||
|
||||
const isTerminal = (destination: Writable): boolean =>
|
||||
"isTTY" in destination && destination.isTTY === true;
|
||||
|
||||
const writePart = (destination: Writable, part: string): Promise<void> =>
|
||||
export const writeOutputText = (
|
||||
destination: Writable,
|
||||
part: string,
|
||||
): Promise<void> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const onError = (cause: unknown): void => reject(cause);
|
||||
destination.once("error", onError);
|
||||
|
|
|
|||
|
|
@ -12,20 +12,64 @@ import type { Logger } from "pino";
|
|||
const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
/** Identify caller-visible CLI outcomes that mean the requested operation failed. */
|
||||
export const isCliOperationFailure = (value: unknown): boolean => {
|
||||
if (!isRecord(value)) return false;
|
||||
if (
|
||||
isRecord(value.error) &&
|
||||
analysisErrorProjectionSchema.safeParse(value.error).success
|
||||
)
|
||||
return true;
|
||||
if (analysisCliErrorEnvelopeSchema.safeParse(value).success) return true;
|
||||
if (analysisErrorProjectionSchema.safeParse(value).success) return true;
|
||||
if (value.healthy === false) return true;
|
||||
return false;
|
||||
const loggedFailures = new WeakMap<object, unknown>();
|
||||
|
||||
/** Return the canonical failure projection owned by the CLI result boundary. */
|
||||
const projectCliOperationFailure = (
|
||||
value: unknown,
|
||||
): AnalysisErrorProjection | undefined => {
|
||||
if (!isRecord(value)) return undefined;
|
||||
const direct = analysisErrorProjectionSchema.safeParse(value);
|
||||
if (direct.success) return value as AnalysisErrorProjection;
|
||||
const envelope = analysisCliErrorEnvelopeSchema.safeParse(value);
|
||||
if (envelope.success) {
|
||||
const {
|
||||
error: _label,
|
||||
input_path: inputPath,
|
||||
input_reason: inputReason,
|
||||
...projection
|
||||
} = value;
|
||||
const parsed = projection as AnalysisErrorProjection;
|
||||
if (typeof inputPath !== "string" && typeof inputReason !== "string")
|
||||
return parsed;
|
||||
return {
|
||||
...parsed,
|
||||
details: {
|
||||
...parsed.details,
|
||||
...(typeof inputPath === "string" ? { input_path: inputPath } : {}),
|
||||
...(typeof inputReason === "string"
|
||||
? { input_reason: inputReason }
|
||||
: {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
if (isRecord(value.error)) {
|
||||
const nested = analysisErrorProjectionSchema.safeParse(value.error);
|
||||
if (nested.success) return value.error as AnalysisErrorProjection;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Return the canonical projection or original record classified as failed. */
|
||||
const inferCliOperationFailure = (value: unknown): unknown | undefined => {
|
||||
const projection = projectCliOperationFailure(value);
|
||||
if (projection !== undefined) return projection;
|
||||
if (isRecord(value) && value.healthy === false) return value;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const cliOperationFailurePayload = (
|
||||
value: unknown,
|
||||
): unknown | undefined => {
|
||||
if (isRecord(value) && loggedFailures.has(value))
|
||||
return loggedFailures.get(value);
|
||||
return inferCliOperationFailure(value);
|
||||
};
|
||||
|
||||
/** Identify caller-visible CLI outcomes that mean the requested operation failed. */
|
||||
export const isCliOperationFailure = (value: unknown): boolean =>
|
||||
cliOperationFailurePayload(value) !== undefined;
|
||||
|
||||
/** Log one CLI command with duration and a stable success or failure status. */
|
||||
export const logCliCommand = async <Value>(
|
||||
logger: Logger,
|
||||
|
|
@ -36,7 +80,13 @@ export const logCliCommand = async <Value>(
|
|||
const startedAt = performance.now();
|
||||
try {
|
||||
const value = await execute();
|
||||
const failed = isCommandFailure?.(value) ?? isCliOperationFailure(value);
|
||||
const inferredFailure = inferCliOperationFailure(value);
|
||||
const failed = isCommandFailure?.(value) ?? inferredFailure !== undefined;
|
||||
if (isRecord(value))
|
||||
loggedFailures.set(
|
||||
value,
|
||||
failed ? (inferredFailure ?? value) : undefined,
|
||||
);
|
||||
logger[failed ? "error" : "info"](
|
||||
{
|
||||
command,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ interface ParsedCliOutputArguments {
|
|||
readonly parseError: boolean;
|
||||
readonly tokenCount: boolean;
|
||||
readonly tokenWindow: boolean;
|
||||
readonly tokenLimit: number | undefined;
|
||||
readonly tokenOffset: number | undefined;
|
||||
}
|
||||
|
||||
/** Read effective Incur output controls at the executable boundary. */
|
||||
|
|
@ -30,14 +32,18 @@ export const parseCliOutputArguments = (
|
|||
let filterOutput: string | undefined;
|
||||
let fullOutput = false;
|
||||
let tokenCount = false;
|
||||
let tokenWindow = false;
|
||||
let tokenLimit: number | undefined;
|
||||
let tokenOffset: number | undefined;
|
||||
const parsed = (parseError: boolean): ParsedCliOutputArguments => ({
|
||||
filterOutput,
|
||||
format,
|
||||
fullOutput,
|
||||
parseError,
|
||||
tokenCount,
|
||||
tokenWindow: parseError ? false : tokenWindow,
|
||||
tokenWindow:
|
||||
!parseError && (tokenLimit !== undefined || tokenOffset !== undefined),
|
||||
tokenLimit: parseError ? undefined : tokenLimit,
|
||||
tokenOffset: parseError ? undefined : tokenOffset,
|
||||
});
|
||||
for (let index = 0; index < arguments_.length; index += 1) {
|
||||
const argument = arguments_[index];
|
||||
|
|
@ -71,7 +77,8 @@ export const parseCliOutputArguments = (
|
|||
const numericValue = Number(value);
|
||||
if (!Number.isFinite(numericValue) || value.trim() === "")
|
||||
return parsed(true);
|
||||
tokenWindow = true;
|
||||
if (argument === "--token-limit") tokenLimit = numericValue;
|
||||
else tokenOffset = numericValue;
|
||||
index += 1;
|
||||
}
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ describe("executable dispatcher", () => {
|
|||
"export const validateCliOutputArguments = () => ({ ok: true }); export const renderCliOutputArgumentError = () => ''; export const renderEmptyFilteredCliOutput = () => undefined;\n",
|
||||
),
|
||||
workspace.write(
|
||||
"dist/cli-runtime/streamedJsonOutput.js",
|
||||
"export const createStreamedCliJsonOutput = () => undefined;\n",
|
||||
"dist/cli-runtime/cliResultOutput.js",
|
||||
"export const createCliResultOutput = () => undefined;\n",
|
||||
),
|
||||
workspace.write(
|
||||
"dist/cli-runtime/mcpDoctor.js",
|
||||
|
|
|
|||
|
|
@ -2,15 +2,19 @@ import { constants } from "node:buffer";
|
|||
import { createHash } from "node:crypto";
|
||||
import { Writable } from "node:stream";
|
||||
|
||||
import { Cli } from "incur";
|
||||
import { Cli, Formatter } from "incur";
|
||||
import { estimateTokenCount } from "tokenx";
|
||||
import { parse as parseYaml } from "yaml";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { writeJsonOutput } from "../../../src/cli/jsonOutput.js";
|
||||
import {
|
||||
createStreamedCliJsonOutput,
|
||||
createCliResultOutput,
|
||||
streamCliCommandResults,
|
||||
} from "../../../src/cli/streamedJsonOutput.js";
|
||||
} from "../../../src/cli/cliResultOutput.js";
|
||||
import { logCliCommand } from "../../../src/cliLogging.js";
|
||||
import { registerManagedCommands } from "../../../src/cli/managedCommands.js";
|
||||
import { analysisErrorProjectionSchema } from "../../../src/contracts/errorSchemas.js";
|
||||
import { silentLogger } from "../../../src/logger.js";
|
||||
|
||||
const collect = async (
|
||||
|
|
@ -157,16 +161,283 @@ describe("incremental CLI JSON", () => {
|
|||
);
|
||||
});
|
||||
|
||||
describe("streamed CLI result surface", () => {
|
||||
describe("CLI result output surface", () => {
|
||||
it.each(["json", "yaml", "toon", "md"] as const)(
|
||||
"places typed analysis failures in the full-output error envelope (%s)",
|
||||
async (format) => {
|
||||
const chunks: Buffer[] = [];
|
||||
const destination = new Writable({
|
||||
write(chunk: Buffer, _encoding, callback) {
|
||||
chunks.push(chunk);
|
||||
callback();
|
||||
},
|
||||
});
|
||||
const failure = {
|
||||
error: "Analysis failed",
|
||||
code: "target_unavailable",
|
||||
category: "unavailable",
|
||||
message: "The selected artifact is missing.",
|
||||
retryable: false,
|
||||
remediation: { action: "Check that the path exists." },
|
||||
details: { source: "artifact-open" },
|
||||
input_path: "missing.dll",
|
||||
input_reason: "read-failed",
|
||||
};
|
||||
const argv = [
|
||||
"inspect-managed-artifact",
|
||||
"missing.dll",
|
||||
"--format",
|
||||
format,
|
||||
"--full-output",
|
||||
"--filter-output",
|
||||
"message",
|
||||
];
|
||||
const output = createCliResultOutput(argv, destination);
|
||||
if (output === undefined)
|
||||
throw new Error("Missing structured output surface");
|
||||
const cli = Cli.create("rea-output-test", { sync: false });
|
||||
registerManagedCommands(cli, silentLogger, async () => failure);
|
||||
streamCliCommandResults(cli, output);
|
||||
let fallback = "";
|
||||
const previousExitCode = process.exitCode;
|
||||
try {
|
||||
await cli.serve(argv, {
|
||||
stdout: (text) => {
|
||||
if (!output.handled) fallback += text;
|
||||
},
|
||||
});
|
||||
expect(process.exitCode).toBe(1);
|
||||
} finally {
|
||||
process.exitCode = previousExitCode;
|
||||
}
|
||||
|
||||
const text = Buffer.concat(chunks).toString("utf8");
|
||||
expect(output.handled).toBe(true);
|
||||
expect(fallback).toBe("");
|
||||
if (format === "json" || format === "yaml") {
|
||||
const document = format === "json" ? JSON.parse(text) : parseYaml(text);
|
||||
expect(document).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
code: failure.code,
|
||||
category: failure.category,
|
||||
message: failure.message,
|
||||
retryable: false,
|
||||
remediation: failure.remediation,
|
||||
details: {
|
||||
...failure.details,
|
||||
input_path: failure.input_path,
|
||||
input_reason: failure.input_reason,
|
||||
},
|
||||
},
|
||||
meta: { command: "inspect-managed-artifact" },
|
||||
});
|
||||
expect(() =>
|
||||
analysisErrorProjectionSchema.parse(document.error),
|
||||
).not.toThrow();
|
||||
expect(document).not.toHaveProperty("data");
|
||||
} else {
|
||||
expect(text).toContain("false");
|
||||
expect(text).toContain(failure.code);
|
||||
expect(text).not.toContain("true");
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "custom command failure",
|
||||
value: {
|
||||
status: "failed",
|
||||
message: "Update was not applied.",
|
||||
remediation: "Check the package manager, then retry.",
|
||||
details: { stage: "install" },
|
||||
},
|
||||
isFailure: () => true,
|
||||
},
|
||||
{
|
||||
name: "unhealthy doctor result",
|
||||
value: { healthy: false, checks: [{ name: "provider", ok: false }] },
|
||||
isFailure: undefined,
|
||||
},
|
||||
])("preserves $name as a full-output error payload", async (scenario) => {
|
||||
const chunks: Buffer[] = [];
|
||||
const destination = new Writable({
|
||||
write(chunk: Buffer, _encoding, callback) {
|
||||
chunks.push(chunk);
|
||||
callback();
|
||||
},
|
||||
});
|
||||
const argv = ["fixture", "--json", "--full-output"];
|
||||
const output = createCliResultOutput(argv, destination);
|
||||
if (output === undefined) throw new Error("Missing JSON output surface");
|
||||
const cli = Cli.create("rea-output-test", { sync: false });
|
||||
cli.command("fixture", {
|
||||
run: () =>
|
||||
logCliCommand(
|
||||
silentLogger,
|
||||
"fixture",
|
||||
async () => scenario.value,
|
||||
scenario.isFailure,
|
||||
),
|
||||
});
|
||||
streamCliCommandResults(cli, output);
|
||||
const previousExitCode = process.exitCode;
|
||||
try {
|
||||
await cli.serve(argv, { stdout: () => undefined });
|
||||
expect(process.exitCode).toBe(1);
|
||||
} finally {
|
||||
process.exitCode = previousExitCode;
|
||||
}
|
||||
expect(JSON.parse(Buffer.concat(chunks).toString("utf8"))).toMatchObject({
|
||||
ok: false,
|
||||
error: scenario.value,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("full-output token windows", () => {
|
||||
it("preserves Incur token-window metadata when truncating a TOON failure", async () => {
|
||||
const chunks: Buffer[] = [];
|
||||
const destination = new Writable({
|
||||
write(chunk: Buffer, _encoding, callback) {
|
||||
chunks.push(chunk);
|
||||
callback();
|
||||
},
|
||||
});
|
||||
const argv = [
|
||||
"inspect-managed-artifact",
|
||||
"missing.dll",
|
||||
"--format",
|
||||
"toon",
|
||||
"--full-output",
|
||||
"--token-limit",
|
||||
"3",
|
||||
];
|
||||
const output = createCliResultOutput(argv, destination);
|
||||
if (output === undefined) throw new Error("Missing TOON output surface");
|
||||
const cli = Cli.create("rea-output-test", { sync: false });
|
||||
registerManagedCommands(cli, silentLogger, async () => ({
|
||||
code: "target_unavailable",
|
||||
category: "unavailable",
|
||||
message: "The selected artifact is missing.",
|
||||
retryable: false,
|
||||
remediation: { action: "Check that the path exists." },
|
||||
details: { source: "artifact-open" },
|
||||
}));
|
||||
streamCliCommandResults(cli, output);
|
||||
const previousExitCode = process.exitCode;
|
||||
try {
|
||||
await cli.serve(argv, { stdout: () => undefined });
|
||||
expect(process.exitCode).toBe(1);
|
||||
} finally {
|
||||
process.exitCode = previousExitCode;
|
||||
}
|
||||
const text = Buffer.concat(chunks).toString("utf8");
|
||||
expect(text).toContain("[truncated: showing tokens 0–3 of");
|
||||
expect(text).toMatch(/nextOffset:?\s*3/);
|
||||
expect(text).toContain("ok: false");
|
||||
});
|
||||
});
|
||||
|
||||
describe("logger classification handoff", () => {
|
||||
it("honors an explicit success for data containing error-shaped fields", async () => {
|
||||
const chunks: Buffer[] = [];
|
||||
const destination = new Writable({
|
||||
write(chunk: Buffer, _encoding, callback) {
|
||||
chunks.push(chunk);
|
||||
callback();
|
||||
},
|
||||
});
|
||||
const result = {
|
||||
error: {
|
||||
code: "target_unavailable",
|
||||
category: "unavailable",
|
||||
message: "Source data describes a missing target.",
|
||||
retryable: false,
|
||||
remediation: { action: "Use the value as source data." },
|
||||
},
|
||||
source: "captured-metadata",
|
||||
};
|
||||
const argv = ["fixture", "--json", "--full-output"];
|
||||
const output = createCliResultOutput(argv, destination);
|
||||
if (output === undefined) throw new Error("Missing JSON output surface");
|
||||
const cli = Cli.create("rea-output-test", { sync: false });
|
||||
cli.command("fixture", {
|
||||
run: () =>
|
||||
logCliCommand(
|
||||
silentLogger,
|
||||
"fixture",
|
||||
async () => result,
|
||||
() => false,
|
||||
),
|
||||
});
|
||||
streamCliCommandResults(cli, output);
|
||||
const previousExitCode = process.exitCode;
|
||||
try {
|
||||
await cli.serve(argv, { stdout: () => undefined });
|
||||
expect(process.exitCode).toBe(previousExitCode);
|
||||
} finally {
|
||||
process.exitCode = previousExitCode;
|
||||
}
|
||||
expect(JSON.parse(Buffer.concat(chunks).toString("utf8"))).toMatchObject({
|
||||
ok: true,
|
||||
data: result,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("full-output token counts", () => {
|
||||
it("counts the full failure payload before applying output filters", async () => {
|
||||
const chunks: Buffer[] = [];
|
||||
const destination = new Writable({
|
||||
write(chunk: Buffer, _encoding, callback) {
|
||||
chunks.push(chunk);
|
||||
callback();
|
||||
},
|
||||
});
|
||||
const failure = {
|
||||
code: "target_unavailable",
|
||||
category: "unavailable",
|
||||
message: "The selected artifact is missing.",
|
||||
retryable: false,
|
||||
remediation: { action: "Check that the path exists." },
|
||||
details: { source: "artifact-open" },
|
||||
};
|
||||
const argv = [
|
||||
"inspect-managed-artifact",
|
||||
"missing.dll",
|
||||
"--json",
|
||||
"--full-output",
|
||||
"--token-count",
|
||||
"--filter-output",
|
||||
"message",
|
||||
];
|
||||
const output = createCliResultOutput(argv, destination);
|
||||
if (output === undefined) throw new Error("Missing JSON output surface");
|
||||
const cli = Cli.create("rea-output-test", { sync: false });
|
||||
registerManagedCommands(cli, silentLogger, async () => failure);
|
||||
streamCliCommandResults(cli, output);
|
||||
const previousExitCode = process.exitCode;
|
||||
try {
|
||||
await cli.serve(argv, { stdout: () => undefined });
|
||||
expect(process.exitCode).toBe(1);
|
||||
} finally {
|
||||
process.exitCode = previousExitCode;
|
||||
}
|
||||
expect(output.handled).toBe(true);
|
||||
expect(Buffer.concat(chunks).toString("utf8")).toBe(
|
||||
`${estimateTokenCount(Formatter.format(failure, "json"))}\n`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("CLI result output surface", () => {
|
||||
it.each([
|
||||
[],
|
||||
["--format", "md"],
|
||||
["--json", "--token-limit", "5"],
|
||||
["--json", "--format", "invalid"],
|
||||
])("delegates unsupported output controls %j", (...arguments_) => {
|
||||
expect(createStreamedCliJsonOutput(arguments_, new Writable())).toBe(
|
||||
undefined,
|
||||
);
|
||||
expect(createCliResultOutput(arguments_, new Writable())).toBe(undefined);
|
||||
});
|
||||
|
||||
it("keeps ownership after a failed write so no second document is emitted", async () => {
|
||||
|
|
@ -176,7 +447,7 @@ describe("streamed CLI result surface", () => {
|
|||
callback(cause);
|
||||
},
|
||||
});
|
||||
const output = createStreamedCliJsonOutput(["--json"], destination);
|
||||
const output = createCliResultOutput(["--json"], destination);
|
||||
if (output === undefined) throw new Error("Missing JSON output surface");
|
||||
await expect(
|
||||
output.write(
|
||||
|
|
@ -228,7 +499,7 @@ describe("streamed CLI result surface", () => {
|
|||
// A terminal destination exercises the indented encoding past the limit.
|
||||
Object.defineProperty(destination, "isTTY", { value: true });
|
||||
const argv = ["inspect-managed-members", "fixture.dll", "--json"];
|
||||
const output = createStreamedCliJsonOutput(argv, destination);
|
||||
const output = createCliResultOutput(argv, destination);
|
||||
if (output === undefined) throw new Error("Missing JSON output surface");
|
||||
streamCliCommandResults(cli, output);
|
||||
let exitCode = 0;
|
||||
|
|
@ -248,7 +519,7 @@ describe("streamed CLI result surface", () => {
|
|||
|
||||
const chunks: Buffer[] = [];
|
||||
const tokenArgv = [...argv, "--token-count", "--full-output"];
|
||||
const tokenOutput = createStreamedCliJsonOutput(
|
||||
const tokenOutput = createCliResultOutput(
|
||||
tokenArgv,
|
||||
new Writable({
|
||||
write(chunk: Buffer, _encoding, callback) {
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export const lazyUrl = import.meta.url;`,
|
|||
'export const generatedPackageMetadata = { version: "1.0.0" };',
|
||||
"dist/cli.js": 'export { facts, run } from "./main.js";',
|
||||
"dist/cliOutput.js": 'export { facts } from "./provider/probe λ.js";',
|
||||
"dist/cli/streamedJsonOutput.js":
|
||||
"dist/cli/cliResultOutput.js":
|
||||
'export { facts } from "../provider/probe λ.js";',
|
||||
"dist/mcpDoctor.js": 'export { run } from "./main.js";',
|
||||
"dist/main.js": `
|
||||
|
|
@ -98,7 +98,7 @@ export const run = async () => {
|
|||
[
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
`const { facts } = await import(${JSON.stringify(pathToFileURL(join(cliDirectory, "cli.js")).href)}); const output = await import(${JSON.stringify(pathToFileURL(join(cliDirectory, "cliOutput.js")).href)}); const streamed = await import(${JSON.stringify(pathToFileURL(join(cliDirectory, "streamedJsonOutput.js")).href)}); process.stdout.write(JSON.stringify(facts === output.facts && facts === streamed.facts));`,
|
||||
`const { facts } = await import(${JSON.stringify(pathToFileURL(join(cliDirectory, "cli.js")).href)}); const output = await import(${JSON.stringify(pathToFileURL(join(cliDirectory, "cliOutput.js")).href)}); const streamed = await import(${JSON.stringify(pathToFileURL(join(cliDirectory, "cliResultOutput.js")).href)}); process.stdout.write(JSON.stringify(facts === output.facts && facts === streamed.facts));`,
|
||||
],
|
||||
{ cwd: workspace.root },
|
||||
);
|
||||
|
|
@ -136,7 +136,7 @@ export const run = async () => {
|
|||
await workspace.write("package.json", '{"type":"module"}');
|
||||
for (const path of [
|
||||
"dist/cliOutput.js",
|
||||
"dist/cli/streamedJsonOutput.js",
|
||||
"dist/cli/cliResultOutput.js",
|
||||
"dist/mcpDoctor.js",
|
||||
])
|
||||
await workspace.write(path, "export {};\n");
|
||||
|
|
|
|||
Loading…
Reference in New Issue