@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,831 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } 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
- createJob,
7
- updateJob,
8
- getJob,
9
- listJobs,
10
- deleteJob,
11
- type JobLogger,
12
- } from "../job-metadata.js";
13
- import { type JobMetadata } from "../schemas/job-metadata.js";
14
- import { StateFileError } from "../errors.js";
15
-
16
- // Helper to create a temp directory
17
- async function createTempDir(): Promise<string> {
18
- const baseDir = join(
19
- tmpdir(),
20
- `herdctl-job-test-${Date.now()}-${Math.random().toString(36).slice(2)}`
21
- );
22
- await mkdir(baseDir, { recursive: true });
23
- // Resolve to real path to handle macOS /var -> /private/var symlink
24
- return await realpath(baseDir);
25
- }
26
-
27
- // Helper to create a mock logger
28
- function createMockLogger(): JobLogger & { warnings: string[] } {
29
- const warnings: string[] = [];
30
- return {
31
- warnings,
32
- warn: (message: string) => warnings.push(message),
33
- };
34
- }
35
-
36
- // Helper to create a valid job YAML file
37
- async function writeJobFile(
38
- dir: string,
39
- job: JobMetadata
40
- ): Promise<string> {
41
- const { stringify } = await import("yaml");
42
- const filePath = join(dir, `${job.id}.yaml`);
43
- await writeFile(filePath, stringify(job), "utf-8");
44
- return filePath;
45
- }
46
-
47
- describe("createJob", () => {
48
- let tempDir: string;
49
-
50
- beforeEach(async () => {
51
- tempDir = await createTempDir();
52
- });
53
-
54
- afterEach(async () => {
55
- await rm(tempDir, { recursive: true, force: true });
56
- });
57
-
58
- it("creates a job file with correct content", async () => {
59
- const job = await createJob(tempDir, {
60
- agent: "test-agent",
61
- trigger_type: "manual",
62
- prompt: "Test prompt",
63
- });
64
-
65
- expect(job.id).toMatch(/^job-\d{4}-\d{2}-\d{2}-[a-z0-9]{6}$/);
66
- expect(job.agent).toBe("test-agent");
67
- expect(job.trigger_type).toBe("manual");
68
- expect(job.status).toBe("pending");
69
- expect(job.prompt).toBe("Test prompt");
70
-
71
- // Verify file exists
72
- const filePath = join(tempDir, `${job.id}.yaml`);
73
- const content = await readFile(filePath, "utf-8");
74
- expect(content).toContain("agent: test-agent");
75
- expect(content).toContain("trigger_type: manual");
76
- });
77
-
78
- it("creates scheduled job with schedule name", async () => {
79
- const job = await createJob(tempDir, {
80
- agent: "cron-agent",
81
- trigger_type: "schedule",
82
- schedule: "hourly",
83
- });
84
-
85
- expect(job.schedule).toBe("hourly");
86
- expect(job.trigger_type).toBe("schedule");
87
- });
88
-
89
- it("creates forked job with parent reference", async () => {
90
- // Create parent job first
91
- const parentJob = await createJob(tempDir, {
92
- agent: "test-agent",
93
- trigger_type: "manual",
94
- });
95
-
96
- const forkedJob = await createJob(tempDir, {
97
- agent: "test-agent",
98
- trigger_type: "fork",
99
- forked_from: parentJob.id,
100
- });
101
-
102
- expect(forkedJob.trigger_type).toBe("fork");
103
- expect(forkedJob.forked_from).toBe(parentJob.id);
104
- });
105
-
106
- it("sets started_at to current time", async () => {
107
- const before = new Date();
108
- const job = await createJob(tempDir, {
109
- agent: "test-agent",
110
- trigger_type: "manual",
111
- });
112
- const after = new Date();
113
-
114
- const startedAt = new Date(job.started_at);
115
- expect(startedAt.getTime()).toBeGreaterThanOrEqual(before.getTime() - 1000);
116
- expect(startedAt.getTime()).toBeLessThanOrEqual(after.getTime() + 1000);
117
- });
118
-
119
- it("throws StateFileError when directory does not exist", async () => {
120
- const nonExistentDir = join(tempDir, "does-not-exist");
121
-
122
- await expect(
123
- createJob(nonExistentDir, {
124
- agent: "test-agent",
125
- trigger_type: "manual",
126
- })
127
- ).rejects.toThrow(StateFileError);
128
- });
129
-
130
- it("generates unique job IDs", async () => {
131
- const jobs = await Promise.all([
132
- createJob(tempDir, { agent: "agent-1", trigger_type: "manual" }),
133
- createJob(tempDir, { agent: "agent-2", trigger_type: "manual" }),
134
- createJob(tempDir, { agent: "agent-3", trigger_type: "manual" }),
135
- ]);
136
-
137
- const ids = jobs.map((j) => j.id);
138
- const uniqueIds = new Set(ids);
139
- expect(uniqueIds.size).toBe(3);
140
- });
141
- });
142
-
143
- describe("updateJob", () => {
144
- let tempDir: string;
145
-
146
- beforeEach(async () => {
147
- tempDir = await createTempDir();
148
- });
149
-
150
- afterEach(async () => {
151
- await rm(tempDir, { recursive: true, force: true });
152
- });
153
-
154
- it("updates job status", async () => {
155
- const job = await createJob(tempDir, {
156
- agent: "test-agent",
157
- trigger_type: "manual",
158
- });
159
-
160
- const updated = await updateJob(tempDir, job.id, {
161
- status: "running",
162
- session_id: "session-123",
163
- });
164
-
165
- expect(updated.status).toBe("running");
166
- expect(updated.session_id).toBe("session-123");
167
- expect(updated.agent).toBe("test-agent"); // Preserved
168
- });
169
-
170
- it("auto-calculates duration when finished_at is set", async () => {
171
- const job = await createJob(tempDir, {
172
- agent: "test-agent",
173
- trigger_type: "manual",
174
- });
175
-
176
- // Wait a bit to have a measurable duration
177
- const startTime = new Date(job.started_at);
178
- const finishTime = new Date(startTime.getTime() + 5000); // 5 seconds later
179
-
180
- const updated = await updateJob(tempDir, job.id, {
181
- status: "completed",
182
- exit_reason: "success",
183
- finished_at: finishTime.toISOString(),
184
- });
185
-
186
- expect(updated.duration_seconds).toBe(5);
187
- });
188
-
189
- it("preserves explicit duration_seconds over auto-calculation", async () => {
190
- const job = await createJob(tempDir, {
191
- agent: "test-agent",
192
- trigger_type: "manual",
193
- });
194
-
195
- const updated = await updateJob(tempDir, job.id, {
196
- status: "completed",
197
- finished_at: new Date().toISOString(),
198
- duration_seconds: 999, // Explicit value
199
- });
200
-
201
- expect(updated.duration_seconds).toBe(999);
202
- });
203
-
204
- it("updates summary and output_file", async () => {
205
- const job = await createJob(tempDir, {
206
- agent: "test-agent",
207
- trigger_type: "manual",
208
- });
209
-
210
- const updated = await updateJob(tempDir, job.id, {
211
- summary: "All tests passed",
212
- output_file: "/path/to/output.log",
213
- });
214
-
215
- expect(updated.summary).toBe("All tests passed");
216
- expect(updated.output_file).toBe("/path/to/output.log");
217
- });
218
-
219
- it("throws StateFileError for non-existent job", async () => {
220
- await expect(
221
- updateJob(tempDir, "job-2024-01-15-noexis", { status: "running" })
222
- ).rejects.toThrow(StateFileError);
223
- });
224
-
225
- it("throws StateFileError for corrupted job file", async () => {
226
- const corruptedPath = join(tempDir, "job-2024-01-15-corupt.yaml");
227
- await writeFile(corruptedPath, "invalid: [yaml", "utf-8");
228
-
229
- await expect(
230
- updateJob(tempDir, "job-2024-01-15-corupt", { status: "running" })
231
- ).rejects.toThrow(StateFileError);
232
- });
233
-
234
- it("persists updates to file", async () => {
235
- const job = await createJob(tempDir, {
236
- agent: "test-agent",
237
- trigger_type: "manual",
238
- });
239
-
240
- await updateJob(tempDir, job.id, {
241
- status: "completed",
242
- summary: "Done!",
243
- });
244
-
245
- // Read file directly
246
- const content = await readFile(join(tempDir, `${job.id}.yaml`), "utf-8");
247
- expect(content).toContain("status: completed");
248
- expect(content).toContain("summary: Done!");
249
- });
250
-
251
- it("handles multiple sequential updates", async () => {
252
- const job = await createJob(tempDir, {
253
- agent: "test-agent",
254
- trigger_type: "manual",
255
- });
256
-
257
- await updateJob(tempDir, job.id, { status: "running" });
258
- await updateJob(tempDir, job.id, { session_id: "sess-1" });
259
- const final = await updateJob(tempDir, job.id, {
260
- status: "completed",
261
- exit_reason: "success",
262
- });
263
-
264
- expect(final.status).toBe("completed");
265
- expect(final.session_id).toBe("sess-1");
266
- expect(final.exit_reason).toBe("success");
267
- });
268
- });
269
-
270
- describe("getJob", () => {
271
- let tempDir: string;
272
-
273
- beforeEach(async () => {
274
- tempDir = await createTempDir();
275
- });
276
-
277
- afterEach(async () => {
278
- await rm(tempDir, { recursive: true, force: true });
279
- });
280
-
281
- it("returns job when it exists", async () => {
282
- const created = await createJob(tempDir, {
283
- agent: "test-agent",
284
- trigger_type: "manual",
285
- prompt: "Test prompt",
286
- });
287
-
288
- const retrieved = await getJob(tempDir, created.id);
289
-
290
- expect(retrieved).not.toBeNull();
291
- expect(retrieved!.id).toBe(created.id);
292
- expect(retrieved!.agent).toBe("test-agent");
293
- expect(retrieved!.prompt).toBe("Test prompt");
294
- });
295
-
296
- it("returns null for non-existent job", async () => {
297
- const result = await getJob(tempDir, "job-2024-01-15-noexis");
298
- expect(result).toBeNull();
299
- });
300
-
301
- it("returns null and logs warning for corrupted job file", async () => {
302
- const logger = createMockLogger();
303
- const corruptedPath = join(tempDir, "job-2024-01-15-corpt1.yaml");
304
- // Use actually parseable YAML but with invalid schema
305
- await writeFile(corruptedPath, "agent: ''\nstatus: invalid\n", "utf-8");
306
-
307
- const result = await getJob(tempDir, "job-2024-01-15-corpt1", { logger });
308
-
309
- expect(result).toBeNull();
310
- expect(logger.warnings.length).toBeGreaterThan(0);
311
- });
312
-
313
- it("returns null for job with invalid schema", async () => {
314
- const logger = createMockLogger();
315
- const invalidJob = {
316
- id: "job-2024-01-15-invali",
317
- agent: "", // Invalid: empty
318
- trigger_type: "manual",
319
- status: "running",
320
- started_at: "2024-01-15T10:00:00Z",
321
- };
322
- await writeJobFile(tempDir, invalidJob as JobMetadata);
323
-
324
- const result = await getJob(tempDir, "job-2024-01-15-invali", { logger });
325
-
326
- expect(result).toBeNull();
327
- expect(logger.warnings.length).toBeGreaterThan(0);
328
- });
329
-
330
- it("throws for permission errors", async () => {
331
- // This is hard to test portably - skip or use mock
332
- });
333
- });
334
-
335
- describe("listJobs", () => {
336
- let tempDir: string;
337
-
338
- beforeEach(async () => {
339
- tempDir = await createTempDir();
340
- });
341
-
342
- afterEach(async () => {
343
- await rm(tempDir, { recursive: true, force: true });
344
- });
345
-
346
- it("returns empty list when directory is empty", async () => {
347
- const result = await listJobs(tempDir);
348
- expect(result.jobs).toEqual([]);
349
- expect(result.errors).toBe(0);
350
- });
351
-
352
- it("returns empty list when directory does not exist", async () => {
353
- const nonExistent = join(tempDir, "does-not-exist");
354
- const result = await listJobs(nonExistent);
355
- expect(result.jobs).toEqual([]);
356
- expect(result.errors).toBe(0);
357
- });
358
-
359
- it("lists all jobs in directory", async () => {
360
- await createJob(tempDir, { agent: "agent-1", trigger_type: "manual" });
361
- await createJob(tempDir, { agent: "agent-2", trigger_type: "schedule" });
362
- await createJob(tempDir, { agent: "agent-3", trigger_type: "webhook" });
363
-
364
- const result = await listJobs(tempDir);
365
-
366
- expect(result.jobs).toHaveLength(3);
367
- expect(result.errors).toBe(0);
368
- });
369
-
370
- it("sorts jobs by started_at descending (most recent first)", async () => {
371
- // Create jobs with known timestamps
372
- const job1: JobMetadata = {
373
- id: "job-2024-01-15-first1",
374
- agent: "agent",
375
- trigger_type: "manual",
376
- status: "completed",
377
- started_at: "2024-01-15T10:00:00Z",
378
- schedule: null,
379
- exit_reason: null,
380
- session_id: null,
381
- forked_from: null,
382
- finished_at: null,
383
- duration_seconds: null,
384
- prompt: null,
385
- summary: null,
386
- output_file: null,
387
- };
388
- const job2: JobMetadata = {
389
- ...job1,
390
- id: "job-2024-01-15-middl2",
391
- started_at: "2024-01-15T12:00:00Z",
392
- };
393
- const job3: JobMetadata = {
394
- ...job1,
395
- id: "job-2024-01-15-last03",
396
- started_at: "2024-01-15T14:00:00Z",
397
- };
398
-
399
- await writeJobFile(tempDir, job1);
400
- await writeJobFile(tempDir, job2);
401
- await writeJobFile(tempDir, job3);
402
-
403
- const result = await listJobs(tempDir);
404
-
405
- expect(result.jobs[0].id).toBe("job-2024-01-15-last03"); // Most recent
406
- expect(result.jobs[1].id).toBe("job-2024-01-15-middl2");
407
- expect(result.jobs[2].id).toBe("job-2024-01-15-first1"); // Oldest
408
- });
409
-
410
- describe("filtering by agent", () => {
411
- it("returns only jobs for specified agent", async () => {
412
- await createJob(tempDir, { agent: "agent-a", trigger_type: "manual" });
413
- await createJob(tempDir, { agent: "agent-b", trigger_type: "manual" });
414
- await createJob(tempDir, { agent: "agent-a", trigger_type: "schedule" });
415
-
416
- const result = await listJobs(tempDir, { agent: "agent-a" });
417
-
418
- expect(result.jobs).toHaveLength(2);
419
- expect(result.jobs.every((j) => j.agent === "agent-a")).toBe(true);
420
- });
421
- });
422
-
423
- describe("filtering by status", () => {
424
- it("returns only jobs with specified status", async () => {
425
- const job1 = await createJob(tempDir, {
426
- agent: "agent",
427
- trigger_type: "manual",
428
- });
429
- const job2 = await createJob(tempDir, {
430
- agent: "agent",
431
- trigger_type: "manual",
432
- });
433
- await createJob(tempDir, { agent: "agent", trigger_type: "manual" });
434
-
435
- await updateJob(tempDir, job1.id, { status: "completed" });
436
- await updateJob(tempDir, job2.id, { status: "failed" });
437
-
438
- const completedJobs = await listJobs(tempDir, { status: "completed" });
439
- expect(completedJobs.jobs).toHaveLength(1);
440
- expect(completedJobs.jobs[0].id).toBe(job1.id);
441
-
442
- const failedJobs = await listJobs(tempDir, { status: "failed" });
443
- expect(failedJobs.jobs).toHaveLength(1);
444
- expect(failedJobs.jobs[0].id).toBe(job2.id);
445
-
446
- const pendingJobs = await listJobs(tempDir, { status: "pending" });
447
- expect(pendingJobs.jobs).toHaveLength(1);
448
- });
449
- });
450
-
451
- describe("filtering by date range", () => {
452
- it("filters jobs started after a date", async () => {
453
- const oldJob: JobMetadata = {
454
- id: "job-2024-01-10-old001",
455
- agent: "agent",
456
- trigger_type: "manual",
457
- status: "completed",
458
- started_at: "2024-01-10T10:00:00Z",
459
- schedule: null,
460
- exit_reason: null,
461
- session_id: null,
462
- forked_from: null,
463
- finished_at: null,
464
- duration_seconds: null,
465
- prompt: null,
466
- summary: null,
467
- output_file: null,
468
- };
469
- const newJob: JobMetadata = {
470
- ...oldJob,
471
- id: "job-2024-01-20-new001",
472
- started_at: "2024-01-20T10:00:00Z",
473
- };
474
-
475
- await writeJobFile(tempDir, oldJob);
476
- await writeJobFile(tempDir, newJob);
477
-
478
- const result = await listJobs(tempDir, {
479
- startedAfter: "2024-01-15T00:00:00Z",
480
- });
481
-
482
- expect(result.jobs).toHaveLength(1);
483
- expect(result.jobs[0].id).toBe("job-2024-01-20-new001");
484
- });
485
-
486
- it("filters jobs started before a date", async () => {
487
- const oldJob: JobMetadata = {
488
- id: "job-2024-01-10-old002",
489
- agent: "agent",
490
- trigger_type: "manual",
491
- status: "completed",
492
- started_at: "2024-01-10T10:00:00Z",
493
- schedule: null,
494
- exit_reason: null,
495
- session_id: null,
496
- forked_from: null,
497
- finished_at: null,
498
- duration_seconds: null,
499
- prompt: null,
500
- summary: null,
501
- output_file: null,
502
- };
503
- const newJob: JobMetadata = {
504
- ...oldJob,
505
- id: "job-2024-01-20-new002",
506
- started_at: "2024-01-20T10:00:00Z",
507
- };
508
-
509
- await writeJobFile(tempDir, oldJob);
510
- await writeJobFile(tempDir, newJob);
511
-
512
- const result = await listJobs(tempDir, {
513
- startedBefore: "2024-01-15T00:00:00Z",
514
- });
515
-
516
- expect(result.jobs).toHaveLength(1);
517
- expect(result.jobs[0].id).toBe("job-2024-01-10-old002");
518
- });
519
-
520
- it("filters jobs within a date range", async () => {
521
- const veryOldJob: JobMetadata = {
522
- id: "job-2024-01-01-very01",
523
- agent: "agent",
524
- trigger_type: "manual",
525
- status: "completed",
526
- started_at: "2024-01-01T10:00:00Z",
527
- schedule: null,
528
- exit_reason: null,
529
- session_id: null,
530
- forked_from: null,
531
- finished_at: null,
532
- duration_seconds: null,
533
- prompt: null,
534
- summary: null,
535
- output_file: null,
536
- };
537
- const middleJob: JobMetadata = {
538
- ...veryOldJob,
539
- id: "job-2024-01-15-midd01",
540
- started_at: "2024-01-15T10:00:00Z",
541
- };
542
- const veryNewJob: JobMetadata = {
543
- ...veryOldJob,
544
- id: "job-2024-01-30-vnew01",
545
- started_at: "2024-01-30T10:00:00Z",
546
- };
547
-
548
- await writeJobFile(tempDir, veryOldJob);
549
- await writeJobFile(tempDir, middleJob);
550
- await writeJobFile(tempDir, veryNewJob);
551
-
552
- const result = await listJobs(tempDir, {
553
- startedAfter: "2024-01-10T00:00:00Z",
554
- startedBefore: "2024-01-20T00:00:00Z",
555
- });
556
-
557
- expect(result.jobs).toHaveLength(1);
558
- expect(result.jobs[0].id).toBe("job-2024-01-15-midd01");
559
- });
560
-
561
- it("accepts Date objects for date filters", async () => {
562
- const job: JobMetadata = {
563
- id: "job-2024-01-15-date01",
564
- agent: "agent",
565
- trigger_type: "manual",
566
- status: "completed",
567
- started_at: "2024-01-15T10:00:00Z",
568
- schedule: null,
569
- exit_reason: null,
570
- session_id: null,
571
- forked_from: null,
572
- finished_at: null,
573
- duration_seconds: null,
574
- prompt: null,
575
- summary: null,
576
- output_file: null,
577
- };
578
-
579
- await writeJobFile(tempDir, job);
580
-
581
- const result = await listJobs(tempDir, {
582
- startedAfter: new Date("2024-01-10T00:00:00Z"),
583
- startedBefore: new Date("2024-01-20T00:00:00Z"),
584
- });
585
-
586
- expect(result.jobs).toHaveLength(1);
587
- });
588
- });
589
-
590
- describe("combining filters", () => {
591
- it("applies multiple filters together", async () => {
592
- const job1: JobMetadata = {
593
- id: "job-2024-01-15-comb01",
594
- agent: "agent-a",
595
- trigger_type: "manual",
596
- status: "completed",
597
- started_at: "2024-01-15T10:00:00Z",
598
- schedule: null,
599
- exit_reason: null,
600
- session_id: null,
601
- forked_from: null,
602
- finished_at: null,
603
- duration_seconds: null,
604
- prompt: null,
605
- summary: null,
606
- output_file: null,
607
- };
608
- const job2: JobMetadata = {
609
- ...job1,
610
- id: "job-2024-01-15-comb02",
611
- agent: "agent-b",
612
- status: "completed",
613
- };
614
- const job3: JobMetadata = {
615
- ...job1,
616
- id: "job-2024-01-15-comb03",
617
- agent: "agent-a",
618
- status: "failed",
619
- };
620
- const job4: JobMetadata = {
621
- ...job1,
622
- id: "job-2024-01-10-comb04",
623
- started_at: "2024-01-10T10:00:00Z",
624
- status: "completed",
625
- };
626
-
627
- await writeJobFile(tempDir, job1);
628
- await writeJobFile(tempDir, job2);
629
- await writeJobFile(tempDir, job3);
630
- await writeJobFile(tempDir, job4);
631
-
632
- const result = await listJobs(tempDir, {
633
- agent: "agent-a",
634
- status: "completed",
635
- startedAfter: "2024-01-14T00:00:00Z",
636
- });
637
-
638
- expect(result.jobs).toHaveLength(1);
639
- expect(result.jobs[0].id).toBe("job-2024-01-15-comb01");
640
- });
641
- });
642
-
643
- describe("error handling", () => {
644
- it("counts and reports parse errors", async () => {
645
- const logger = createMockLogger();
646
- await createJob(tempDir, { agent: "agent", trigger_type: "manual" });
647
-
648
- // Create corrupted job file
649
- await writeFile(
650
- join(tempDir, "job-2024-01-15-corupt.yaml"),
651
- "invalid: [yaml",
652
- "utf-8"
653
- );
654
-
655
- const result = await listJobs(tempDir, {}, { logger });
656
-
657
- expect(result.jobs).toHaveLength(1);
658
- expect(result.errors).toBe(1);
659
- expect(logger.warnings.length).toBeGreaterThan(0);
660
- });
661
-
662
- it("ignores non-job files", async () => {
663
- await createJob(tempDir, { agent: "agent", trigger_type: "manual" });
664
-
665
- // Create non-job files
666
- await writeFile(join(tempDir, "other.yaml"), "foo: bar", "utf-8");
667
- await writeFile(join(tempDir, "job-.txt"), "not yaml", "utf-8");
668
-
669
- const result = await listJobs(tempDir);
670
-
671
- expect(result.jobs).toHaveLength(1);
672
- expect(result.errors).toBe(0);
673
- });
674
- });
675
- });
676
-
677
- describe("deleteJob", () => {
678
- let tempDir: string;
679
-
680
- beforeEach(async () => {
681
- tempDir = await createTempDir();
682
- });
683
-
684
- afterEach(async () => {
685
- await rm(tempDir, { recursive: true, force: true });
686
- });
687
-
688
- it("deletes existing job and returns true", async () => {
689
- const job = await createJob(tempDir, {
690
- agent: "test-agent",
691
- trigger_type: "manual",
692
- });
693
-
694
- const deleted = await deleteJob(tempDir, job.id);
695
-
696
- expect(deleted).toBe(true);
697
-
698
- // Verify file is gone
699
- const retrieved = await getJob(tempDir, job.id);
700
- expect(retrieved).toBeNull();
701
- });
702
-
703
- it("returns false for non-existent job", async () => {
704
- const deleted = await deleteJob(tempDir, "job-2024-01-15-noexis");
705
- expect(deleted).toBe(false);
706
- });
707
-
708
- it("does not affect other jobs", async () => {
709
- const job1 = await createJob(tempDir, {
710
- agent: "agent-1",
711
- trigger_type: "manual",
712
- });
713
- const job2 = await createJob(tempDir, {
714
- agent: "agent-2",
715
- trigger_type: "manual",
716
- });
717
-
718
- await deleteJob(tempDir, job1.id);
719
-
720
- const remaining = await listJobs(tempDir);
721
- expect(remaining.jobs).toHaveLength(1);
722
- expect(remaining.jobs[0].id).toBe(job2.id);
723
- });
724
- });
725
-
726
- describe("atomic write behavior", () => {
727
- let tempDir: string;
728
-
729
- beforeEach(async () => {
730
- tempDir = await createTempDir();
731
- });
732
-
733
- afterEach(async () => {
734
- await rm(tempDir, { recursive: true, force: true });
735
- });
736
-
737
- it("does not leave temp files on successful write", async () => {
738
- await createJob(tempDir, {
739
- agent: "test-agent",
740
- trigger_type: "manual",
741
- });
742
-
743
- const { readdir } = await import("node:fs/promises");
744
- const files = await readdir(tempDir);
745
- const tempFiles = files.filter((f) => f.includes(".tmp."));
746
- expect(tempFiles).toHaveLength(0);
747
- });
748
-
749
- it("preserves original file on update validation failure", async () => {
750
- const job = await createJob(tempDir, {
751
- agent: "test-agent",
752
- trigger_type: "manual",
753
- });
754
-
755
- // Try to update with invalid data (would fail validation if we didn't catch it)
756
- // The current implementation validates before write, so invalid updates fail early
757
- const retrieved = await getJob(tempDir, job.id);
758
- expect(retrieved).not.toBeNull();
759
- expect(retrieved!.agent).toBe("test-agent");
760
- });
761
- });
762
-
763
- describe("concurrent operations", () => {
764
- let tempDir: string;
765
-
766
- beforeEach(async () => {
767
- tempDir = await createTempDir();
768
- });
769
-
770
- afterEach(async () => {
771
- await rm(tempDir, { recursive: true, force: true });
772
- });
773
-
774
- it("handles multiple concurrent creates", async () => {
775
- const creates = [];
776
- for (let i = 0; i < 20; i++) {
777
- creates.push(
778
- createJob(tempDir, {
779
- agent: `agent-${i}`,
780
- trigger_type: "manual",
781
- })
782
- );
783
- }
784
-
785
- const jobs = await Promise.all(creates);
786
-
787
- // All jobs should have unique IDs
788
- const ids = new Set(jobs.map((j) => j.id));
789
- expect(ids.size).toBe(20);
790
-
791
- // All jobs should be retrievable
792
- const result = await listJobs(tempDir);
793
- expect(result.jobs).toHaveLength(20);
794
- });
795
-
796
- it("handles multiple concurrent reads", async () => {
797
- const job = await createJob(tempDir, {
798
- agent: "test-agent",
799
- trigger_type: "manual",
800
- });
801
-
802
- const reads = [];
803
- for (let i = 0; i < 50; i++) {
804
- reads.push(getJob(tempDir, job.id));
805
- }
806
-
807
- const results = await Promise.all(reads);
808
-
809
- for (const result of results) {
810
- expect(result).not.toBeNull();
811
- expect(result!.id).toBe(job.id);
812
- }
813
- });
814
-
815
- it("handles sequential updates correctly", async () => {
816
- const job = await createJob(tempDir, {
817
- agent: "test-agent",
818
- trigger_type: "manual",
819
- });
820
-
821
- // Sequential updates (not concurrent to avoid race conditions)
822
- for (let i = 0; i < 10; i++) {
823
- await updateJob(tempDir, job.id, {
824
- summary: `Update ${i}`,
825
- });
826
- }
827
-
828
- const final = await getJob(tempDir, job.id);
829
- expect(final!.summary).toBe("Update 9");
830
- });
831
- });