fix(cron): surface classified run failure causes

Surface classified cron failure causes without changing raw cron JSON error text.

- add additive CLI `cause` output for finished run entries with `errorReason`
- persist/backfill full `FailoverReason` values on cron run-log entries
- thread provider context through cron finalization so provider-specific failure causes stay accurate
- extend protocol/Swift models and regression coverage for CLI JSON, run-log parsing/search, alerts, and protocol conformance

Verification:
- `pnpm lint --threads=8`
- `pnpm protocol:check`
- `pnpm exec oxfmt --check src/cli/cron-cli/shared.ts src/cli/cron-cli/shared.cause-display.test.ts src/cron/run-log.ts src/cron/run-log.error-reason.test.ts src/cron/cron-protocol-conformance.test.ts src/cron/service.failure-alert.test.ts src/cron/service/timer.ts src/cron/service/ops.ts src/gateway/protocol/schema/cron.ts scripts/protocol-gen-swift.ts`
- `git diff --check`
- AWS Crabbox `cbx_8a6a65ab83b0` / `run_42b73a4a9750`: 4 files, 20 tests passed
- autoreview clean, no accepted/actionable findings
- GitHub CI/CodeQL/OpenGrep/Workflow Sanity green/skipped/neutral on `aa29b087b2587d0aed3d409de5e7a2c706c32cdf`

Co-authored-by: Yoshikazu Terashi <yterashi@peperon-works.jp>
This commit is contained in:
Yoshikazu Terashi
2026-05-27 17:03:17 +09:00
committed by GitHub
parent 57b1c0b3d9
commit 3104f36329
11 changed files with 420 additions and 22 deletions

View File

@@ -0,0 +1,52 @@
import { describe, expect, it } from "vitest";
import { defaultRuntime } from "../../runtime.js";
import { printCronJson } from "./shared.js";
describe("printCronJson cause display", () => {
it("adds an additive cause without changing raw cron run errors", () => {
let written: unknown;
const original = defaultRuntime.writeJson;
defaultRuntime.writeJson = (value: unknown) => {
written = value;
};
try {
printCronJson({
entries: [
{
ts: 1,
jobId: "job-1",
action: "finished",
status: "error",
errorReason: "timeout",
error: "cron: job execution timed out",
},
],
});
} finally {
defaultRuntime.writeJson = original;
}
const result = written as { entries: Array<Record<string, unknown>> };
expect(result.entries[0]?.cause).toBe("timeout");
expect(result.entries[0]?.error).toBe("cron: job execution timed out");
expect(result.entries[0]?.errorReason).toBe("timeout");
});
it("does not add cause fields to non-run-log entries", () => {
let written: unknown;
const original = defaultRuntime.writeJson;
defaultRuntime.writeJson = (value: unknown) => {
written = value;
};
try {
printCronJson({
entries: [{ errorReason: "timeout", status: "error" }],
});
} finally {
defaultRuntime.writeJson = original;
}
const result = written as { entries: Array<Record<string, unknown>> };
expect(result.entries[0]?.cause).toBeUndefined();
});
});

View File

@@ -25,8 +25,31 @@ export const getCronChannelOptions = () => {
return pluginIds.length > 0 ? ["last", ...pluginIds].join("|") : "last|<channel-id>";
};
function addCronRunCauseFields(value: unknown): unknown {
if (!value || typeof value !== "object") {
return value;
}
const record = value as Record<string, unknown>;
const entries = record.entries;
if (!Array.isArray(entries)) {
return value;
}
const nextEntries = entries.map((entry) => {
if (!entry || typeof entry !== "object") {
return entry;
}
const item = entry as Record<string, unknown>;
if (item.action !== "finished" || typeof item.errorReason !== "string") {
return item;
}
const cause = item.errorReason.trim();
return cause ? Object.assign({}, item, { cause }) : item;
});
return { ...record, entries: nextEntries };
}
export function printCronJson(value: unknown) {
defaultRuntime.writeJson(value);
defaultRuntime.writeJson(addCronRunCauseFields(value));
}
/**