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

autopilot.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.

autopilot.test.tsBlame152 lines · 1 contributor
2b821b7Claude1/**
2 * Autopilot — unit tests.
3 *
4 * Uses the injected-tasks shape so the tick never touches the DB. Real
5 * helpers (syncAllDue, sendDigestsToAll, scanRepositoryForAlerts, peekHead)
6 * are covered by their own suites — here we only test the loop itself.
7 */
8
9import { describe, it, expect, beforeEach, afterEach } from "bun:test";
10import {
11 startAutopilot,
12 runAutopilotTick,
13 __test,
14 type AutopilotTask,
15} from "../lib/autopilot";
16
17describe("autopilot — startAutopilot", () => {
18 const originalDisabled = process.env.AUTOPILOT_DISABLED;
19 const originalInterval = process.env.AUTOPILOT_INTERVAL_MS;
20
21 afterEach(() => {
22 if (originalDisabled === undefined) delete process.env.AUTOPILOT_DISABLED;
23 else process.env.AUTOPILOT_DISABLED = originalDisabled;
24 if (originalInterval === undefined) delete process.env.AUTOPILOT_INTERVAL_MS;
25 else process.env.AUTOPILOT_INTERVAL_MS = originalInterval;
26 });
27
28 it("is a no-op when AUTOPILOT_DISABLED=1 and does not schedule a tick", async () => {
29 process.env.AUTOPILOT_DISABLED = "1";
30 let ran = 0;
31 const tasks: AutopilotTask[] = [
32 { name: "probe", run: async () => { ran++; } },
33 ];
34 const { stop } = startAutopilot({ intervalMs: 5, tasks });
35 // Wait long enough that any scheduled tick would have fired.
36 await new Promise((r) => setTimeout(r, 40));
37 stop();
38 expect(ran).toBe(0);
39 });
40
41 it("does not run the first tick synchronously (boot stays fast)", () => {
42 delete process.env.AUTOPILOT_DISABLED;
43 let ran = 0;
44 const tasks: AutopilotTask[] = [
45 { name: "probe", run: async () => { ran++; } },
46 ];
47 const { stop } = startAutopilot({ intervalMs: 60_000, tasks });
48 // Synchronously — the interval has not elapsed.
49 expect(ran).toBe(0);
50 stop();
51 });
52
53 it("stop() clears the interval so no further ticks run", async () => {
54 delete process.env.AUTOPILOT_DISABLED;
55 let ran = 0;
56 const tasks: AutopilotTask[] = [
57 { name: "probe", run: async () => { ran++; } },
58 ];
59 const { stop } = startAutopilot({ intervalMs: 10, tasks });
60 await new Promise((r) => setTimeout(r, 45));
61 stop();
62 const snapshot = ran;
63 await new Promise((r) => setTimeout(r, 40));
64 // Allow for one tick that was already in flight at stop(), but not more.
65 expect(ran - snapshot).toBeLessThanOrEqual(1);
66 expect(snapshot).toBeGreaterThan(0);
67 });
68});
69
70describe("autopilot — runAutopilotTick", () => {
71 it("returns the expected shape with startedAt/finishedAt/tasks", async () => {
72 const tasks: AutopilotTask[] = [
73 { name: "a", run: async () => {} },
74 { name: "b", run: async () => {} },
75 ];
76 const result = await runAutopilotTick({ tasks });
77 expect(typeof result.startedAt).toBe("string");
78 expect(typeof result.finishedAt).toBe("string");
79 expect(Array.isArray(result.tasks)).toBe(true);
80 expect(result.tasks.length).toBe(2);
81 expect(result.tasks[0]).toMatchObject({ name: "a", ok: true });
82 expect(result.tasks[1]).toMatchObject({ name: "b", ok: true });
83 expect(typeof result.tasks[0].durationMs).toBe("number");
84 });
85
86 it("catches a throwing task and reports { ok:false, error } without crashing the tick", async () => {
87 let secondRan = false;
88 const tasks: AutopilotTask[] = [
89 {
90 name: "boom",
91 run: async () => {
92 throw new Error("kaboom");
93 },
94 },
95 {
96 name: "after",
97 run: async () => {
98 secondRan = true;
99 },
100 },
101 ];
102 const result = await runAutopilotTick({ tasks });
103 expect(result.tasks.length).toBe(2);
104 expect(result.tasks[0].ok).toBe(false);
105 expect(result.tasks[0].error).toBe("kaboom");
106 expect(result.tasks[1].ok).toBe(true);
107 expect(secondRan).toBe(true);
108 });
109
110 it("handles non-Error throws gracefully", async () => {
111 const tasks: AutopilotTask[] = [
112 {
113 name: "string-throw",
114 run: async () => {
115 throw "bad-thing" as unknown as Error;
116 },
117 },
118 ];
119 const result = await runAutopilotTick({ tasks });
120 expect(result.tasks[0].ok).toBe(false);
121 expect(result.tasks[0].error).toBe("bad-thing");
122 });
123});
124
125describe("autopilot — resolveIntervalMs", () => {
126 const originalInterval = process.env.AUTOPILOT_INTERVAL_MS;
127
128 afterEach(() => {
129 if (originalInterval === undefined) delete process.env.AUTOPILOT_INTERVAL_MS;
130 else process.env.AUTOPILOT_INTERVAL_MS = originalInterval;
131 });
132
133 it("prefers explicit opts over env", () => {
134 process.env.AUTOPILOT_INTERVAL_MS = "1234";
135 expect(__test.resolveIntervalMs(42)).toBe(42);
136 });
137
138 it("falls back to env when opts is missing", () => {
139 process.env.AUTOPILOT_INTERVAL_MS = "9999";
140 expect(__test.resolveIntervalMs()).toBe(9999);
141 });
142
143 it("falls back to the default when neither is set", () => {
144 delete process.env.AUTOPILOT_INTERVAL_MS;
145 expect(__test.resolveIntervalMs()).toBe(__test.DEFAULT_INTERVAL_MS);
146 });
147
148 it("ignores non-positive env values", () => {
149 process.env.AUTOPILOT_INTERVAL_MS = "-1";
150 expect(__test.resolveIntervalMs()).toBe(__test.DEFAULT_INTERVAL_MS);
151 });
152});