@herdctl/core 0.0.1 → 0.0.2

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 (275) hide show
  1. package/dist/config/__tests__/agent.test.js +31 -13
  2. package/dist/config/__tests__/agent.test.js.map +1 -1
  3. package/dist/config/__tests__/merge.test.js +9 -2
  4. package/dist/config/__tests__/merge.test.js.map +1 -1
  5. package/dist/config/__tests__/schema.test.js +350 -1
  6. package/dist/config/__tests__/schema.test.js.map +1 -1
  7. package/dist/config/index.d.ts +1 -1
  8. package/dist/config/index.d.ts.map +1 -1
  9. package/dist/config/index.js +3 -1
  10. package/dist/config/index.js.map +1 -1
  11. package/dist/config/schema.d.ts +828 -24
  12. package/dist/config/schema.d.ts.map +1 -1
  13. package/dist/config/schema.js +118 -6
  14. package/dist/config/schema.js.map +1 -1
  15. package/dist/fleet-manager/__tests__/coverage.test.js +11 -332
  16. package/dist/fleet-manager/__tests__/coverage.test.js.map +1 -1
  17. package/dist/fleet-manager/__tests__/errors.test.js +1 -49
  18. package/dist/fleet-manager/__tests__/errors.test.js.map +1 -1
  19. package/dist/fleet-manager/__tests__/integration.test.js +109 -0
  20. package/dist/fleet-manager/__tests__/integration.test.js.map +1 -1
  21. package/dist/fleet-manager/__tests__/reload.test.js +1 -1
  22. package/dist/fleet-manager/__tests__/reload.test.js.map +1 -1
  23. package/dist/fleet-manager/config-reload.d.ts +164 -0
  24. package/dist/fleet-manager/config-reload.d.ts.map +1 -0
  25. package/dist/fleet-manager/config-reload.js +445 -0
  26. package/dist/fleet-manager/config-reload.js.map +1 -0
  27. package/dist/fleet-manager/context.d.ts +76 -0
  28. package/dist/fleet-manager/context.d.ts.map +1 -0
  29. package/dist/fleet-manager/context.js +11 -0
  30. package/dist/fleet-manager/context.js.map +1 -0
  31. package/dist/fleet-manager/errors.d.ts +0 -25
  32. package/dist/fleet-manager/errors.d.ts.map +1 -1
  33. package/dist/fleet-manager/errors.js +0 -38
  34. package/dist/fleet-manager/errors.js.map +1 -1
  35. package/dist/fleet-manager/event-emitters.d.ts +123 -0
  36. package/dist/fleet-manager/event-emitters.d.ts.map +1 -0
  37. package/dist/fleet-manager/event-emitters.js +136 -0
  38. package/dist/fleet-manager/event-emitters.js.map +1 -0
  39. package/dist/fleet-manager/event-types.d.ts +0 -15
  40. package/dist/fleet-manager/event-types.d.ts.map +1 -1
  41. package/dist/fleet-manager/fleet-manager.d.ts +40 -653
  42. package/dist/fleet-manager/fleet-manager.d.ts.map +1 -1
  43. package/dist/fleet-manager/fleet-manager.js +95 -1720
  44. package/dist/fleet-manager/fleet-manager.js.map +1 -1
  45. package/dist/fleet-manager/index.d.ts +13 -2
  46. package/dist/fleet-manager/index.d.ts.map +1 -1
  47. package/dist/fleet-manager/index.js +19 -6
  48. package/dist/fleet-manager/index.js.map +1 -1
  49. package/dist/fleet-manager/job-control.d.ts +64 -0
  50. package/dist/fleet-manager/job-control.d.ts.map +1 -0
  51. package/dist/fleet-manager/job-control.js +296 -0
  52. package/dist/fleet-manager/job-control.js.map +1 -0
  53. package/dist/fleet-manager/log-streaming.d.ts +171 -0
  54. package/dist/fleet-manager/log-streaming.d.ts.map +1 -0
  55. package/dist/fleet-manager/log-streaming.js +503 -0
  56. package/dist/fleet-manager/log-streaming.js.map +1 -0
  57. package/dist/fleet-manager/schedule-executor.d.ts +63 -0
  58. package/dist/fleet-manager/schedule-executor.d.ts.map +1 -0
  59. package/dist/fleet-manager/schedule-executor.js +209 -0
  60. package/dist/fleet-manager/schedule-executor.js.map +1 -0
  61. package/dist/fleet-manager/schedule-management.d.ts +71 -0
  62. package/dist/fleet-manager/schedule-management.d.ts.map +1 -0
  63. package/dist/fleet-manager/schedule-management.js +171 -0
  64. package/dist/fleet-manager/schedule-management.js.map +1 -0
  65. package/dist/fleet-manager/status-queries.d.ts +105 -0
  66. package/dist/fleet-manager/status-queries.d.ts.map +1 -0
  67. package/dist/fleet-manager/status-queries.js +247 -0
  68. package/dist/fleet-manager/status-queries.js.map +1 -0
  69. package/dist/fleet-manager/types.d.ts +0 -39
  70. package/dist/fleet-manager/types.d.ts.map +1 -1
  71. package/dist/runner/__tests__/job-executor.test.js +206 -1
  72. package/dist/runner/__tests__/job-executor.test.js.map +1 -1
  73. package/dist/runner/job-executor.d.ts +9 -0
  74. package/dist/runner/job-executor.d.ts.map +1 -1
  75. package/dist/runner/job-executor.js +78 -4
  76. package/dist/runner/job-executor.js.map +1 -1
  77. package/dist/runner/types.d.ts +2 -0
  78. package/dist/runner/types.d.ts.map +1 -1
  79. package/dist/scheduler/__tests__/cron.test.d.ts +2 -0
  80. package/dist/scheduler/__tests__/cron.test.d.ts.map +1 -0
  81. package/dist/scheduler/__tests__/cron.test.js +867 -0
  82. package/dist/scheduler/__tests__/cron.test.js.map +1 -0
  83. package/dist/scheduler/__tests__/scheduler.test.js +164 -5
  84. package/dist/scheduler/__tests__/scheduler.test.js.map +1 -1
  85. package/dist/scheduler/cron.d.ts +126 -0
  86. package/dist/scheduler/cron.d.ts.map +1 -0
  87. package/dist/scheduler/cron.js +390 -0
  88. package/dist/scheduler/cron.js.map +1 -0
  89. package/dist/scheduler/errors.d.ts +81 -1
  90. package/dist/scheduler/errors.d.ts.map +1 -1
  91. package/dist/scheduler/errors.js +81 -6
  92. package/dist/scheduler/errors.js.map +1 -1
  93. package/dist/scheduler/index.d.ts +1 -0
  94. package/dist/scheduler/index.d.ts.map +1 -1
  95. package/dist/scheduler/index.js +2 -0
  96. package/dist/scheduler/index.js.map +1 -1
  97. package/dist/scheduler/schedule-runner.d.ts +2 -2
  98. package/dist/scheduler/schedule-runner.d.ts.map +1 -1
  99. package/dist/scheduler/schedule-runner.js +20 -8
  100. package/dist/scheduler/schedule-runner.js.map +1 -1
  101. package/dist/scheduler/scheduler.d.ts +4 -4
  102. package/dist/scheduler/scheduler.d.ts.map +1 -1
  103. package/dist/scheduler/scheduler.js +86 -20
  104. package/dist/scheduler/scheduler.js.map +1 -1
  105. package/dist/scheduler/types.d.ts +1 -1
  106. package/dist/scheduler/types.d.ts.map +1 -1
  107. package/dist/state/schemas/job-metadata.d.ts +2 -2
  108. package/package.json +33 -8
  109. package/.turbo/turbo-build.log +0 -4
  110. package/.turbo/turbo-test.log +0 -219
  111. package/.turbo/turbo-typecheck.log +0 -4
  112. package/coverage/base.css +0 -224
  113. package/coverage/block-navigation.js +0 -87
  114. package/coverage/coverage-final.json +0 -51
  115. package/coverage/favicon.png +0 -0
  116. package/coverage/index.html +0 -251
  117. package/coverage/prettify.css +0 -1
  118. package/coverage/prettify.js +0 -2
  119. package/coverage/sort-arrow-sprite.png +0 -0
  120. package/coverage/sorter.js +0 -210
  121. package/coverage/src/config/index.html +0 -191
  122. package/coverage/src/config/index.ts.html +0 -442
  123. package/coverage/src/config/interpolate.ts.html +0 -652
  124. package/coverage/src/config/loader.ts.html +0 -1501
  125. package/coverage/src/config/merge.ts.html +0 -823
  126. package/coverage/src/config/parser.ts.html +0 -1213
  127. package/coverage/src/config/schema.ts.html +0 -1123
  128. package/coverage/src/fleet-manager/errors.ts.html +0 -2326
  129. package/coverage/src/fleet-manager/event-types.ts.html +0 -1219
  130. package/coverage/src/fleet-manager/fleet-manager.ts.html +0 -7030
  131. package/coverage/src/fleet-manager/index.html +0 -206
  132. package/coverage/src/fleet-manager/index.ts.html +0 -469
  133. package/coverage/src/fleet-manager/job-manager.ts.html +0 -2074
  134. package/coverage/src/fleet-manager/job-queue.ts.html +0 -2479
  135. package/coverage/src/fleet-manager/types.ts.html +0 -2602
  136. package/coverage/src/index.html +0 -116
  137. package/coverage/src/index.ts.html +0 -181
  138. package/coverage/src/runner/errors.ts.html +0 -1006
  139. package/coverage/src/runner/index.html +0 -191
  140. package/coverage/src/runner/index.ts.html +0 -256
  141. package/coverage/src/runner/job-executor.ts.html +0 -1429
  142. package/coverage/src/runner/message-processor.ts.html +0 -1150
  143. package/coverage/src/runner/sdk-adapter.ts.html +0 -658
  144. package/coverage/src/runner/types.ts.html +0 -559
  145. package/coverage/src/scheduler/errors.ts.html +0 -388
  146. package/coverage/src/scheduler/index.html +0 -206
  147. package/coverage/src/scheduler/index.ts.html +0 -244
  148. package/coverage/src/scheduler/interval.ts.html +0 -652
  149. package/coverage/src/scheduler/schedule-runner.ts.html +0 -1411
  150. package/coverage/src/scheduler/schedule-state.ts.html +0 -718
  151. package/coverage/src/scheduler/scheduler.ts.html +0 -1795
  152. package/coverage/src/scheduler/types.ts.html +0 -733
  153. package/coverage/src/state/directory.ts.html +0 -736
  154. package/coverage/src/state/errors.ts.html +0 -376
  155. package/coverage/src/state/fleet-state.ts.html +0 -937
  156. package/coverage/src/state/index.html +0 -221
  157. package/coverage/src/state/index.ts.html +0 -322
  158. package/coverage/src/state/job-metadata.ts.html +0 -1420
  159. package/coverage/src/state/job-output.ts.html +0 -1033
  160. package/coverage/src/state/schemas/fleet-state.ts.html +0 -445
  161. package/coverage/src/state/schemas/index.html +0 -176
  162. package/coverage/src/state/schemas/index.ts.html +0 -286
  163. package/coverage/src/state/schemas/job-metadata.ts.html +0 -628
  164. package/coverage/src/state/schemas/job-output.ts.html +0 -616
  165. package/coverage/src/state/schemas/session-info.ts.html +0 -361
  166. package/coverage/src/state/session.ts.html +0 -844
  167. package/coverage/src/state/types.ts.html +0 -262
  168. package/coverage/src/state/utils/atomic.ts.html +0 -748
  169. package/coverage/src/state/utils/index.html +0 -146
  170. package/coverage/src/state/utils/index.ts.html +0 -103
  171. package/coverage/src/state/utils/reads.ts.html +0 -1621
  172. package/coverage/src/work-sources/adapters/github.ts.html +0 -3583
  173. package/coverage/src/work-sources/adapters/index.html +0 -131
  174. package/coverage/src/work-sources/adapters/index.ts.html +0 -277
  175. package/coverage/src/work-sources/errors.ts.html +0 -298
  176. package/coverage/src/work-sources/index.html +0 -176
  177. package/coverage/src/work-sources/index.ts.html +0 -529
  178. package/coverage/src/work-sources/manager.ts.html +0 -1324
  179. package/coverage/src/work-sources/registry.ts.html +0 -619
  180. package/coverage/src/work-sources/types.ts.html +0 -568
  181. package/dist/fleet-manager/__tests__/event-helpers.test.d.ts +0 -7
  182. package/dist/fleet-manager/__tests__/event-helpers.test.d.ts.map +0 -1
  183. package/dist/fleet-manager/__tests__/event-helpers.test.js +0 -368
  184. package/dist/fleet-manager/__tests__/event-helpers.test.js.map +0 -1
  185. package/src/config/__tests__/agent.test.ts +0 -864
  186. package/src/config/__tests__/interpolate.test.ts +0 -644
  187. package/src/config/__tests__/loader.test.ts +0 -784
  188. package/src/config/__tests__/merge.test.ts +0 -751
  189. package/src/config/__tests__/parser.test.ts +0 -533
  190. package/src/config/__tests__/schema.test.ts +0 -873
  191. package/src/config/index.ts +0 -119
  192. package/src/config/interpolate.ts +0 -189
  193. package/src/config/loader.ts +0 -472
  194. package/src/config/merge.ts +0 -246
  195. package/src/config/parser.ts +0 -376
  196. package/src/config/schema.ts +0 -346
  197. package/src/fleet-manager/__tests__/coverage.test.ts +0 -2869
  198. package/src/fleet-manager/__tests__/errors.test.ts +0 -660
  199. package/src/fleet-manager/__tests__/event-helpers.test.ts +0 -448
  200. package/src/fleet-manager/__tests__/integration.test.ts +0 -1209
  201. package/src/fleet-manager/__tests__/job-control.test.ts +0 -283
  202. package/src/fleet-manager/__tests__/job-manager.test.ts +0 -869
  203. package/src/fleet-manager/__tests__/job-queue.test.ts +0 -401
  204. package/src/fleet-manager/__tests__/reload.test.ts +0 -751
  205. package/src/fleet-manager/__tests__/status-queries.test.ts +0 -595
  206. package/src/fleet-manager/__tests__/trigger.test.ts +0 -601
  207. package/src/fleet-manager/errors.ts +0 -747
  208. package/src/fleet-manager/event-types.ts +0 -378
  209. package/src/fleet-manager/fleet-manager.ts +0 -2315
  210. package/src/fleet-manager/index.ts +0 -128
  211. package/src/fleet-manager/job-manager.ts +0 -663
  212. package/src/fleet-manager/job-queue.ts +0 -798
  213. package/src/fleet-manager/types.ts +0 -839
  214. package/src/index.ts +0 -32
  215. package/src/runner/__tests__/errors.test.ts +0 -382
  216. package/src/runner/__tests__/job-executor.test.ts +0 -1708
  217. package/src/runner/__tests__/message-processor.test.ts +0 -960
  218. package/src/runner/__tests__/sdk-adapter.test.ts +0 -626
  219. package/src/runner/errors.ts +0 -307
  220. package/src/runner/index.ts +0 -57
  221. package/src/runner/job-executor.ts +0 -448
  222. package/src/runner/message-processor.ts +0 -355
  223. package/src/runner/sdk-adapter.ts +0 -191
  224. package/src/runner/types.ts +0 -158
  225. package/src/scheduler/__tests__/errors.test.ts +0 -159
  226. package/src/scheduler/__tests__/interval.test.ts +0 -515
  227. package/src/scheduler/__tests__/schedule-runner.test.ts +0 -798
  228. package/src/scheduler/__tests__/schedule-state.test.ts +0 -671
  229. package/src/scheduler/__tests__/scheduler.test.ts +0 -1280
  230. package/src/scheduler/errors.ts +0 -101
  231. package/src/scheduler/index.ts +0 -53
  232. package/src/scheduler/interval.ts +0 -189
  233. package/src/scheduler/schedule-runner.ts +0 -442
  234. package/src/scheduler/schedule-state.ts +0 -211
  235. package/src/scheduler/scheduler.ts +0 -570
  236. package/src/scheduler/types.ts +0 -216
  237. package/src/state/__tests__/directory.test.ts +0 -595
  238. package/src/state/__tests__/fleet-state.test.ts +0 -868
  239. package/src/state/__tests__/job-metadata-schema.test.ts +0 -414
  240. package/src/state/__tests__/job-metadata.test.ts +0 -831
  241. package/src/state/__tests__/job-output.test.ts +0 -856
  242. package/src/state/__tests__/session-schema.test.ts +0 -378
  243. package/src/state/__tests__/session.test.ts +0 -604
  244. package/src/state/directory.ts +0 -217
  245. package/src/state/errors.ts +0 -97
  246. package/src/state/fleet-state.ts +0 -284
  247. package/src/state/index.ts +0 -79
  248. package/src/state/job-metadata.ts +0 -445
  249. package/src/state/job-output.ts +0 -316
  250. package/src/state/schemas/__tests__/job-output.test.ts +0 -338
  251. package/src/state/schemas/fleet-state.ts +0 -120
  252. package/src/state/schemas/index.ts +0 -67
  253. package/src/state/schemas/job-metadata.ts +0 -181
  254. package/src/state/schemas/job-output.ts +0 -177
  255. package/src/state/schemas/session-info.ts +0 -92
  256. package/src/state/session.ts +0 -253
  257. package/src/state/types.ts +0 -59
  258. package/src/state/utils/__tests__/atomic.test.ts +0 -723
  259. package/src/state/utils/__tests__/reads.test.ts +0 -1071
  260. package/src/state/utils/atomic.ts +0 -221
  261. package/src/state/utils/index.ts +0 -6
  262. package/src/state/utils/reads.ts +0 -512
  263. package/src/work-sources/__tests__/github.test.ts +0 -1800
  264. package/src/work-sources/__tests__/manager.test.ts +0 -529
  265. package/src/work-sources/__tests__/registry.test.ts +0 -477
  266. package/src/work-sources/__tests__/types.test.ts +0 -479
  267. package/src/work-sources/adapters/github.ts +0 -1166
  268. package/src/work-sources/adapters/index.ts +0 -64
  269. package/src/work-sources/errors.ts +0 -71
  270. package/src/work-sources/index.ts +0 -148
  271. package/src/work-sources/manager.ts +0 -413
  272. package/src/work-sources/registry.ts +0 -178
  273. package/src/work-sources/types.ts +0 -161
  274. package/tsconfig.json +0 -9
  275. package/vitest.config.ts +0 -19
@@ -1,159 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import {
3
- SchedulerError,
4
- IntervalParseError,
5
- ScheduleTriggerError,
6
- } from "../errors.js";
7
-
8
- // =============================================================================
9
- // SchedulerError (Base Class)
10
- // =============================================================================
11
-
12
- describe("SchedulerError", () => {
13
- it("creates error with message", () => {
14
- const error = new SchedulerError("test error message");
15
-
16
- expect(error.message).toBe("test error message");
17
- expect(error.name).toBe("SchedulerError");
18
- expect(error).toBeInstanceOf(Error);
19
- expect(error).toBeInstanceOf(SchedulerError);
20
- });
21
-
22
- it("preserves cause when provided", () => {
23
- const cause = new Error("original error");
24
- const error = new SchedulerError("wrapped error", { cause });
25
-
26
- expect(error.message).toBe("wrapped error");
27
- expect(error.cause).toBe(cause);
28
- });
29
-
30
- it("has undefined cause when not provided", () => {
31
- const error = new SchedulerError("no cause");
32
-
33
- expect(error.cause).toBeUndefined();
34
- });
35
- });
36
-
37
- // =============================================================================
38
- // IntervalParseError
39
- // =============================================================================
40
-
41
- describe("IntervalParseError", () => {
42
- it("creates error with message and input", () => {
43
- const error = new IntervalParseError("invalid interval", "5x");
44
-
45
- expect(error.message).toBe("invalid interval");
46
- expect(error.name).toBe("IntervalParseError");
47
- expect(error.input).toBe("5x");
48
- expect(error).toBeInstanceOf(Error);
49
- expect(error).toBeInstanceOf(SchedulerError);
50
- expect(error).toBeInstanceOf(IntervalParseError);
51
- });
52
-
53
- it("preserves input string exactly", () => {
54
- const inputs = ["", " ", "5", "-5m", "1.5h", "invalid"];
55
-
56
- for (const input of inputs) {
57
- const error = new IntervalParseError("test", input);
58
- expect(error.input).toBe(input);
59
- }
60
- });
61
-
62
- it("preserves cause when provided", () => {
63
- const cause = new Error("original error");
64
- const error = new IntervalParseError("wrapped error", "5x", { cause });
65
-
66
- expect(error.cause).toBe(cause);
67
- });
68
- });
69
-
70
- // =============================================================================
71
- // ScheduleTriggerError
72
- // =============================================================================
73
-
74
- describe("ScheduleTriggerError", () => {
75
- it("creates error with message, agentName, and scheduleName", () => {
76
- const error = new ScheduleTriggerError(
77
- "trigger failed",
78
- "my-agent",
79
- "hourly-schedule"
80
- );
81
-
82
- expect(error.message).toBe("trigger failed");
83
- expect(error.name).toBe("ScheduleTriggerError");
84
- expect(error.agentName).toBe("my-agent");
85
- expect(error.scheduleName).toBe("hourly-schedule");
86
- expect(error).toBeInstanceOf(Error);
87
- expect(error).toBeInstanceOf(SchedulerError);
88
- expect(error).toBeInstanceOf(ScheduleTriggerError);
89
- });
90
-
91
- it("preserves agent and schedule names exactly", () => {
92
- const error = new ScheduleTriggerError(
93
- "test",
94
- "agent-with-dashes",
95
- "schedule_with_underscores"
96
- );
97
-
98
- expect(error.agentName).toBe("agent-with-dashes");
99
- expect(error.scheduleName).toBe("schedule_with_underscores");
100
- });
101
-
102
- it("preserves cause when provided", () => {
103
- const cause = new Error("underlying failure");
104
- const error = new ScheduleTriggerError(
105
- "trigger failed",
106
- "my-agent",
107
- "my-schedule",
108
- { cause }
109
- );
110
-
111
- expect(error.cause).toBe(cause);
112
- });
113
-
114
- it("has undefined cause when not provided", () => {
115
- const error = new ScheduleTriggerError(
116
- "trigger failed",
117
- "my-agent",
118
- "my-schedule"
119
- );
120
-
121
- expect(error.cause).toBeUndefined();
122
- });
123
-
124
- it("can be caught as SchedulerError", () => {
125
- const error = new ScheduleTriggerError(
126
- "failed",
127
- "agent",
128
- "schedule"
129
- );
130
-
131
- let caught = false;
132
- try {
133
- throw error;
134
- } catch (e) {
135
- if (e instanceof SchedulerError) {
136
- caught = true;
137
- }
138
- }
139
-
140
- expect(caught).toBe(true);
141
- });
142
-
143
- it("can wrap another error as cause", () => {
144
- const networkError = new Error("Connection refused");
145
- const triggerError = new ScheduleTriggerError(
146
- `Failed to trigger agent: ${networkError.message}`,
147
- "remote-agent",
148
- "sync-schedule",
149
- { cause: networkError }
150
- );
151
-
152
- expect(triggerError.message).toBe(
153
- "Failed to trigger agent: Connection refused"
154
- );
155
- expect(triggerError.cause).toBe(networkError);
156
- expect(triggerError.agentName).toBe("remote-agent");
157
- expect(triggerError.scheduleName).toBe("sync-schedule");
158
- });
159
- });
@@ -1,515 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import {
3
- parseInterval,
4
- calculateNextTrigger,
5
- isScheduleDue,
6
- } from "../interval.js";
7
- import { IntervalParseError } from "../errors.js";
8
-
9
- // =============================================================================
10
- // parseInterval - Valid inputs
11
- // =============================================================================
12
-
13
- describe("parseInterval", () => {
14
- describe("valid intervals", () => {
15
- it("parses seconds correctly", () => {
16
- expect(parseInterval("5s")).toBe(5000);
17
- expect(parseInterval("1s")).toBe(1000);
18
- expect(parseInterval("30s")).toBe(30000);
19
- expect(parseInterval("60s")).toBe(60000);
20
- });
21
-
22
- it("parses minutes correctly", () => {
23
- expect(parseInterval("5m")).toBe(300000);
24
- expect(parseInterval("1m")).toBe(60000);
25
- expect(parseInterval("30m")).toBe(1800000);
26
- expect(parseInterval("60m")).toBe(3600000);
27
- });
28
-
29
- it("parses hours correctly", () => {
30
- expect(parseInterval("1h")).toBe(3600000);
31
- expect(parseInterval("2h")).toBe(7200000);
32
- expect(parseInterval("24h")).toBe(86400000);
33
- });
34
-
35
- it("parses days correctly", () => {
36
- expect(parseInterval("1d")).toBe(86400000);
37
- expect(parseInterval("7d")).toBe(604800000);
38
- expect(parseInterval("30d")).toBe(2592000000);
39
- });
40
-
41
- it("handles uppercase units", () => {
42
- expect(parseInterval("5S")).toBe(5000);
43
- expect(parseInterval("5M")).toBe(300000);
44
- expect(parseInterval("1H")).toBe(3600000);
45
- expect(parseInterval("1D")).toBe(86400000);
46
- });
47
-
48
- it("handles whitespace around the interval", () => {
49
- expect(parseInterval(" 5m ")).toBe(300000);
50
- expect(parseInterval("\t1h\t")).toBe(3600000);
51
- expect(parseInterval("\n30s\n")).toBe(30000);
52
- });
53
-
54
- it("handles large values", () => {
55
- expect(parseInterval("1000s")).toBe(1000000);
56
- expect(parseInterval("999m")).toBe(59940000);
57
- expect(parseInterval("100h")).toBe(360000000);
58
- expect(parseInterval("365d")).toBe(31536000000);
59
- });
60
- });
61
-
62
- // =============================================================================
63
- // parseInterval - Empty string
64
- // =============================================================================
65
-
66
- describe("empty string handling", () => {
67
- it("throws IntervalParseError for empty string", () => {
68
- expect(() => parseInterval("")).toThrow(IntervalParseError);
69
- expect(() => parseInterval("")).toThrow(/cannot be empty/);
70
- });
71
-
72
- it("throws IntervalParseError for whitespace-only string", () => {
73
- expect(() => parseInterval(" ")).toThrow(IntervalParseError);
74
- expect(() => parseInterval("\t")).toThrow(IntervalParseError);
75
- expect(() => parseInterval("\n")).toThrow(IntervalParseError);
76
- });
77
-
78
- it("includes the empty input in the error", () => {
79
- try {
80
- parseInterval("");
81
- } catch (e) {
82
- expect(e).toBeInstanceOf(IntervalParseError);
83
- expect((e as IntervalParseError).input).toBe("");
84
- }
85
- });
86
- });
87
-
88
- // =============================================================================
89
- // parseInterval - Missing unit
90
- // =============================================================================
91
-
92
- describe("missing unit handling", () => {
93
- it("throws IntervalParseError for number without unit", () => {
94
- expect(() => parseInterval("5")).toThrow(IntervalParseError);
95
- expect(() => parseInterval("5")).toThrow(/Missing time unit/);
96
- });
97
-
98
- it("throws IntervalParseError for various numbers without unit", () => {
99
- expect(() => parseInterval("1")).toThrow(IntervalParseError);
100
- expect(() => parseInterval("100")).toThrow(IntervalParseError);
101
- expect(() => parseInterval("9999")).toThrow(IntervalParseError);
102
- });
103
-
104
- it("includes the input in the error", () => {
105
- try {
106
- parseInterval("42");
107
- } catch (e) {
108
- expect(e).toBeInstanceOf(IntervalParseError);
109
- expect((e as IntervalParseError).input).toBe("42");
110
- }
111
- });
112
- });
113
-
114
- // =============================================================================
115
- // parseInterval - Invalid unit
116
- // =============================================================================
117
-
118
- describe("invalid unit handling", () => {
119
- it("throws IntervalParseError for invalid unit", () => {
120
- expect(() => parseInterval("5x")).toThrow(IntervalParseError);
121
- expect(() => parseInterval("5x")).toThrow(/Invalid time unit "x"/);
122
- });
123
-
124
- it("throws IntervalParseError for various invalid units", () => {
125
- expect(() => parseInterval("5ms")).toThrow(IntervalParseError); // milliseconds not supported
126
- expect(() => parseInterval("5sec")).toThrow(IntervalParseError); // "sec" not supported
127
- expect(() => parseInterval("5min")).toThrow(IntervalParseError); // "min" not supported
128
- expect(() => parseInterval("5hr")).toThrow(IntervalParseError); // "hr" not supported
129
- expect(() => parseInterval("5w")).toThrow(IntervalParseError); // weeks not supported
130
- expect(() => parseInterval("5y")).toThrow(IntervalParseError); // years not supported
131
- });
132
-
133
- it("includes valid units in the error message", () => {
134
- expect(() => parseInterval("5x")).toThrow(/s \(seconds\)/);
135
- expect(() => parseInterval("5x")).toThrow(/m \(minutes\)/);
136
- expect(() => parseInterval("5x")).toThrow(/h \(hours\)/);
137
- expect(() => parseInterval("5x")).toThrow(/d \(days\)/);
138
- });
139
- });
140
-
141
- // =============================================================================
142
- // parseInterval - Negative numbers
143
- // =============================================================================
144
-
145
- describe("negative number handling", () => {
146
- it("throws IntervalParseError for negative numbers", () => {
147
- expect(() => parseInterval("-5m")).toThrow(IntervalParseError);
148
- expect(() => parseInterval("-5m")).toThrow(/Negative intervals are not allowed/);
149
- });
150
-
151
- it("throws IntervalParseError for various negative values", () => {
152
- expect(() => parseInterval("-1s")).toThrow(IntervalParseError);
153
- expect(() => parseInterval("-100h")).toThrow(IntervalParseError);
154
- expect(() => parseInterval("-1d")).toThrow(IntervalParseError);
155
- });
156
-
157
- it("includes the input in the error", () => {
158
- try {
159
- parseInterval("-10m");
160
- } catch (e) {
161
- expect(e).toBeInstanceOf(IntervalParseError);
162
- expect((e as IntervalParseError).input).toBe("-10m");
163
- }
164
- });
165
- });
166
-
167
- // =============================================================================
168
- // parseInterval - Zero value
169
- // =============================================================================
170
-
171
- describe("zero value handling", () => {
172
- it("throws IntervalParseError for zero", () => {
173
- expect(() => parseInterval("0s")).toThrow(IntervalParseError);
174
- expect(() => parseInterval("0s")).toThrow(/Zero interval is not allowed/);
175
- });
176
-
177
- it("throws IntervalParseError for zero with any unit", () => {
178
- expect(() => parseInterval("0m")).toThrow(IntervalParseError);
179
- expect(() => parseInterval("0h")).toThrow(IntervalParseError);
180
- expect(() => parseInterval("0d")).toThrow(IntervalParseError);
181
- });
182
- });
183
-
184
- // =============================================================================
185
- // parseInterval - Decimal values
186
- // =============================================================================
187
-
188
- describe("decimal value handling", () => {
189
- it("throws IntervalParseError for decimal values", () => {
190
- expect(() => parseInterval("1.5h")).toThrow(IntervalParseError);
191
- expect(() => parseInterval("1.5h")).toThrow(/Decimal values are not supported/);
192
- });
193
-
194
- it("throws IntervalParseError for various decimal values", () => {
195
- expect(() => parseInterval("2.5m")).toThrow(IntervalParseError);
196
- expect(() => parseInterval("0.5s")).toThrow(IntervalParseError);
197
- expect(() => parseInterval("1.5d")).toThrow(IntervalParseError);
198
- });
199
-
200
- it("suggests using integers in the error message", () => {
201
- expect(() => parseInterval("1.5h")).toThrow(/Use integers only/);
202
- });
203
- });
204
-
205
- // =============================================================================
206
- // parseInterval - Invalid format
207
- // =============================================================================
208
-
209
- describe("invalid format handling", () => {
210
- it("throws IntervalParseError for letter-only input", () => {
211
- expect(() => parseInterval("abc")).toThrow(IntervalParseError);
212
- expect(() => parseInterval("abc")).toThrow(/Missing numeric value/);
213
- });
214
-
215
- it("throws IntervalParseError for random invalid formats", () => {
216
- expect(() => parseInterval("m5")).toThrow(IntervalParseError);
217
- expect(() => parseInterval("5 m 5")).toThrow(IntervalParseError);
218
- expect(() => parseInterval("5m5s")).toThrow(IntervalParseError);
219
- expect(() => parseInterval("five minutes")).toThrow(IntervalParseError);
220
- });
221
-
222
- it("throws IntervalParseError for special characters", () => {
223
- expect(() => parseInterval("5@m")).toThrow(IntervalParseError);
224
- expect(() => parseInterval("5#h")).toThrow(IntervalParseError);
225
- expect(() => parseInterval("5$d")).toThrow(IntervalParseError);
226
- });
227
- });
228
-
229
- // =============================================================================
230
- // IntervalParseError properties
231
- // =============================================================================
232
-
233
- describe("IntervalParseError", () => {
234
- it("has correct name property", () => {
235
- try {
236
- parseInterval("invalid");
237
- } catch (e) {
238
- expect(e).toBeInstanceOf(IntervalParseError);
239
- expect((e as IntervalParseError).name).toBe("IntervalParseError");
240
- }
241
- });
242
-
243
- it("preserves the input string", () => {
244
- const testInputs = ["", "5", "invalid", "-5m", "1.5h"];
245
-
246
- for (const input of testInputs) {
247
- try {
248
- parseInterval(input);
249
- } catch (e) {
250
- expect((e as IntervalParseError).input).toBe(input);
251
- }
252
- }
253
- });
254
-
255
- it("has descriptive error messages", () => {
256
- try {
257
- parseInterval("5x");
258
- } catch (e) {
259
- expect((e as IntervalParseError).message).toContain("5x");
260
- expect((e as IntervalParseError).message.length).toBeGreaterThan(20);
261
- }
262
- });
263
- });
264
- });
265
-
266
- // =============================================================================
267
- // calculateNextTrigger
268
- // =============================================================================
269
-
270
- describe("calculateNextTrigger", () => {
271
- beforeEach(() => {
272
- vi.useFakeTimers();
273
- vi.setSystemTime(new Date("2024-01-15T12:00:00.000Z"));
274
- });
275
-
276
- afterEach(() => {
277
- vi.useRealTimers();
278
- });
279
-
280
- describe("first run (no lastCompletedAt)", () => {
281
- it("returns now when lastCompletedAt is null", () => {
282
- const result = calculateNextTrigger(null, "5m");
283
- expect(result.getTime()).toBe(new Date("2024-01-15T12:00:00.000Z").getTime());
284
- });
285
-
286
- it("returns now regardless of interval when lastCompletedAt is null", () => {
287
- expect(calculateNextTrigger(null, "1s").getTime()).toBe(
288
- new Date("2024-01-15T12:00:00.000Z").getTime()
289
- );
290
- expect(calculateNextTrigger(null, "1h").getTime()).toBe(
291
- new Date("2024-01-15T12:00:00.000Z").getTime()
292
- );
293
- expect(calculateNextTrigger(null, "1d").getTime()).toBe(
294
- new Date("2024-01-15T12:00:00.000Z").getTime()
295
- );
296
- });
297
- });
298
-
299
- describe("subsequent runs", () => {
300
- it("returns lastCompletedAt + interval for basic case", () => {
301
- const lastCompleted = new Date("2024-01-15T11:55:00.000Z");
302
- const result = calculateNextTrigger(lastCompleted, "5m");
303
- // 11:55 + 5m = 12:00
304
- expect(result.getTime()).toBe(new Date("2024-01-15T12:00:00.000Z").getTime());
305
- });
306
-
307
- it("calculates next trigger with seconds interval", () => {
308
- const lastCompleted = new Date("2024-01-15T11:59:30.000Z");
309
- const result = calculateNextTrigger(lastCompleted, "30s");
310
- // 11:59:30 + 30s = 12:00:00
311
- expect(result.getTime()).toBe(new Date("2024-01-15T12:00:00.000Z").getTime());
312
- });
313
-
314
- it("calculates next trigger with hours interval", () => {
315
- const lastCompleted = new Date("2024-01-15T11:00:00.000Z");
316
- const result = calculateNextTrigger(lastCompleted, "1h");
317
- // 11:00 + 1h = 12:00
318
- expect(result.getTime()).toBe(new Date("2024-01-15T12:00:00.000Z").getTime());
319
- });
320
-
321
- it("calculates next trigger with days interval", () => {
322
- const lastCompleted = new Date("2024-01-14T12:00:00.000Z");
323
- const result = calculateNextTrigger(lastCompleted, "1d");
324
- // Jan 14 12:00 + 1d = Jan 15 12:00
325
- expect(result.getTime()).toBe(new Date("2024-01-15T12:00:00.000Z").getTime());
326
- });
327
-
328
- it("returns future time when next trigger is in the future", () => {
329
- const lastCompleted = new Date("2024-01-15T11:58:00.000Z");
330
- const result = calculateNextTrigger(lastCompleted, "5m");
331
- // 11:58 + 5m = 12:03 (in the future)
332
- expect(result.getTime()).toBe(new Date("2024-01-15T12:03:00.000Z").getTime());
333
- });
334
- });
335
-
336
- describe("clock skew handling", () => {
337
- it("returns now when calculated next trigger is in the past", () => {
338
- // Last completed was a long time ago
339
- const lastCompleted = new Date("2024-01-15T10:00:00.000Z");
340
- const result = calculateNextTrigger(lastCompleted, "5m");
341
- // 10:00 + 5m = 10:05, which is in the past (now is 12:00)
342
- // Should return now instead
343
- expect(result.getTime()).toBe(new Date("2024-01-15T12:00:00.000Z").getTime());
344
- });
345
-
346
- it("handles very old lastCompletedAt gracefully", () => {
347
- const lastCompleted = new Date("2024-01-01T00:00:00.000Z");
348
- const result = calculateNextTrigger(lastCompleted, "1h");
349
- // Jan 1 00:00 + 1h = Jan 1 01:00, way in the past
350
- expect(result.getTime()).toBe(new Date("2024-01-15T12:00:00.000Z").getTime());
351
- });
352
- });
353
-
354
- describe("jitter", () => {
355
- it("adds no jitter when jitterPercent is 0", () => {
356
- const lastCompleted = new Date("2024-01-15T11:55:00.000Z");
357
- const result = calculateNextTrigger(lastCompleted, "5m", 0);
358
- expect(result.getTime()).toBe(new Date("2024-01-15T12:00:00.000Z").getTime());
359
- });
360
-
361
- it("adds no jitter when jitterPercent is undefined", () => {
362
- const lastCompleted = new Date("2024-01-15T11:55:00.000Z");
363
- const result = calculateNextTrigger(lastCompleted, "5m");
364
- expect(result.getTime()).toBe(new Date("2024-01-15T12:00:00.000Z").getTime());
365
- });
366
-
367
- it("adds jitter within expected range", () => {
368
- // Mock Math.random to return 0.5 (middle of range)
369
- vi.spyOn(Math, "random").mockReturnValue(0.5);
370
-
371
- const lastCompleted = new Date("2024-01-15T11:00:00.000Z");
372
- // 1h = 3600000ms, 5% jitter = 180000ms max
373
- // With random = 0.5, jitter = 90000ms (1.5 minutes)
374
- const result = calculateNextTrigger(lastCompleted, "1h", 5);
375
-
376
- // 11:00 + 1h + 1.5m = 12:01:30
377
- expect(result.getTime()).toBe(
378
- new Date("2024-01-15T12:01:30.000Z").getTime()
379
- );
380
-
381
- vi.spyOn(Math, "random").mockRestore();
382
- });
383
-
384
- it("clamps jitter to maximum 10%", () => {
385
- vi.spyOn(Math, "random").mockReturnValue(1.0); // Max random
386
-
387
- const lastCompleted = new Date("2024-01-15T11:00:00.000Z");
388
- // Request 20% jitter, but should be clamped to 10%
389
- // 1h = 3600000ms, 10% = 360000ms (6 minutes)
390
- const result = calculateNextTrigger(lastCompleted, "1h", 20);
391
-
392
- // 11:00 + 1h + 6m = 12:06
393
- expect(result.getTime()).toBe(
394
- new Date("2024-01-15T12:06:00.000Z").getTime()
395
- );
396
-
397
- vi.spyOn(Math, "random").mockRestore();
398
- });
399
-
400
- it("handles negative jitter by treating as 0", () => {
401
- const lastCompleted = new Date("2024-01-15T11:55:00.000Z");
402
- const result = calculateNextTrigger(lastCompleted, "5m", -5);
403
- // Negative jitter should be clamped to 0
404
- expect(result.getTime()).toBe(new Date("2024-01-15T12:00:00.000Z").getTime());
405
- });
406
-
407
- it("jitter range is 0 to jitterPercent% of interval", () => {
408
- // Test with random = 0 (minimum jitter)
409
- vi.spyOn(Math, "random").mockReturnValue(0);
410
- const lastCompleted = new Date("2024-01-15T11:00:00.000Z");
411
- const resultMin = calculateNextTrigger(lastCompleted, "1h", 10);
412
- expect(resultMin.getTime()).toBe(
413
- new Date("2024-01-15T12:00:00.000Z").getTime()
414
- );
415
-
416
- // Test with random = 1 (maximum jitter)
417
- vi.spyOn(Math, "random").mockReturnValue(1.0);
418
- const resultMax = calculateNextTrigger(lastCompleted, "1h", 10);
419
- // 10% of 1h = 6 minutes
420
- expect(resultMax.getTime()).toBe(
421
- new Date("2024-01-15T12:06:00.000Z").getTime()
422
- );
423
-
424
- vi.spyOn(Math, "random").mockRestore();
425
- });
426
- });
427
-
428
- describe("error handling", () => {
429
- it("throws IntervalParseError for invalid interval", () => {
430
- const lastCompleted = new Date("2024-01-15T11:00:00.000Z");
431
- expect(() => calculateNextTrigger(lastCompleted, "invalid")).toThrow(
432
- IntervalParseError
433
- );
434
- });
435
-
436
- it("throws for empty interval", () => {
437
- const lastCompleted = new Date("2024-01-15T11:00:00.000Z");
438
- expect(() => calculateNextTrigger(lastCompleted, "")).toThrow(
439
- IntervalParseError
440
- );
441
- });
442
- });
443
- });
444
-
445
- // =============================================================================
446
- // isScheduleDue
447
- // =============================================================================
448
-
449
- describe("isScheduleDue", () => {
450
- describe("with explicit now parameter", () => {
451
- it("returns false when nextRunAt is in the future", () => {
452
- const nextRun = new Date("2024-01-15T12:05:00.000Z");
453
- const now = new Date("2024-01-15T12:00:00.000Z");
454
- expect(isScheduleDue(nextRun, now)).toBe(false);
455
- });
456
-
457
- it("returns true when nextRunAt equals now", () => {
458
- const nextRun = new Date("2024-01-15T12:00:00.000Z");
459
- const now = new Date("2024-01-15T12:00:00.000Z");
460
- expect(isScheduleDue(nextRun, now)).toBe(true);
461
- });
462
-
463
- it("returns true when nextRunAt is in the past", () => {
464
- const nextRun = new Date("2024-01-15T11:55:00.000Z");
465
- const now = new Date("2024-01-15T12:00:00.000Z");
466
- expect(isScheduleDue(nextRun, now)).toBe(true);
467
- });
468
-
469
- it("handles millisecond precision", () => {
470
- const nextRun = new Date("2024-01-15T12:00:00.001Z");
471
- const now = new Date("2024-01-15T12:00:00.000Z");
472
- expect(isScheduleDue(nextRun, now)).toBe(false);
473
-
474
- const nextRun2 = new Date("2024-01-15T12:00:00.000Z");
475
- const now2 = new Date("2024-01-15T12:00:00.001Z");
476
- expect(isScheduleDue(nextRun2, now2)).toBe(true);
477
- });
478
- });
479
-
480
- describe("with default now", () => {
481
- beforeEach(() => {
482
- vi.useFakeTimers();
483
- vi.setSystemTime(new Date("2024-01-15T12:00:00.000Z"));
484
- });
485
-
486
- afterEach(() => {
487
- vi.useRealTimers();
488
- });
489
-
490
- it("uses current time when now is not provided", () => {
491
- const futureRun = new Date("2024-01-15T12:05:00.000Z");
492
- expect(isScheduleDue(futureRun)).toBe(false);
493
-
494
- const pastRun = new Date("2024-01-15T11:55:00.000Z");
495
- expect(isScheduleDue(pastRun)).toBe(true);
496
-
497
- const currentRun = new Date("2024-01-15T12:00:00.000Z");
498
- expect(isScheduleDue(currentRun)).toBe(true);
499
- });
500
- });
501
-
502
- describe("edge cases", () => {
503
- it("handles very old dates", () => {
504
- const veryOldDate = new Date("2000-01-01T00:00:00.000Z");
505
- const now = new Date("2024-01-15T12:00:00.000Z");
506
- expect(isScheduleDue(veryOldDate, now)).toBe(true);
507
- });
508
-
509
- it("handles very future dates", () => {
510
- const veryFutureDate = new Date("2100-01-01T00:00:00.000Z");
511
- const now = new Date("2024-01-15T12:00:00.000Z");
512
- expect(isScheduleDue(veryFutureDate, now)).toBe(false);
513
- });
514
- });
515
- });