Skip to content

Commit 3631371

Browse files
committed
fix(cron): normalize agentId filter through the same agent-id normalizer
Codex follow-up on PR #77191 noted: cron job ownership is normalized via normalizeOptionalAgentId/normalizeAgentId on create/update paths, but the new agentId filter compared raw trimmed strings. As a result, requests like cron.list({ agentId: 'MAIN' }) missed jobs stored as 'main', and implicit default-agent jobs could also be dropped when the configured default-agent id casing differed from caller input — even though the same IDs are treated as equivalent in create/update paths. Route both the requested filter and the resolved default-agent id through normalizeOptionalAgentId, and normalize each job's agentId the same way when computing the effective owner. Add a regression test covering the 'MAIN' vs 'main' casing scenario and a whitespace+case variant.
1 parent d1581a0 commit 3631371

2 files changed

Lines changed: 40 additions & 5 deletions

File tree

src/cron/service.list-page-agent-filter.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,32 @@ describe("cron listPage agentId filter (#77118)", () => {
104104
]);
105105
expect(mainPage.total).toBe(2);
106106
});
107+
108+
it("normalizes the agentId filter so case-only mismatches still match (#77118)", async () => {
109+
// Regression for the codex follow-up on PR #77191: cron job ownership is
110+
// normalized via `normalizeOptionalAgentId`/`normalizeAgentId` (lowercased
111+
// + path-safe) on create/update paths. Before the fix, the filter
112+
// compared raw trimmed strings, so requests like
113+
// `cron.list({ agentId: "MAIN" })` against jobs stored as `"main"` (or
114+
// implicit default-agent jobs whose default-agent id was uppercase)
115+
// dropped valid jobs even though the same IDs are treated as equivalent
116+
// elsewhere.
117+
const jobs = [
118+
createBaseJob({ id: "main-1", name: "lowercase main", agentId: "main" }),
119+
createBaseJob({ id: "default-implicit", name: "implicit default", agentId: undefined }),
120+
createBaseJob({ id: "alpha-only", name: "alpha", agentId: "alpha" }),
121+
];
122+
const state = createMockCronStateForJobs({ jobs, defaultAgentId: "MAIN" });
123+
124+
// Caller uses uppercase but the filter normalizes through the same
125+
// `normalizeAgentId` used by job create paths, so `MAIN` matches `main`
126+
// and the implicit default-agent job (since defaultAgentId normalizes the
127+
// same way).
128+
const upperPage = await listPage(state, { agentId: "MAIN" });
129+
expect(upperPage.jobs.map((j) => j.id).toSorted()).toEqual(["default-implicit", "main-1"]);
130+
131+
// Whitespace+casing variant resolves the same way.
132+
const messyPage = await listPage(state, { agentId: " Main " });
133+
expect(messyPage.jobs.map((j) => j.id).toSorted()).toEqual(["default-implicit", "main-1"]);
134+
});
107135
});

src/cron/service/ops.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import type {
3030
CronSortDir,
3131
} from "./list-page-types.js";
3232
import { locked } from "./locked.js";
33+
import { normalizeOptionalAgentId } from "./normalize.js";
3334
import type { CronServiceState } from "./state.js";
3435
import { ensureLoaded, persist, warnIfDisabled } from "./store.js";
3536
import {
@@ -277,13 +278,18 @@ export async function listPage(state: CronServiceState, opts?: CronListPageOptio
277278
await ensureLoadedForRead(state);
278279
const query = normalizeLowercaseStringOrEmpty(opts?.query);
279280
const enabledFilter = resolveEnabledFilter(opts);
280-
const agentIdFilter =
281-
typeof opts?.agentId === "string" && opts.agentId.trim() ? opts.agentId.trim() : null;
281+
// Normalize the requested agentId through the same agent-id normalizer
282+
// used by job create/update paths (`normalizeOptionalAgentId` →
283+
// `normalizeAgentId`) so case-only mismatches like
284+
// `cron.list({ agentId: "MAIN" })` against jobs stored as `"main"` are
285+
// treated as equivalent. Returns `undefined` when no filter was requested.
286+
const agentIdFilter = normalizeOptionalAgentId(opts?.agentId) ?? null;
282287
// Cron jobs are allowed to omit `agentId` for the default agent — delivery
283288
// and session-cleanup code already treats missing agentId as the default.
284289
// Mirror that here so `cron.list({ agentId: defaultAgentId })` returns
285-
// default-agent jobs that omit the field, not an empty page. See #77118.
286-
const defaultAgentId = state.deps.defaultAgentId?.trim() || undefined;
290+
// default-agent jobs that omit the field, not an empty page. Normalize the
291+
// default-agent id too so the comparison is symmetric. See #77118.
292+
const defaultAgentId = normalizeOptionalAgentId(state.deps.defaultAgentId);
287293
const sortBy = opts?.sortBy ?? "nextRunAtMs";
288294
const sortDir = opts?.sortDir ?? "asc";
289295
const source = state.store?.jobs ?? [];
@@ -295,7 +301,8 @@ export async function listPage(state: CronServiceState, opts?: CronListPageOptio
295301
return false;
296302
}
297303
if (agentIdFilter !== null) {
298-
const effectiveOwner = (job.agentId ?? "").trim() || defaultAgentId || "";
304+
const normalizedJobOwner = normalizeOptionalAgentId(job.agentId);
305+
const effectiveOwner = normalizedJobOwner ?? defaultAgentId ?? "";
299306
if (effectiveOwner !== agentIdFilter) {
300307
return false;
301308
}

0 commit comments

Comments
 (0)