mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-21 18:08:05 -06:00
fix(cron): show skipped automation tasks as failed (#123787)
* fix(cron): report skipped runs as failed tasks * test(cron): align skipped task expectations * fix(cron): keep skipped tasks failed
This commit is contained in:
committed by
GitHub
parent
986be4558f
commit
80e046d30e
@@ -1450,9 +1450,7 @@ describe("cron service ops seam coverage", () => {
|
||||
expect(restored?.state.lastRunAtMs).toBe(startedAt);
|
||||
expect(restored?.state.lastRunStatus).toBe(status);
|
||||
expect(runIsolatedAgentJob).not.toHaveBeenCalled();
|
||||
expect(findTaskByRunId(taskRunId)?.status).toBe(
|
||||
status === "error" ? "failed" : "succeeded",
|
||||
);
|
||||
expect(findTaskByRunId(taskRunId)?.status).toBe(status === "ok" ? "succeeded" : "failed");
|
||||
} finally {
|
||||
stop(state);
|
||||
}
|
||||
|
||||
@@ -164,9 +164,21 @@ describe("cron task run terminal records", () => {
|
||||
tryFinishCronTaskRunWithoutHistory(state, {
|
||||
taskRunId: runIds[0],
|
||||
status: "skipped",
|
||||
error: "cron: job execution timed out",
|
||||
endedAt: 1_501,
|
||||
});
|
||||
expect(childSessionKey(systemEventJob)).toBeUndefined();
|
||||
expect(
|
||||
listTaskRegistryRecordsByRuntimeSourceIdFromSqlite({
|
||||
runtime: "cron",
|
||||
sourceId: systemEventJob.id,
|
||||
}),
|
||||
).toEqual([
|
||||
expect.objectContaining({
|
||||
status: "failed",
|
||||
error: "cron: job execution timed out",
|
||||
}),
|
||||
]);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -429,7 +441,7 @@ describe("cron task run terminal records", () => {
|
||||
action: "finished",
|
||||
job,
|
||||
status: "skipped",
|
||||
error: "trigger condition not met",
|
||||
error: "cron: job execution timed out",
|
||||
runId: "manual:skipped-job:1",
|
||||
runAtMs: startedAt,
|
||||
durationMs: 0,
|
||||
@@ -446,10 +458,10 @@ describe("cron task run terminal records", () => {
|
||||
runtime: "cron",
|
||||
sourceId: job.id,
|
||||
agentId: "finn",
|
||||
status: "succeeded",
|
||||
status: "failed",
|
||||
startedAt,
|
||||
endedAt: startedAt,
|
||||
error: "trigger condition not met",
|
||||
error: "cron: job execution timed out",
|
||||
detail: {
|
||||
kind: "cron-run",
|
||||
status: "skipped",
|
||||
|
||||
@@ -5,7 +5,6 @@ import type { DatabaseSync } from "node:sqlite";
|
||||
import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce";
|
||||
import { normalizeAgentId, resolveAgentIdFromSessionKey } from "../../routing/session-key.js";
|
||||
import { resolveCronJobEffectiveAgentId } from "../agent-id.js";
|
||||
import { isCronTimeoutErrorText } from "../execution-error-constants.js";
|
||||
|
||||
function requireCronAgentId(agentId: string | undefined): string {
|
||||
if (!agentId?.trim()) {
|
||||
@@ -321,7 +320,10 @@ export function tryFinishCronTaskRunWithoutHistory(
|
||||
if (!result.taskRunId) {
|
||||
return;
|
||||
}
|
||||
const error = result.status === "error" ? normalizeCronRunErrorText(result.error) : undefined;
|
||||
const error =
|
||||
result.status !== "ok" && result.error !== undefined
|
||||
? normalizeCronRunErrorText(result.error)
|
||||
: undefined;
|
||||
const quietTriggerEval =
|
||||
result.triggerEval?.fired === false
|
||||
? { ...result.triggerEval, fired: false as const }
|
||||
@@ -330,12 +332,7 @@ export function tryFinishCronTaskRunWithoutHistory(
|
||||
finalizeTaskRunByRunIdCore({
|
||||
runId: result.taskRunId,
|
||||
runtime: "cron",
|
||||
status:
|
||||
result.status === "ok" || result.status === "skipped"
|
||||
? "succeeded"
|
||||
: isCronTimeoutErrorText(error)
|
||||
? "timed_out"
|
||||
: "failed",
|
||||
status: cronRunStatusToTaskStatus({ status: result.status, error }),
|
||||
endedAt: result.endedAt,
|
||||
lastEventAt: result.endedAt,
|
||||
error,
|
||||
|
||||
@@ -2822,7 +2822,7 @@ describe("cron service timer regressions", () => {
|
||||
outcome: "skip",
|
||||
status: "skipped",
|
||||
error: "agent skipped after removal",
|
||||
taskStatus: "succeeded",
|
||||
taskStatus: "failed",
|
||||
},
|
||||
] as const)(
|
||||
"finalizes a removed job's $outcome outcome in operator history",
|
||||
|
||||
@@ -262,12 +262,12 @@ export function cronTaskRecordToScriptRunResult(
|
||||
|
||||
/** Maps the cron outcome vocabulary onto generic task terminal states. */
|
||||
export function cronRunStatusToTaskStatus(
|
||||
entry: CronRunLogEntry,
|
||||
entry: Pick<CronRunLogEntry, "status" | "error"> & Partial<CronRunLogEntry>,
|
||||
): Extract<TaskStatus, "succeeded" | "failed" | "timed_out"> {
|
||||
if (entry.status === "ok" || entry.status === "skipped") {
|
||||
if (entry.status === "ok") {
|
||||
return "succeeded";
|
||||
}
|
||||
return isCronTimeoutErrorText(entry.error) ? "timed_out" : "failed";
|
||||
return entry.status === "error" && isCronTimeoutErrorText(entry.error) ? "timed_out" : "failed";
|
||||
}
|
||||
|
||||
/** Reconstructs the unchanged CronRunLogEntry wire shape from a cron task row. */
|
||||
|
||||
@@ -1632,6 +1632,67 @@ describe("gateway server cron", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("reports skipped isolated cron runs as failed tasks", async () => {
|
||||
const { prevSkipCron } = await setupCronTestRun({
|
||||
tempPrefix: "openclaw-gw-cron-run-skipped-task-",
|
||||
cronEnabled: false,
|
||||
});
|
||||
cronIsolatedRun.mockResolvedValueOnce({
|
||||
status: "skipped",
|
||||
error: "model endpoint unavailable",
|
||||
});
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
|
||||
try {
|
||||
const addRes = await rpcReq(ws, "cron.add", {
|
||||
name: "skipped task projection",
|
||||
enabled: true,
|
||||
schedule: { kind: "every", everyMs: 60_000 },
|
||||
sessionTarget: "isolated",
|
||||
wakeMode: "next-heartbeat",
|
||||
payload: { kind: "agentTurn", message: "do work" },
|
||||
delivery: { mode: "none" },
|
||||
});
|
||||
const jobId = expectCronJobIdFromResponse(addRes);
|
||||
const finished = waitForCronEvent(
|
||||
ws,
|
||||
(payload) => payload?.jobId === jobId && payload?.action === "finished",
|
||||
);
|
||||
|
||||
await runCronJobForce(ws, jobId);
|
||||
expect(await finished).toMatchObject({
|
||||
jobId,
|
||||
status: "skipped",
|
||||
error: "model endpoint unavailable",
|
||||
});
|
||||
|
||||
const history = await rpcReq(ws, "cron.runs", { id: jobId, limit: 1 });
|
||||
expect(history.ok).toBe(true);
|
||||
expect(history.payload).toMatchObject({
|
||||
entries: [
|
||||
expect.objectContaining({
|
||||
jobId,
|
||||
status: "skipped",
|
||||
error: "model endpoint unavailable",
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const taskList = await rpcReq(ws, "tasks.list", {});
|
||||
expect(taskList.ok).toBe(true);
|
||||
const tasks = (taskList.payload as { tasks?: Array<Record<string, unknown>> } | undefined)
|
||||
?.tasks;
|
||||
expect(tasks?.find((task) => task.sourceId === jobId)).toMatchObject({
|
||||
runtime: "cron",
|
||||
status: "failed",
|
||||
error: "model endpoint unavailable",
|
||||
});
|
||||
} finally {
|
||||
await cleanupCronTestRun({ ws, server, prevSkipCron });
|
||||
}
|
||||
});
|
||||
|
||||
test("returns already-running without starting background work", async () => {
|
||||
const now = Date.now();
|
||||
let resolveRun: ((result: { status: "ok"; summary: string }) => void) | undefined;
|
||||
|
||||
Reference in New Issue
Block a user