Cron+Slack: fix cooldown omission and cache cap enforcement

This commit is contained in:
Onur
2026-03-01 18:35:18 +01:00
committed by Onur Solmaz
parent 8292401719
commit 18033d3962
4 changed files with 75 additions and 5 deletions

View File

@@ -55,4 +55,13 @@ describe("slack sent-thread-cache", () => {
vi.spyOn(Date, "now").mockReturnValue(Date.now() + 25 * 60 * 60 * 1000);
expect(hasSlackThreadParticipation("A1", "C123", "1700000000.000001")).toBe(false);
});
it("enforces maximum entries by evicting oldest fresh entries", () => {
for (let i = 0; i < 5001; i += 1) {
recordSlackThreadParticipation("A1", "C123", `1700000000.${String(i).padStart(6, "0")}`);
}
expect(hasSlackThreadParticipation("A1", "C123", "1700000000.000000")).toBe(false);
expect(hasSlackThreadParticipation("A1", "C123", "1700000000.005000")).toBe(true);
});
});

View File

@@ -22,6 +22,13 @@ function evictExpired(): void {
}
}
function evictOldest(): void {
const oldest = threadParticipation.keys().next().value;
if (oldest) {
threadParticipation.delete(oldest);
}
}
export function recordSlackThreadParticipation(
accountId: string,
channelId: string,
@@ -33,6 +40,9 @@ export function recordSlackThreadParticipation(
if (threadParticipation.size >= MAX_ENTRIES) {
evictExpired();
}
if (threadParticipation.size >= MAX_ENTRIES) {
evictOldest();
}
threadParticipation.set(makeKey(accountId, channelId, threadTs), Date.now());
}

View File

@@ -346,6 +346,55 @@ describe("cron controller", () => {
});
});
it("omits failureAlert.cooldownMs when custom cooldown is left blank", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "cron.update") {
return { id: "job-alert-no-cooldown" };
}
if (method === "cron.list") {
return { jobs: [{ id: "job-alert-no-cooldown" }] };
}
if (method === "cron.status") {
return { enabled: true, jobs: 1, nextWakeAtMs: null };
}
return {};
});
const state = createState({
client: { request } as unknown as CronState["client"],
cronEditingJobId: "job-alert-no-cooldown",
cronForm: {
...DEFAULT_CRON_FORM,
name: "alert job no cooldown",
payloadKind: "agentTurn",
payloadText: "run it",
failureAlertMode: "custom",
failureAlertAfter: "3",
failureAlertCooldownSeconds: "",
failureAlertChannel: "telegram",
failureAlertTo: "123456",
},
});
await addCronJob(state);
const updateCall = request.mock.calls.find(([method]) => method === "cron.update");
expect(updateCall).toBeDefined();
expect(updateCall?.[1]).toMatchObject({
id: "job-alert-no-cooldown",
patch: {
failureAlert: {
after: 3,
channel: "telegram",
to: "123456",
},
},
});
expect(
(updateCall?.[1] as { patch?: { failureAlert?: { cooldownMs?: number } } })?.patch
?.failureAlert,
).not.toHaveProperty("cooldownMs");
});
it("includes failureAlert=false when disabled per job", async () => {
const request = vi.fn(async (method: string, _payload?: unknown) => {
if (method === "cron.update") {

View File

@@ -579,15 +579,17 @@ function buildFailureAlert(form: CronFormState) {
return undefined;
}
const after = toNumber(form.failureAlertAfter.trim(), 0);
const cooldownSeconds = toNumber(form.failureAlertCooldownSeconds.trim(), 0);
const cooldownRaw = form.failureAlertCooldownSeconds.trim();
const cooldownSeconds = cooldownRaw.length > 0 ? toNumber(cooldownRaw, 0) : undefined;
const cooldownMs =
cooldownSeconds !== undefined && Number.isFinite(cooldownSeconds) && cooldownSeconds >= 0
? Math.floor(cooldownSeconds * 1000)
: undefined;
return {
after: after > 0 ? Math.floor(after) : undefined,
channel: form.failureAlertChannel.trim() || CRON_CHANNEL_LAST,
to: form.failureAlertTo.trim() || undefined,
cooldownMs:
Number.isFinite(cooldownSeconds) && cooldownSeconds >= 0
? Math.floor(cooldownSeconds * 1000)
: undefined,
...(cooldownMs !== undefined ? { cooldownMs } : {}),
};
}