@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,604 +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
- getSessionInfo,
7
- updateSessionInfo,
8
- clearSession,
9
- type SessionLogger,
10
- } from "../session.js";
11
- import { type SessionInfo } from "../schemas/session-info.js";
12
- import { StateFileError } from "../errors.js";
13
-
14
- // Helper to create a temp directory
15
- async function createTempDir(): Promise<string> {
16
- const baseDir = join(
17
- tmpdir(),
18
- `herdctl-session-test-${Date.now()}-${Math.random().toString(36).slice(2)}`
19
- );
20
- await mkdir(baseDir, { recursive: true });
21
- // Resolve to real path to handle macOS /var -> /private/var symlink
22
- return await realpath(baseDir);
23
- }
24
-
25
- // Helper to create a mock logger
26
- function createMockLogger(): SessionLogger & { warnings: string[] } {
27
- const warnings: string[] = [];
28
- return {
29
- warnings,
30
- warn: (message: string) => warnings.push(message),
31
- };
32
- }
33
-
34
- // Helper to create a valid session JSON file
35
- async function writeSessionFile(
36
- dir: string,
37
- agentName: string,
38
- session: SessionInfo
39
- ): Promise<string> {
40
- const filePath = join(dir, `${agentName}.json`);
41
- await writeFile(filePath, JSON.stringify(session, null, 2), "utf-8");
42
- return filePath;
43
- }
44
-
45
- // Helper to create a valid session object
46
- function createValidSession(agentName: string): SessionInfo {
47
- const now = new Date().toISOString();
48
- return {
49
- agent_name: agentName,
50
- session_id: `session-${Math.random().toString(36).slice(2)}`,
51
- created_at: now,
52
- last_used_at: now,
53
- job_count: 0,
54
- mode: "autonomous",
55
- };
56
- }
57
-
58
- describe("getSessionInfo", () => {
59
- let tempDir: string;
60
-
61
- beforeEach(async () => {
62
- tempDir = await createTempDir();
63
- });
64
-
65
- afterEach(async () => {
66
- await rm(tempDir, { recursive: true, force: true });
67
- });
68
-
69
- it("returns session info when file exists", async () => {
70
- const session = createValidSession("test-agent");
71
- await writeSessionFile(tempDir, "test-agent", session);
72
-
73
- const result = await getSessionInfo(tempDir, "test-agent");
74
-
75
- expect(result).not.toBeNull();
76
- expect(result!.agent_name).toBe("test-agent");
77
- expect(result!.session_id).toBe(session.session_id);
78
- expect(result!.mode).toBe("autonomous");
79
- });
80
-
81
- it("returns null for non-existent session (handles missing file gracefully)", async () => {
82
- const result = await getSessionInfo(tempDir, "non-existent-agent");
83
- expect(result).toBeNull();
84
- });
85
-
86
- it("returns null for empty sessions directory", async () => {
87
- const result = await getSessionInfo(tempDir, "any-agent");
88
- expect(result).toBeNull();
89
- });
90
-
91
- it("returns null and logs warning for corrupted session file", async () => {
92
- const logger = createMockLogger();
93
- const corruptedPath = join(tempDir, "corrupted-agent.json");
94
- await writeFile(corruptedPath, "{ invalid json", "utf-8");
95
-
96
- const result = await getSessionInfo(tempDir, "corrupted-agent", { logger });
97
-
98
- expect(result).toBeNull();
99
- expect(logger.warnings.length).toBeGreaterThan(0);
100
- });
101
-
102
- it("returns null for session with invalid schema", async () => {
103
- const logger = createMockLogger();
104
- const invalidSession = {
105
- agent_name: "", // Invalid: empty
106
- session_id: "valid-session",
107
- created_at: "2024-01-15T10:00:00Z",
108
- last_used_at: "2024-01-15T10:00:00Z",
109
- job_count: 0,
110
- mode: "autonomous",
111
- };
112
- await writeSessionFile(tempDir, "invalid-agent", invalidSession as SessionInfo);
113
-
114
- const result = await getSessionInfo(tempDir, "invalid-agent", { logger });
115
-
116
- expect(result).toBeNull();
117
- expect(logger.warnings.length).toBeGreaterThan(0);
118
- });
119
-
120
- it("returns null for session with invalid mode", async () => {
121
- const logger = createMockLogger();
122
- const invalidSession = {
123
- agent_name: "test-agent",
124
- session_id: "valid-session",
125
- created_at: "2024-01-15T10:00:00Z",
126
- last_used_at: "2024-01-15T10:00:00Z",
127
- job_count: 0,
128
- mode: "invalid-mode",
129
- };
130
- await writeSessionFile(tempDir, "test-agent", invalidSession as SessionInfo);
131
-
132
- const result = await getSessionInfo(tempDir, "test-agent", { logger });
133
-
134
- expect(result).toBeNull();
135
- expect(logger.warnings.length).toBeGreaterThan(0);
136
- });
137
-
138
- it("returns null for session with invalid datetime format", async () => {
139
- const logger = createMockLogger();
140
- const invalidSession = {
141
- agent_name: "test-agent",
142
- session_id: "valid-session",
143
- created_at: "not-a-date",
144
- last_used_at: "2024-01-15T10:00:00Z",
145
- job_count: 0,
146
- mode: "autonomous",
147
- };
148
- await writeSessionFile(tempDir, "test-agent", invalidSession as SessionInfo);
149
-
150
- const result = await getSessionInfo(tempDir, "test-agent", { logger });
151
-
152
- expect(result).toBeNull();
153
- expect(logger.warnings.length).toBeGreaterThan(0);
154
- });
155
-
156
- it("validates all session fields correctly", async () => {
157
- const session: SessionInfo = {
158
- agent_name: "full-test-agent",
159
- session_id: "claude-session-abc123",
160
- created_at: "2024-01-15T10:00:00.000Z",
161
- last_used_at: "2024-01-15T12:30:00.000Z",
162
- job_count: 5,
163
- mode: "interactive",
164
- };
165
- await writeSessionFile(tempDir, "full-test-agent", session);
166
-
167
- const result = await getSessionInfo(tempDir, "full-test-agent");
168
-
169
- expect(result).not.toBeNull();
170
- expect(result!.agent_name).toBe("full-test-agent");
171
- expect(result!.session_id).toBe("claude-session-abc123");
172
- expect(result!.created_at).toBe("2024-01-15T10:00:00.000Z");
173
- expect(result!.last_used_at).toBe("2024-01-15T12:30:00.000Z");
174
- expect(result!.job_count).toBe(5);
175
- expect(result!.mode).toBe("interactive");
176
- });
177
-
178
- it("handles all valid session modes", async () => {
179
- const modes = ["autonomous", "interactive", "review"] as const;
180
-
181
- for (const mode of modes) {
182
- const session = createValidSession(`${mode}-agent`);
183
- session.mode = mode;
184
- await writeSessionFile(tempDir, `${mode}-agent`, session);
185
-
186
- const result = await getSessionInfo(tempDir, `${mode}-agent`);
187
- expect(result).not.toBeNull();
188
- expect(result!.mode).toBe(mode);
189
- }
190
- });
191
- });
192
-
193
- describe("updateSessionInfo", () => {
194
- let tempDir: string;
195
-
196
- beforeEach(async () => {
197
- tempDir = await createTempDir();
198
- });
199
-
200
- afterEach(async () => {
201
- await rm(tempDir, { recursive: true, force: true });
202
- });
203
-
204
- it("creates new session when none exists", async () => {
205
- const result = await updateSessionInfo(tempDir, "new-agent", {
206
- session_id: "new-session-123",
207
- mode: "autonomous",
208
- });
209
-
210
- expect(result.agent_name).toBe("new-agent");
211
- expect(result.session_id).toBe("new-session-123");
212
- expect(result.mode).toBe("autonomous");
213
- expect(result.job_count).toBe(0);
214
-
215
- // Verify file was created
216
- const content = await readFile(join(tempDir, "new-agent.json"), "utf-8");
217
- const parsed = JSON.parse(content);
218
- expect(parsed.agent_name).toBe("new-agent");
219
- });
220
-
221
- it("throws StateFileError when creating session without session_id", async () => {
222
- await expect(
223
- updateSessionInfo(tempDir, "new-agent", {
224
- mode: "autonomous",
225
- })
226
- ).rejects.toThrow(StateFileError);
227
- });
228
-
229
- it("updates existing session", async () => {
230
- const existingSession = createValidSession("test-agent");
231
- existingSession.job_count = 3;
232
- await writeSessionFile(tempDir, "test-agent", existingSession);
233
-
234
- const updated = await updateSessionInfo(tempDir, "test-agent", {
235
- job_count: 5,
236
- mode: "interactive",
237
- });
238
-
239
- expect(updated.job_count).toBe(5);
240
- expect(updated.mode).toBe("interactive");
241
- expect(updated.session_id).toBe(existingSession.session_id); // Preserved
242
- expect(updated.created_at).toBe(existingSession.created_at); // Preserved
243
- });
244
-
245
- it("automatically updates last_used_at", async () => {
246
- const existingSession = createValidSession("test-agent");
247
- const originalLastUsed = existingSession.last_used_at;
248
- await writeSessionFile(tempDir, "test-agent", existingSession);
249
-
250
- // Wait a bit to ensure timestamp differs
251
- await new Promise((resolve) => setTimeout(resolve, 10));
252
-
253
- const updated = await updateSessionInfo(tempDir, "test-agent", {
254
- job_count: 1,
255
- });
256
-
257
- expect(updated.last_used_at).not.toBe(originalLastUsed);
258
- expect(new Date(updated.last_used_at).getTime()).toBeGreaterThan(
259
- new Date(originalLastUsed).getTime()
260
- );
261
- });
262
-
263
- it("preserves agent_name even if update tries to change it", async () => {
264
- const existingSession = createValidSession("original-agent");
265
- await writeSessionFile(tempDir, "original-agent", existingSession);
266
-
267
- const updated = await updateSessionInfo(tempDir, "original-agent", {
268
- session_id: "new-session",
269
- });
270
-
271
- expect(updated.agent_name).toBe("original-agent");
272
- });
273
-
274
- it("preserves created_at even if update tries to change it", async () => {
275
- const existingSession = createValidSession("test-agent");
276
- const originalCreatedAt = existingSession.created_at;
277
- await writeSessionFile(tempDir, "test-agent", existingSession);
278
-
279
- const updated = await updateSessionInfo(tempDir, "test-agent", {
280
- job_count: 1,
281
- });
282
-
283
- expect(updated.created_at).toBe(originalCreatedAt);
284
- });
285
-
286
- it("updates session_id when provided", async () => {
287
- const existingSession = createValidSession("test-agent");
288
- await writeSessionFile(tempDir, "test-agent", existingSession);
289
-
290
- const updated = await updateSessionInfo(tempDir, "test-agent", {
291
- session_id: "new-session-456",
292
- });
293
-
294
- expect(updated.session_id).toBe("new-session-456");
295
- });
296
-
297
- it("handles corrupted existing file by treating as new", async () => {
298
- const corruptedPath = join(tempDir, "corrupted-agent.json");
299
- await writeFile(corruptedPath, "{ invalid json", "utf-8");
300
-
301
- const updated = await updateSessionInfo(tempDir, "corrupted-agent", {
302
- session_id: "fresh-session",
303
- mode: "autonomous",
304
- });
305
-
306
- expect(updated.agent_name).toBe("corrupted-agent");
307
- expect(updated.session_id).toBe("fresh-session");
308
- });
309
-
310
- it("throws StateFileError when directory does not exist", async () => {
311
- const nonExistentDir = join(tempDir, "does-not-exist");
312
-
313
- await expect(
314
- updateSessionInfo(nonExistentDir, "test-agent", {
315
- session_id: "test-session",
316
- })
317
- ).rejects.toThrow(StateFileError);
318
- });
319
-
320
- it("persists all fields correctly", async () => {
321
- await updateSessionInfo(tempDir, "test-agent", {
322
- session_id: "session-abc",
323
- mode: "review",
324
- job_count: 10,
325
- });
326
-
327
- // Read file directly and verify
328
- const content = await readFile(join(tempDir, "test-agent.json"), "utf-8");
329
- const parsed = JSON.parse(content);
330
-
331
- expect(parsed.agent_name).toBe("test-agent");
332
- expect(parsed.session_id).toBe("session-abc");
333
- expect(parsed.mode).toBe("review");
334
- expect(parsed.job_count).toBe(10);
335
- expect(parsed.created_at).toBeDefined();
336
- expect(parsed.last_used_at).toBeDefined();
337
- });
338
-
339
- it("handles multiple sequential updates", async () => {
340
- await updateSessionInfo(tempDir, "test-agent", {
341
- session_id: "session-1",
342
- });
343
-
344
- await updateSessionInfo(tempDir, "test-agent", {
345
- job_count: 1,
346
- });
347
-
348
- await updateSessionInfo(tempDir, "test-agent", {
349
- mode: "interactive",
350
- });
351
-
352
- const final = await getSessionInfo(tempDir, "test-agent");
353
- expect(final!.session_id).toBe("session-1");
354
- expect(final!.job_count).toBe(1);
355
- expect(final!.mode).toBe("interactive");
356
- });
357
- });
358
-
359
- describe("clearSession", () => {
360
- let tempDir: string;
361
-
362
- beforeEach(async () => {
363
- tempDir = await createTempDir();
364
- });
365
-
366
- afterEach(async () => {
367
- await rm(tempDir, { recursive: true, force: true });
368
- });
369
-
370
- it("deletes existing session and returns true", async () => {
371
- const session = createValidSession("test-agent");
372
- await writeSessionFile(tempDir, "test-agent", session);
373
-
374
- const deleted = await clearSession(tempDir, "test-agent");
375
-
376
- expect(deleted).toBe(true);
377
-
378
- // Verify file is gone
379
- const retrieved = await getSessionInfo(tempDir, "test-agent");
380
- expect(retrieved).toBeNull();
381
- });
382
-
383
- it("returns false for non-existent session", async () => {
384
- const deleted = await clearSession(tempDir, "non-existent-agent");
385
- expect(deleted).toBe(false);
386
- });
387
-
388
- it("does not affect other sessions", async () => {
389
- const session1 = createValidSession("agent-1");
390
- const session2 = createValidSession("agent-2");
391
- await writeSessionFile(tempDir, "agent-1", session1);
392
- await writeSessionFile(tempDir, "agent-2", session2);
393
-
394
- await clearSession(tempDir, "agent-1");
395
-
396
- const remaining = await getSessionInfo(tempDir, "agent-2");
397
- expect(remaining).not.toBeNull();
398
- expect(remaining!.agent_name).toBe("agent-2");
399
- });
400
-
401
- it("allows creating new session after clearing", async () => {
402
- const session = createValidSession("test-agent");
403
- await writeSessionFile(tempDir, "test-agent", session);
404
-
405
- await clearSession(tempDir, "test-agent");
406
-
407
- const newSession = await updateSessionInfo(tempDir, "test-agent", {
408
- session_id: "new-session-after-clear",
409
- });
410
-
411
- expect(newSession.session_id).toBe("new-session-after-clear");
412
- expect(newSession.job_count).toBe(0); // Fresh session
413
- });
414
- });
415
-
416
- describe("SessionInfoSchema validation", () => {
417
- let tempDir: string;
418
-
419
- beforeEach(async () => {
420
- tempDir = await createTempDir();
421
- });
422
-
423
- afterEach(async () => {
424
- await rm(tempDir, { recursive: true, force: true });
425
- });
426
-
427
- it("rejects negative job_count", async () => {
428
- const logger = createMockLogger();
429
- const invalidSession = {
430
- agent_name: "test-agent",
431
- session_id: "valid-session",
432
- created_at: "2024-01-15T10:00:00Z",
433
- last_used_at: "2024-01-15T10:00:00Z",
434
- job_count: -1,
435
- mode: "autonomous",
436
- };
437
- await writeSessionFile(tempDir, "test-agent", invalidSession as SessionInfo);
438
-
439
- const result = await getSessionInfo(tempDir, "test-agent", { logger });
440
- expect(result).toBeNull();
441
- });
442
-
443
- it("rejects non-integer job_count", async () => {
444
- const logger = createMockLogger();
445
- const invalidSession = {
446
- agent_name: "test-agent",
447
- session_id: "valid-session",
448
- created_at: "2024-01-15T10:00:00Z",
449
- last_used_at: "2024-01-15T10:00:00Z",
450
- job_count: 1.5,
451
- mode: "autonomous",
452
- };
453
- await writeSessionFile(tempDir, "test-agent", invalidSession as SessionInfo);
454
-
455
- const result = await getSessionInfo(tempDir, "test-agent", { logger });
456
- expect(result).toBeNull();
457
- });
458
-
459
- it("rejects empty session_id", async () => {
460
- const logger = createMockLogger();
461
- const invalidSession = {
462
- agent_name: "test-agent",
463
- session_id: "",
464
- created_at: "2024-01-15T10:00:00Z",
465
- last_used_at: "2024-01-15T10:00:00Z",
466
- job_count: 0,
467
- mode: "autonomous",
468
- };
469
- await writeSessionFile(tempDir, "test-agent", invalidSession as SessionInfo);
470
-
471
- const result = await getSessionInfo(tempDir, "test-agent", { logger });
472
- expect(result).toBeNull();
473
- });
474
- });
475
-
476
- describe("atomic write behavior", () => {
477
- let tempDir: string;
478
-
479
- beforeEach(async () => {
480
- tempDir = await createTempDir();
481
- });
482
-
483
- afterEach(async () => {
484
- await rm(tempDir, { recursive: true, force: true });
485
- });
486
-
487
- it("does not leave temp files on successful write", async () => {
488
- await updateSessionInfo(tempDir, "test-agent", {
489
- session_id: "test-session",
490
- });
491
-
492
- const { readdir } = await import("node:fs/promises");
493
- const files = await readdir(tempDir);
494
- const tempFiles = files.filter((f) => f.includes(".tmp."));
495
- expect(tempFiles).toHaveLength(0);
496
- });
497
-
498
- it("creates valid JSON file", async () => {
499
- await updateSessionInfo(tempDir, "test-agent", {
500
- session_id: "test-session",
501
- });
502
-
503
- const content = await readFile(join(tempDir, "test-agent.json"), "utf-8");
504
- // Should not throw
505
- const parsed = JSON.parse(content);
506
- expect(parsed).toBeDefined();
507
- });
508
- });
509
-
510
- describe("concurrent operations", () => {
511
- let tempDir: string;
512
-
513
- beforeEach(async () => {
514
- tempDir = await createTempDir();
515
- });
516
-
517
- afterEach(async () => {
518
- await rm(tempDir, { recursive: true, force: true });
519
- });
520
-
521
- it("handles multiple concurrent reads", async () => {
522
- const session = createValidSession("test-agent");
523
- await writeSessionFile(tempDir, "test-agent", session);
524
-
525
- const reads = [];
526
- for (let i = 0; i < 50; i++) {
527
- reads.push(getSessionInfo(tempDir, "test-agent"));
528
- }
529
-
530
- const results = await Promise.all(reads);
531
-
532
- for (const result of results) {
533
- expect(result).not.toBeNull();
534
- expect(result!.agent_name).toBe("test-agent");
535
- }
536
- });
537
-
538
- it("handles sequential updates correctly", async () => {
539
- await updateSessionInfo(tempDir, "test-agent", {
540
- session_id: "initial-session",
541
- });
542
-
543
- for (let i = 0; i < 10; i++) {
544
- await updateSessionInfo(tempDir, "test-agent", {
545
- job_count: i + 1,
546
- });
547
- }
548
-
549
- const final = await getSessionInfo(tempDir, "test-agent");
550
- expect(final!.job_count).toBe(10);
551
- });
552
-
553
- it("handles multiple agents concurrently", async () => {
554
- const updates = [];
555
- for (let i = 0; i < 10; i++) {
556
- updates.push(
557
- updateSessionInfo(tempDir, `agent-${i}`, {
558
- session_id: `session-${i}`,
559
- })
560
- );
561
- }
562
-
563
- const results = await Promise.all(updates);
564
-
565
- expect(results).toHaveLength(10);
566
- for (let i = 0; i < 10; i++) {
567
- expect(results[i].agent_name).toBe(`agent-${i}`);
568
- expect(results[i].session_id).toBe(`session-${i}`);
569
- }
570
- });
571
- });
572
-
573
- describe("file path handling", () => {
574
- let tempDir: string;
575
-
576
- beforeEach(async () => {
577
- tempDir = await createTempDir();
578
- });
579
-
580
- afterEach(async () => {
581
- await rm(tempDir, { recursive: true, force: true });
582
- });
583
-
584
- it("stores sessions at correct path", async () => {
585
- await updateSessionInfo(tempDir, "my-agent", {
586
- session_id: "test-session",
587
- });
588
-
589
- const expectedPath = join(tempDir, "my-agent.json");
590
- const content = await readFile(expectedPath, "utf-8");
591
- expect(content).toContain("my-agent");
592
- });
593
-
594
- it("handles agent names with special characters", async () => {
595
- // Agent names with dashes and underscores
596
- await updateSessionInfo(tempDir, "my-special_agent-123", {
597
- session_id: "test-session",
598
- });
599
-
600
- const result = await getSessionInfo(tempDir, "my-special_agent-123");
601
- expect(result).not.toBeNull();
602
- expect(result!.agent_name).toBe("my-special_agent-123");
603
- });
604
- });