diff --git a/apps/server/test/jobs/cron.test.ts b/apps/server/test/jobs/cron.test.ts index bc21b1b0..628c5a8f 100644 --- a/apps/server/test/jobs/cron.test.ts +++ b/apps/server/test/jobs/cron.test.ts @@ -1,6 +1,10 @@ import { describe, expect, it } from "vitest"; -import { getNextRun, validateCronExpression } from "../../src/jobs/cron.js"; +import { + getNextRun, + validateCronExpression, + validateCronInterval, +} from "../../src/jobs/cron.js"; describe("cron utilities", () => { describe("getNextRun", () => { @@ -49,4 +53,35 @@ describe("cron utilities", () => { expect(validateCronExpression("60 * * * *")).toBe(false); }); }); + + describe("validateCronInterval", () => { + it("returns null for schedules at or above 5-minute interval", () => { + expect(validateCronInterval("*/5 * * * *")).toBeNull(); + expect(validateCronInterval("0 * * * *")).toBeNull(); + expect(validateCronInterval("0 0 * * *")).toBeNull(); + }); + + it("returns error for schedules under 5-minute interval", () => { + const result = validateCronInterval("* * * * *"); + expect(result).toMatch(/runs too frequently/); + expect(result).toMatch(/every 60s/); + }); + + it("returns error for every-2-minute schedule", () => { + const result = validateCronInterval("*/2 * * * *"); + expect(result).toMatch(/runs too frequently/); + expect(result).toMatch(/every 120s/); + }); + + it("returns error for invalid cron expression", () => { + expect(validateCronInterval("not valid")).toBe( + "Invalid cron expression." + ); + }); + + it("returns null for schedules that only fire once", () => { + const result = validateCronInterval("0 12 1 1 *"); + expect(result).toBeNull(); + }); + }); }); diff --git a/apps/server/test/jobs/job-notifier.test.ts b/apps/server/test/jobs/job-notifier.test.ts index 5cc7f123..0aa516a7 100644 --- a/apps/server/test/jobs/job-notifier.test.ts +++ b/apps/server/test/jobs/job-notifier.test.ts @@ -181,4 +181,160 @@ describe("JobNotifier", () => { const body = JSON.parse(fetchSpy.mock.calls[0][1].body); expect(body.attachments[0].color).toBe("#ef4444"); }); + + it("displays 'timed out' as status label for timed_out runs", async () => { + await setSetting( + pool, + "slack_webhook_url", + "https://hooks.slack.test/test" + ); + await notifier.onJobRunStateChange(makeRun({ status: "timed_out" })); + + const body = JSON.parse(fetchSpy.mock.calls[0][1].body); + const contextText = body.attachments[0].blocks[1].elements[0].text; + expect(contextText).toContain("`timed out`"); + }); + + it("escapes special characters in job name and summary", async () => { + await setSetting( + pool, + "slack_webhook_url", + "https://hooks.slack.test/test" + ); + await notifier.onJobRunStateChange( + makeRun({ + config: { + ...makeRun().config, + name: "job & special chars", + }, + report: { + status: "completed", + summary: "Fixed