

























1+import { describe, expect, it, vi } from "vitest";
2+import { CronService } from "./service.js";
3+import {
4+createCronStoreHarness,
5+createNoopLogger,
6+installCronTestHooks,
7+writeCronStoreSnapshot,
8+} from "./service.test-harness.js";
9+import { loadCronJobsStore } from "./store.js";
10+import type { CronJob } from "./types.js";
11+12+const noopLogger = createNoopLogger();
13+const { makeStorePath } = createCronStoreHarness();
14+installCronTestHooks({ logger: noopLogger });
15+16+const base = Date.parse("2025-12-13T00:00:00.000Z");
17+18+function dueEveryJob(): CronJob {
19+return {
20+id: "due-every",
21+name: "due every 10s",
22+enabled: true,
23+createdAtMs: base - 3_600_000,
24+updatedAtMs: base - 10_000,
25+schedule: { kind: "every", everyMs: 10_000 },
26+sessionTarget: "isolated",
27+wakeMode: "next-heartbeat",
28+payload: { kind: "agentTurn", message: "tick" },
29+delivery: { mode: "none" },
30+state: { nextRunAtMs: base - 5_000 },
31+};
32+}
33+34+function missingNextRunJob(): CronJob {
35+return {
36+id: "missing-next",
37+name: "enabled daily without next run",
38+enabled: true,
39+createdAtMs: base - 3_600_000,
40+updatedAtMs: base - 10_000,
41+schedule: { kind: "cron", expr: "0 9 * * *", tz: "UTC" },
42+sessionTarget: "isolated",
43+wakeMode: "next-heartbeat",
44+payload: { kind: "agentTurn", message: "daily" },
45+delivery: { mode: "none" },
46+state: {},
47+};
48+}
49+50+function jobToRemove(): CronJob {
51+return {
52+id: "to-remove",
53+name: "obsolete job",
54+enabled: true,
55+createdAtMs: base - 3_600_000,
56+updatedAtMs: base - 10_000,
57+schedule: { kind: "cron", expr: "0 12 * * *", tz: "UTC" },
58+sessionTarget: "isolated",
59+wakeMode: "next-heartbeat",
60+payload: { kind: "agentTurn", message: "noop" },
61+delivery: { mode: "none" },
62+state: { nextRunAtMs: base + 3_600_000 },
63+};
64+}
65+66+describe("remove() must not drop a due every-job's pending run", () => {
67+it("preserves a due sibling while backfilling another enabled sibling on cold-store remove", async () => {
68+const store = await makeStorePath();
69+await writeCronStoreSnapshot({
70+storePath: store.storePath,
71+jobs: [dueEveryJob(), missingNextRunJob(), jobToRemove()],
72+});
73+74+const cron = new CronService({
75+storePath: store.storePath,
76+cronEnabled: true,
77+log: noopLogger,
78+enqueueSystemEvent: vi.fn(),
79+requestHeartbeat: vi.fn(),
80+runIsolatedAgentJob: vi.fn(async () => ({ status: "ok" as const })),
81+});
82+83+const result = await cron.remove("to-remove");
84+expect(result).toEqual({ ok: true, removed: true });
85+86+const persisted = await loadCronJobsStore(store.storePath);
87+const byId = new Map(persisted.jobs.map((job) => [job.id, job]));
88+89+expect(byId.has("to-remove")).toBe(false);
90+expect(byId.get("due-every")?.state.nextRunAtMs).toBe(base - 5_000);
91+expect(byId.get("missing-next")?.state.nextRunAtMs).toBeGreaterThan(base);
92+93+cron.stop();
94+});
95+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。