@herdctl/core 0.0.1 → 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 (284) hide show
  1. package/dist/config/__tests__/agent.test.js +61 -13
  2. package/dist/config/__tests__/agent.test.js.map +1 -1
  3. package/dist/config/__tests__/merge.test.js +10 -3
  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 +841 -27
  12. package/dist/config/schema.d.ts.map +1 -1
  13. package/dist/config/schema.js +129 -10
  14. package/dist/config/schema.js.map +1 -1
  15. package/dist/fleet-manager/__tests__/coverage.test.js +14 -331
  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 +114 -0
  20. package/dist/fleet-manager/__tests__/integration.test.js.map +1 -1
  21. package/dist/fleet-manager/__tests__/job-control.test.js +13 -14
  22. package/dist/fleet-manager/__tests__/job-control.test.js.map +1 -1
  23. package/dist/fleet-manager/__tests__/reload.test.js +12 -2
  24. package/dist/fleet-manager/__tests__/reload.test.js.map +1 -1
  25. package/dist/fleet-manager/__tests__/status-queries.test.js +6 -0
  26. package/dist/fleet-manager/__tests__/status-queries.test.js.map +1 -1
  27. package/dist/fleet-manager/__tests__/trigger.test.js +10 -2
  28. package/dist/fleet-manager/__tests__/trigger.test.js.map +1 -1
  29. package/dist/fleet-manager/config-reload.d.ts +164 -0
  30. package/dist/fleet-manager/config-reload.d.ts.map +1 -0
  31. package/dist/fleet-manager/config-reload.js +445 -0
  32. package/dist/fleet-manager/config-reload.js.map +1 -0
  33. package/dist/fleet-manager/context.d.ts +76 -0
  34. package/dist/fleet-manager/context.d.ts.map +1 -0
  35. package/dist/fleet-manager/context.js +11 -0
  36. package/dist/fleet-manager/context.js.map +1 -0
  37. package/dist/fleet-manager/errors.d.ts +0 -25
  38. package/dist/fleet-manager/errors.d.ts.map +1 -1
  39. package/dist/fleet-manager/errors.js +0 -38
  40. package/dist/fleet-manager/errors.js.map +1 -1
  41. package/dist/fleet-manager/event-emitters.d.ts +123 -0
  42. package/dist/fleet-manager/event-emitters.d.ts.map +1 -0
  43. package/dist/fleet-manager/event-emitters.js +136 -0
  44. package/dist/fleet-manager/event-emitters.js.map +1 -0
  45. package/dist/fleet-manager/event-types.d.ts +0 -15
  46. package/dist/fleet-manager/event-types.d.ts.map +1 -1
  47. package/dist/fleet-manager/fleet-manager.d.ts +40 -653
  48. package/dist/fleet-manager/fleet-manager.d.ts.map +1 -1
  49. package/dist/fleet-manager/fleet-manager.js +95 -1720
  50. package/dist/fleet-manager/fleet-manager.js.map +1 -1
  51. package/dist/fleet-manager/index.d.ts +13 -2
  52. package/dist/fleet-manager/index.d.ts.map +1 -1
  53. package/dist/fleet-manager/index.js +19 -6
  54. package/dist/fleet-manager/index.js.map +1 -1
  55. package/dist/fleet-manager/job-control.d.ts +67 -0
  56. package/dist/fleet-manager/job-control.d.ts.map +1 -0
  57. package/dist/fleet-manager/job-control.js +333 -0
  58. package/dist/fleet-manager/job-control.js.map +1 -0
  59. package/dist/fleet-manager/log-streaming.d.ts +171 -0
  60. package/dist/fleet-manager/log-streaming.d.ts.map +1 -0
  61. package/dist/fleet-manager/log-streaming.js +503 -0
  62. package/dist/fleet-manager/log-streaming.js.map +1 -0
  63. package/dist/fleet-manager/schedule-executor.d.ts +63 -0
  64. package/dist/fleet-manager/schedule-executor.d.ts.map +1 -0
  65. package/dist/fleet-manager/schedule-executor.js +209 -0
  66. package/dist/fleet-manager/schedule-executor.js.map +1 -0
  67. package/dist/fleet-manager/schedule-management.d.ts +71 -0
  68. package/dist/fleet-manager/schedule-management.d.ts.map +1 -0
  69. package/dist/fleet-manager/schedule-management.js +171 -0
  70. package/dist/fleet-manager/schedule-management.js.map +1 -0
  71. package/dist/fleet-manager/status-queries.d.ts +105 -0
  72. package/dist/fleet-manager/status-queries.d.ts.map +1 -0
  73. package/dist/fleet-manager/status-queries.js +247 -0
  74. package/dist/fleet-manager/status-queries.js.map +1 -0
  75. package/dist/fleet-manager/types.d.ts +0 -39
  76. package/dist/fleet-manager/types.d.ts.map +1 -1
  77. package/dist/runner/__tests__/job-executor.test.js +206 -1
  78. package/dist/runner/__tests__/job-executor.test.js.map +1 -1
  79. package/dist/runner/job-executor.d.ts +9 -0
  80. package/dist/runner/job-executor.d.ts.map +1 -1
  81. package/dist/runner/job-executor.js +78 -4
  82. package/dist/runner/job-executor.js.map +1 -1
  83. package/dist/runner/message-processor.d.ts.map +1 -1
  84. package/dist/runner/message-processor.js +53 -0
  85. package/dist/runner/message-processor.js.map +1 -1
  86. package/dist/runner/types.d.ts +3 -1
  87. package/dist/runner/types.d.ts.map +1 -1
  88. package/dist/scheduler/__tests__/cron.test.d.ts +2 -0
  89. package/dist/scheduler/__tests__/cron.test.d.ts.map +1 -0
  90. package/dist/scheduler/__tests__/cron.test.js +867 -0
  91. package/dist/scheduler/__tests__/cron.test.js.map +1 -0
  92. package/dist/scheduler/__tests__/scheduler.test.js +164 -5
  93. package/dist/scheduler/__tests__/scheduler.test.js.map +1 -1
  94. package/dist/scheduler/cron.d.ts +126 -0
  95. package/dist/scheduler/cron.d.ts.map +1 -0
  96. package/dist/scheduler/cron.js +390 -0
  97. package/dist/scheduler/cron.js.map +1 -0
  98. package/dist/scheduler/errors.d.ts +81 -1
  99. package/dist/scheduler/errors.d.ts.map +1 -1
  100. package/dist/scheduler/errors.js +81 -6
  101. package/dist/scheduler/errors.js.map +1 -1
  102. package/dist/scheduler/index.d.ts +1 -0
  103. package/dist/scheduler/index.d.ts.map +1 -1
  104. package/dist/scheduler/index.js +2 -0
  105. package/dist/scheduler/index.js.map +1 -1
  106. package/dist/scheduler/schedule-runner.d.ts +2 -2
  107. package/dist/scheduler/schedule-runner.d.ts.map +1 -1
  108. package/dist/scheduler/schedule-runner.js +20 -8
  109. package/dist/scheduler/schedule-runner.js.map +1 -1
  110. package/dist/scheduler/scheduler.d.ts +4 -4
  111. package/dist/scheduler/scheduler.d.ts.map +1 -1
  112. package/dist/scheduler/scheduler.js +95 -20
  113. package/dist/scheduler/scheduler.js.map +1 -1
  114. package/dist/scheduler/types.d.ts +1 -1
  115. package/dist/scheduler/types.d.ts.map +1 -1
  116. package/dist/state/schemas/job-metadata.d.ts +2 -2
  117. package/package.json +33 -8
  118. package/.turbo/turbo-build.log +0 -4
  119. package/.turbo/turbo-test.log +0 -219
  120. package/.turbo/turbo-typecheck.log +0 -4
  121. package/coverage/base.css +0 -224
  122. package/coverage/block-navigation.js +0 -87
  123. package/coverage/coverage-final.json +0 -51
  124. package/coverage/favicon.png +0 -0
  125. package/coverage/index.html +0 -251
  126. package/coverage/prettify.css +0 -1
  127. package/coverage/prettify.js +0 -2
  128. package/coverage/sort-arrow-sprite.png +0 -0
  129. package/coverage/sorter.js +0 -210
  130. package/coverage/src/config/index.html +0 -191
  131. package/coverage/src/config/index.ts.html +0 -442
  132. package/coverage/src/config/interpolate.ts.html +0 -652
  133. package/coverage/src/config/loader.ts.html +0 -1501
  134. package/coverage/src/config/merge.ts.html +0 -823
  135. package/coverage/src/config/parser.ts.html +0 -1213
  136. package/coverage/src/config/schema.ts.html +0 -1123
  137. package/coverage/src/fleet-manager/errors.ts.html +0 -2326
  138. package/coverage/src/fleet-manager/event-types.ts.html +0 -1219
  139. package/coverage/src/fleet-manager/fleet-manager.ts.html +0 -7030
  140. package/coverage/src/fleet-manager/index.html +0 -206
  141. package/coverage/src/fleet-manager/index.ts.html +0 -469
  142. package/coverage/src/fleet-manager/job-manager.ts.html +0 -2074
  143. package/coverage/src/fleet-manager/job-queue.ts.html +0 -2479
  144. package/coverage/src/fleet-manager/types.ts.html +0 -2602
  145. package/coverage/src/index.html +0 -116
  146. package/coverage/src/index.ts.html +0 -181
  147. package/coverage/src/runner/errors.ts.html +0 -1006
  148. package/coverage/src/runner/index.html +0 -191
  149. package/coverage/src/runner/index.ts.html +0 -256
  150. package/coverage/src/runner/job-executor.ts.html +0 -1429
  151. package/coverage/src/runner/message-processor.ts.html +0 -1150
  152. package/coverage/src/runner/sdk-adapter.ts.html +0 -658
  153. package/coverage/src/runner/types.ts.html +0 -559
  154. package/coverage/src/scheduler/errors.ts.html +0 -388
  155. package/coverage/src/scheduler/index.html +0 -206
  156. package/coverage/src/scheduler/index.ts.html +0 -244
  157. package/coverage/src/scheduler/interval.ts.html +0 -652
  158. package/coverage/src/scheduler/schedule-runner.ts.html +0 -1411
  159. package/coverage/src/scheduler/schedule-state.ts.html +0 -718
  160. package/coverage/src/scheduler/scheduler.ts.html +0 -1795
  161. package/coverage/src/scheduler/types.ts.html +0 -733
  162. package/coverage/src/state/directory.ts.html +0 -736
  163. package/coverage/src/state/errors.ts.html +0 -376
  164. package/coverage/src/state/fleet-state.ts.html +0 -937
  165. package/coverage/src/state/index.html +0 -221
  166. package/coverage/src/state/index.ts.html +0 -322
  167. package/coverage/src/state/job-metadata.ts.html +0 -1420
  168. package/coverage/src/state/job-output.ts.html +0 -1033
  169. package/coverage/src/state/schemas/fleet-state.ts.html +0 -445
  170. package/coverage/src/state/schemas/index.html +0 -176
  171. package/coverage/src/state/schemas/index.ts.html +0 -286
  172. package/coverage/src/state/schemas/job-metadata.ts.html +0 -628
  173. package/coverage/src/state/schemas/job-output.ts.html +0 -616
  174. package/coverage/src/state/schemas/session-info.ts.html +0 -361
  175. package/coverage/src/state/session.ts.html +0 -844
  176. package/coverage/src/state/types.ts.html +0 -262
  177. package/coverage/src/state/utils/atomic.ts.html +0 -748
  178. package/coverage/src/state/utils/index.html +0 -146
  179. package/coverage/src/state/utils/index.ts.html +0 -103
  180. package/coverage/src/state/utils/reads.ts.html +0 -1621
  181. package/coverage/src/work-sources/adapters/github.ts.html +0 -3583
  182. package/coverage/src/work-sources/adapters/index.html +0 -131
  183. package/coverage/src/work-sources/adapters/index.ts.html +0 -277
  184. package/coverage/src/work-sources/errors.ts.html +0 -298
  185. package/coverage/src/work-sources/index.html +0 -176
  186. package/coverage/src/work-sources/index.ts.html +0 -529
  187. package/coverage/src/work-sources/manager.ts.html +0 -1324
  188. package/coverage/src/work-sources/registry.ts.html +0 -619
  189. package/coverage/src/work-sources/types.ts.html +0 -568
  190. package/dist/fleet-manager/__tests__/event-helpers.test.d.ts +0 -7
  191. package/dist/fleet-manager/__tests__/event-helpers.test.d.ts.map +0 -1
  192. package/dist/fleet-manager/__tests__/event-helpers.test.js +0 -368
  193. package/dist/fleet-manager/__tests__/event-helpers.test.js.map +0 -1
  194. package/src/config/__tests__/agent.test.ts +0 -864
  195. package/src/config/__tests__/interpolate.test.ts +0 -644
  196. package/src/config/__tests__/loader.test.ts +0 -784
  197. package/src/config/__tests__/merge.test.ts +0 -751
  198. package/src/config/__tests__/parser.test.ts +0 -533
  199. package/src/config/__tests__/schema.test.ts +0 -873
  200. package/src/config/index.ts +0 -119
  201. package/src/config/interpolate.ts +0 -189
  202. package/src/config/loader.ts +0 -472
  203. package/src/config/merge.ts +0 -246
  204. package/src/config/parser.ts +0 -376
  205. package/src/config/schema.ts +0 -346
  206. package/src/fleet-manager/__tests__/coverage.test.ts +0 -2869
  207. package/src/fleet-manager/__tests__/errors.test.ts +0 -660
  208. package/src/fleet-manager/__tests__/event-helpers.test.ts +0 -448
  209. package/src/fleet-manager/__tests__/integration.test.ts +0 -1209
  210. package/src/fleet-manager/__tests__/job-control.test.ts +0 -283
  211. package/src/fleet-manager/__tests__/job-manager.test.ts +0 -869
  212. package/src/fleet-manager/__tests__/job-queue.test.ts +0 -401
  213. package/src/fleet-manager/__tests__/reload.test.ts +0 -751
  214. package/src/fleet-manager/__tests__/status-queries.test.ts +0 -595
  215. package/src/fleet-manager/__tests__/trigger.test.ts +0 -601
  216. package/src/fleet-manager/errors.ts +0 -747
  217. package/src/fleet-manager/event-types.ts +0 -378
  218. package/src/fleet-manager/fleet-manager.ts +0 -2315
  219. package/src/fleet-manager/index.ts +0 -128
  220. package/src/fleet-manager/job-manager.ts +0 -663
  221. package/src/fleet-manager/job-queue.ts +0 -798
  222. package/src/fleet-manager/types.ts +0 -839
  223. package/src/index.ts +0 -32
  224. package/src/runner/__tests__/errors.test.ts +0 -382
  225. package/src/runner/__tests__/job-executor.test.ts +0 -1708
  226. package/src/runner/__tests__/message-processor.test.ts +0 -960
  227. package/src/runner/__tests__/sdk-adapter.test.ts +0 -626
  228. package/src/runner/errors.ts +0 -307
  229. package/src/runner/index.ts +0 -57
  230. package/src/runner/job-executor.ts +0 -448
  231. package/src/runner/message-processor.ts +0 -355
  232. package/src/runner/sdk-adapter.ts +0 -191
  233. package/src/runner/types.ts +0 -158
  234. package/src/scheduler/__tests__/errors.test.ts +0 -159
  235. package/src/scheduler/__tests__/interval.test.ts +0 -515
  236. package/src/scheduler/__tests__/schedule-runner.test.ts +0 -798
  237. package/src/scheduler/__tests__/schedule-state.test.ts +0 -671
  238. package/src/scheduler/__tests__/scheduler.test.ts +0 -1280
  239. package/src/scheduler/errors.ts +0 -101
  240. package/src/scheduler/index.ts +0 -53
  241. package/src/scheduler/interval.ts +0 -189
  242. package/src/scheduler/schedule-runner.ts +0 -442
  243. package/src/scheduler/schedule-state.ts +0 -211
  244. package/src/scheduler/scheduler.ts +0 -570
  245. package/src/scheduler/types.ts +0 -216
  246. package/src/state/__tests__/directory.test.ts +0 -595
  247. package/src/state/__tests__/fleet-state.test.ts +0 -868
  248. package/src/state/__tests__/job-metadata-schema.test.ts +0 -414
  249. package/src/state/__tests__/job-metadata.test.ts +0 -831
  250. package/src/state/__tests__/job-output.test.ts +0 -856
  251. package/src/state/__tests__/session-schema.test.ts +0 -378
  252. package/src/state/__tests__/session.test.ts +0 -604
  253. package/src/state/directory.ts +0 -217
  254. package/src/state/errors.ts +0 -97
  255. package/src/state/fleet-state.ts +0 -284
  256. package/src/state/index.ts +0 -79
  257. package/src/state/job-metadata.ts +0 -445
  258. package/src/state/job-output.ts +0 -316
  259. package/src/state/schemas/__tests__/job-output.test.ts +0 -338
  260. package/src/state/schemas/fleet-state.ts +0 -120
  261. package/src/state/schemas/index.ts +0 -67
  262. package/src/state/schemas/job-metadata.ts +0 -181
  263. package/src/state/schemas/job-output.ts +0 -177
  264. package/src/state/schemas/session-info.ts +0 -92
  265. package/src/state/session.ts +0 -253
  266. package/src/state/types.ts +0 -59
  267. package/src/state/utils/__tests__/atomic.test.ts +0 -723
  268. package/src/state/utils/__tests__/reads.test.ts +0 -1071
  269. package/src/state/utils/atomic.ts +0 -221
  270. package/src/state/utils/index.ts +0 -6
  271. package/src/state/utils/reads.ts +0 -512
  272. package/src/work-sources/__tests__/github.test.ts +0 -1800
  273. package/src/work-sources/__tests__/manager.test.ts +0 -529
  274. package/src/work-sources/__tests__/registry.test.ts +0 -477
  275. package/src/work-sources/__tests__/types.test.ts +0 -479
  276. package/src/work-sources/adapters/github.ts +0 -1166
  277. package/src/work-sources/adapters/index.ts +0 -64
  278. package/src/work-sources/errors.ts +0 -71
  279. package/src/work-sources/index.ts +0 -148
  280. package/src/work-sources/manager.ts +0 -413
  281. package/src/work-sources/registry.ts +0 -178
  282. package/src/work-sources/types.ts +0 -161
  283. package/tsconfig.json +0 -9
  284. package/vitest.config.ts +0 -19
@@ -1,868 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
- import { mkdir, rm, realpath, writeFile, readFile } from "node:fs/promises";
3
- import { join } from "node:path";
4
- import { tmpdir } from "node:os";
5
- import {
6
- readFleetState,
7
- writeFleetState,
8
- updateAgentState,
9
- initializeFleetState,
10
- removeAgentState,
11
- type StateLogger,
12
- } from "../fleet-state.js";
13
- import {
14
- createInitialFleetState,
15
- type FleetState,
16
- type AgentState,
17
- } from "../schemas/fleet-state.js";
18
- import { StateFileError } from "../errors.js";
19
-
20
- // Helper to create a temp directory
21
- async function createTempDir(): Promise<string> {
22
- const baseDir = join(
23
- tmpdir(),
24
- `herdctl-fleet-state-test-${Date.now()}-${Math.random().toString(36).slice(2)}`
25
- );
26
- await mkdir(baseDir, { recursive: true });
27
- // Resolve to real path to handle macOS /var -> /private/var symlink
28
- return await realpath(baseDir);
29
- }
30
-
31
- // Helper to create a mock logger
32
- function createMockLogger(): StateLogger & { warnings: string[] } {
33
- const warnings: string[] = [];
34
- return {
35
- warnings,
36
- warn: (message: string) => warnings.push(message),
37
- };
38
- }
39
-
40
- describe("readFleetState", () => {
41
- let tempDir: string;
42
-
43
- beforeEach(async () => {
44
- tempDir = await createTempDir();
45
- });
46
-
47
- afterEach(async () => {
48
- await rm(tempDir, { recursive: true, force: true });
49
- });
50
-
51
- describe("valid state files", () => {
52
- it("reads and validates state.yaml with full fleet state", async () => {
53
- const stateFile = join(tempDir, "state.yaml");
54
- const stateContent = `
55
- fleet:
56
- started_at: "2024-01-15T10:30:00Z"
57
- agents:
58
- my-agent:
59
- status: running
60
- current_job: job-123
61
- last_job: job-122
62
- next_schedule: hourly
63
- next_trigger_at: "2024-01-15T11:00:00Z"
64
- container_id: abc123
65
- other-agent:
66
- status: idle
67
- `;
68
- await writeFile(stateFile, stateContent, "utf-8");
69
-
70
- const state = await readFleetState(stateFile);
71
-
72
- expect(state.fleet.started_at).toBe("2024-01-15T10:30:00Z");
73
- expect(state.agents["my-agent"]).toEqual({
74
- status: "running",
75
- current_job: "job-123",
76
- last_job: "job-122",
77
- next_schedule: "hourly",
78
- next_trigger_at: "2024-01-15T11:00:00Z",
79
- container_id: "abc123",
80
- });
81
- expect(state.agents["other-agent"]).toEqual({
82
- status: "idle",
83
- });
84
- });
85
-
86
- it("reads state with agent error status", async () => {
87
- const stateFile = join(tempDir, "state.yaml");
88
- const stateContent = `
89
- fleet:
90
- started_at: "2024-01-15T10:30:00Z"
91
- agents:
92
- failed-agent:
93
- status: error
94
- last_job: job-100
95
- error_message: "Container exited with code 1"
96
- `;
97
- await writeFile(stateFile, stateContent, "utf-8");
98
-
99
- const state = await readFleetState(stateFile);
100
-
101
- expect(state.agents["failed-agent"]).toEqual({
102
- status: "error",
103
- last_job: "job-100",
104
- error_message: "Container exited with code 1",
105
- });
106
- });
107
-
108
- it("applies default values for missing optional fields", async () => {
109
- const stateFile = join(tempDir, "state.yaml");
110
- const stateContent = `
111
- agents:
112
- minimal-agent:
113
- status: idle
114
- `;
115
- await writeFile(stateFile, stateContent, "utf-8");
116
-
117
- const state = await readFleetState(stateFile);
118
-
119
- expect(state.fleet).toEqual({});
120
- expect(state.agents["minimal-agent"]).toEqual({
121
- status: "idle",
122
- });
123
- });
124
-
125
- it("handles empty agents map", async () => {
126
- const stateFile = join(tempDir, "state.yaml");
127
- const stateContent = `
128
- fleet:
129
- started_at: "2024-01-15T10:30:00Z"
130
- agents: {}
131
- `;
132
- await writeFile(stateFile, stateContent, "utf-8");
133
-
134
- const state = await readFleetState(stateFile);
135
-
136
- expect(state.fleet.started_at).toBe("2024-01-15T10:30:00Z");
137
- expect(state.agents).toEqual({});
138
- });
139
- });
140
-
141
- describe("missing file handling", () => {
142
- it("returns default empty state when file does not exist", async () => {
143
- const stateFile = join(tempDir, "nonexistent.yaml");
144
-
145
- const state = await readFleetState(stateFile);
146
-
147
- expect(state).toEqual(createInitialFleetState());
148
- expect(state.fleet).toEqual({});
149
- expect(state.agents).toEqual({});
150
- });
151
-
152
- it("does not log warning for missing file", async () => {
153
- const stateFile = join(tempDir, "nonexistent.yaml");
154
- const logger = createMockLogger();
155
-
156
- await readFleetState(stateFile, { logger });
157
-
158
- expect(logger.warnings).toHaveLength(0);
159
- });
160
- });
161
-
162
- describe("empty file handling", () => {
163
- it("returns default state for empty file", async () => {
164
- const stateFile = join(tempDir, "empty.yaml");
165
- await writeFile(stateFile, "", "utf-8");
166
-
167
- const state = await readFleetState(stateFile);
168
-
169
- expect(state).toEqual(createInitialFleetState());
170
- });
171
-
172
- it("returns default state for file with only whitespace", async () => {
173
- const stateFile = join(tempDir, "whitespace.yaml");
174
- await writeFile(stateFile, " \n \n ", "utf-8");
175
-
176
- const state = await readFleetState(stateFile);
177
-
178
- expect(state).toEqual(createInitialFleetState());
179
- });
180
-
181
- it("returns default state for file with only comments", async () => {
182
- const stateFile = join(tempDir, "comments.yaml");
183
- await writeFile(stateFile, "# This is a comment\n", "utf-8");
184
-
185
- const state = await readFleetState(stateFile);
186
-
187
- expect(state).toEqual(createInitialFleetState());
188
- });
189
- });
190
-
191
- describe("corrupted file handling", () => {
192
- it("returns default state and logs warning for invalid YAML syntax", async () => {
193
- const stateFile = join(tempDir, "invalid-syntax.yaml");
194
- await writeFile(stateFile, "fleet: [unclosed", "utf-8");
195
- const logger = createMockLogger();
196
-
197
- const state = await readFleetState(stateFile, { logger });
198
-
199
- expect(state).toEqual(createInitialFleetState());
200
- expect(logger.warnings).toHaveLength(1);
201
- // YAML parse errors come through as read errors
202
- expect(logger.warnings[0]).toContain("Using default state");
203
- });
204
-
205
- it("returns default state and logs warning for invalid status enum", async () => {
206
- const stateFile = join(tempDir, "invalid-status.yaml");
207
- const stateContent = `
208
- agents:
209
- bad-agent:
210
- status: invalid_status
211
- `;
212
- await writeFile(stateFile, stateContent, "utf-8");
213
- const logger = createMockLogger();
214
-
215
- const state = await readFleetState(stateFile, { logger });
216
-
217
- expect(state).toEqual(createInitialFleetState());
218
- expect(logger.warnings).toHaveLength(1);
219
- expect(logger.warnings[0]).toContain("Corrupted state file");
220
- });
221
-
222
- it("returns default state and logs warning for wrong type structure", async () => {
223
- const stateFile = join(tempDir, "wrong-type.yaml");
224
- const stateContent = `
225
- agents: "not an object"
226
- `;
227
- await writeFile(stateFile, stateContent, "utf-8");
228
- const logger = createMockLogger();
229
-
230
- const state = await readFleetState(stateFile, { logger });
231
-
232
- expect(state).toEqual(createInitialFleetState());
233
- expect(logger.warnings).toHaveLength(1);
234
- });
235
-
236
- it("uses default console.warn when no logger provided", async () => {
237
- const stateFile = join(tempDir, "invalid.yaml");
238
- await writeFile(stateFile, "fleet: [unclosed", "utf-8");
239
- const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
240
-
241
- const state = await readFleetState(stateFile);
242
-
243
- expect(state).toEqual(createInitialFleetState());
244
- expect(consoleSpy).toHaveBeenCalled();
245
- consoleSpy.mockRestore();
246
- });
247
- });
248
-
249
- describe("FleetState type validation", () => {
250
- it("validates all AgentStatus enum values", async () => {
251
- const stateFile = join(tempDir, "all-statuses.yaml");
252
- const stateContent = `
253
- agents:
254
- idle-agent:
255
- status: idle
256
- running-agent:
257
- status: running
258
- error-agent:
259
- status: error
260
- `;
261
- await writeFile(stateFile, stateContent, "utf-8");
262
-
263
- const state = await readFleetState(stateFile);
264
-
265
- expect(state.agents["idle-agent"].status).toBe("idle");
266
- expect(state.agents["running-agent"].status).toBe("running");
267
- expect(state.agents["error-agent"].status).toBe("error");
268
- });
269
-
270
- it("allows nullable fields to be null", async () => {
271
- const stateFile = join(tempDir, "nullable.yaml");
272
- const stateContent = `
273
- agents:
274
- agent-with-nulls:
275
- status: idle
276
- current_job: null
277
- last_job: null
278
- next_schedule: null
279
- next_trigger_at: null
280
- container_id: null
281
- error_message: null
282
- `;
283
- await writeFile(stateFile, stateContent, "utf-8");
284
-
285
- const state = await readFleetState(stateFile);
286
-
287
- expect(state.agents["agent-with-nulls"].current_job).toBeNull();
288
- expect(state.agents["agent-with-nulls"].last_job).toBeNull();
289
- expect(state.agents["agent-with-nulls"].error_message).toBeNull();
290
- });
291
- });
292
- });
293
-
294
- describe("writeFleetState", () => {
295
- let tempDir: string;
296
-
297
- beforeEach(async () => {
298
- tempDir = await createTempDir();
299
- });
300
-
301
- afterEach(async () => {
302
- await rm(tempDir, { recursive: true, force: true });
303
- });
304
-
305
- describe("successful writes", () => {
306
- it("writes valid fleet state to file", async () => {
307
- const stateFile = join(tempDir, "state.yaml");
308
- const state: FleetState = {
309
- fleet: {
310
- started_at: "2024-01-15T10:30:00Z",
311
- },
312
- agents: {
313
- "my-agent": {
314
- status: "running",
315
- current_job: "job-123",
316
- },
317
- },
318
- };
319
-
320
- await writeFleetState(stateFile, state);
321
-
322
- const content = await readFile(stateFile, "utf-8");
323
- expect(content).toContain("started_at:");
324
- expect(content).toContain("my-agent:");
325
- expect(content).toContain("status: running");
326
- expect(content).toContain("current_job: job-123");
327
- });
328
-
329
- it("writes empty state correctly", async () => {
330
- const stateFile = join(tempDir, "empty-state.yaml");
331
- const state = createInitialFleetState();
332
-
333
- await writeFleetState(stateFile, state);
334
-
335
- const readState = await readFleetState(stateFile);
336
- expect(readState).toEqual(state);
337
- });
338
-
339
- it("overwrites existing file", async () => {
340
- const stateFile = join(tempDir, "overwrite.yaml");
341
- await writeFile(stateFile, "old: content\n", "utf-8");
342
- const state: FleetState = {
343
- fleet: { started_at: "2024-01-15T10:30:00Z" },
344
- agents: {},
345
- };
346
-
347
- await writeFleetState(stateFile, state);
348
-
349
- const content = await readFile(stateFile, "utf-8");
350
- expect(content).not.toContain("old:");
351
- expect(content).toContain("started_at:");
352
- });
353
-
354
- it("preserves custom indent option", async () => {
355
- const stateFile = join(tempDir, "custom-indent.yaml");
356
- const state: FleetState = {
357
- fleet: {},
358
- agents: {
359
- agent: { status: "idle" },
360
- },
361
- };
362
-
363
- await writeFleetState(stateFile, state, { indent: 4 });
364
-
365
- const content = await readFile(stateFile, "utf-8");
366
- // Check for 4-space indentation
367
- expect(content).toMatch(/^\s{4}agent:/m);
368
- });
369
-
370
- it("round-trips complex state correctly", async () => {
371
- const stateFile = join(tempDir, "roundtrip.yaml");
372
- const originalState: FleetState = {
373
- fleet: {
374
- started_at: "2024-01-15T10:30:00Z",
375
- },
376
- agents: {
377
- "agent-1": {
378
- status: "running",
379
- current_job: "job-123",
380
- last_job: "job-122",
381
- next_schedule: "hourly",
382
- next_trigger_at: "2024-01-15T11:00:00Z",
383
- container_id: "container-abc",
384
- },
385
- "agent-2": {
386
- status: "error",
387
- last_job: "job-456",
388
- error_message: "Out of memory",
389
- },
390
- "agent-3": {
391
- status: "idle",
392
- },
393
- },
394
- };
395
-
396
- await writeFleetState(stateFile, originalState);
397
- const readState = await readFleetState(stateFile);
398
-
399
- expect(readState.fleet.started_at).toBe(originalState.fleet.started_at);
400
- expect(readState.agents["agent-1"]).toEqual(originalState.agents["agent-1"]);
401
- expect(readState.agents["agent-2"]).toEqual(originalState.agents["agent-2"]);
402
- expect(readState.agents["agent-3"]).toEqual(originalState.agents["agent-3"]);
403
- });
404
- });
405
-
406
- describe("atomic write behavior", () => {
407
- it("writes atomically (no partial writes)", async () => {
408
- const stateFile = join(tempDir, "atomic.yaml");
409
- const state: FleetState = {
410
- fleet: { started_at: "2024-01-15T10:30:00Z" },
411
- agents: {
412
- agent: {
413
- status: "running",
414
- current_job: "job-1",
415
- },
416
- },
417
- };
418
-
419
- await writeFleetState(stateFile, state);
420
-
421
- // File should be complete and valid
422
- const readState = await readFleetState(stateFile);
423
- expect(readState.fleet.started_at).toBe("2024-01-15T10:30:00Z");
424
- });
425
-
426
- it("does not leave temp files on success", async () => {
427
- const stateFile = join(tempDir, "no-temp.yaml");
428
- const state = createInitialFleetState();
429
-
430
- await writeFleetState(stateFile, state);
431
-
432
- // Check no temp files exist
433
- const { readdir } = await import("node:fs/promises");
434
- const files = await readdir(tempDir);
435
- const tempFiles = files.filter((f) => f.includes(".tmp."));
436
- expect(tempFiles).toHaveLength(0);
437
- });
438
- });
439
-
440
- describe("error handling", () => {
441
- it("throws StateFileError when write fails", async () => {
442
- // Try to write to a non-existent directory
443
- const stateFile = join(tempDir, "nonexistent-dir", "state.yaml");
444
- const state = createInitialFleetState();
445
-
446
- await expect(writeFleetState(stateFile, state)).rejects.toThrow(StateFileError);
447
- });
448
-
449
- it("validates state before writing", async () => {
450
- const stateFile = join(tempDir, "validate.yaml");
451
- const invalidState = {
452
- fleet: {},
453
- agents: {
454
- agent: {
455
- status: "invalid_status", // Invalid status
456
- },
457
- },
458
- } as unknown as FleetState;
459
-
460
- await expect(writeFleetState(stateFile, invalidState)).rejects.toThrow();
461
- });
462
- });
463
- });
464
-
465
- describe("updateAgentState", () => {
466
- let tempDir: string;
467
-
468
- beforeEach(async () => {
469
- tempDir = await createTempDir();
470
- });
471
-
472
- afterEach(async () => {
473
- await rm(tempDir, { recursive: true, force: true });
474
- });
475
-
476
- describe("updating existing agents", () => {
477
- it("updates single field of existing agent", async () => {
478
- const stateFile = join(tempDir, "state.yaml");
479
- const initialState: FleetState = {
480
- fleet: { started_at: "2024-01-15T10:30:00Z" },
481
- agents: {
482
- "my-agent": {
483
- status: "idle",
484
- last_job: "job-100",
485
- },
486
- },
487
- };
488
- await writeFleetState(stateFile, initialState);
489
-
490
- const updatedState = await updateAgentState(stateFile, "my-agent", {
491
- status: "running",
492
- });
493
-
494
- expect(updatedState.agents["my-agent"].status).toBe("running");
495
- expect(updatedState.agents["my-agent"].last_job).toBe("job-100");
496
- });
497
-
498
- it("updates multiple fields of existing agent", async () => {
499
- const stateFile = join(tempDir, "state.yaml");
500
- const initialState: FleetState = {
501
- fleet: {},
502
- agents: {
503
- "my-agent": {
504
- status: "idle",
505
- },
506
- },
507
- };
508
- await writeFleetState(stateFile, initialState);
509
-
510
- const updatedState = await updateAgentState(stateFile, "my-agent", {
511
- status: "running",
512
- current_job: "job-200",
513
- container_id: "container-xyz",
514
- });
515
-
516
- expect(updatedState.agents["my-agent"]).toEqual({
517
- status: "running",
518
- current_job: "job-200",
519
- container_id: "container-xyz",
520
- });
521
- });
522
-
523
- it("can set fields to null", async () => {
524
- const stateFile = join(tempDir, "state.yaml");
525
- const initialState: FleetState = {
526
- fleet: {},
527
- agents: {
528
- "my-agent": {
529
- status: "error",
530
- error_message: "Some error",
531
- current_job: "job-100",
532
- },
533
- },
534
- };
535
- await writeFleetState(stateFile, initialState);
536
-
537
- const updatedState = await updateAgentState(stateFile, "my-agent", {
538
- status: "idle",
539
- error_message: null,
540
- current_job: null,
541
- });
542
-
543
- expect(updatedState.agents["my-agent"].status).toBe("idle");
544
- expect(updatedState.agents["my-agent"].error_message).toBeNull();
545
- expect(updatedState.agents["my-agent"].current_job).toBeNull();
546
- });
547
-
548
- it("preserves other agents when updating one", async () => {
549
- const stateFile = join(tempDir, "state.yaml");
550
- const initialState: FleetState = {
551
- fleet: {},
552
- agents: {
553
- "agent-1": { status: "idle" },
554
- "agent-2": { status: "running", current_job: "job-100" },
555
- },
556
- };
557
- await writeFleetState(stateFile, initialState);
558
-
559
- const updatedState = await updateAgentState(stateFile, "agent-1", {
560
- status: "running",
561
- });
562
-
563
- expect(updatedState.agents["agent-1"].status).toBe("running");
564
- expect(updatedState.agents["agent-2"]).toEqual({
565
- status: "running",
566
- current_job: "job-100",
567
- });
568
- });
569
-
570
- it("preserves fleet metadata when updating agent", async () => {
571
- const stateFile = join(tempDir, "state.yaml");
572
- const initialState: FleetState = {
573
- fleet: { started_at: "2024-01-15T10:30:00Z" },
574
- agents: {
575
- "my-agent": { status: "idle" },
576
- },
577
- };
578
- await writeFleetState(stateFile, initialState);
579
-
580
- const updatedState = await updateAgentState(stateFile, "my-agent", {
581
- status: "running",
582
- });
583
-
584
- expect(updatedState.fleet.started_at).toBe("2024-01-15T10:30:00Z");
585
- });
586
- });
587
-
588
- describe("creating new agents", () => {
589
- it("creates new agent if it does not exist", async () => {
590
- const stateFile = join(tempDir, "state.yaml");
591
- const initialState: FleetState = {
592
- fleet: {},
593
- agents: {},
594
- };
595
- await writeFleetState(stateFile, initialState);
596
-
597
- const updatedState = await updateAgentState(stateFile, "new-agent", {
598
- status: "running",
599
- current_job: "job-1",
600
- });
601
-
602
- expect(updatedState.agents["new-agent"]).toEqual({
603
- status: "running",
604
- current_job: "job-1",
605
- });
606
- });
607
-
608
- it("creates new agent with default status if not provided", async () => {
609
- const stateFile = join(tempDir, "state.yaml");
610
- const initialState: FleetState = {
611
- fleet: {},
612
- agents: {},
613
- };
614
- await writeFleetState(stateFile, initialState);
615
-
616
- const updatedState = await updateAgentState(stateFile, "new-agent", {
617
- last_job: "job-1",
618
- });
619
-
620
- expect(updatedState.agents["new-agent"].status).toBe("idle");
621
- expect(updatedState.agents["new-agent"].last_job).toBe("job-1");
622
- });
623
-
624
- it("creates agent in file that does not exist", async () => {
625
- const stateFile = join(tempDir, "new-state.yaml");
626
-
627
- const updatedState = await updateAgentState(stateFile, "new-agent", {
628
- status: "running",
629
- });
630
-
631
- expect(updatedState.agents["new-agent"].status).toBe("running");
632
-
633
- // Verify it was persisted
634
- const readState = await readFleetState(stateFile);
635
- expect(readState.agents["new-agent"].status).toBe("running");
636
- });
637
- });
638
-
639
- describe("file operations", () => {
640
- it("writes changes back to file", async () => {
641
- const stateFile = join(tempDir, "state.yaml");
642
- const initialState: FleetState = {
643
- fleet: {},
644
- agents: {
645
- "my-agent": { status: "idle" },
646
- },
647
- };
648
- await writeFleetState(stateFile, initialState);
649
-
650
- await updateAgentState(stateFile, "my-agent", { status: "running" });
651
-
652
- // Read from file directly to verify persistence
653
- const persistedState = await readFleetState(stateFile);
654
- expect(persistedState.agents["my-agent"].status).toBe("running");
655
- });
656
-
657
- it("handles corrupted file by starting fresh", async () => {
658
- const stateFile = join(tempDir, "corrupted.yaml");
659
- await writeFile(stateFile, "invalid: [yaml", "utf-8");
660
- const logger = createMockLogger();
661
-
662
- const updatedState = await updateAgentState(
663
- stateFile,
664
- "new-agent",
665
- { status: "running" },
666
- { logger }
667
- );
668
-
669
- expect(updatedState.agents["new-agent"].status).toBe("running");
670
- expect(logger.warnings).toHaveLength(1);
671
- });
672
- });
673
- });
674
-
675
- describe("initializeFleetState", () => {
676
- let tempDir: string;
677
-
678
- beforeEach(async () => {
679
- tempDir = await createTempDir();
680
- });
681
-
682
- afterEach(async () => {
683
- await rm(tempDir, { recursive: true, force: true });
684
- });
685
-
686
- it("sets started_at if not already set", async () => {
687
- const stateFile = join(tempDir, "state.yaml");
688
-
689
- const state = await initializeFleetState(stateFile);
690
-
691
- expect(state.fleet.started_at).toBeDefined();
692
- expect(new Date(state.fleet.started_at!).getTime()).toBeGreaterThan(0);
693
- });
694
-
695
- it("does not overwrite existing started_at", async () => {
696
- const stateFile = join(tempDir, "state.yaml");
697
- const originalTimestamp = "2024-01-01T00:00:00Z";
698
- const initialState: FleetState = {
699
- fleet: { started_at: originalTimestamp },
700
- agents: {},
701
- };
702
- await writeFleetState(stateFile, initialState);
703
-
704
- const state = await initializeFleetState(stateFile);
705
-
706
- expect(state.fleet.started_at).toBe(originalTimestamp);
707
- });
708
-
709
- it("creates file if it does not exist", async () => {
710
- const stateFile = join(tempDir, "new-state.yaml");
711
-
712
- const state = await initializeFleetState(stateFile);
713
-
714
- expect(state.fleet.started_at).toBeDefined();
715
-
716
- // Verify file was created
717
- const persistedState = await readFleetState(stateFile);
718
- expect(persistedState.fleet.started_at).toBe(state.fleet.started_at);
719
- });
720
-
721
- it("preserves existing agents", async () => {
722
- const stateFile = join(tempDir, "state.yaml");
723
- const initialState: FleetState = {
724
- fleet: {},
725
- agents: {
726
- "existing-agent": { status: "idle" },
727
- },
728
- };
729
- await writeFleetState(stateFile, initialState);
730
-
731
- const state = await initializeFleetState(stateFile);
732
-
733
- expect(state.agents["existing-agent"]).toEqual({ status: "idle" });
734
- expect(state.fleet.started_at).toBeDefined();
735
- });
736
- });
737
-
738
- describe("removeAgentState", () => {
739
- let tempDir: string;
740
-
741
- beforeEach(async () => {
742
- tempDir = await createTempDir();
743
- });
744
-
745
- afterEach(async () => {
746
- await rm(tempDir, { recursive: true, force: true });
747
- });
748
-
749
- it("removes specified agent from state", async () => {
750
- const stateFile = join(tempDir, "state.yaml");
751
- const initialState: FleetState = {
752
- fleet: {},
753
- agents: {
754
- "agent-1": { status: "idle" },
755
- "agent-2": { status: "running" },
756
- },
757
- };
758
- await writeFleetState(stateFile, initialState);
759
-
760
- const updatedState = await removeAgentState(stateFile, "agent-1");
761
-
762
- expect(updatedState.agents["agent-1"]).toBeUndefined();
763
- expect(updatedState.agents["agent-2"]).toEqual({ status: "running" });
764
- });
765
-
766
- it("persists removal to file", async () => {
767
- const stateFile = join(tempDir, "state.yaml");
768
- const initialState: FleetState = {
769
- fleet: {},
770
- agents: {
771
- "agent-to-remove": { status: "idle" },
772
- },
773
- };
774
- await writeFleetState(stateFile, initialState);
775
-
776
- await removeAgentState(stateFile, "agent-to-remove");
777
-
778
- const persistedState = await readFleetState(stateFile);
779
- expect(persistedState.agents["agent-to-remove"]).toBeUndefined();
780
- });
781
-
782
- it("handles removal of non-existent agent gracefully", async () => {
783
- const stateFile = join(tempDir, "state.yaml");
784
- const initialState: FleetState = {
785
- fleet: {},
786
- agents: {
787
- "existing-agent": { status: "idle" },
788
- },
789
- };
790
- await writeFleetState(stateFile, initialState);
791
-
792
- const updatedState = await removeAgentState(stateFile, "non-existent");
793
-
794
- expect(updatedState.agents["existing-agent"]).toEqual({ status: "idle" });
795
- expect(Object.keys(updatedState.agents)).toHaveLength(1);
796
- });
797
-
798
- it("preserves fleet metadata when removing agent", async () => {
799
- const stateFile = join(tempDir, "state.yaml");
800
- const initialState: FleetState = {
801
- fleet: { started_at: "2024-01-15T10:30:00Z" },
802
- agents: {
803
- "agent-to-remove": { status: "idle" },
804
- },
805
- };
806
- await writeFleetState(stateFile, initialState);
807
-
808
- const updatedState = await removeAgentState(stateFile, "agent-to-remove");
809
-
810
- expect(updatedState.fleet.started_at).toBe("2024-01-15T10:30:00Z");
811
- });
812
- });
813
-
814
- describe("concurrent operations", () => {
815
- let tempDir: string;
816
-
817
- beforeEach(async () => {
818
- tempDir = await createTempDir();
819
- });
820
-
821
- afterEach(async () => {
822
- await rm(tempDir, { recursive: true, force: true });
823
- });
824
-
825
- it("handles multiple concurrent reads", async () => {
826
- const stateFile = join(tempDir, "state.yaml");
827
- const state: FleetState = {
828
- fleet: { started_at: "2024-01-15T10:30:00Z" },
829
- agents: {
830
- agent: { status: "running" },
831
- },
832
- };
833
- await writeFleetState(stateFile, state);
834
-
835
- const reads = [];
836
- for (let i = 0; i < 50; i++) {
837
- reads.push(readFleetState(stateFile));
838
- }
839
-
840
- const results = await Promise.all(reads);
841
-
842
- for (const result of results) {
843
- expect(result.fleet.started_at).toBe("2024-01-15T10:30:00Z");
844
- expect(result.agents.agent.status).toBe("running");
845
- }
846
- });
847
-
848
- it("handles sequential updates correctly", async () => {
849
- const stateFile = join(tempDir, "state.yaml");
850
- await writeFleetState(stateFile, createInitialFleetState());
851
-
852
- // Sequential updates (not concurrent to avoid race conditions)
853
- for (let i = 0; i < 10; i++) {
854
- await updateAgentState(stateFile, `agent-${i}`, {
855
- status: "running",
856
- current_job: `job-${i}`,
857
- });
858
- }
859
-
860
- const finalState = await readFleetState(stateFile);
861
-
862
- expect(Object.keys(finalState.agents)).toHaveLength(10);
863
- for (let i = 0; i < 10; i++) {
864
- expect(finalState.agents[`agent-${i}`].status).toBe("running");
865
- expect(finalState.agents[`agent-${i}`].current_job).toBe(`job-${i}`);
866
- }
867
- });
868
- });