fix(cron): canonicalize preserved row ids

This commit is contained in:
IWhatsskill
2026-05-25 11:56:12 +02:00
committed by Peter Steinberger
parent c916906584
commit 985bc934a1
2 changed files with 78 additions and 2 deletions

View File

@@ -223,6 +223,82 @@ describe("cron service store seam coverage", () => {
]);
});
it("skips preserved unsupported rows that collide with supported jobs by canonical id", async () => {
const { storePath } = await makeStorePath();
await writeJobStore(storePath, [
{
id: "trimmed-collision",
name: "supported trimmed collision",
enabled: true,
createdAtMs: STORE_TEST_NOW - 60_000,
updatedAtMs: STORE_TEST_NOW - 60_000,
schedule: { kind: "every", everyMs: 60_000 },
sessionTarget: "main",
wakeMode: "now",
payload: { kind: "systemEvent", text: "tick" },
state: {},
},
{
id: " trimmed-collision ",
name: "stale unsupported padded id",
enabled: true,
createdAtMs: STORE_TEST_NOW - 60_000,
schedule: { kind: "cron", expr: "0 8 * * *", tz: "UTC" },
sessionTarget: "main",
wakeMode: "now",
payload: { kind: "command", command: "echo stale" },
},
{
id: "legacy-jobid-collision",
name: "supported legacy jobId collision",
enabled: true,
createdAtMs: STORE_TEST_NOW - 60_000,
updatedAtMs: STORE_TEST_NOW - 60_000,
schedule: { kind: "every", everyMs: 120_000 },
sessionTarget: "main",
wakeMode: "now",
payload: { kind: "systemEvent", text: "tick legacy" },
state: {},
},
{
jobId: " legacy-jobid-collision ",
name: "stale unsupported legacy jobId",
enabled: true,
createdAtMs: STORE_TEST_NOW - 60_000,
schedule: { kind: "cron", expr: "0 9 * * *", tz: "UTC" },
sessionTarget: "main",
wakeMode: "now",
payload: { kind: "agentmessage", message: "summarize stale" },
},
]);
const state = createStoreTestState(storePath);
await ensureLoaded(state, { skipRecompute: true });
expect(state.store?.jobs.map((job) => job.id)).toEqual([
"trimmed-collision",
"legacy-jobid-collision",
]);
await persist(state);
const config = JSON.parse(await fs.readFile(storePath, "utf8")) as {
jobs: Array<Record<string, unknown>>;
};
expect(config.jobs.map((job) => job.id)).toEqual([
"trimmed-collision",
"legacy-jobid-collision",
]);
expect(config.jobs.map((job) => job.name)).toEqual([
"supported trimmed collision",
"supported legacy jobId collision",
]);
expect(config.jobs.some((job) => job.jobId === " legacy-jobid-collision ")).toBe(false);
expect(config.jobs.some((job) => job.name === "stale unsupported padded id")).toBe(false);
expect(config.jobs.some((job) => job.name === "stale unsupported legacy jobId")).toBe(false);
});
it("normalizes jobId-only jobs in memory so scheduler lookups resolve by stable id", async () => {
const { storePath } = await makeStorePath();

View File

@@ -2,6 +2,7 @@ import fs from "node:fs";
import path from "node:path";
import { expandHomePrefix } from "../infra/home-dir.js";
import { replaceFileAtomic } from "../infra/replace-file.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { resolveConfigDir } from "../utils.js";
import { parseJsonWithJson5Fallback } from "../utils/parse-json-compat.js";
import { tryCronScheduleIdentity } from "./schedule-identity.js";
@@ -86,8 +87,7 @@ function stripJobRuntimeFields(job: CronStoreFile["jobs"][number]): Record<strin
}
function persistedJobId(job: Record<string, unknown>): string | null {
const id = job.id;
return typeof id === "string" && id.trim() ? id : null;
return normalizeOptionalString(job.id) ?? normalizeOptionalString(job.jobId) ?? null;
}
function mergePreservedConfigJobs(