Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

scheduled-workflows.test.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

scheduled-workflows.test.tsBlame126 lines · 1 contributor
665c8bfClaude1/**
2 * Tests for src/lib/scheduled-workflows.ts (pure helpers + the
3 * parser-side schedule extractor in workflow-parser.ts).
4 *
5 * The DB-touching pipeline (runScheduledWorkflowsTick) is exercised
6 * indirectly by the autopilot smoke test — here we focus on the
7 * pure decisions that drive it.
8 */
9
10import { describe, it, expect } from "bun:test";
11import {
12 schedulesFromParsedJson,
13 firstCronToFire,
14 runScheduledWorkflowsTick,
15 MAX_RUNS_PER_TICK,
16} from "../lib/scheduled-workflows";
17import { __test as parserTest } from "../lib/workflow-parser";
18
19describe("schedulesFromParsedJson", () => {
20 it("returns [] for empty / malformed input", () => {
21 expect(schedulesFromParsedJson("")).toEqual([]);
22 expect(schedulesFromParsedJson("{}")).toEqual([]);
23 expect(schedulesFromParsedJson("not json")).toEqual([]);
24 });
25
26 it("returns [] when schedules is missing or wrong type", () => {
27 expect(schedulesFromParsedJson('{"on":["push"]}')).toEqual([]);
28 expect(schedulesFromParsedJson('{"schedules":"0 * * * *"}')).toEqual([]);
29 expect(schedulesFromParsedJson('{"schedules":42}')).toEqual([]);
30 });
31
32 it("extracts a non-empty schedules array", () => {
33 const json = '{"schedules":["0 * * * *","30 9 * * 1"]}';
34 expect(schedulesFromParsedJson(json)).toEqual(["0 * * * *", "30 9 * * 1"]);
35 });
36
37 it("filters out non-string and empty entries", () => {
38 const json = '{"schedules":["0 * * * *","",42,null,"15 14 * * *"]}';
39 expect(schedulesFromParsedJson(json)).toEqual(["0 * * * *", "15 14 * * *"]);
40 });
41});
42
43describe("workflow-parser extractSchedules", () => {
44 const ex = parserTest.extractSchedules;
45
46 it("returns [] for non-mapping triggers", () => {
47 expect(ex("push")).toEqual([]);
48 expect(ex(["push"])).toEqual([]);
49 expect(ex(null)).toEqual([]);
50 expect(ex(undefined)).toEqual([]);
51 });
52
53 it("returns [] when the mapping has no schedule key", () => {
54 expect(ex({ push: { branches: ["main"] } })).toEqual([]);
55 });
56
57 it("extracts a single schedule entry (object form)", () => {
58 expect(ex({ schedule: { cron: "0 * * * *" } })).toEqual(["0 * * * *"]);
59 });
60
61 it("extracts an array of schedule entries", () => {
62 expect(
63 ex({ schedule: [{ cron: "0 * * * *" }, { cron: "30 12 * * 5" }] })
64 ).toEqual(["0 * * * *", "30 12 * * 5"]);
65 });
66
67 it("tolerates a bare string in schedule", () => {
68 expect(ex({ schedule: "0 * * * *" })).toEqual(["0 * * * *"]);
69 expect(ex({ schedule: ["0 * * * *", "30 9 * * 1"] })).toEqual([
70 "0 * * * *",
71 "30 9 * * 1",
72 ]);
73 });
74
75 it("ignores entries that are missing or non-string cron", () => {
76 expect(ex({ schedule: [{ cron: "" }, { cron: 42 }, {}] })).toEqual([]);
77 });
78});
79
80describe("firstCronToFire", () => {
81 const since = new Date("2026-04-30T12:00:00Z");
82 const until = new Date("2026-04-30T13:05:00Z");
83
84 it("returns null when schedules is empty", () => {
85 expect(firstCronToFire([], since, until)).toBeNull();
86 });
87
88 it("returns the first cron that fires in the window", () => {
89 // 0 13 * * * fires at 13:00 — in (12:00, 13:05]
90 expect(firstCronToFire(["0 13 * * *"], since, until)).toBe("0 13 * * *");
91 });
92
93 it("returns null when no cron in the list fires", () => {
94 // 0 23 fires at 23:00; not in our 12:00–13:05 window
95 expect(firstCronToFire(["0 23 * * *"], since, until)).toBeNull();
96 });
97
98 it("skips invalid cron strings without crashing", () => {
99 expect(
100 firstCronToFire(["@hourly", "0 13 * * *"], since, until)
101 ).toBe("0 13 * * *");
102 });
103
104 it("returns the first matching cron, not all of them", () => {
105 expect(
106 firstCronToFire(["0 13 * * *", "30 13 * * *"], since, until)
107 ).toBe("0 13 * * *");
108 });
109});
110
111describe("runScheduledWorkflowsTick — fail-open", () => {
112 it("returns a result object with the expected keys, never throws", async () => {
113 const r = await runScheduledWorkflowsTick(new Date("2026-04-30T13:00:00Z"));
114 expect(typeof r.considered).toBe("number");
115 expect(typeof r.fired).toBe("number");
116 expect(typeof r.errors).toBe("number");
117 expect(r.considered).toBeGreaterThanOrEqual(0);
118 expect(r.fired).toBeGreaterThanOrEqual(0);
119 expect(r.errors).toBeGreaterThanOrEqual(0);
120 });
121
122 it("MAX_RUNS_PER_TICK is a safe positive integer", () => {
123 expect(MAX_RUNS_PER_TICK).toBeGreaterThan(0);
124 expect(MAX_RUNS_PER_TICK).toBeLessThanOrEqual(1000);
125 });
126});