diff --git a/src/cron/service.list-page-sort-guards.test.ts b/src/cron/service.list-page-sort-guards.test.ts new file mode 100644 index 00000000000..69349147adf --- /dev/null +++ b/src/cron/service.list-page-sort-guards.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from "vitest"; +import { createMockCronStateForJobs } from "./service.test-harness.js"; +import { listPage } from "./service/ops.js"; +import type { CronJob } from "./types.js"; + +function createBaseJob(overrides?: Partial): CronJob { + return { + id: "job-1", + name: "job", + enabled: true, + schedule: { kind: "cron", expr: "*/5 * * * *", tz: "UTC" }, + sessionTarget: "main", + wakeMode: "now", + payload: { kind: "systemEvent", text: "tick" }, + state: { nextRunAtMs: Date.parse("2026-02-27T15:30:00.000Z") }, + createdAtMs: Date.parse("2026-02-27T15:00:00.000Z"), + updatedAtMs: Date.parse("2026-02-27T15:05:00.000Z"), + ...overrides, + }; +} + +describe("cron listPage sort guards", () => { + it("does not throw when sorting by name with malformed name fields", async () => { + const jobs = [ + createBaseJob({ id: "job-a", name: undefined as unknown as string }), + createBaseJob({ id: "job-b", name: "beta" }), + ]; + const state = createMockCronStateForJobs({ jobs }); + + const page = await listPage(state, { sortBy: "name", sortDir: "asc" }); + expect(page.jobs).toHaveLength(2); + }); + + it("does not throw when tie-break sorting encounters missing ids", async () => { + const nextRunAtMs = Date.parse("2026-02-27T15:30:00.000Z"); + const jobs = [ + createBaseJob({ + id: undefined as unknown as string, + name: "alpha", + state: { nextRunAtMs }, + }), + createBaseJob({ + id: undefined as unknown as string, + name: "alpha", + state: { nextRunAtMs }, + }), + ]; + const state = createMockCronStateForJobs({ jobs }); + + const page = await listPage(state, { sortBy: "nextRunAtMs", sortDir: "asc" }); + expect(page.jobs).toHaveLength(2); + }); +}); diff --git a/src/cron/service/ops.ts b/src/cron/service/ops.ts index bc2ec0934a4..af552acaabb 100644 --- a/src/cron/service/ops.ts +++ b/src/cron/service/ops.ts @@ -164,7 +164,9 @@ function sortJobs(jobs: CronJob[], sortBy: CronJobsSortBy, sortDir: CronSortDir) return jobs.toSorted((a, b) => { let cmp = 0; if (sortBy === "name") { - cmp = a.name.localeCompare(b.name, undefined, { sensitivity: "base" }); + const aName = typeof a.name === "string" ? a.name : ""; + const bName = typeof b.name === "string" ? b.name : ""; + cmp = aName.localeCompare(bName, undefined, { sensitivity: "base" }); } else if (sortBy === "updatedAtMs") { cmp = a.updatedAtMs - b.updatedAtMs; } else { @@ -183,7 +185,9 @@ function sortJobs(jobs: CronJob[], sortBy: CronJobsSortBy, sortDir: CronSortDir) if (cmp !== 0) { return cmp * dir; } - return a.id.localeCompare(b.id); + const aId = typeof a.id === "string" ? a.id : ""; + const bId = typeof b.id === "string" ? b.id : ""; + return aId.localeCompare(bId); }); }