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:
commit
00eb03d0f2
|
|
@ -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);
|
||||
|
|
@ -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,
|
||||
),
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
@ -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("");
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Loading…
Reference in New Issue