





















@@ -370,6 +370,125 @@ describe("tasks commands", () => {
370370});
371371});
372372373+it("preserves both cron-run session key shapes for a running non-slug job id", async () => {
374+await withTaskCommandStateDir(async (state) => {
375+const now = Date.now();
376+const old = now - 8 * 24 * 60 * 60_000;
377+await saveCronStore(state.statePath("cron", "jobs.json"), {
378+version: 1,
379+jobs: [
380+{
381+id: "Daily Report",
382+name: "Daily Report",
383+enabled: true,
384+schedule: { kind: "every", everyMs: 60_000 },
385+sessionTarget: "isolated",
386+sessionKey: "cron:daily-report",
387+wakeMode: "now",
388+payload: { kind: "agentTurn", message: "ping" },
389+delivery: { mode: "none" },
390+createdAtMs: now,
391+updatedAtMs: now,
392+state: { runningAtMs: now - 5_000 },
393+},
394+],
395+});
396+397+const sessionsDir = state.sessionsDir("main");
398+const storePath = path.join(sessionsDir, "sessions.json");
399+await fs.mkdir(sessionsDir, { recursive: true });
400+// A running job can be retargeted after its session is created, so maintenance must preserve
401+// both the raw and slugged historical shapes.
402+const slugKey = "agent:main:cron:daily-report:run:old-run";
403+const rawKey = "agent:main:cron:daily report:run:old-run";
404+const retiredKey = "agent:main:cron:retired-job:run:old-run";
405+await fs.writeFile(
406+storePath,
407+JSON.stringify(
408+{
409+[slugKey]: { sessionId: "slug-run", updatedAt: old },
410+[rawKey]: { sessionId: "raw-run", updatedAt: old },
411+[retiredKey]: { sessionId: "retired-run", updatedAt: old },
412+},
413+null,
414+2,
415+),
416+"utf8",
417+);
418+419+const runtime = createRuntime();
420+await tasksMaintenanceCommand({ json: true, apply: true }, runtime);
421+422+const payload = readFirstJsonLog(runtime) as {
423+maintenance: { sessions: { runningCronJobs: number } };
424+};
425+expect(payload.maintenance.sessions.runningCronJobs).toBe(1);
426+const updated = JSON.parse(await fs.readFile(storePath, "utf8")) as Record<string, unknown>;
427+expect(updated[slugKey]).toBeDefined();
428+expect(updated[rawKey]).toBeDefined();
429+expect(updated[retiredKey]).toBeUndefined();
430+});
431+});
432+433+it("preserves a running cron session with an explicit session key", async () => {
434+await withTaskCommandStateDir(async (state) => {
435+const now = Date.now();
436+const old = now - 8 * 24 * 60 * 60_000;
437+await saveCronStore(state.statePath("cron", "jobs.json"), {
438+version: 1,
439+jobs: [
440+{
441+id: "job-uuid",
442+name: "Daily monitor",
443+enabled: true,
444+schedule: { kind: "every", everyMs: 60_000 },
445+sessionTarget: "isolated",
446+sessionKey: "cron:daily-monitor",
447+wakeMode: "now",
448+payload: { kind: "agentTurn", message: "ping" },
449+delivery: { mode: "none" },
450+createdAtMs: now,
451+updatedAtMs: now,
452+state: { runningAtMs: now - 5_000 },
453+},
454+],
455+});
456+457+const sessionsDir = state.sessionsDir("main");
458+const storePath = path.join(sessionsDir, "sessions.json");
459+await fs.mkdir(sessionsDir, { recursive: true });
460+await fs.writeFile(
461+storePath,
462+JSON.stringify(
463+{
464+"agent:main:cron:daily-monitor:run:old-run": {
465+sessionId: "explicit-run",
466+updatedAt: old,
467+},
468+"agent:main:cron:job-uuid:run:old-run": {
469+sessionId: "job-id-run",
470+updatedAt: old,
471+},
472+"agent:main:cron:retired-job:run:old-run": {
473+sessionId: "retired-run",
474+updatedAt: old,
475+},
476+},
477+null,
478+2,
479+),
480+"utf8",
481+);
482+483+const runtime = createRuntime();
484+await tasksMaintenanceCommand({ json: true, apply: true }, runtime);
485+486+const updated = JSON.parse(await fs.readFile(storePath, "utf8")) as Record<string, unknown>;
487+expect(updated["agent:main:cron:daily-monitor:run:old-run"]).toBeDefined();
488+expect(updated["agent:main:cron:retired-job:run:old-run"]).toBeUndefined();
489+});
490+});
491+373492it("does not build JSON-only diagnostics for text maintenance output", async () => {
374493await withTaskCommandStateDir(async () => {
375494const diagnosticsSpy = vi.spyOn(
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。