Merge pull request #735 from kaoru0822-kitauji/fix/reference-import-cli-failure-status

fix(cli): report reference-source import failures
This commit is contained in:
Kaoru0822 2026-10-06 05:09:11 -07:00 committed by GitHub
commit 00eb03d0f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 242 additions and 19 deletions

View File

@ -0,0 +1,14 @@
import { z } from "zod";
import { isCliOperationFailure } from "../cliLogging.js";
const referenceSourceImportFailureSchema = z.strictObject({
error: z.literal("Import failed"),
category: z.enum(["cancelled", "invalid_input", "execution_failure"]),
message: z.string().min(1),
});
/** Identify the historical-source import command's failed operation outputs. */
export const isReferenceSourceImportCliFailure = (value: unknown): boolean =>
referenceSourceImportFailureSchema.safeParse(value).success ||
isCliOperationFailure(value);

View File

@ -14,6 +14,7 @@ import { PRODUCT_IDENTITY } from "../identity.js";
import { logCliCommand } from "../cliLogging.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import type { Logger } from "../logger.js";
import { isReferenceSourceImportCliFailure } from "./referenceSourceImportStatus.js";
import type { CliInstance } from "./types.js";
export const registerUtilityCommands = (
@ -181,26 +182,31 @@ const registerReferenceSourceCommand = (
.describe("Local source directory to import as historical reference"),
}),
run: ({ args }) =>
logCliCommand(logger, "import-reference-source", async () => {
const config = parseConfig(environment);
if (!config.ok)
return {
error: "Import failed",
...projectAnalysisError(config.error),
};
const imported = await importReferenceSource({
root: args.root,
caller: "rea-cli",
policy: config.value.referenceSourcePolicy,
importer: PRODUCT_IDENTITY.packageName,
importerVersion: null,
});
return imported.ok
? imported.value
: {
logCliCommand(
logger,
"import-reference-source",
async () => {
const config = parseConfig(environment);
if (!config.ok)
return {
error: "Import failed",
...projectReferenceSourceImportError(imported.error),
...projectAnalysisError(config.error),
};
}),
const imported = await importReferenceSource({
root: args.root,
caller: "rea-cli",
policy: config.value.referenceSourcePolicy,
importer: PRODUCT_IDENTITY.packageName,
importerVersion: null,
});
return imported.ok
? imported.value
: {
error: "Import failed",
...projectReferenceSourceImportError(imported.error),
};
},
isReferenceSourceImportCliFailure,
),
});
};

View File

@ -0,0 +1,125 @@
import pino from "pino";
import { describe, expect, it } from "vitest";
import {
projectReferenceSourceImportError,
type ReferenceSourceImportError,
} from "./application/ReferenceSourceImportTypes.js";
import { isReferenceSourceImportCliFailure } from "./cli/referenceSourceImportStatus.js";
import { isCliOperationFailure, logCliCommand } from "./cliLogging.js";
import { ConfigurationError } from "./domain/configurationErrors.js";
import { projectAnalysisError } from "./domain/analysisErrorProjection.js";
const importFailure = (code: ReferenceSourceImportError["code"]) => ({
error: "Import failed",
...projectReferenceSourceImportError({
tag: "reference-source-import",
code,
message: "Internal import diagnostic",
}),
});
const configurationFailure = {
error: "Import failed",
...projectAnalysisError(new ConfigurationError("Invalid import setting")),
};
const invalidRootFailure = importFailure("invalid-root");
const successfulValues: readonly (readonly [string, unknown])[] = [
["undefined", undefined],
["null", null],
["array", [invalidRootFailure]],
["string", "Import failed"],
["missing error label", { category: "invalid_input", message: "failure" }],
["missing category", { error: "Import failed", message: "failure" }],
["missing message", { error: "Import failed", category: "invalid_input" }],
["empty message", { ...invalidRootFailure, message: "" }],
["non-string message", { ...invalidRootFailure, message: 1 }],
["null message", { ...invalidRootFailure, message: null }],
["empty category", { ...invalidRootFailure, category: "" }],
["non-string category", { ...invalidRootFailure, category: 1 }],
["near category", { ...invalidRootFailure, category: "invalid-root" }],
["foreign category", { ...invalidRootFailure, category: "timeout" }],
["near error label", { ...invalidRootFailure, error: "Import failure" }],
["label suffix", { ...invalidRootFailure, error: "Import failed " }],
["empty error label", { ...invalidRootFailure, error: "" }],
["non-string error label", { ...invalidRootFailure, error: true }],
["extra field", { ...invalidRootFailure, source: "ordinary data" }],
["extra undefined field", { ...invalidRootFailure, source: undefined }],
["nested legacy error", { error: invalidRootFailure }],
["ordinary error-like data", { error: "source error", message: "data" }],
[
"ordinary code and message",
{ code: "configuration_invalid", message: "data" },
],
["nested evidence", { result: invalidRootFailure }],
["partial result", { status: "partial" }],
["unsupported result", { status: "unsupported" }],
["successful result", { status: "complete", entries: [] }],
];
describe("reference-source import CLI failure classification", () => {
it.each(["cancelled", "invalid-root", "io", "parse"] as const)(
"recognizes the projected %s failure only at this command boundary",
(code) => {
const value = importFailure(code);
expect(isCliOperationFailure(value)).toBe(false);
expect(isReferenceSourceImportCliFailure(value)).toBe(true);
},
);
it("retains canonical configuration failure classification", () => {
expect(isReferenceSourceImportCliFailure(configurationFailure)).toBe(true);
});
it.each(successfulValues)("keeps %s successful", (_label, value) => {
expect(isReferenceSourceImportCliFailure(value)).toBe(false);
});
});
describe("reference-source import CLI logging seam", () => {
it.each([
["cancelled", importFailure("cancelled"), true],
["invalid-root", invalidRootFailure, true],
["io", importFailure("io"), true],
["parse", importFailure("parse"), true],
["configuration", configurationFailure, true],
["success", { status: "complete", entries: [] }, false],
] as const)(
"preserves %s output and logs its status",
async (_label, value, failed) => {
const records: unknown[] = [];
const logger = pino(
{ level: "info" },
{
write: (message) => {
records.push(JSON.parse(message) as unknown);
},
},
);
const previousExitCode = process.exitCode;
try {
process.exitCode = undefined;
const output = await logCliCommand(
logger,
"import-reference-source",
async () => value,
isReferenceSourceImportCliFailure,
);
expect(output).toBe(value);
expect.soft(process.exitCode).toBe(failed ? 1 : undefined);
expect(records).toEqual([
expect.objectContaining({
command: "import-reference-source",
status: failed ? "error" : "ok",
level: failed ? 50 : 30,
msg: failed ? "CLI command failed" : "CLI command completed",
}),
]);
} finally {
process.exitCode = previousExitCode;
}
},
);
});

View File

@ -0,0 +1,78 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
const invalidRootOutput = {
error: "Import failed",
category: "invalid_input",
message:
"Reference source directory could not be opened. Check that the path exists, is readable, and points to a directory.",
};
describe("compiled reference-source import preflight failures", () => {
for (const rootKind of ["missing", "regular-file"] as const) {
for (const fullOutput of [false, true]) {
for (const logging of [false, true]) {
cliTest(
`${rootKind} root, ${fullOutput ? "full-output" : "JSON"}, logging ${String(logging)}`,
async ({ cli }) => {
const directory = await createTestTempDirectory(
"rea-reference-cli-status-",
);
const root = join(directory, "input");
if (rootKind === "regular-file")
await writeFile(root, "inert input\n");
const result = await cli.run({
arguments: [
"import-reference-source",
root,
"--json",
...(fullOutput ? ["--full-output"] : []),
],
cwd: directory,
environment: {
HOME: directory,
USERPROFILE: directory,
XDG_CONFIG_HOME: directory,
XDG_CACHE_HOME: directory,
...(logging ? { REA_LOG_LEVEL: "info" } : {}),
},
});
if (fullOutput) {
// Incur's legacy wrapper remains unchanged; operation status is
// conveyed by the exit code and command log, not this `ok` field.
expect(result.json).toMatchObject({
ok: true,
data: invalidRootOutput,
});
expect(result.json).toHaveProperty("data", invalidRootOutput);
} else {
expect(result.json).toEqual(invalidRootOutput);
}
expect.soft(result.exitCode).toBe(1);
if (logging) {
const records: unknown[] = result.stderr
.trim()
.split("\n")
.map((line) => JSON.parse(line) as unknown);
expect(records).toEqual([
expect.objectContaining({
command: "import-reference-source",
status: "error",
level: 50,
msg: "CLI command failed",
}),
]);
} else {
expect(result.stderr).toBe("");
}
},
);
}
}
}
});