@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,784 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
- import { mkdir, writeFile, rm, realpath } from "node:fs/promises";
3
- import { join } from "node:path";
4
- import { tmpdir } from "node:os";
5
- import {
6
- loadConfig,
7
- safeLoadConfig,
8
- findConfigFile,
9
- CONFIG_FILE_NAMES,
10
- ConfigNotFoundError,
11
- AgentLoadError,
12
- } from "../loader.js";
13
- import { FileReadError, SchemaValidationError } from "../parser.js";
14
- import { UndefinedVariableError } from "../interpolate.js";
15
-
16
- // Helper to create a temp directory structure
17
- async function createTempDir(): Promise<string> {
18
- const baseDir = join(tmpdir(), `herdctl-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
19
- await mkdir(baseDir, { recursive: true });
20
- // Resolve to real path to handle macOS /var -> /private/var symlink
21
- return await realpath(baseDir);
22
- }
23
-
24
- // Helper to create a file
25
- async function createFile(filePath: string, content: string): Promise<void> {
26
- await mkdir(join(filePath, ".."), { recursive: true });
27
- await writeFile(filePath, content, "utf-8");
28
- }
29
-
30
- describe("CONFIG_FILE_NAMES", () => {
31
- it("includes herdctl.yaml and herdctl.yml", () => {
32
- expect(CONFIG_FILE_NAMES).toContain("herdctl.yaml");
33
- expect(CONFIG_FILE_NAMES).toContain("herdctl.yml");
34
- expect(CONFIG_FILE_NAMES).toHaveLength(2);
35
- });
36
- });
37
-
38
- describe("findConfigFile", () => {
39
- let tempDir: string;
40
-
41
- beforeEach(async () => {
42
- tempDir = await createTempDir();
43
- });
44
-
45
- afterEach(async () => {
46
- await rm(tempDir, { recursive: true, force: true });
47
- });
48
-
49
- it("finds herdctl.yaml in the current directory", async () => {
50
- await createFile(join(tempDir, "herdctl.yaml"), "version: 1");
51
-
52
- const result = await findConfigFile(tempDir);
53
-
54
- expect(result).not.toBeNull();
55
- expect(result!.path).toBe(join(tempDir, "herdctl.yaml"));
56
- });
57
-
58
- it("finds herdctl.yml in the current directory", async () => {
59
- await createFile(join(tempDir, "herdctl.yml"), "version: 1");
60
-
61
- const result = await findConfigFile(tempDir);
62
-
63
- expect(result).not.toBeNull();
64
- expect(result!.path).toBe(join(tempDir, "herdctl.yml"));
65
- });
66
-
67
- it("prefers herdctl.yaml over herdctl.yml", async () => {
68
- await createFile(join(tempDir, "herdctl.yaml"), "version: 1");
69
- await createFile(join(tempDir, "herdctl.yml"), "version: 1");
70
-
71
- const result = await findConfigFile(tempDir);
72
-
73
- expect(result).not.toBeNull();
74
- expect(result!.path).toBe(join(tempDir, "herdctl.yaml"));
75
- });
76
-
77
- it("finds config file in parent directory", async () => {
78
- const subDir = join(tempDir, "sub", "deep", "nested");
79
- await mkdir(subDir, { recursive: true });
80
- await createFile(join(tempDir, "herdctl.yaml"), "version: 1");
81
-
82
- const result = await findConfigFile(subDir);
83
-
84
- expect(result).not.toBeNull();
85
- expect(result!.path).toBe(join(tempDir, "herdctl.yaml"));
86
- });
87
-
88
- it("finds config file in intermediate parent directory", async () => {
89
- const subDir = join(tempDir, "sub", "deep", "nested");
90
- const intermediateDir = join(tempDir, "sub");
91
- await mkdir(subDir, { recursive: true });
92
- await createFile(join(intermediateDir, "herdctl.yaml"), "version: 1");
93
-
94
- const result = await findConfigFile(subDir);
95
-
96
- expect(result).not.toBeNull();
97
- expect(result!.path).toBe(join(intermediateDir, "herdctl.yaml"));
98
- });
99
-
100
- it("returns null when no config file is found", async () => {
101
- const result = await findConfigFile(tempDir);
102
-
103
- expect(result).toBeNull();
104
- });
105
-
106
- it("returns searched paths when config is found", async () => {
107
- const subDir = join(tempDir, "sub");
108
- await mkdir(subDir, { recursive: true });
109
- await createFile(join(tempDir, "herdctl.yaml"), "version: 1");
110
-
111
- const result = await findConfigFile(subDir);
112
-
113
- expect(result).not.toBeNull();
114
- expect(result!.searchedPaths).toContain(join(subDir, "herdctl.yaml"));
115
- expect(result!.searchedPaths).toContain(join(subDir, "herdctl.yml"));
116
- expect(result!.searchedPaths).toContain(join(tempDir, "herdctl.yaml"));
117
- });
118
- });
119
-
120
- describe("ConfigNotFoundError", () => {
121
- it("creates error with correct properties", () => {
122
- const searchedPaths = ["/path/a", "/path/b"];
123
- const error = new ConfigNotFoundError("/start/dir", searchedPaths);
124
-
125
- expect(error.name).toBe("ConfigNotFoundError");
126
- expect(error.startDirectory).toBe("/start/dir");
127
- expect(error.searchedPaths).toEqual(searchedPaths);
128
- expect(error.message).toContain("No herdctl configuration file found");
129
- expect(error.message).toContain("/start/dir");
130
- });
131
- });
132
-
133
- describe("AgentLoadError", () => {
134
- it("creates error with correct properties", () => {
135
- const cause = new Error("File not found");
136
- const error = new AgentLoadError("./agents/test.yaml", cause, "test-agent");
137
-
138
- expect(error.name).toBe("AgentLoadError");
139
- expect(error.agentPath).toBe("./agents/test.yaml");
140
- expect(error.agentName).toBe("test-agent");
141
- expect(error.cause).toBe(cause);
142
- expect(error.message).toContain("test.yaml");
143
- expect(error.message).toContain("test-agent");
144
- expect(error.message).toContain("File not found");
145
- });
146
-
147
- it("creates error without agent name", () => {
148
- const cause = new Error("File not found");
149
- const error = new AgentLoadError("./agents/test.yaml", cause);
150
-
151
- expect(error.agentName).toBeUndefined();
152
- expect(error.message).not.toContain("(undefined)");
153
- });
154
- });
155
-
156
- describe("loadConfig", () => {
157
- let tempDir: string;
158
-
159
- beforeEach(async () => {
160
- tempDir = await createTempDir();
161
- });
162
-
163
- afterEach(async () => {
164
- await rm(tempDir, { recursive: true, force: true });
165
- });
166
-
167
- describe("file discovery", () => {
168
- it("loads config from explicit yaml file path", async () => {
169
- const configPath = join(tempDir, "herdctl.yaml");
170
- await createFile(configPath, "version: 1");
171
-
172
- const result = await loadConfig(configPath);
173
-
174
- expect(result.fleet.version).toBe(1);
175
- expect(result.configPath).toBe(configPath);
176
- expect(result.configDir).toBe(tempDir);
177
- });
178
-
179
- it("loads config from explicit yml file path", async () => {
180
- const configPath = join(tempDir, "herdctl.yml");
181
- await createFile(configPath, "version: 1");
182
-
183
- const result = await loadConfig(configPath);
184
-
185
- expect(result.fleet.version).toBe(1);
186
- expect(result.configPath).toBe(configPath);
187
- });
188
-
189
- it("searches from directory when path is a directory", async () => {
190
- await createFile(join(tempDir, "herdctl.yaml"), "version: 1");
191
-
192
- const result = await loadConfig(tempDir);
193
-
194
- expect(result.fleet.version).toBe(1);
195
- expect(result.configPath).toBe(join(tempDir, "herdctl.yaml"));
196
- });
197
-
198
- it("throws ConfigNotFoundError when no config found in directory", async () => {
199
- await expect(loadConfig(tempDir)).rejects.toThrow(ConfigNotFoundError);
200
- });
201
-
202
- it("throws FileReadError when specified file does not exist", async () => {
203
- const nonExistentPath = join(tempDir, "nonexistent.yaml");
204
-
205
- await expect(loadConfig(nonExistentPath)).rejects.toThrow(FileReadError);
206
- });
207
- });
208
-
209
- describe("fleet configuration parsing", () => {
210
- it("parses empty config with defaults", async () => {
211
- await createFile(join(tempDir, "herdctl.yaml"), "");
212
-
213
- const result = await loadConfig(tempDir);
214
-
215
- expect(result.fleet.version).toBe(1);
216
- expect(result.fleet.agents).toEqual([]);
217
- });
218
-
219
- it("parses complete fleet configuration", async () => {
220
- const config = `
221
- version: 1
222
- fleet:
223
- name: test-fleet
224
- description: A test fleet
225
- defaults:
226
- model: claude-sonnet-4-20250514
227
- max_turns: 50
228
- workspace:
229
- root: ./workspace
230
- `;
231
- await createFile(join(tempDir, "herdctl.yaml"), config);
232
-
233
- const result = await loadConfig(tempDir);
234
-
235
- expect(result.fleet.fleet?.name).toBe("test-fleet");
236
- expect(result.fleet.fleet?.description).toBe("A test fleet");
237
- expect(result.fleet.defaults?.model).toBe("claude-sonnet-4-20250514");
238
- expect(result.fleet.defaults?.max_turns).toBe(50);
239
- expect(result.fleet.workspace?.root).toBe("./workspace");
240
- });
241
-
242
- it("throws on invalid YAML syntax", async () => {
243
- await createFile(join(tempDir, "herdctl.yaml"), "invalid: yaml: syntax:");
244
-
245
- await expect(loadConfig(tempDir)).rejects.toThrow("Invalid YAML syntax");
246
- });
247
-
248
- it("throws SchemaValidationError on invalid schema", async () => {
249
- await createFile(join(tempDir, "herdctl.yaml"), "version: -1");
250
-
251
- await expect(loadConfig(tempDir)).rejects.toThrow(SchemaValidationError);
252
- });
253
- });
254
-
255
- describe("agent loading", () => {
256
- it("loads agents referenced in fleet config", async () => {
257
- const fleetConfig = `
258
- version: 1
259
- agents:
260
- - path: ./agents/test-agent.yaml
261
- `;
262
- const agentConfig = `
263
- name: test-agent
264
- description: A test agent
265
- `;
266
- await createFile(join(tempDir, "herdctl.yaml"), fleetConfig);
267
- await createFile(join(tempDir, "agents", "test-agent.yaml"), agentConfig);
268
-
269
- const result = await loadConfig(tempDir);
270
-
271
- expect(result.agents).toHaveLength(1);
272
- expect(result.agents[0].name).toBe("test-agent");
273
- expect(result.agents[0].description).toBe("A test agent");
274
- expect(result.agents[0].configPath).toBe(join(tempDir, "agents", "test-agent.yaml"));
275
- });
276
-
277
- it("loads multiple agents", async () => {
278
- const fleetConfig = `
279
- version: 1
280
- agents:
281
- - path: ./agents/agent1.yaml
282
- - path: ./agents/agent2.yaml
283
- `;
284
- await createFile(join(tempDir, "herdctl.yaml"), fleetConfig);
285
- await createFile(join(tempDir, "agents", "agent1.yaml"), "name: agent-one");
286
- await createFile(join(tempDir, "agents", "agent2.yaml"), "name: agent-two");
287
-
288
- const result = await loadConfig(tempDir);
289
-
290
- expect(result.agents).toHaveLength(2);
291
- expect(result.agents[0].name).toBe("agent-one");
292
- expect(result.agents[1].name).toBe("agent-two");
293
- });
294
-
295
- it("throws AgentLoadError when agent file not found", async () => {
296
- const fleetConfig = `
297
- version: 1
298
- agents:
299
- - path: ./agents/nonexistent.yaml
300
- `;
301
- await createFile(join(tempDir, "herdctl.yaml"), fleetConfig);
302
-
303
- await expect(loadConfig(tempDir)).rejects.toThrow(AgentLoadError);
304
- });
305
-
306
- it("throws AgentLoadError when agent YAML is invalid", async () => {
307
- const fleetConfig = `
308
- version: 1
309
- agents:
310
- - path: ./agents/invalid.yaml
311
- `;
312
- await createFile(join(tempDir, "herdctl.yaml"), fleetConfig);
313
- await createFile(join(tempDir, "agents", "invalid.yaml"), "invalid: yaml: syntax:");
314
-
315
- await expect(loadConfig(tempDir)).rejects.toThrow(AgentLoadError);
316
- });
317
-
318
- it("throws AgentLoadError when agent schema is invalid", async () => {
319
- const fleetConfig = `
320
- version: 1
321
- agents:
322
- - path: ./agents/invalid.yaml
323
- `;
324
- await createFile(join(tempDir, "herdctl.yaml"), fleetConfig);
325
- // Missing required 'name' field
326
- await createFile(join(tempDir, "agents", "invalid.yaml"), "description: no name");
327
-
328
- await expect(loadConfig(tempDir)).rejects.toThrow(AgentLoadError);
329
- });
330
-
331
- it("resolves absolute agent paths correctly", async () => {
332
- const agentPath = join(tempDir, "elsewhere", "agent.yaml");
333
- const fleetConfig = `
334
- version: 1
335
- agents:
336
- - path: ${agentPath}
337
- `;
338
- await createFile(join(tempDir, "herdctl.yaml"), fleetConfig);
339
- await createFile(agentPath, "name: elsewhere-agent");
340
-
341
- const result = await loadConfig(tempDir);
342
-
343
- expect(result.agents[0].name).toBe("elsewhere-agent");
344
- expect(result.agents[0].configPath).toBe(agentPath);
345
- });
346
- });
347
-
348
- describe("defaults merging", () => {
349
- it("merges fleet defaults into agent config", async () => {
350
- const fleetConfig = `
351
- version: 1
352
- defaults:
353
- model: claude-sonnet-4-20250514
354
- max_turns: 100
355
- permission_mode: acceptEdits
356
- agents:
357
- - path: ./agents/test.yaml
358
- `;
359
- const agentConfig = `
360
- name: test-agent
361
- `;
362
- await createFile(join(tempDir, "herdctl.yaml"), fleetConfig);
363
- await createFile(join(tempDir, "agents", "test.yaml"), agentConfig);
364
-
365
- const result = await loadConfig(tempDir);
366
-
367
- expect(result.agents[0].model).toBe("claude-sonnet-4-20250514");
368
- expect(result.agents[0].max_turns).toBe(100);
369
- expect(result.agents[0].permission_mode).toBe("acceptEdits");
370
- });
371
-
372
- it("agent values override fleet defaults", async () => {
373
- const fleetConfig = `
374
- version: 1
375
- defaults:
376
- model: claude-sonnet-4-20250514
377
- max_turns: 100
378
- agents:
379
- - path: ./agents/test.yaml
380
- `;
381
- const agentConfig = `
382
- name: test-agent
383
- model: claude-opus-4-20250514
384
- max_turns: 50
385
- `;
386
- await createFile(join(tempDir, "herdctl.yaml"), fleetConfig);
387
- await createFile(join(tempDir, "agents", "test.yaml"), agentConfig);
388
-
389
- const result = await loadConfig(tempDir);
390
-
391
- expect(result.agents[0].model).toBe("claude-opus-4-20250514");
392
- expect(result.agents[0].max_turns).toBe(50);
393
- });
394
-
395
- it("deep merges nested objects", async () => {
396
- const fleetConfig = `
397
- version: 1
398
- defaults:
399
- permissions:
400
- mode: acceptEdits
401
- allowed_tools:
402
- - Read
403
- - Write
404
- agents:
405
- - path: ./agents/test.yaml
406
- `;
407
- const agentConfig = `
408
- name: test-agent
409
- permissions:
410
- allowed_tools:
411
- - Bash
412
- `;
413
- await createFile(join(tempDir, "herdctl.yaml"), fleetConfig);
414
- await createFile(join(tempDir, "agents", "test.yaml"), agentConfig);
415
-
416
- const result = await loadConfig(tempDir);
417
-
418
- // Mode should come from defaults
419
- expect(result.agents[0].permissions?.mode).toBe("acceptEdits");
420
- // Arrays are replaced, not merged
421
- expect(result.agents[0].permissions?.allowed_tools).toEqual(["Bash"]);
422
- });
423
-
424
- it("skips merging when mergeDefaults is false", async () => {
425
- const fleetConfig = `
426
- version: 1
427
- defaults:
428
- model: claude-sonnet-4-20250514
429
- agents:
430
- - path: ./agents/test.yaml
431
- `;
432
- const agentConfig = `
433
- name: test-agent
434
- `;
435
- await createFile(join(tempDir, "herdctl.yaml"), fleetConfig);
436
- await createFile(join(tempDir, "agents", "test.yaml"), agentConfig);
437
-
438
- const result = await loadConfig(tempDir, { mergeDefaults: false });
439
-
440
- expect(result.agents[0].model).toBeUndefined();
441
- });
442
- });
443
-
444
- describe("environment interpolation", () => {
445
- it("interpolates environment variables in fleet config", async () => {
446
- const fleetConfig = `
447
- version: 1
448
- fleet:
449
- name: \${FLEET_NAME}
450
- `;
451
- await createFile(join(tempDir, "herdctl.yaml"), fleetConfig);
452
-
453
- const result = await loadConfig(tempDir, {
454
- env: { FLEET_NAME: "my-fleet" },
455
- });
456
-
457
- expect(result.fleet.fleet?.name).toBe("my-fleet");
458
- });
459
-
460
- it("interpolates environment variables in agent config", async () => {
461
- const fleetConfig = `
462
- version: 1
463
- agents:
464
- - path: ./agents/test.yaml
465
- `;
466
- const agentConfig = `
467
- name: test-agent
468
- model: \${MODEL_NAME}
469
- `;
470
- await createFile(join(tempDir, "herdctl.yaml"), fleetConfig);
471
- await createFile(join(tempDir, "agents", "test.yaml"), agentConfig);
472
-
473
- const result = await loadConfig(tempDir, {
474
- env: { MODEL_NAME: "claude-sonnet-4-20250514" },
475
- });
476
-
477
- expect(result.agents[0].model).toBe("claude-sonnet-4-20250514");
478
- });
479
-
480
- it("uses default values for undefined variables", async () => {
481
- const fleetConfig = `
482
- version: 1
483
- fleet:
484
- name: \${FLEET_NAME:-default-fleet}
485
- `;
486
- await createFile(join(tempDir, "herdctl.yaml"), fleetConfig);
487
-
488
- const result = await loadConfig(tempDir, { env: {} });
489
-
490
- expect(result.fleet.fleet?.name).toBe("default-fleet");
491
- });
492
-
493
- it("throws UndefinedVariableError for undefined variables without default", async () => {
494
- const fleetConfig = `
495
- version: 1
496
- fleet:
497
- name: \${UNDEFINED_VAR}
498
- `;
499
- await createFile(join(tempDir, "herdctl.yaml"), fleetConfig);
500
-
501
- await expect(loadConfig(tempDir, { env: {} })).rejects.toThrow(UndefinedVariableError);
502
- });
503
-
504
- it("skips interpolation when interpolate is false", async () => {
505
- const fleetConfig = `
506
- version: 1
507
- fleet:
508
- name: \${FLEET_NAME}
509
- `;
510
- await createFile(join(tempDir, "herdctl.yaml"), fleetConfig);
511
-
512
- const result = await loadConfig(tempDir, {
513
- interpolate: false,
514
- env: { FLEET_NAME: "my-fleet" },
515
- });
516
-
517
- expect(result.fleet.fleet?.name).toBe("${FLEET_NAME}");
518
- });
519
- });
520
-
521
- describe("result structure", () => {
522
- it("returns correct structure with all fields", async () => {
523
- const fleetConfig = `
524
- version: 1
525
- fleet:
526
- name: test-fleet
527
- agents:
528
- - path: ./agents/test.yaml
529
- `;
530
- await createFile(join(tempDir, "herdctl.yaml"), fleetConfig);
531
- await createFile(join(tempDir, "agents", "test.yaml"), "name: test-agent");
532
-
533
- const result = await loadConfig(tempDir);
534
-
535
- expect(result).toHaveProperty("fleet");
536
- expect(result).toHaveProperty("agents");
537
- expect(result).toHaveProperty("configPath");
538
- expect(result).toHaveProperty("configDir");
539
- expect(result.configPath).toBe(join(tempDir, "herdctl.yaml"));
540
- expect(result.configDir).toBe(tempDir);
541
- });
542
-
543
- it("returns empty agents array when no agents defined", async () => {
544
- await createFile(join(tempDir, "herdctl.yaml"), "version: 1");
545
-
546
- const result = await loadConfig(tempDir);
547
-
548
- expect(result.agents).toEqual([]);
549
- });
550
- });
551
- });
552
-
553
- describe("safeLoadConfig", () => {
554
- let tempDir: string;
555
-
556
- beforeEach(async () => {
557
- tempDir = await createTempDir();
558
- });
559
-
560
- afterEach(async () => {
561
- await rm(tempDir, { recursive: true, force: true });
562
- });
563
-
564
- it("returns success result when config loads successfully", async () => {
565
- await createFile(join(tempDir, "herdctl.yaml"), "version: 1");
566
-
567
- const result = await safeLoadConfig(tempDir);
568
-
569
- expect(result.success).toBe(true);
570
- if (result.success) {
571
- expect(result.data.fleet.version).toBe(1);
572
- }
573
- });
574
-
575
- it("returns failure result with ConfigNotFoundError", async () => {
576
- const result = await safeLoadConfig(tempDir);
577
-
578
- expect(result.success).toBe(false);
579
- if (!result.success) {
580
- expect(result.error).toBeInstanceOf(ConfigNotFoundError);
581
- }
582
- });
583
-
584
- it("returns failure result with FileReadError", async () => {
585
- const result = await safeLoadConfig(join(tempDir, "nonexistent.yaml"));
586
-
587
- expect(result.success).toBe(false);
588
- if (!result.success) {
589
- expect(result.error).toBeInstanceOf(FileReadError);
590
- }
591
- });
592
-
593
- it("returns failure result with SchemaValidationError", async () => {
594
- await createFile(join(tempDir, "herdctl.yaml"), "version: invalid");
595
-
596
- const result = await safeLoadConfig(tempDir);
597
-
598
- expect(result.success).toBe(false);
599
- if (!result.success) {
600
- expect(result.error).toBeInstanceOf(SchemaValidationError);
601
- }
602
- });
603
-
604
- it("returns failure result with AgentLoadError", async () => {
605
- const fleetConfig = `
606
- version: 1
607
- agents:
608
- - path: ./nonexistent.yaml
609
- `;
610
- await createFile(join(tempDir, "herdctl.yaml"), fleetConfig);
611
-
612
- const result = await safeLoadConfig(tempDir);
613
-
614
- expect(result.success).toBe(false);
615
- if (!result.success) {
616
- expect(result.error).toBeInstanceOf(AgentLoadError);
617
- }
618
- });
619
-
620
- it("wraps non-ConfigError errors", async () => {
621
- // Create a file that will cause an unexpected error
622
- // This is difficult to trigger, so we'll test the general behavior
623
- await createFile(join(tempDir, "herdctl.yaml"), "version: 1");
624
-
625
- const result = await safeLoadConfig(tempDir);
626
-
627
- expect(result.success).toBe(true);
628
- });
629
- });
630
-
631
- describe("auto-discovery from cwd", () => {
632
- let tempDir: string;
633
- let originalCwd: string;
634
-
635
- beforeEach(async () => {
636
- tempDir = await createTempDir();
637
- originalCwd = process.cwd();
638
- });
639
-
640
- afterEach(async () => {
641
- process.chdir(originalCwd);
642
- await rm(tempDir, { recursive: true, force: true });
643
- });
644
-
645
- it("finds config from current working directory when no path provided", async () => {
646
- await createFile(join(tempDir, "herdctl.yaml"), "version: 1");
647
- process.chdir(tempDir);
648
-
649
- const result = await loadConfig();
650
-
651
- expect(result.fleet.version).toBe(1);
652
- expect(result.configPath).toBe(join(tempDir, "herdctl.yaml"));
653
- });
654
-
655
- it("throws ConfigNotFoundError when no config in cwd hierarchy", async () => {
656
- process.chdir(tempDir);
657
-
658
- await expect(loadConfig()).rejects.toThrow(ConfigNotFoundError);
659
- });
660
- });
661
-
662
- describe("integration scenarios", () => {
663
- let tempDir: string;
664
-
665
- beforeEach(async () => {
666
- tempDir = await createTempDir();
667
- });
668
-
669
- afterEach(async () => {
670
- await rm(tempDir, { recursive: true, force: true });
671
- });
672
-
673
- it("handles a realistic fleet configuration", async () => {
674
- const fleetConfig = `
675
- version: 1
676
-
677
- fleet:
678
- name: example-fleet
679
- description: An example herdctl fleet
680
-
681
- defaults:
682
- model: claude-sonnet-4-20250514
683
- max_turns: 50
684
- permission_mode: acceptEdits
685
- permissions:
686
- allowed_tools:
687
- - Read
688
- - Edit
689
- - Write
690
- - Bash
691
- - Glob
692
- - Grep
693
-
694
- workspace:
695
- root: ./workspace
696
- auto_clone: true
697
- clone_depth: 1
698
-
699
- agents:
700
- - path: ./agents/coder.yaml
701
- - path: ./agents/reviewer.yaml
702
-
703
- chat:
704
- discord:
705
- enabled: true
706
- token_env: DISCORD_TOKEN
707
- `;
708
-
709
- const coderAgent = `
710
- name: coder
711
- description: Writes code based on issues
712
- system_prompt: |
713
- You are a coding assistant.
714
- Write clean, maintainable code.
715
- permissions:
716
- allowed_tools:
717
- - Read
718
- - Edit
719
- - Write
720
- - Bash
721
- `;
722
-
723
- const reviewerAgent = `
724
- name: reviewer
725
- description: Reviews pull requests
726
- model: claude-opus-4-20250514
727
- max_turns: 25
728
- system_prompt: |
729
- You are a code reviewer.
730
- Focus on code quality and security.
731
- `;
732
-
733
- await createFile(join(tempDir, "herdctl.yaml"), fleetConfig);
734
- await createFile(join(tempDir, "agents", "coder.yaml"), coderAgent);
735
- await createFile(join(tempDir, "agents", "reviewer.yaml"), reviewerAgent);
736
-
737
- const result = await loadConfig(tempDir, {
738
- env: { DISCORD_TOKEN: "test-token" },
739
- });
740
-
741
- // Fleet config
742
- expect(result.fleet.fleet?.name).toBe("example-fleet");
743
- expect(result.fleet.defaults?.model).toBe("claude-sonnet-4-20250514");
744
- expect(result.fleet.workspace?.root).toBe("./workspace");
745
- expect(result.fleet.chat?.discord?.enabled).toBe(true);
746
-
747
- // Agents loaded
748
- expect(result.agents).toHaveLength(2);
749
-
750
- // Coder agent - inherits defaults
751
- const coder = result.agents.find(a => a.name === "coder");
752
- expect(coder).toBeDefined();
753
- expect(coder!.model).toBe("claude-sonnet-4-20250514"); // from defaults
754
- expect(coder!.max_turns).toBe(50); // from defaults
755
- expect(coder!.permission_mode).toBe("acceptEdits"); // from defaults
756
- expect(coder!.permissions?.allowed_tools).toEqual(["Read", "Edit", "Write", "Bash"]); // agent override
757
-
758
- // Reviewer agent - overrides defaults
759
- const reviewer = result.agents.find(a => a.name === "reviewer");
760
- expect(reviewer).toBeDefined();
761
- expect(reviewer!.model).toBe("claude-opus-4-20250514"); // agent override
762
- expect(reviewer!.max_turns).toBe(25); // agent override
763
- expect(reviewer!.permission_mode).toBe("acceptEdits"); // from defaults
764
- });
765
-
766
- it("handles nested directory structure", async () => {
767
- const subDir = join(tempDir, "projects", "myproject", "src");
768
- await mkdir(subDir, { recursive: true });
769
-
770
- await createFile(join(tempDir, "projects", "myproject", "herdctl.yaml"), `
771
- version: 1
772
- agents:
773
- - path: ./config/agent.yaml
774
- `);
775
- await createFile(join(tempDir, "projects", "myproject", "config", "agent.yaml"), `
776
- name: nested-agent
777
- `);
778
-
779
- const result = await loadConfig(subDir);
780
-
781
- expect(result.configPath).toBe(join(tempDir, "projects", "myproject", "herdctl.yaml"));
782
- expect(result.agents[0].name).toBe("nested-agent");
783
- });
784
- });