1import { describe, expect, it } from "vite-plus/test";
2import * as Schema from "effect/Schema";
3
4import {
5 MIN_SCHEDULED_TASK_INTERVAL_MS,
6 ScheduledTaskSchedule,
7 ScheduledTaskUpsertSchedule,
8} from "./scheduledTask.ts";
9
10const decodeSchedule = Schema.decodeUnknownSync(ScheduledTaskSchedule);
11const decodeUpsertSchedule = Schema.decodeUnknownSync(ScheduledTaskUpsertSchedule);
12
13describe("ScheduledTaskSchedule", () => {
14 it("keeps legacy sub-minute persisted schedules readable", () => {
15 expect(
16 decodeSchedule({
17 type: "interval",
18 everyMs: MIN_SCHEDULED_TASK_INTERVAL_MS - 1,
19 }),
20 ).toEqual({
21 type: "interval",
22 everyMs: MIN_SCHEDULED_TASK_INTERVAL_MS - 1,
23 });
24 });
25
26 it("accepts a one-time run only with an offset", () => {
27 for (const runAt of ["2026-11-03T09:00Z", "2026-11-03T09:00:00.000-08:00"]) {
28 expect(decodeUpsertSchedule({ type: "once", runAt })).toEqual({ type: "once", runAt });
29 }
30 for (const runAt of [
31 "2026-11-03T09:00",
32 "2026-13-03T09:00Z",
33 "2026-02-30T09:00Z",
34 "9999-12-31T23:30-01:00",
35 "0000-01-01T00:00+00:01",
36 "tomorrow",
37 ]) {
38 expect(() => decodeUpsertSchedule({ type: "once", runAt })).toThrow();
39 }
40 });
41});
42
43it.each(["literal", " ", "\t\n", "{{body.bad", "{{unknown.field}}"])(
44 "rejects invalid dedupe template %s",
45 (replayKey) => {
46 expect(() => decodeUpsertSchedule({ type: "webhook", dedupe: { replayKey } })).toThrow();
47 expect(() =>
48 decodeUpsertSchedule({ type: "webhook", dedupe: { coalesceKey: replayKey } }),
49 ).toThrow();
50 },
51);
52describe("webhook filters", () => {
53 const accepts = (filters: unknown) => {
54 try {
55 decodeUpsertSchedule({ type: "webhook", filters });
56 return true;
57 } catch {
58 return false;
59 }
60 };
61
62 it("rejects empty or unknown paths, unknown ops, and values that do not fit the op", () => {
63 expect(accepts([{ path: "", op: "exists" }])).toBe(false);
64 expect(accepts([{ path: " ", op: "exists" }])).toBe(false);
65 expect(accepts([{ path: "request", op: "exists" }])).toBe(false);
66 expect(accepts([{ path: "header.x-github-event", op: "exists" }])).toBe(false);
67 expect(accepts([{ path: "body.action", op: "in", value: "opened" }])).toBe(false);
68 expect(accepts([{ path: "body.action", op: "in", value: [] }])).toBe(false);
69 for (const value of ["", " opened", "opened ", "a,b"]) {
70 expect(accepts([{ path: "body.action", op: "in", value: [value] }])).toBe(false);
71 }
72 expect(accepts([{ path: "body.label.name", op: "in", value: ["needs review"] }])).toBe(true);
73 expect(accepts([{ path: "body.action", op: "equals" }])).toBe(false);
74 expect(accepts([{ path: "body.action", op: "matches", value: "^opened$" }])).toBe(false);
75 });
76});