Files
openclaw/src/cron/run-log.error-reason.test.ts
Yoshikazu Terashi 3104f36329 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>
2026-05-27 09:03:17 +01:00

126 lines
3.7 KiB
TypeScript

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { readCronRunLogEntriesPage } from "./run-log.js";
describe("cron run log errorReason", () => {
it("backfills errorReason from timeout error text for older entries", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "cron-run-log-"));
const file = path.join(dir, "job.jsonl");
await fs.writeFile(
file,
`${JSON.stringify({
ts: 1,
jobId: "job-1",
action: "finished",
status: "error",
error: "cron: job execution timed out",
})}\n`,
"utf8",
);
const page = await readCronRunLogEntriesPage(file, { limit: 10 });
expect(page.entries[0]?.errorReason).toBe("timeout");
});
it("validates persisted errorReason against the full failover reason set", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "cron-run-log-"));
const file = path.join(dir, "job.jsonl");
const reasons = [
"auth",
"auth_permanent",
"format",
"rate_limit",
"overloaded",
"billing",
"server_error",
"timeout",
"model_not_found",
"session_expired",
"empty_response",
"no_error_details",
"unclassified",
"unknown",
];
await fs.writeFile(
file,
reasons
.map((errorReason, index) =>
JSON.stringify({
ts: index + 1,
jobId: "job-1",
action: "finished",
status: "error",
errorReason,
}),
)
.join("\n") + "\n",
"utf8",
);
const page = await readCronRunLogEntriesPage(file, { limit: 50, sortDir: "asc" });
expect(page.entries.map((entry) => entry.errorReason)).toEqual(reasons);
});
it("derives an invalid persisted reason from raw error text before exposing entries", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "cron-run-log-"));
const file = path.join(dir, "job.jsonl");
await fs.writeFile(
file,
`${JSON.stringify({
ts: 1,
jobId: "job-1",
action: "finished",
status: "error",
error: "upstream unavailable: 503 overloaded",
errorReason: "not-a-real-reason",
})}\n`,
"utf8",
);
const page = await readCronRunLogEntriesPage(file, { limit: 10 });
expect(page.entries[0]?.errorReason).toBe("overloaded");
});
it("uses provider context when deriving persisted run-log reasons", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "cron-run-log-"));
const file = path.join(dir, "job.jsonl");
await fs.writeFile(
file,
`${JSON.stringify({
ts: 1,
jobId: "job-1",
action: "finished",
status: "error",
error: "403 Key limit exceeded (monthly limit)",
provider: "openrouter",
})}\n`,
"utf8",
);
const page = await readCronRunLogEntriesPage(file, { limit: 10 });
expect(page.entries[0]?.errorReason).toBe("billing");
});
it("includes derived errorReason values in run-log search", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "cron-run-log-"));
const file = path.join(dir, "job.jsonl");
await fs.writeFile(
file,
`${JSON.stringify({
ts: 1,
jobId: "job-1",
action: "finished",
status: "error",
error: "cron: job execution timed out",
})}\n`,
"utf8",
);
const page = await readCronRunLogEntriesPage(file, { limit: 10, query: "timeout" });
expect(page.entries).toHaveLength(1);
expect(page.entries[0]?.errorReason).toBe("timeout");
});
});