fix(cron): align list ordering and visible-name search (#116834)

Co-authored-by: Peter Steinberger <steipete@macos.shared>
This commit is contained in:
Peter Steinberger
2026-07-31 05:50:19 -07:00
committed by GitHub
parent 67943535b1
commit dbd8a0981a
2 changed files with 65 additions and 3 deletions
+57 -1
View File
@@ -4,7 +4,7 @@ import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { createMockCronStateForJobs } from "./service.test-harness.js";
import { listPage } from "./service/ops-read.js";
import { list, listPage } from "./service/ops-read.js";
import type { CronJob } from "./types.js";
function createBaseJob(overrides?: Partial<CronJob>): CronJob {
@@ -86,6 +86,62 @@ describe("cron listPage sort guards", () => {
},
);
it("keeps unscheduled jobs after scheduled jobs in the unpaginated list", async () => {
const jobs = [
createBaseJob({ id: "paused-z", enabled: false, state: {} }),
createBaseJob({ id: "later", state: { nextRunAtMs: 200 } }),
createBaseJob({ id: "paused-a", enabled: false, state: {} }),
createBaseJob({ id: "earlier", state: { nextRunAtMs: 100 } }),
];
const state = createMockCronStateForJobs({ jobs });
const unpaginated = await list(state, { includeDisabled: true });
const page = await listPage(state, { enabled: "all", sortBy: "nextRunAtMs" });
expect(unpaginated.map((job) => job.id)).toEqual(["earlier", "later", "paused-a", "paused-z"]);
expect(unpaginated.map((job) => job.id)).toEqual(page.jobs.map((job) => job.id));
});
it("applies the same stable id tiebreaker to unpaginated cron jobs", async () => {
const nextRunAtMs = Date.parse("2026-02-27T15:30:00.000Z");
const jobs = [
createBaseJob({ id: "scheduled-z", state: { nextRunAtMs } }),
createBaseJob({ id: "scheduled-a", state: { nextRunAtMs } }),
];
const state = createMockCronStateForJobs({ jobs });
const unpaginated = await list(state);
expect(unpaginated.map((job) => job.id)).toEqual(["scheduled-a", "scheduled-z"]);
});
it("matches the operator-visible display name when filtering cron jobs", async () => {
const job = createBaseJob({
id: "report-job",
name: "internal-report-name",
displayName: "Daily summary",
});
const state = createMockCronStateForJobs({ jobs: [job] });
const page = await listPage(state, { query: "Daily summary" });
expect(page.jobs.map((entry) => entry.id)).toEqual(["report-job"]);
});
it("preserves phrase searches across existing cron job fields", async () => {
const job = createBaseJob({
id: "report-job",
name: "Daily report",
description: "Quarterly summary",
displayName: "Executive overview",
});
const state = createMockCronStateForJobs({ jobs: [job] });
const page = await listPage(state, { query: "report Quarterly" });
expect(page.jobs.map((entry) => entry.id)).toEqual(["report-job"]);
});
it("normalizes requested agent ids before filtering", async () => {
const jobs = [
createBaseJob({ id: "job-main", agentId: "main", name: "main" }),
+8 -2
View File
@@ -50,7 +50,7 @@ export async function list(state: CronServiceState, opts?: { includeDisabled?: b
await ensureLoadedForRead(state);
const includeDisabled = opts?.includeDisabled === true;
const jobs = (state.store?.jobs ?? []).filter((j) => includeDisabled || isJobEnabled(j));
return jobs.toSorted((a, b) => (a.state.nextRunAtMs ?? 0) - (b.state.nextRunAtMs ?? 0));
return sortCronJobs(jobs, "nextRunAtMs", "asc");
});
}
@@ -289,7 +289,13 @@ export async function listPage(state: CronServiceState, opts?: CronListPageOptio
return true;
}
const haystack = normalizeLowercaseStringOrEmpty(
[job.id, job.name, job.description ?? "", job.agentId ?? ""].join(" "),
[
job.id,
job.name,
job.description ?? "",
job.agentId ?? "",
...(job.displayName ? [job.displayName] : []),
].join(" "),
);
return haystack.includes(query);
});