@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,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
- });