Skip to content

Commit 29a5594

Browse files
SidTakhoffman
andauthored
fix(cron): guard list sorting against malformed legacy jobs (#28896)
* fix(cron): guard list sorting against malformed legacy jobs Prevent list operations from crashing when old or corrupted cron entries are missing name/id fields by hardening sort comparators. Closes #28862 * cron: format list sort guard test imports --------- Co-authored-by: Tak Hoffman <[email protected]>
1 parent 645d963 commit 29a5594

File tree

2 files changed

+59
-2
lines changed

2 files changed

+59
-2
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { describe, expect, it } from "vitest";
2+
import { createMockCronStateForJobs } from "./service.test-harness.js";
3+
import { listPage } from "./service/ops.js";
4+
import type { CronJob } from "./types.js";
5+
6+
function createBaseJob(overrides?: Partial<CronJob>): CronJob {
7+
return {
8+
id: "job-1",
9+
name: "job",
10+
enabled: true,
11+
schedule: { kind: "cron", expr: "*/5 * * * *", tz: "UTC" },
12+
sessionTarget: "main",
13+
wakeMode: "now",
14+
payload: { kind: "systemEvent", text: "tick" },
15+
state: { nextRunAtMs: Date.parse("2026-02-27T15:30:00.000Z") },
16+
createdAtMs: Date.parse("2026-02-27T15:00:00.000Z"),
17+
updatedAtMs: Date.parse("2026-02-27T15:05:00.000Z"),
18+
...overrides,
19+
};
20+
}
21+
22+
describe("cron listPage sort guards", () => {
23+
it("does not throw when sorting by name with malformed name fields", async () => {
24+
const jobs = [
25+
createBaseJob({ id: "job-a", name: undefined as unknown as string }),
26+
createBaseJob({ id: "job-b", name: "beta" }),
27+
];
28+
const state = createMockCronStateForJobs({ jobs });
29+
30+
const page = await listPage(state, { sortBy: "name", sortDir: "asc" });
31+
expect(page.jobs).toHaveLength(2);
32+
});
33+
34+
it("does not throw when tie-break sorting encounters missing ids", async () => {
35+
const nextRunAtMs = Date.parse("2026-02-27T15:30:00.000Z");
36+
const jobs = [
37+
createBaseJob({
38+
id: undefined as unknown as string,
39+
name: "alpha",
40+
state: { nextRunAtMs },
41+
}),
42+
createBaseJob({
43+
id: undefined as unknown as string,
44+
name: "alpha",
45+
state: { nextRunAtMs },
46+
}),
47+
];
48+
const state = createMockCronStateForJobs({ jobs });
49+
50+
const page = await listPage(state, { sortBy: "nextRunAtMs", sortDir: "asc" });
51+
expect(page.jobs).toHaveLength(2);
52+
});
53+
});

src/cron/service/ops.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,9 @@ function sortJobs(jobs: CronJob[], sortBy: CronJobsSortBy, sortDir: CronSortDir)
164164
return jobs.toSorted((a, b) => {
165165
let cmp = 0;
166166
if (sortBy === "name") {
167-
cmp = a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
167+
const aName = typeof a.name === "string" ? a.name : "";
168+
const bName = typeof b.name === "string" ? b.name : "";
169+
cmp = aName.localeCompare(bName, undefined, { sensitivity: "base" });
168170
} else if (sortBy === "updatedAtMs") {
169171
cmp = a.updatedAtMs - b.updatedAtMs;
170172
} else {
@@ -183,7 +185,9 @@ function sortJobs(jobs: CronJob[], sortBy: CronJobsSortBy, sortDir: CronSortDir)
183185
if (cmp !== 0) {
184186
return cmp * dir;
185187
}
186-
return a.id.localeCompare(b.id);
188+
const aId = typeof a.id === "string" ? a.id : "";
189+
const bId = typeof b.id === "string" ? b.id : "";
190+
return aId.localeCompare(bId);
187191
});
188192
}
189193

0 commit comments

Comments
 (0)