mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-18 11:37:26 +00:00
test(perf): merge duplicate one-shot retry regression paths
This commit is contained in:
@@ -724,50 +724,76 @@ describe("Cron issue regressions", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("#24355: one-shot job retries on transient error, then succeeds", async () => {
|
||||
const store = await makeStorePath();
|
||||
it("#24355: one-shot retries then succeeds (with and without deleteAfterRun)", async () => {
|
||||
const scheduledAt = Date.parse("2026-02-06T10:00:00.000Z");
|
||||
|
||||
const cronJob = createIsolatedRegressionJob({
|
||||
const runRetryScenario = async (params: {
|
||||
id: string;
|
||||
deleteAfterRun: boolean;
|
||||
}): Promise<{
|
||||
state: ReturnType<typeof createCronServiceState>;
|
||||
runIsolatedAgentJob: ReturnType<typeof vi.fn>;
|
||||
firstRetryAtMs: number;
|
||||
}> => {
|
||||
const store = await makeStorePath();
|
||||
const cronJob = createIsolatedRegressionJob({
|
||||
id: params.id,
|
||||
name: "reminder",
|
||||
scheduledAt,
|
||||
schedule: { kind: "at", at: new Date(scheduledAt).toISOString() },
|
||||
payload: { kind: "agentTurn", message: "remind me" },
|
||||
state: { nextRunAtMs: scheduledAt },
|
||||
});
|
||||
cronJob.deleteAfterRun = params.deleteAfterRun;
|
||||
await writeCronJobs(store.storePath, [cronJob]);
|
||||
|
||||
let now = scheduledAt;
|
||||
const runIsolatedAgentJob = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ status: "error", error: "429 rate limit exceeded" })
|
||||
.mockResolvedValueOnce({ status: "ok", summary: "done" });
|
||||
const state = createCronServiceState({
|
||||
cronEnabled: true,
|
||||
storePath: store.storePath,
|
||||
log: noopLogger,
|
||||
nowMs: () => now,
|
||||
enqueueSystemEvent: vi.fn(),
|
||||
requestHeartbeatNow: vi.fn(),
|
||||
runIsolatedAgentJob,
|
||||
});
|
||||
|
||||
await onTimer(state);
|
||||
const jobAfterRetry = state.store?.jobs.find((j) => j.id === params.id);
|
||||
expect(jobAfterRetry).toBeDefined();
|
||||
expect(jobAfterRetry!.enabled).toBe(true);
|
||||
expect(jobAfterRetry!.state.lastStatus).toBe("error");
|
||||
expect(jobAfterRetry!.state.nextRunAtMs).toBeDefined();
|
||||
expect(jobAfterRetry!.state.nextRunAtMs).toBeGreaterThan(scheduledAt);
|
||||
|
||||
const firstRetryAtMs = (jobAfterRetry!.state.nextRunAtMs ?? 0) + 1;
|
||||
now = firstRetryAtMs;
|
||||
await onTimer(state);
|
||||
return { state, runIsolatedAgentJob, firstRetryAtMs };
|
||||
};
|
||||
|
||||
const keepResult = await runRetryScenario({
|
||||
id: "oneshot-retry",
|
||||
name: "reminder",
|
||||
scheduledAt,
|
||||
schedule: { kind: "at", at: new Date(scheduledAt).toISOString() },
|
||||
payload: { kind: "agentTurn", message: "remind me" },
|
||||
state: { nextRunAtMs: scheduledAt },
|
||||
deleteAfterRun: false,
|
||||
});
|
||||
cronJob.deleteAfterRun = false;
|
||||
await writeCronJobs(store.storePath, [cronJob]);
|
||||
const keepJob = keepResult.state.store?.jobs.find((j) => j.id === "oneshot-retry");
|
||||
expect(keepJob).toBeDefined();
|
||||
expect(keepJob!.state.lastStatus).toBe("ok");
|
||||
expect(keepResult.runIsolatedAgentJob).toHaveBeenCalledTimes(2);
|
||||
|
||||
let now = scheduledAt;
|
||||
const runIsolatedAgentJob = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ status: "error", error: "429 rate limit exceeded" })
|
||||
.mockResolvedValueOnce({ status: "ok", summary: "done" });
|
||||
const state = createCronServiceState({
|
||||
cronEnabled: true,
|
||||
storePath: store.storePath,
|
||||
log: noopLogger,
|
||||
nowMs: () => now,
|
||||
enqueueSystemEvent: vi.fn(),
|
||||
requestHeartbeatNow: vi.fn(),
|
||||
runIsolatedAgentJob,
|
||||
const deleteResult = await runRetryScenario({
|
||||
id: "oneshot-deleteAfterRun-retry",
|
||||
deleteAfterRun: true,
|
||||
});
|
||||
|
||||
await onTimer(state);
|
||||
let job = state.store?.jobs.find((j) => j.id === "oneshot-retry");
|
||||
expect(job).toBeDefined();
|
||||
expect(job!.enabled).toBe(true);
|
||||
expect(job!.state.lastStatus).toBe("error");
|
||||
expect(job!.state.nextRunAtMs).toBeDefined();
|
||||
expect(job!.state.nextRunAtMs).toBeGreaterThan(scheduledAt);
|
||||
|
||||
now = (job!.state.nextRunAtMs ?? 0) + 1;
|
||||
await onTimer(state);
|
||||
job = state.store?.jobs.find((j) => j.id === "oneshot-retry");
|
||||
expect(job).toBeDefined();
|
||||
expect(job!.state.lastStatus).toBe("ok");
|
||||
expect(runIsolatedAgentJob).toHaveBeenCalledTimes(2);
|
||||
const deletedJob = deleteResult.state.store?.jobs.find(
|
||||
(j) => j.id === "oneshot-deleteAfterRun-retry",
|
||||
);
|
||||
expect(deletedJob).toBeUndefined();
|
||||
expect(deleteResult.runIsolatedAgentJob).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("#24355: one-shot job disabled after max transient retries", async () => {
|
||||
@@ -896,52 +922,6 @@ describe("Cron issue regressions", () => {
|
||||
expect(job!.state.nextRunAtMs).toBeUndefined();
|
||||
});
|
||||
|
||||
it("#24355: deleteAfterRun:true one-shot job is deleted after successful retry", async () => {
|
||||
const store = await makeStorePath();
|
||||
const scheduledAt = Date.parse("2026-02-06T10:00:00.000Z");
|
||||
|
||||
const cronJob = createIsolatedRegressionJob({
|
||||
id: "oneshot-deleteAfterRun-retry",
|
||||
name: "reminder",
|
||||
scheduledAt,
|
||||
schedule: { kind: "at", at: new Date(scheduledAt).toISOString() },
|
||||
payload: { kind: "agentTurn", message: "remind me" },
|
||||
state: { nextRunAtMs: scheduledAt },
|
||||
});
|
||||
cronJob.deleteAfterRun = true;
|
||||
await writeCronJobs(store.storePath, [cronJob]);
|
||||
|
||||
let now = scheduledAt;
|
||||
const runIsolatedAgentJob = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ status: "error", error: "429 rate limit exceeded" })
|
||||
.mockResolvedValueOnce({ status: "ok", summary: "done" });
|
||||
const state = createCronServiceState({
|
||||
cronEnabled: true,
|
||||
storePath: store.storePath,
|
||||
log: noopLogger,
|
||||
nowMs: () => now,
|
||||
enqueueSystemEvent: vi.fn(),
|
||||
requestHeartbeatNow: vi.fn(),
|
||||
runIsolatedAgentJob,
|
||||
});
|
||||
|
||||
// First run: transient error → retry scheduled, job still in store.
|
||||
await onTimer(state);
|
||||
let job = state.store?.jobs.find((j) => j.id === "oneshot-deleteAfterRun-retry");
|
||||
expect(job).toBeDefined();
|
||||
expect(job!.enabled).toBe(true);
|
||||
expect(job!.state.lastStatus).toBe("error");
|
||||
expect(job!.state.nextRunAtMs).toBeGreaterThan(scheduledAt);
|
||||
|
||||
// Second run: success → deleteAfterRun removes the job from the store.
|
||||
now = (job!.state.nextRunAtMs ?? 0) + 1;
|
||||
await onTimer(state);
|
||||
const deleted = state.store?.jobs.find((j) => j.id === "oneshot-deleteAfterRun-retry");
|
||||
expect(deleted).toBeUndefined();
|
||||
expect(runIsolatedAgentJob).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("prevents spin loop when cron job completes within the scheduled second (#17821)", async () => {
|
||||
const store = await makeStorePath();
|
||||
// Simulate a cron job "0 13 * * *" (daily 13:00 UTC) that fires exactly
|
||||
|
||||
Reference in New Issue
Block a user