@mclawnet/scheduler 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/dist/__tests__/executor-oneshot.test.d.ts +2 -0
  2. package/dist/__tests__/executor-oneshot.test.d.ts.map +1 -0
  3. package/dist/__tests__/executor-oneshot.test.js +141 -0
  4. package/dist/__tests__/executor-oneshot.test.js.map +1 -0
  5. package/dist/__tests__/executor-swarm.test.d.ts +2 -0
  6. package/dist/__tests__/executor-swarm.test.d.ts.map +1 -0
  7. package/dist/__tests__/executor-swarm.test.js +142 -0
  8. package/dist/__tests__/executor-swarm.test.js.map +1 -0
  9. package/dist/__tests__/json-schedule-repository-prompt-cap.test.d.ts +2 -0
  10. package/dist/__tests__/json-schedule-repository-prompt-cap.test.d.ts.map +1 -0
  11. package/dist/__tests__/json-schedule-repository-prompt-cap.test.js +64 -0
  12. package/dist/__tests__/json-schedule-repository-prompt-cap.test.js.map +1 -0
  13. package/dist/__tests__/json-schedule-repository.test.d.ts +2 -0
  14. package/dist/__tests__/json-schedule-repository.test.d.ts.map +1 -0
  15. package/dist/__tests__/json-schedule-repository.test.js +178 -0
  16. package/dist/__tests__/json-schedule-repository.test.js.map +1 -0
  17. package/dist/__tests__/paths.test.d.ts +2 -0
  18. package/dist/__tests__/paths.test.d.ts.map +1 -0
  19. package/dist/__tests__/paths.test.js +19 -0
  20. package/dist/__tests__/paths.test.js.map +1 -0
  21. package/dist/__tests__/scheduler-maxrun-cap.test.d.ts +2 -0
  22. package/dist/__tests__/scheduler-maxrun-cap.test.d.ts.map +1 -0
  23. package/dist/__tests__/scheduler-maxrun-cap.test.js +109 -0
  24. package/dist/__tests__/scheduler-maxrun-cap.test.js.map +1 -0
  25. package/dist/__tests__/scheduler-service-events-typed.test.d.ts +2 -0
  26. package/dist/__tests__/scheduler-service-events-typed.test.d.ts.map +1 -0
  27. package/dist/__tests__/scheduler-service-events-typed.test.js +27 -0
  28. package/dist/__tests__/scheduler-service-events-typed.test.js.map +1 -0
  29. package/dist/__tests__/scheduler-service-register.test.d.ts +2 -0
  30. package/dist/__tests__/scheduler-service-register.test.d.ts.map +1 -0
  31. package/dist/__tests__/scheduler-service-register.test.js +54 -0
  32. package/dist/__tests__/scheduler-service-register.test.js.map +1 -0
  33. package/dist/__tests__/scheduler-service-tick-23.test.d.ts +2 -0
  34. package/dist/__tests__/scheduler-service-tick-23.test.d.ts.map +1 -0
  35. package/dist/__tests__/scheduler-service-tick-23.test.js +72 -0
  36. package/dist/__tests__/scheduler-service-tick-23.test.js.map +1 -0
  37. package/dist/__tests__/scheduler-service-tick-24.test.d.ts +2 -0
  38. package/dist/__tests__/scheduler-service-tick-24.test.d.ts.map +1 -0
  39. package/dist/__tests__/scheduler-service-tick-24.test.js +80 -0
  40. package/dist/__tests__/scheduler-service-tick-24.test.js.map +1 -0
  41. package/dist/__tests__/scheduler-service-tick-25.test.d.ts +2 -0
  42. package/dist/__tests__/scheduler-service-tick-25.test.d.ts.map +1 -0
  43. package/dist/__tests__/scheduler-service-tick-25.test.js +111 -0
  44. package/dist/__tests__/scheduler-service-tick-25.test.js.map +1 -0
  45. package/dist/__tests__/scheduler-service-tick-26.test.d.ts +2 -0
  46. package/dist/__tests__/scheduler-service-tick-26.test.d.ts.map +1 -0
  47. package/dist/__tests__/scheduler-service-tick-26.test.js +97 -0
  48. package/dist/__tests__/scheduler-service-tick-26.test.js.map +1 -0
  49. package/dist/__tests__/scheduler-service-tick-27.test.d.ts +2 -0
  50. package/dist/__tests__/scheduler-service-tick-27.test.d.ts.map +1 -0
  51. package/dist/__tests__/scheduler-service-tick-27.test.js +69 -0
  52. package/dist/__tests__/scheduler-service-tick-27.test.js.map +1 -0
  53. package/dist/__tests__/scheduler-service-tick-coalesce.test.d.ts +2 -0
  54. package/dist/__tests__/scheduler-service-tick-coalesce.test.d.ts.map +1 -0
  55. package/dist/__tests__/scheduler-service-tick-coalesce.test.js +119 -0
  56. package/dist/__tests__/scheduler-service-tick-coalesce.test.js.map +1 -0
  57. package/dist/__tests__/scheduler-service-trigger-gating.test.d.ts +2 -0
  58. package/dist/__tests__/scheduler-service-trigger-gating.test.d.ts.map +1 -0
  59. package/dist/__tests__/scheduler-service-trigger-gating.test.js +55 -0
  60. package/dist/__tests__/scheduler-service-trigger-gating.test.js.map +1 -0
  61. package/dist/__tests__/tick-helpers.d.ts +17 -0
  62. package/dist/__tests__/tick-helpers.d.ts.map +1 -0
  63. package/dist/__tests__/tick-helpers.js +97 -0
  64. package/dist/__tests__/tick-helpers.js.map +1 -0
  65. package/dist/executor.d.ts +20 -0
  66. package/dist/executor.d.ts.map +1 -0
  67. package/dist/executor.js +2 -0
  68. package/dist/executor.js.map +1 -0
  69. package/dist/executors/oneshot.d.ts +22 -0
  70. package/dist/executors/oneshot.d.ts.map +1 -0
  71. package/dist/executors/oneshot.js +55 -0
  72. package/dist/executors/oneshot.js.map +1 -0
  73. package/dist/executors/swarm.d.ts +26 -0
  74. package/dist/executors/swarm.d.ts.map +1 -0
  75. package/dist/executors/swarm.js +40 -0
  76. package/dist/executors/swarm.js.map +1 -0
  77. package/dist/index.d.ts +11 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +8 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/json-schedule-repository.d.ts +39 -0
  82. package/dist/json-schedule-repository.d.ts.map +1 -0
  83. package/dist/json-schedule-repository.js +374 -0
  84. package/dist/json-schedule-repository.js.map +1 -0
  85. package/dist/paths.d.ts +7 -0
  86. package/dist/paths.d.ts.map +1 -0
  87. package/dist/paths.js +22 -0
  88. package/dist/paths.js.map +1 -0
  89. package/dist/scheduler-service.d.ts +71 -0
  90. package/dist/scheduler-service.d.ts.map +1 -0
  91. package/dist/scheduler-service.js +395 -0
  92. package/dist/scheduler-service.js.map +1 -0
  93. package/dist/types.d.ts +92 -0
  94. package/dist/types.d.ts.map +1 -0
  95. package/dist/types.js +8 -0
  96. package/dist/types.js.map +1 -0
  97. package/package.json +36 -0
@@ -0,0 +1,97 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdtempSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import FakeTimers from "@sinonjs/fake-timers";
6
+ import { SchedulerService } from "../scheduler-service.js";
7
+ import { JsonScheduleRepository } from "../json-schedule-repository.js";
8
+ import { makeDeferredExecutor, makeImmediateExecutor, sample, drainFactory, } from "./tick-helpers.js";
9
+ let home;
10
+ let repo;
11
+ let clock;
12
+ let drain;
13
+ beforeEach(() => {
14
+ home = mkdtempSync(join(tmpdir(), "sched-tick-"));
15
+ repo = new JsonScheduleRepository({ home });
16
+ clock = FakeTimers.install({
17
+ now: new Date("2026-04-30T00:00:00.000Z"),
18
+ shouldAdvanceTime: true,
19
+ advanceTimeDelta: 20,
20
+ });
21
+ drain = drainFactory(clock);
22
+ });
23
+ afterEach(() => {
24
+ clock.uninstall();
25
+ });
26
+ describe("SchedulerService — triggerNow and cancel APIs (Task 2.6)", () => {
27
+ it("triggerNow runs immediately and returns the persisted run", async () => {
28
+ const s = await repo.create(sample({ workDir: "/tmp/proj-tick-26a", cronExpression: "0 9 * * *" }));
29
+ const exec = makeImmediateExecutor({ exitCode: 0, summary: "manual" });
30
+ const svc = new SchedulerService(repo, {
31
+ oneshot: exec,
32
+ swarm: makeImmediateExecutor({ exitCode: 0 }),
33
+ });
34
+ await svc.start();
35
+ const run = await svc.triggerNow(s.id);
36
+ expect(run.status).toBe("completed");
37
+ expect(run.summary).toBe("manual");
38
+ expect(exec.invocations).toHaveLength(1);
39
+ await svc.stop();
40
+ });
41
+ it("triggerNow respects single-flight and returns cancelled record", async () => {
42
+ const s = await repo.create(sample({ workDir: "/tmp/proj-tick-26b" }));
43
+ const exec = makeDeferredExecutor();
44
+ const svc = new SchedulerService(repo, {
45
+ oneshot: exec,
46
+ swarm: makeImmediateExecutor({ exitCode: 0 }),
47
+ });
48
+ await svc.start();
49
+ await clock.tickAsync(60_000);
50
+ await drain();
51
+ const second = await svc.triggerNow(s.id);
52
+ expect(second.status).toBe("cancelled");
53
+ expect(second.error).toBe("previous_run_in_progress");
54
+ exec.resolveNext({ exitCode: 0 });
55
+ await drain();
56
+ await svc.stop();
57
+ });
58
+ it("cancel(runId) aborts an in-flight executor and marks the run cancelled", async () => {
59
+ const s = await repo.create(sample({ workDir: "/tmp/proj-tick-26c" }));
60
+ let receivedSignal;
61
+ const exec = {
62
+ mode: "oneshot",
63
+ async execute(ctx) {
64
+ receivedSignal = ctx.signal;
65
+ await new Promise((resolve) => {
66
+ ctx.signal.addEventListener("abort", () => resolve(), { once: true });
67
+ });
68
+ return { exitCode: 0, error: "cancelled" };
69
+ },
70
+ };
71
+ const svc = new SchedulerService(repo, {
72
+ oneshot: exec,
73
+ swarm: makeImmediateExecutor({ exitCode: 0 }),
74
+ });
75
+ await svc.start();
76
+ await clock.tickAsync(60_000);
77
+ await drain();
78
+ const inflight = await repo.listRuns(s.id);
79
+ expect(inflight).toHaveLength(1);
80
+ expect(inflight[0].status).toBe("running");
81
+ const ok = svc.cancel(inflight[0].id);
82
+ expect(ok).toBe(true);
83
+ expect(receivedSignal?.aborted).toBe(true);
84
+ await drain();
85
+ const after = await repo.listRuns(s.id);
86
+ expect(after[0].status).toBe("cancelled");
87
+ await svc.stop();
88
+ });
89
+ it("cancel returns false for unknown run ids", async () => {
90
+ const svc = new SchedulerService(repo, {
91
+ oneshot: makeImmediateExecutor({ exitCode: 0 }),
92
+ swarm: makeImmediateExecutor({ exitCode: 0 }),
93
+ });
94
+ expect(svc.cancel("does-not-exist")).toBe(false);
95
+ });
96
+ });
97
+ //# sourceMappingURL=scheduler-service-tick-26.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduler-service-tick-26.test.js","sourceRoot":"","sources":["../../src/__tests__/scheduler-service-tick-26.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=scheduler-service-tick-27.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduler-service-tick-27.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/scheduler-service-tick-27.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,69 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdtempSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import FakeTimers from "@sinonjs/fake-timers";
6
+ import { Cron } from "croner";
7
+ import { SchedulerService } from "../scheduler-service.js";
8
+ import { JsonScheduleRepository } from "../json-schedule-repository.js";
9
+ import { makeDeferredExecutor, makeImmediateExecutor, sample, drainFactory, } from "./tick-helpers.js";
10
+ let home;
11
+ let repo;
12
+ let clock;
13
+ let drain;
14
+ beforeEach(() => {
15
+ home = mkdtempSync(join(tmpdir(), "sched-tick-"));
16
+ repo = new JsonScheduleRepository({ home });
17
+ clock = FakeTimers.install({
18
+ now: new Date("2026-04-30T00:00:00.000Z"),
19
+ shouldAdvanceTime: true,
20
+ advanceTimeDelta: 20,
21
+ });
22
+ drain = drainFactory(clock);
23
+ });
24
+ afterEach(() => {
25
+ clock.uninstall();
26
+ });
27
+ describe("SchedulerService.tick — lastRunAt + nextRunAt persistence (Task 2.7)", () => {
28
+ it("persists lastRunAt + nextRunAt after each successful tick", async () => {
29
+ const s = await repo.create(sample({ workDir: "/tmp/proj-tick-27a", cronExpression: "*/5 * * * *", timezone: "UTC" }));
30
+ const exec = makeImmediateExecutor({ exitCode: 0 });
31
+ const svc = new SchedulerService(repo, {
32
+ oneshot: exec,
33
+ swarm: makeImmediateExecutor({ exitCode: 0 }),
34
+ });
35
+ await svc.start();
36
+ await clock.tickAsync(5 * 60_000);
37
+ await drain();
38
+ const reloaded = await repo.getById(s.id);
39
+ expect(reloaded?.lastRunAt).toBeTruthy();
40
+ expect(reloaded?.nextRunAt).toBeTruthy();
41
+ const expectedNext = new Cron("*/5 * * * *", { timezone: "UTC", paused: true })
42
+ .nextRun(new Date(reloaded.lastRunAt));
43
+ expect(reloaded?.nextRunAt).toBe(expectedNext.toISOString());
44
+ expect(new Date(reloaded.nextRunAt).getTime()).toBeGreaterThan(new Date(reloaded.lastRunAt).getTime());
45
+ await svc.stop();
46
+ });
47
+ it("persists lastRunAt + nextRunAt for cancelled (single-flight) runs too", async () => {
48
+ const s = await repo.create(sample({ workDir: "/tmp/proj-tick-27b" }));
49
+ const exec = makeDeferredExecutor();
50
+ const svc = new SchedulerService(repo, {
51
+ oneshot: exec,
52
+ swarm: makeImmediateExecutor({ exitCode: 0 }),
53
+ });
54
+ await svc.start();
55
+ await clock.tickAsync(60_000);
56
+ await drain();
57
+ await clock.tickAsync(60_000);
58
+ await drain();
59
+ const reloaded = await repo.getById(s.id);
60
+ expect(reloaded?.lastRunAt).toBeTruthy();
61
+ expect(reloaded?.nextRunAt).toBeTruthy();
62
+ expect(new Date(reloaded.nextRunAt).getTime())
63
+ .toBeGreaterThan(new Date(reloaded.lastRunAt).getTime());
64
+ exec.resolveNext({ exitCode: 0 });
65
+ await drain();
66
+ await svc.stop();
67
+ });
68
+ });
69
+ //# sourceMappingURL=scheduler-service-tick-27.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduler-service-tick-27.test.js","sourceRoot":"","sources":["../../src/__tests__/scheduler-service-tick-27.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=scheduler-service-tick-coalesce.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduler-service-tick-coalesce.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/scheduler-service-tick-coalesce.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,119 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdtempSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import FakeTimers from "@sinonjs/fake-timers";
6
+ import { SchedulerService } from "../scheduler-service.js";
7
+ import { JsonScheduleRepository } from "../json-schedule-repository.js";
8
+ import { makeImmediateExecutor, sample, drainUntilFactory, } from "./tick-helpers.js";
9
+ let home;
10
+ let repo;
11
+ let clock;
12
+ let drainUntil;
13
+ beforeEach(() => {
14
+ home = mkdtempSync(join(tmpdir(), "sched-tick-"));
15
+ repo = new JsonScheduleRepository({ home });
16
+ clock = FakeTimers.install({
17
+ now: new Date("2026-04-30T00:00:00.000Z"),
18
+ shouldAdvanceTime: true,
19
+ advanceTimeDelta: 20,
20
+ });
21
+ drainUntil = drainUntilFactory(clock);
22
+ });
23
+ afterEach(() => {
24
+ clock.uninstall();
25
+ });
26
+ describe("SchedulerService.tick — coalesced repo.update + change emit (Phase 2 batch 2 fix)", () => {
27
+ it("emits exactly one 'change' event per failed tick with both counter and timestamps", async () => {
28
+ const s = await repo.create(sample({
29
+ workDir: "/tmp/proj-tick-coal-a",
30
+ maxConsecutiveFailures: 5,
31
+ }));
32
+ const exec = makeImmediateExecutor({ exitCode: 1, error: "boom" });
33
+ const svc = new SchedulerService(repo, {
34
+ oneshot: exec,
35
+ swarm: makeImmediateExecutor({ exitCode: 0 }),
36
+ });
37
+ const changes = [];
38
+ svc.events.on("change", (sched) => { changes.push(sched); });
39
+ await svc.start();
40
+ await clock.tickAsync(60_000);
41
+ await drainUntil(() => changes.length >= 1);
42
+ expect(changes).toHaveLength(1);
43
+ const evt = changes[0];
44
+ expect(evt.consecutiveFailures).toBe(1);
45
+ expect(evt.lastRunAt).toBeTruthy();
46
+ expect(evt.nextRunAt).toBeTruthy();
47
+ await svc.stop();
48
+ });
49
+ it("clears nextRunAt and emits one 'change' when circuit breaker pauses the schedule", async () => {
50
+ const s = await repo.create(sample({
51
+ workDir: "/tmp/proj-tick-coal-b",
52
+ maxConsecutiveFailures: 5,
53
+ }));
54
+ const exec = makeImmediateExecutor({ exitCode: 1, error: "boom" });
55
+ const svc = new SchedulerService(repo, {
56
+ oneshot: exec,
57
+ swarm: makeImmediateExecutor({ exitCode: 0 }),
58
+ });
59
+ const allChanges = [];
60
+ const pausedChanges = [];
61
+ svc.events.on("change", (sched) => {
62
+ allChanges.push(sched);
63
+ if (sched.status === "paused")
64
+ pausedChanges.push(sched);
65
+ });
66
+ await svc.start();
67
+ for (let i = 0; i < 5; i++) {
68
+ await clock.tickAsync(60_000);
69
+ // Wait for THIS iter's change to land before advancing to next cron
70
+ // boundary. Using predicate drainUntil avoids over-shooting timer
71
+ // budgets that might cross the next 60s boundary on slow runners.
72
+ await drainUntil(() => allChanges.length >= i + 1);
73
+ }
74
+ const reloaded = await repo.getById(s.id);
75
+ expect(reloaded?.status).toBe("paused");
76
+ expect(reloaded?.consecutiveFailures).toBe(5);
77
+ expect(reloaded?.lastRunAt).toBeTruthy();
78
+ expect(reloaded?.nextRunAt).toBeUndefined();
79
+ expect(pausedChanges).toHaveLength(1);
80
+ expect(pausedChanges[0].status).toBe("paused");
81
+ expect(pausedChanges[0].nextRunAt).toBeUndefined();
82
+ await svc.stop();
83
+ });
84
+ it("emits exactly one 'change' for a successful tick that resets the counter", async () => {
85
+ await repo.create(sample({
86
+ workDir: "/tmp/proj-tick-coal-c",
87
+ maxConsecutiveFailures: 5,
88
+ }));
89
+ // First fail once to set counter to 1, then succeed.
90
+ let calls = 0;
91
+ const svc = new SchedulerService(repo, {
92
+ oneshot: {
93
+ mode: "oneshot",
94
+ async execute() {
95
+ calls += 1;
96
+ return calls === 1 ? { exitCode: 1, error: "x" } : { exitCode: 0 };
97
+ },
98
+ },
99
+ swarm: makeImmediateExecutor({ exitCode: 0 }),
100
+ });
101
+ const changes = [];
102
+ svc.events.on("change", (sched) => { changes.push(sched); });
103
+ await svc.start();
104
+ await clock.tickAsync(60_000);
105
+ await drainUntil(() => changes.length >= 1);
106
+ // One change after fail.
107
+ expect(changes).toHaveLength(1);
108
+ expect(changes[0].consecutiveFailures).toBe(1);
109
+ await clock.tickAsync(60_000);
110
+ await drainUntil(() => changes.length >= 2);
111
+ // One additional change after success.
112
+ expect(changes).toHaveLength(2);
113
+ expect(changes[1].consecutiveFailures).toBe(0);
114
+ expect(changes[1].lastRunAt).toBeTruthy();
115
+ expect(changes[1].nextRunAt).toBeTruthy();
116
+ await svc.stop();
117
+ });
118
+ });
119
+ //# sourceMappingURL=scheduler-service-tick-coalesce.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduler-service-tick-coalesce.test.js","sourceRoot":"","sources":["../../src/__tests__/scheduler-service-tick-coalesce.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=scheduler-service-trigger-gating.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduler-service-trigger-gating.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/scheduler-service-trigger-gating.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,55 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdtempSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import FakeTimers from "@sinonjs/fake-timers";
6
+ import { SchedulerService } from "../scheduler-service.js";
7
+ import { JsonScheduleRepository } from "../json-schedule-repository.js";
8
+ import { makeImmediateExecutor, sample } from "./tick-helpers.js";
9
+ let home;
10
+ let repo;
11
+ let clock;
12
+ beforeEach(() => {
13
+ home = mkdtempSync(join(tmpdir(), "sched-tick-"));
14
+ repo = new JsonScheduleRepository({ home });
15
+ clock = FakeTimers.install({
16
+ now: new Date("2026-04-30T00:00:00.000Z"),
17
+ shouldAdvanceTime: true,
18
+ advanceTimeDelta: 20,
19
+ });
20
+ });
21
+ afterEach(() => {
22
+ clock.uninstall();
23
+ });
24
+ describe("SchedulerService.triggerNow — gating (Phase 2 batch 2 fix)", () => {
25
+ it("throws when the scheduler has been stopped", async () => {
26
+ const s = await repo.create(sample({ workDir: "/tmp/proj-trig-stop" }));
27
+ const svc = new SchedulerService(repo, {
28
+ oneshot: makeImmediateExecutor({ exitCode: 0 }),
29
+ swarm: makeImmediateExecutor({ exitCode: 0 }),
30
+ });
31
+ await svc.start();
32
+ await svc.stop();
33
+ await expect(svc.triggerNow(s.id)).rejects.toThrow(/scheduler stopped/);
34
+ });
35
+ it("returns a synthetic cancelled run with error 'schedule_paused' when the schedule is paused", async () => {
36
+ const s = await repo.create(sample({ workDir: "/tmp/proj-trig-paused" }));
37
+ // Mark paused directly via the public patch.
38
+ await repo.update(s.id, { status: "paused" });
39
+ const exec = makeImmediateExecutor({ exitCode: 0 });
40
+ const svc = new SchedulerService(repo, {
41
+ oneshot: exec,
42
+ swarm: makeImmediateExecutor({ exitCode: 0 }),
43
+ });
44
+ await svc.start();
45
+ const run = await svc.triggerNow(s.id);
46
+ expect(run.status).toBe("cancelled");
47
+ expect(run.error).toBe("schedule_paused");
48
+ expect(exec.invocations).toHaveLength(0);
49
+ // The cancelled run should be persisted in the runs file.
50
+ const persisted = await repo.listRuns(s.id);
51
+ expect(persisted.some(r => r.status === "cancelled" && r.error === "schedule_paused")).toBe(true);
52
+ await svc.stop();
53
+ });
54
+ });
55
+ //# sourceMappingURL=scheduler-service-trigger-gating.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduler-service-trigger-gating.test.js","sourceRoot":"","sources":["../../src/__tests__/scheduler-service-trigger-gating.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import type FakeTimers from "@sinonjs/fake-timers";
2
+ import type { ScheduleExecutor, ExecutorContext, ExecutorResult } from "../executor.js";
3
+ import type { CreateScheduleInput } from "../types.js";
4
+ export declare const sample: (over?: Partial<CreateScheduleInput>) => CreateScheduleInput;
5
+ export interface DeferredExecutor extends ScheduleExecutor {
6
+ invocations: ExecutorContext[];
7
+ resolveNext: (result: ExecutorResult) => void;
8
+ rejectNext: (err: Error) => void;
9
+ }
10
+ export declare function makeDeferredExecutor(mode?: "swarm" | "oneshot"): DeferredExecutor;
11
+ export interface ImmediateExecutor extends ScheduleExecutor {
12
+ invocations: ExecutorContext[];
13
+ }
14
+ export declare function makeImmediateExecutor(result: ExecutorResult, mode?: "swarm" | "oneshot"): ImmediateExecutor;
15
+ export declare function drainFactory(clock: FakeTimers.InstalledClock, iters?: number): () => Promise<void>;
16
+ export declare function drainUntilFactory(clock: FakeTimers.InstalledClock, maxIters?: number): (cond: () => boolean) => Promise<void>;
17
+ //# sourceMappingURL=tick-helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tick-helpers.d.ts","sourceRoot":"","sources":["../../src/__tests__/tick-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,UAAU,MAAM,sBAAsB,CAAC;AACnD,OAAO,KAAK,EACV,gBAAgB,EAAE,eAAe,EAAE,cAAc,EAClD,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEvD,eAAO,MAAM,MAAM,GAAI,OAAM,OAAO,CAAC,mBAAmB,CAAM,KAAG,mBAS/D,CAAC;AAEH,MAAM,WAAW,gBAAiB,SAAQ,gBAAgB;IACxD,WAAW,EAAE,eAAe,EAAE,CAAC;IAC/B,WAAW,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,CAAC;IAC9C,UAAU,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC;AAED,wBAAgB,oBAAoB,CAAC,IAAI,GAAE,OAAO,GAAG,SAAqB,GAAG,gBAAgB,CAyB5F;AAED,MAAM,WAAW,iBAAkB,SAAQ,gBAAgB;IACzD,WAAW,EAAE,eAAe,EAAE,CAAC;CAChC;AAED,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,cAAc,EACtB,IAAI,GAAE,OAAO,GAAG,SAAqB,GACpC,iBAAiB,CAUnB;AAmBD,wBAAgB,YAAY,CAC1B,KAAK,EAAE,UAAU,CAAC,cAAc,EAChC,KAAK,SAAM,GACV,MAAM,OAAO,CAAC,IAAI,CAAC,CAMrB;AAaD,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,UAAU,CAAC,cAAc,EAChC,QAAQ,SAAO,GACd,CAAC,IAAI,EAAE,MAAM,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAYxC"}
@@ -0,0 +1,97 @@
1
+ export const sample = (over = {}) => ({
2
+ agentId: "agent-A",
3
+ workDir: "/tmp/proj-tick-1",
4
+ label: "Test",
5
+ mode: "oneshot",
6
+ cronExpression: "* * * * *",
7
+ timezone: "UTC",
8
+ launchConfig: { prompt: "hi" },
9
+ ...over,
10
+ });
11
+ export function makeDeferredExecutor(mode = "oneshot") {
12
+ const invocations = [];
13
+ const resolvers = [];
14
+ const rejecters = [];
15
+ return {
16
+ mode,
17
+ invocations,
18
+ resolveNext: (result) => {
19
+ const r = resolvers.shift();
20
+ rejecters.shift();
21
+ if (r)
22
+ r(result);
23
+ },
24
+ rejectNext: (err) => {
25
+ resolvers.shift();
26
+ const rj = rejecters.shift();
27
+ if (rj)
28
+ rj(err);
29
+ },
30
+ async execute(ctx) {
31
+ invocations.push(ctx);
32
+ return new Promise((resolve, reject) => {
33
+ resolvers.push(resolve);
34
+ rejecters.push(reject);
35
+ });
36
+ },
37
+ };
38
+ }
39
+ export function makeImmediateExecutor(result, mode = "oneshot") {
40
+ const invocations = [];
41
+ return {
42
+ mode,
43
+ invocations,
44
+ async execute(ctx) {
45
+ invocations.push(ctx);
46
+ return result;
47
+ },
48
+ };
49
+ }
50
+ // proper-lockfile uses setTimeout for retry/touch loops, so a single
51
+ // clock.tickAsync(50) is not enough to flush the tick→appendRun→updateRun
52
+ // chain. Drain in 50ms steps to clear all queued timers.
53
+ //
54
+ // Trade-off: too short and slow CI runners don't finish the proper-lockfile
55
+ // retry chain (the change-event count comes up short). Too long and we cross
56
+ // another 60_000ms cron boundary inside a "tickAsync(60s) + drain" iter,
57
+ // causing extra fires that overlap-and-clobber the previous chain (the
58
+ // breaker counter stops counting cleanly).
59
+ //
60
+ // 200 iters = 10s fake time. Each "tickAsync(60_000) + drain" iter therefore
61
+ // advances 70s of fake time, leaving 50s of headroom before the next cron
62
+ // boundary — safe for the multi-iter loop pattern in tick-25/26/etc.
63
+ //
64
+ // For tests that need to wait for a *specific* event to land, prefer the
65
+ // predicate-based drainUntilFactory below — it stops the moment the
66
+ // condition flips, both faster and more deterministic on slow CI runners.
67
+ export function drainFactory(clock, iters = 200) {
68
+ return async function drain() {
69
+ for (let i = 0; i < iters; i++) {
70
+ await clock.tickAsync(50);
71
+ }
72
+ };
73
+ }
74
+ // Predicate-based drain: keep ticking 50ms until `cond()` is true (or until
75
+ // `maxIters` steps elapse — guards against busy-loops if the condition never
76
+ // becomes true). Stops as soon as the condition flips, so:
77
+ // - on local: returns in a few iters (no wasted fake-time accumulation)
78
+ // - on slow CI: keeps draining proper-lockfile retry chains until the
79
+ // awaited side-effect actually lands
80
+ // - never accumulates more fake-time than necessary, so it won't
81
+ // accidentally roll past the next 60s cron boundary
82
+ //
83
+ // Use in place of plain `await drain()` when a test wants to assert a
84
+ // specific post-state right after a `clock.tickAsync(60_000)`.
85
+ export function drainUntilFactory(clock, maxIters = 1200) {
86
+ return async function drainUntil(cond) {
87
+ for (let i = 0; i < maxIters; i++) {
88
+ if (cond())
89
+ return;
90
+ await clock.tickAsync(50);
91
+ }
92
+ if (!cond()) {
93
+ throw new Error(`drainUntil: condition still false after ${maxIters} iters (${maxIters * 50}ms fake time)`);
94
+ }
95
+ };
96
+ }
97
+ //# sourceMappingURL=tick-helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tick-helpers.js","sourceRoot":"","sources":["../../src/__tests__/tick-helpers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import type { Schedule, ScheduleRun } from "./types.js";
2
+ export interface ExecutorContext {
3
+ schedule: Schedule;
4
+ run: ScheduleRun;
5
+ signal: AbortSignal;
6
+ onStdout: (chunk: string) => void;
7
+ onStderr: (chunk: string) => void;
8
+ }
9
+ export interface ExecutorResult {
10
+ exitCode: number;
11
+ summary?: string;
12
+ swarmId?: string;
13
+ sessionId?: string;
14
+ error?: string;
15
+ }
16
+ export interface ScheduleExecutor {
17
+ readonly mode: "swarm" | "oneshot";
18
+ execute(ctx: ExecutorContext): Promise<ExecutorResult>;
19
+ }
20
+ //# sourceMappingURL=executor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../src/executor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAExD,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,QAAQ,CAAC;IACnB,GAAG,EAAE,WAAW,CAAC;IACjB,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,SAAS,CAAC;IACnC,OAAO,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CACxD"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=executor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"executor.js","sourceRoot":"","sources":["../src/executor.ts"],"names":[],"mappings":""}
@@ -0,0 +1,22 @@
1
+ import type { ScheduleExecutor } from "../executor.js";
2
+ export interface SpawnFn {
3
+ (args: string[], opts: {
4
+ cwd: string;
5
+ env: NodeJS.ProcessEnv;
6
+ signal: AbortSignal;
7
+ }): Promise<{
8
+ exitCode: number;
9
+ stdout: string;
10
+ stderr: string;
11
+ lastAssistantText?: string;
12
+ }>;
13
+ }
14
+ /**
15
+ * OneShot executor: invokes `claude -p` (or any compatible spawnFn) with the
16
+ * scheduled prompt and returns the resulting exit code + summary text. The
17
+ * spawnFn abstraction keeps this module decoupled from a real child_process
18
+ * implementation so it can be unit-tested in isolation and swapped at the
19
+ * runtime layer (Phase 6).
20
+ */
21
+ export declare function createOneShotExecutor(spawnFn: SpawnFn): ScheduleExecutor;
22
+ //# sourceMappingURL=oneshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oneshot.d.ts","sourceRoot":"","sources":["../../src/executors/oneshot.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAEvD,MAAM,WAAW,OAAO;IACtB,CACE,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;QAAC,MAAM,EAAE,WAAW,CAAA;KAAE,GACjE,OAAO,CAAC;QACT,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,MAAM,CAAC;QACf,iBAAiB,CAAC,EAAE,MAAM,CAAC;KAC5B,CAAC,CAAC;CACJ;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,gBAAgB,CA8CxE"}