@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,1708 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
- import { mkdir, rm, realpath, readdir } from "node:fs/promises";
3
- import { join } from "node:path";
4
- import { tmpdir } from "node:os";
5
- import { JobExecutor, executeJob, type SDKQueryFunction } from "../job-executor.js";
6
- import type { SDKMessage, RunnerOptionsWithCallbacks } from "../types.js";
7
- import type { ResolvedAgent } from "../../config/index.js";
8
- import {
9
- getJob,
10
- readJobOutputAll,
11
- initStateDirectory,
12
- getSessionInfo,
13
- } from "../../state/index.js";
14
-
15
- // =============================================================================
16
- // Test Helpers
17
- // =============================================================================
18
-
19
- async function createTempDir(): Promise<string> {
20
- const baseDir = join(
21
- tmpdir(),
22
- `herdctl-executor-test-${Date.now()}-${Math.random().toString(36).slice(2)}`
23
- );
24
- await mkdir(baseDir, { recursive: true });
25
- return await realpath(baseDir);
26
- }
27
-
28
- function createTestAgent(overrides: Partial<ResolvedAgent> = {}): ResolvedAgent {
29
- return {
30
- name: "test-agent",
31
- configPath: "/path/to/agent.yaml",
32
- ...overrides,
33
- };
34
- }
35
-
36
- function createMockLogger() {
37
- return {
38
- warnings: [] as string[],
39
- errors: [] as string[],
40
- infos: [] as string[],
41
- warn: (msg: string) => {
42
- // Suppress warnings during tests
43
- },
44
- error: (msg: string) => {
45
- // Suppress errors during tests
46
- },
47
- info: (msg: string) => {
48
- // Suppress info during tests
49
- },
50
- };
51
- }
52
-
53
- // Helper to create a mock SDK query function
54
- function createMockSDKQuery(messages: SDKMessage[]): SDKQueryFunction {
55
- return async function* mockQuery() {
56
- for (const message of messages) {
57
- yield message;
58
- }
59
- };
60
- }
61
-
62
- // Helper to create a mock SDK query that yields messages with delays
63
- function createDelayedSDKQuery(
64
- messages: SDKMessage[],
65
- delayMs: number = 10
66
- ): SDKQueryFunction {
67
- return async function* mockQuery() {
68
- for (const message of messages) {
69
- await new Promise((resolve) => setTimeout(resolve, delayMs));
70
- yield message;
71
- }
72
- };
73
- }
74
-
75
- // Helper to create a mock SDK query that throws an error
76
- function createErrorSDKQuery(error: Error): SDKQueryFunction {
77
- return async function* mockQuery() {
78
- throw error;
79
- };
80
- }
81
-
82
- // =============================================================================
83
- // JobExecutor tests
84
- // =============================================================================
85
-
86
- describe("JobExecutor", () => {
87
- let tempDir: string;
88
- let stateDir: string;
89
-
90
- beforeEach(async () => {
91
- tempDir = await createTempDir();
92
- stateDir = join(tempDir, ".herdctl");
93
- await initStateDirectory({ path: stateDir });
94
- });
95
-
96
- afterEach(async () => {
97
- await rm(tempDir, { recursive: true, force: true });
98
- });
99
-
100
- describe("job lifecycle", () => {
101
- it("creates job record before execution", async () => {
102
- const messages: SDKMessage[] = [
103
- { type: "system", content: "Initialized" },
104
- { type: "assistant", content: "Done" },
105
- ];
106
-
107
- const executor = new JobExecutor(createMockSDKQuery(messages), {
108
- logger: createMockLogger(),
109
- });
110
-
111
- const result = await executor.execute({
112
- agent: createTestAgent(),
113
- prompt: "Test prompt",
114
- stateDir,
115
- });
116
-
117
- expect(result.jobId).toMatch(/^job-\d{4}-\d{2}-\d{2}-[a-z0-9]+$/);
118
-
119
- const job = await getJob(join(stateDir, "jobs"), result.jobId);
120
- expect(job).not.toBeNull();
121
- expect(job?.agent).toBe("test-agent");
122
- });
123
-
124
- it("updates job status to running", async () => {
125
- let jobIdDuringExecution: string | undefined;
126
-
127
- const sdkQuery: SDKQueryFunction = async function* () {
128
- // During execution, we can check the job status
129
- yield { type: "system", content: "Running" };
130
- };
131
-
132
- const executor = new JobExecutor(sdkQuery, {
133
- logger: createMockLogger(),
134
- });
135
-
136
- const result = await executor.execute({
137
- agent: createTestAgent(),
138
- prompt: "Test prompt",
139
- stateDir,
140
- });
141
-
142
- // Job should be completed now, but was running during execution
143
- const job = await getJob(join(stateDir, "jobs"), result.jobId);
144
- expect(job?.status).toBe("completed");
145
- });
146
-
147
- it("updates job with final status on success", async () => {
148
- const messages: SDKMessage[] = [
149
- { type: "system", content: "Start" },
150
- { type: "assistant", content: "Task completed!" },
151
- ];
152
-
153
- const executor = new JobExecutor(createMockSDKQuery(messages), {
154
- logger: createMockLogger(),
155
- });
156
-
157
- const result = await executor.execute({
158
- agent: createTestAgent(),
159
- prompt: "Test prompt",
160
- stateDir,
161
- });
162
-
163
- expect(result.success).toBe(true);
164
-
165
- const job = await getJob(join(stateDir, "jobs"), result.jobId);
166
- expect(job?.status).toBe("completed");
167
- expect(job?.exit_reason).toBe("success");
168
- expect(job?.finished_at).toBeDefined();
169
- expect(job?.duration_seconds).toBeGreaterThanOrEqual(0);
170
- });
171
-
172
- it("updates job with failed status on error", async () => {
173
- const messages: SDKMessage[] = [
174
- { type: "system", content: "Start" },
175
- { type: "error", message: "Something went wrong" },
176
- ];
177
-
178
- const executor = new JobExecutor(createMockSDKQuery(messages), {
179
- logger: createMockLogger(),
180
- });
181
-
182
- const result = await executor.execute({
183
- agent: createTestAgent(),
184
- prompt: "Test prompt",
185
- stateDir,
186
- });
187
-
188
- expect(result.success).toBe(false);
189
- expect(result.error?.message).toContain("Something went wrong");
190
-
191
- const job = await getJob(join(stateDir, "jobs"), result.jobId);
192
- expect(job?.status).toBe("failed");
193
- expect(job?.exit_reason).toBe("error");
194
- });
195
-
196
- it("handles SDK query throwing an error", async () => {
197
- const executor = new JobExecutor(
198
- createErrorSDKQuery(new Error("SDK error")),
199
- { logger: createMockLogger() }
200
- );
201
-
202
- const result = await executor.execute({
203
- agent: createTestAgent(),
204
- prompt: "Test prompt",
205
- stateDir,
206
- });
207
-
208
- expect(result.success).toBe(false);
209
- expect(result.error?.message).toContain("SDK error");
210
-
211
- const job = await getJob(join(stateDir, "jobs"), result.jobId);
212
- expect(job?.status).toBe("failed");
213
- });
214
- });
215
-
216
- describe("streaming output", () => {
217
- it("writes all messages to job output", async () => {
218
- const messages: SDKMessage[] = [
219
- { type: "system", content: "Init" },
220
- { type: "assistant", content: "Hello" },
221
- { type: "tool_use", tool_name: "bash", input: { command: "ls" } },
222
- { type: "tool_result", result: "file1\nfile2", success: true },
223
- { type: "assistant", content: "Done" },
224
- ];
225
-
226
- const executor = new JobExecutor(createMockSDKQuery(messages), {
227
- logger: createMockLogger(),
228
- });
229
-
230
- const result = await executor.execute({
231
- agent: createTestAgent(),
232
- prompt: "Test prompt",
233
- stateDir,
234
- });
235
-
236
- const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
237
- expect(output).toHaveLength(5);
238
- expect(output[0].type).toBe("system");
239
- expect(output[1].type).toBe("assistant");
240
- expect(output[2].type).toBe("tool_use");
241
- expect(output[3].type).toBe("tool_result");
242
- expect(output[4].type).toBe("assistant");
243
- });
244
-
245
- it("writes output immediately without buffering", async () => {
246
- let outputCountDuringExecution = 0;
247
- const jobsDir = join(stateDir, "jobs");
248
-
249
- const sdkQuery: SDKQueryFunction = async function* () {
250
- yield { type: "system", content: "First" };
251
- yield { type: "assistant", content: "Second" };
252
- yield { type: "assistant", content: "Third" };
253
- };
254
-
255
- // We can't easily verify real-time writing in a unit test,
256
- // but we can verify all messages are written
257
- const executor = new JobExecutor(sdkQuery, {
258
- logger: createMockLogger(),
259
- });
260
-
261
- const result = await executor.execute({
262
- agent: createTestAgent(),
263
- prompt: "Test prompt",
264
- stateDir,
265
- });
266
-
267
- const output = await readJobOutputAll(jobsDir, result.jobId);
268
- expect(output).toHaveLength(3);
269
- });
270
-
271
- it("preserves message content and metadata", async () => {
272
- const messages: SDKMessage[] = [
273
- { type: "system", content: "Session init", subtype: "session_start" },
274
- {
275
- type: "assistant",
276
- content: "Response",
277
- usage: { input_tokens: 100, output_tokens: 50 },
278
- },
279
- {
280
- type: "tool_use",
281
- tool_name: "read_file",
282
- tool_use_id: "tool-123",
283
- input: { path: "/etc/hosts" },
284
- },
285
- {
286
- type: "tool_result",
287
- tool_use_id: "tool-123",
288
- result: "localhost",
289
- success: true,
290
- },
291
- ];
292
-
293
- const executor = new JobExecutor(createMockSDKQuery(messages), {
294
- logger: createMockLogger(),
295
- });
296
-
297
- const result = await executor.execute({
298
- agent: createTestAgent(),
299
- prompt: "Test prompt",
300
- stateDir,
301
- });
302
-
303
- const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
304
-
305
- // Check system message
306
- expect(output[0].type).toBe("system");
307
- if (output[0].type === "system") {
308
- expect(output[0].content).toBe("Session init");
309
- expect(output[0].subtype).toBe("session_start");
310
- }
311
-
312
- // Check assistant message
313
- expect(output[1].type).toBe("assistant");
314
- if (output[1].type === "assistant") {
315
- expect(output[1].content).toBe("Response");
316
- expect(output[1].usage?.input_tokens).toBe(100);
317
- expect(output[1].usage?.output_tokens).toBe(50);
318
- }
319
-
320
- // Check tool_use message
321
- expect(output[2].type).toBe("tool_use");
322
- if (output[2].type === "tool_use") {
323
- expect(output[2].tool_name).toBe("read_file");
324
- expect(output[2].tool_use_id).toBe("tool-123");
325
- expect(output[2].input).toEqual({ path: "/etc/hosts" });
326
- }
327
-
328
- // Check tool_result message
329
- expect(output[3].type).toBe("tool_result");
330
- if (output[3].type === "tool_result") {
331
- expect(output[3].tool_use_id).toBe("tool-123");
332
- expect(output[3].result).toBe("localhost");
333
- expect(output[3].success).toBe(true);
334
- }
335
- });
336
-
337
- it("writes error message to output when SDK throws", async () => {
338
- const executor = new JobExecutor(
339
- createErrorSDKQuery(new Error("Connection failed")),
340
- { logger: createMockLogger() }
341
- );
342
-
343
- const result = await executor.execute({
344
- agent: createTestAgent(),
345
- prompt: "Test prompt",
346
- stateDir,
347
- });
348
-
349
- const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
350
- expect(output.some((m) => m.type === "error")).toBe(true);
351
-
352
- const errorMsg = output.find((m) => m.type === "error");
353
- if (errorMsg?.type === "error") {
354
- expect(errorMsg.message).toContain("Connection failed");
355
- }
356
- });
357
- });
358
-
359
- describe("message type handling", () => {
360
- it("handles all message types correctly", async () => {
361
- const messages: SDKMessage[] = [
362
- { type: "system", content: "System message", subtype: "init" },
363
- { type: "assistant", content: "Assistant message", partial: false },
364
- { type: "tool_use", tool_name: "bash", input: { cmd: "test" } },
365
- { type: "tool_result", result: "output", success: true },
366
- { type: "error", message: "Error message", code: "ERR_TEST" },
367
- ];
368
-
369
- const executor = new JobExecutor(createMockSDKQuery(messages), {
370
- logger: createMockLogger(),
371
- });
372
-
373
- const result = await executor.execute({
374
- agent: createTestAgent(),
375
- prompt: "Test prompt",
376
- stateDir,
377
- });
378
-
379
- const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
380
-
381
- expect(output[0].type).toBe("system");
382
- expect(output[1].type).toBe("assistant");
383
- expect(output[2].type).toBe("tool_use");
384
- expect(output[3].type).toBe("tool_result");
385
- expect(output[4].type).toBe("error");
386
- });
387
-
388
- it("handles partial assistant messages", async () => {
389
- const messages: SDKMessage[] = [
390
- { type: "assistant", content: "Part 1...", partial: true },
391
- { type: "assistant", content: "Part 1... Part 2...", partial: true },
392
- { type: "assistant", content: "Part 1... Part 2... Done!", partial: false },
393
- ];
394
-
395
- const executor = new JobExecutor(createMockSDKQuery(messages), {
396
- logger: createMockLogger(),
397
- });
398
-
399
- const result = await executor.execute({
400
- agent: createTestAgent(),
401
- prompt: "Test prompt",
402
- stateDir,
403
- });
404
-
405
- const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
406
-
407
- expect(output).toHaveLength(3);
408
- if (output[0].type === "assistant") {
409
- expect(output[0].partial).toBe(true);
410
- }
411
- if (output[2].type === "assistant") {
412
- expect(output[2].partial).toBe(false);
413
- }
414
- });
415
-
416
- it("handles tool_result with error", async () => {
417
- const messages: SDKMessage[] = [
418
- { type: "tool_use", tool_name: "read_file", input: { path: "/nope" } },
419
- { type: "tool_result", success: false, error: "File not found" },
420
- ];
421
-
422
- const executor = new JobExecutor(createMockSDKQuery(messages), {
423
- logger: createMockLogger(),
424
- });
425
-
426
- const result = await executor.execute({
427
- agent: createTestAgent(),
428
- prompt: "Test prompt",
429
- stateDir,
430
- });
431
-
432
- const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
433
-
434
- if (output[1].type === "tool_result") {
435
- expect(output[1].success).toBe(false);
436
- expect(output[1].error).toBe("File not found");
437
- }
438
- });
439
- });
440
-
441
- describe("session handling", () => {
442
- it("extracts session ID from system message with init subtype", async () => {
443
- const messages: SDKMessage[] = [
444
- { type: "system", content: "Init", subtype: "init", session_id: "session-abc123" },
445
- { type: "assistant", content: "Done" },
446
- ];
447
-
448
- const executor = new JobExecutor(createMockSDKQuery(messages), {
449
- logger: createMockLogger(),
450
- });
451
-
452
- const result = await executor.execute({
453
- agent: createTestAgent(),
454
- prompt: "Test prompt",
455
- stateDir,
456
- });
457
-
458
- expect(result.sessionId).toBe("session-abc123");
459
-
460
- const job = await getJob(join(stateDir, "jobs"), result.jobId);
461
- expect(job?.session_id).toBe("session-abc123");
462
- });
463
-
464
- it("does not extract session ID from non-init system messages", async () => {
465
- const messages: SDKMessage[] = [
466
- { type: "system", content: "Progress", subtype: "progress", session_id: "should-ignore" },
467
- { type: "assistant", content: "Done" },
468
- ];
469
-
470
- const executor = new JobExecutor(createMockSDKQuery(messages), {
471
- logger: createMockLogger(),
472
- });
473
-
474
- const result = await executor.execute({
475
- agent: createTestAgent(),
476
- prompt: "Test prompt",
477
- stateDir,
478
- });
479
-
480
- expect(result.sessionId).toBeUndefined();
481
-
482
- const job = await getJob(join(stateDir, "jobs"), result.jobId);
483
- expect(job?.session_id).toBeUndefined();
484
- });
485
-
486
- it("persists session info via updateSessionInfo", async () => {
487
- const messages: SDKMessage[] = [
488
- { type: "system", content: "Init", subtype: "init", session_id: "session-persist-123" },
489
- { type: "assistant", content: "Done" },
490
- ];
491
-
492
- const executor = new JobExecutor(createMockSDKQuery(messages), {
493
- logger: createMockLogger(),
494
- });
495
-
496
- await executor.execute({
497
- agent: createTestAgent({ name: "persist-agent" }),
498
- prompt: "Test prompt",
499
- stateDir,
500
- });
501
-
502
- // Verify session info was persisted
503
- const sessionInfo = await getSessionInfo(join(stateDir, "sessions"), "persist-agent");
504
- expect(sessionInfo).not.toBeNull();
505
- expect(sessionInfo?.session_id).toBe("session-persist-123");
506
- expect(sessionInfo?.agent_name).toBe("persist-agent");
507
- expect(sessionInfo?.job_count).toBe(1);
508
- expect(sessionInfo?.mode).toBe("autonomous");
509
- });
510
-
511
- it("increments job_count on subsequent runs with session", async () => {
512
- const messages: SDKMessage[] = [
513
- { type: "system", content: "Init", subtype: "init", session_id: "session-multi-123" },
514
- { type: "assistant", content: "Done" },
515
- ];
516
-
517
- const executor = new JobExecutor(createMockSDKQuery(messages), {
518
- logger: createMockLogger(),
519
- });
520
-
521
- // Run twice
522
- await executor.execute({
523
- agent: createTestAgent({ name: "multi-agent" }),
524
- prompt: "First run",
525
- stateDir,
526
- });
527
-
528
- await executor.execute({
529
- agent: createTestAgent({ name: "multi-agent" }),
530
- prompt: "Second run",
531
- stateDir,
532
- });
533
-
534
- // Verify job_count was incremented
535
- const sessionInfo = await getSessionInfo(join(stateDir, "sessions"), "multi-agent");
536
- expect(sessionInfo?.job_count).toBe(2);
537
- });
538
-
539
- it("stores timestamps in session info", async () => {
540
- const messages: SDKMessage[] = [
541
- { type: "system", content: "Init", subtype: "init", session_id: "session-ts-123" },
542
- { type: "assistant", content: "Done" },
543
- ];
544
-
545
- const executor = new JobExecutor(createMockSDKQuery(messages), {
546
- logger: createMockLogger(),
547
- });
548
-
549
- await executor.execute({
550
- agent: createTestAgent({ name: "ts-agent" }),
551
- prompt: "Test prompt",
552
- stateDir,
553
- });
554
-
555
- const sessionInfo = await getSessionInfo(join(stateDir, "sessions"), "ts-agent");
556
- expect(sessionInfo?.created_at).toBeDefined();
557
- expect(sessionInfo?.last_used_at).toBeDefined();
558
-
559
- // Verify they are valid ISO timestamps
560
- expect(() => new Date(sessionInfo!.created_at)).not.toThrow();
561
- expect(() => new Date(sessionInfo!.last_used_at)).not.toThrow();
562
- });
563
-
564
- it("returns session ID in RunnerResult for caller use", async () => {
565
- const messages: SDKMessage[] = [
566
- { type: "system", content: "Init", subtype: "init", session_id: "session-result-123" },
567
- { type: "assistant", content: "Done" },
568
- ];
569
-
570
- const executor = new JobExecutor(createMockSDKQuery(messages), {
571
- logger: createMockLogger(),
572
- });
573
-
574
- const result = await executor.execute({
575
- agent: createTestAgent(),
576
- prompt: "Test prompt",
577
- stateDir,
578
- });
579
-
580
- // Session ID should be available in the result for caller use
581
- expect(result.sessionId).toBe("session-result-123");
582
- });
583
-
584
- it("passes resume option to SDK", async () => {
585
- let receivedOptions: Record<string, unknown> | undefined;
586
-
587
- const sdkQuery: SDKQueryFunction = async function* (params) {
588
- receivedOptions = params.options;
589
- yield { type: "assistant", content: "Resumed" };
590
- };
591
-
592
- const executor = new JobExecutor(sdkQuery, {
593
- logger: createMockLogger(),
594
- });
595
-
596
- await executor.execute({
597
- agent: createTestAgent(),
598
- prompt: "Test prompt",
599
- stateDir,
600
- resume: "session-to-resume",
601
- });
602
-
603
- expect(receivedOptions?.resume).toBe("session-to-resume");
604
- });
605
-
606
- it("passes fork option to SDK", async () => {
607
- let receivedOptions: Record<string, unknown> | undefined;
608
-
609
- const sdkQuery: SDKQueryFunction = async function* (params) {
610
- receivedOptions = params.options;
611
- yield { type: "assistant", content: "Forked" };
612
- };
613
-
614
- const executor = new JobExecutor(sdkQuery, {
615
- logger: createMockLogger(),
616
- });
617
-
618
- await executor.execute({
619
- agent: createTestAgent(),
620
- prompt: "Test prompt",
621
- stateDir,
622
- fork: "session-to-fork",
623
- });
624
-
625
- expect(receivedOptions?.forkSession).toBe(true);
626
- });
627
-
628
- it("creates job with trigger_type 'fork' and forked_from when forking", async () => {
629
- const messages: SDKMessage[] = [
630
- { type: "system", content: "Init", subtype: "init", session_id: "forked-session-123" },
631
- { type: "assistant", content: "Forked session started" },
632
- ];
633
-
634
- const executor = new JobExecutor(createMockSDKQuery(messages), {
635
- logger: createMockLogger(),
636
- });
637
-
638
- const result = await executor.execute({
639
- agent: createTestAgent({ name: "fork-agent" }),
640
- prompt: "Continue from where we left off",
641
- stateDir,
642
- fork: "original-session-id",
643
- forkedFrom: "job-2024-01-15-abc123",
644
- });
645
-
646
- expect(result.success).toBe(true);
647
-
648
- const job = await getJob(join(stateDir, "jobs"), result.jobId);
649
- expect(job?.trigger_type).toBe("fork");
650
- expect(job?.forked_from).toBe("job-2024-01-15-abc123");
651
- });
652
-
653
- it("sets trigger_type to fork even if triggerType option provided", async () => {
654
- const messages: SDKMessage[] = [
655
- { type: "assistant", content: "Forked" },
656
- ];
657
-
658
- const executor = new JobExecutor(createMockSDKQuery(messages), {
659
- logger: createMockLogger(),
660
- });
661
-
662
- const result = await executor.execute({
663
- agent: createTestAgent(),
664
- prompt: "Test prompt",
665
- stateDir,
666
- fork: "session-to-fork",
667
- triggerType: "manual", // Should be overridden by fork
668
- });
669
-
670
- const job = await getJob(join(stateDir, "jobs"), result.jobId);
671
- expect(job?.trigger_type).toBe("fork");
672
- });
673
-
674
- it("does not set forked_from when not forking", async () => {
675
- const messages: SDKMessage[] = [
676
- { type: "assistant", content: "Normal run" },
677
- ];
678
-
679
- const executor = new JobExecutor(createMockSDKQuery(messages), {
680
- logger: createMockLogger(),
681
- });
682
-
683
- const result = await executor.execute({
684
- agent: createTestAgent(),
685
- prompt: "Test prompt",
686
- stateDir,
687
- });
688
-
689
- const job = await getJob(join(stateDir, "jobs"), result.jobId);
690
- expect(job?.trigger_type).toBe("manual");
691
- expect(job?.forked_from).toBeNull();
692
- });
693
-
694
- it("updates session info job_count after resume", async () => {
695
- const messages: SDKMessage[] = [
696
- { type: "system", content: "Init", subtype: "init", session_id: "resume-session-123" },
697
- { type: "assistant", content: "Resumed" },
698
- ];
699
-
700
- const executor = new JobExecutor(createMockSDKQuery(messages), {
701
- logger: createMockLogger(),
702
- });
703
-
704
- // First run to establish the session
705
- await executor.execute({
706
- agent: createTestAgent({ name: "resume-test-agent" }),
707
- prompt: "First run",
708
- stateDir,
709
- });
710
-
711
- // Check initial job count
712
- let sessionInfo = await getSessionInfo(join(stateDir, "sessions"), "resume-test-agent");
713
- expect(sessionInfo?.job_count).toBe(1);
714
-
715
- // Resume the session
716
- await executor.execute({
717
- agent: createTestAgent({ name: "resume-test-agent" }),
718
- prompt: "Resume run",
719
- stateDir,
720
- resume: "resume-session-123",
721
- });
722
-
723
- // Check job count was incremented
724
- sessionInfo = await getSessionInfo(join(stateDir, "sessions"), "resume-test-agent");
725
- expect(sessionInfo?.job_count).toBe(2);
726
- });
727
-
728
- it("updates session info job_count after fork", async () => {
729
- const messages: SDKMessage[] = [
730
- { type: "system", content: "Init", subtype: "init", session_id: "fork-session-123" },
731
- { type: "assistant", content: "Forked" },
732
- ];
733
-
734
- const executor = new JobExecutor(createMockSDKQuery(messages), {
735
- logger: createMockLogger(),
736
- });
737
-
738
- // First run to establish a session
739
- await executor.execute({
740
- agent: createTestAgent({ name: "fork-test-agent" }),
741
- prompt: "First run",
742
- stateDir,
743
- });
744
-
745
- // Check initial job count
746
- let sessionInfo = await getSessionInfo(join(stateDir, "sessions"), "fork-test-agent");
747
- expect(sessionInfo?.job_count).toBe(1);
748
-
749
- // Fork the session
750
- await executor.execute({
751
- agent: createTestAgent({ name: "fork-test-agent" }),
752
- prompt: "Fork run",
753
- stateDir,
754
- fork: "original-session-id",
755
- forkedFrom: "job-2024-01-15-parent",
756
- });
757
-
758
- // Check job count was incremented
759
- sessionInfo = await getSessionInfo(join(stateDir, "sessions"), "fork-test-agent");
760
- expect(sessionInfo?.job_count).toBe(2);
761
- });
762
-
763
- it("does not persist session info when no session ID is present", async () => {
764
- const messages: SDKMessage[] = [
765
- { type: "system", content: "No session" },
766
- { type: "assistant", content: "Done" },
767
- ];
768
-
769
- const executor = new JobExecutor(createMockSDKQuery(messages), {
770
- logger: createMockLogger(),
771
- });
772
-
773
- await executor.execute({
774
- agent: createTestAgent({ name: "no-session-agent" }),
775
- prompt: "Test prompt",
776
- stateDir,
777
- });
778
-
779
- // Verify no session info was created
780
- const sessionInfo = await getSessionInfo(join(stateDir, "sessions"), "no-session-agent");
781
- expect(sessionInfo).toBeNull();
782
- });
783
- });
784
-
785
- describe("summary extraction", () => {
786
- it("extracts summary from explicit summary field", async () => {
787
- const messages: SDKMessage[] = [
788
- { type: "assistant", content: "Working..." },
789
- { type: "assistant", content: "Done!", summary: "Task completed" },
790
- ];
791
-
792
- const executor = new JobExecutor(createMockSDKQuery(messages), {
793
- logger: createMockLogger(),
794
- });
795
-
796
- const result = await executor.execute({
797
- agent: createTestAgent(),
798
- prompt: "Test prompt",
799
- stateDir,
800
- });
801
-
802
- expect(result.summary).toBe("Task completed");
803
-
804
- const job = await getJob(join(stateDir, "jobs"), result.jobId);
805
- expect(job?.summary).toBe("Task completed");
806
- });
807
-
808
- it("extracts summary from short final assistant message", async () => {
809
- const messages: SDKMessage[] = [
810
- { type: "assistant", content: "Working..." },
811
- { type: "assistant", content: "All tasks finished successfully." },
812
- ];
813
-
814
- const executor = new JobExecutor(createMockSDKQuery(messages), {
815
- logger: createMockLogger(),
816
- });
817
-
818
- const result = await executor.execute({
819
- agent: createTestAgent(),
820
- prompt: "Test prompt",
821
- stateDir,
822
- });
823
-
824
- expect(result.summary).toBe("All tasks finished successfully.");
825
- });
826
-
827
- it("returns undefined summary when no assistant messages exist", async () => {
828
- const messages: SDKMessage[] = [
829
- { type: "system", content: "Initialized" },
830
- { type: "tool_use", tool_name: "bash", input: { command: "ls" } },
831
- { type: "tool_result", result: "file1\nfile2", success: true },
832
- ];
833
-
834
- const executor = new JobExecutor(createMockSDKQuery(messages), {
835
- logger: createMockLogger(),
836
- });
837
-
838
- const result = await executor.execute({
839
- agent: createTestAgent(),
840
- prompt: "Test prompt",
841
- stateDir,
842
- });
843
-
844
- expect(result.summary).toBeUndefined();
845
-
846
- const job = await getJob(join(stateDir, "jobs"), result.jobId);
847
- expect(job?.summary).toBeUndefined();
848
- });
849
-
850
- it("returns undefined summary when all assistant messages are partial", async () => {
851
- const messages: SDKMessage[] = [
852
- { type: "assistant", content: "Partial 1...", partial: true },
853
- { type: "assistant", content: "Partial 2...", partial: true },
854
- ];
855
-
856
- const executor = new JobExecutor(createMockSDKQuery(messages), {
857
- logger: createMockLogger(),
858
- });
859
-
860
- const result = await executor.execute({
861
- agent: createTestAgent(),
862
- prompt: "Test prompt",
863
- stateDir,
864
- });
865
-
866
- expect(result.summary).toBeUndefined();
867
- });
868
-
869
- it("returns undefined summary when all assistant messages are too long", async () => {
870
- const longContent = "x".repeat(501);
871
- const messages: SDKMessage[] = [
872
- { type: "assistant", content: longContent },
873
- { type: "assistant", content: longContent },
874
- ];
875
-
876
- const executor = new JobExecutor(createMockSDKQuery(messages), {
877
- logger: createMockLogger(),
878
- });
879
-
880
- const result = await executor.execute({
881
- agent: createTestAgent(),
882
- prompt: "Test prompt",
883
- stateDir,
884
- });
885
-
886
- expect(result.summary).toBeUndefined();
887
- });
888
-
889
- it("truncates long explicit summary to 500 chars", async () => {
890
- const longSummary = "x".repeat(600);
891
- const messages: SDKMessage[] = [
892
- { type: "assistant", content: "Done!", summary: longSummary },
893
- ];
894
-
895
- const executor = new JobExecutor(createMockSDKQuery(messages), {
896
- logger: createMockLogger(),
897
- });
898
-
899
- const result = await executor.execute({
900
- agent: createTestAgent(),
901
- prompt: "Test prompt",
902
- stateDir,
903
- });
904
-
905
- expect(result.summary).toBeDefined();
906
- expect(result.summary!.length).toBe(500);
907
- expect(result.summary!.endsWith("...")).toBe(true);
908
-
909
- const job = await getJob(join(stateDir, "jobs"), result.jobId);
910
- expect(job?.summary?.length).toBe(500);
911
- });
912
-
913
- it("uses latest summary when multiple messages have summaries", async () => {
914
- const messages: SDKMessage[] = [
915
- { type: "assistant", content: "First", summary: "First summary" },
916
- { type: "assistant", content: "Second", summary: "Second summary" },
917
- { type: "assistant", content: "Third", summary: "Final summary" },
918
- ];
919
-
920
- const executor = new JobExecutor(createMockSDKQuery(messages), {
921
- logger: createMockLogger(),
922
- });
923
-
924
- const result = await executor.execute({
925
- agent: createTestAgent(),
926
- prompt: "Test prompt",
927
- stateDir,
928
- });
929
-
930
- expect(result.summary).toBe("Final summary");
931
- });
932
-
933
- it("handles empty message stream with undefined summary", async () => {
934
- const messages: SDKMessage[] = [];
935
-
936
- const executor = new JobExecutor(createMockSDKQuery(messages), {
937
- logger: createMockLogger(),
938
- });
939
-
940
- const result = await executor.execute({
941
- agent: createTestAgent(),
942
- prompt: "Test prompt",
943
- stateDir,
944
- });
945
-
946
- expect(result.summary).toBeUndefined();
947
- });
948
- });
949
-
950
- describe("callbacks", () => {
951
- it("calls onMessage for each SDK message", async () => {
952
- const messages: SDKMessage[] = [
953
- { type: "system", content: "Init" },
954
- { type: "assistant", content: "Hello" },
955
- { type: "assistant", content: "World" },
956
- ];
957
-
958
- const receivedMessages: SDKMessage[] = [];
959
- const onMessage = vi.fn((msg: SDKMessage) => {
960
- receivedMessages.push(msg);
961
- });
962
-
963
- const executor = new JobExecutor(createMockSDKQuery(messages), {
964
- logger: createMockLogger(),
965
- });
966
-
967
- await executor.execute({
968
- agent: createTestAgent(),
969
- prompt: "Test prompt",
970
- stateDir,
971
- onMessage,
972
- });
973
-
974
- expect(onMessage).toHaveBeenCalledTimes(3);
975
- expect(receivedMessages).toHaveLength(3);
976
- expect(receivedMessages[0].type).toBe("system");
977
- expect(receivedMessages[1].type).toBe("assistant");
978
- expect(receivedMessages[2].type).toBe("assistant");
979
- });
980
-
981
- it("continues execution if onMessage throws", async () => {
982
- const messages: SDKMessage[] = [
983
- { type: "system", content: "Init" },
984
- { type: "assistant", content: "Continue" },
985
- ];
986
-
987
- const onMessage = vi.fn(() => {
988
- throw new Error("Callback error");
989
- });
990
-
991
- const executor = new JobExecutor(createMockSDKQuery(messages), {
992
- logger: createMockLogger(),
993
- });
994
-
995
- const result = await executor.execute({
996
- agent: createTestAgent(),
997
- prompt: "Test prompt",
998
- stateDir,
999
- onMessage,
1000
- });
1001
-
1002
- // Should still succeed despite callback error
1003
- expect(result.success).toBe(true);
1004
- expect(onMessage).toHaveBeenCalledTimes(2);
1005
- });
1006
-
1007
- it("supports async onMessage callback", async () => {
1008
- const messages: SDKMessage[] = [
1009
- { type: "assistant", content: "Hello" },
1010
- ];
1011
-
1012
- const onMessage = vi.fn(async () => {
1013
- await new Promise((resolve) => setTimeout(resolve, 10));
1014
- });
1015
-
1016
- const executor = new JobExecutor(createMockSDKQuery(messages), {
1017
- logger: createMockLogger(),
1018
- });
1019
-
1020
- await executor.execute({
1021
- agent: createTestAgent(),
1022
- prompt: "Test prompt",
1023
- stateDir,
1024
- onMessage,
1025
- });
1026
-
1027
- expect(onMessage).toHaveBeenCalledTimes(1);
1028
- });
1029
- });
1030
-
1031
- describe("trigger types", () => {
1032
- it("sets trigger type to manual by default", async () => {
1033
- const messages: SDKMessage[] = [{ type: "assistant", content: "Done" }];
1034
-
1035
- const executor = new JobExecutor(createMockSDKQuery(messages), {
1036
- logger: createMockLogger(),
1037
- });
1038
-
1039
- const result = await executor.execute({
1040
- agent: createTestAgent(),
1041
- prompt: "Test prompt",
1042
- stateDir,
1043
- });
1044
-
1045
- const job = await getJob(join(stateDir, "jobs"), result.jobId);
1046
- expect(job?.trigger_type).toBe("manual");
1047
- });
1048
-
1049
- it("sets trigger type from options", async () => {
1050
- const messages: SDKMessage[] = [{ type: "assistant", content: "Done" }];
1051
-
1052
- const executor = new JobExecutor(createMockSDKQuery(messages), {
1053
- logger: createMockLogger(),
1054
- });
1055
-
1056
- const result = await executor.execute({
1057
- agent: createTestAgent(),
1058
- prompt: "Test prompt",
1059
- stateDir,
1060
- triggerType: "schedule",
1061
- schedule: "daily-cleanup",
1062
- });
1063
-
1064
- const job = await getJob(join(stateDir, "jobs"), result.jobId);
1065
- expect(job?.trigger_type).toBe("schedule");
1066
- expect(job?.schedule).toBe("daily-cleanup");
1067
- });
1068
- });
1069
-
1070
- describe("duration tracking", () => {
1071
- it("calculates duration in seconds", async () => {
1072
- const messages: SDKMessage[] = [
1073
- { type: "system", content: "Start" },
1074
- { type: "assistant", content: "End" },
1075
- ];
1076
-
1077
- const executor = new JobExecutor(
1078
- createDelayedSDKQuery(messages, 50),
1079
- { logger: createMockLogger() }
1080
- );
1081
-
1082
- const result = await executor.execute({
1083
- agent: createTestAgent(),
1084
- prompt: "Test prompt",
1085
- stateDir,
1086
- });
1087
-
1088
- expect(result.durationSeconds).toBeGreaterThanOrEqual(0);
1089
-
1090
- const job = await getJob(join(stateDir, "jobs"), result.jobId);
1091
- expect(job?.duration_seconds).toBeGreaterThanOrEqual(0);
1092
- });
1093
- });
1094
- });
1095
-
1096
- // =============================================================================
1097
- // executeJob convenience function tests
1098
- // =============================================================================
1099
-
1100
- describe("executeJob", () => {
1101
- let tempDir: string;
1102
- let stateDir: string;
1103
-
1104
- beforeEach(async () => {
1105
- tempDir = await createTempDir();
1106
- stateDir = join(tempDir, ".herdctl");
1107
- await initStateDirectory({ path: stateDir });
1108
- });
1109
-
1110
- afterEach(async () => {
1111
- await rm(tempDir, { recursive: true, force: true });
1112
- });
1113
-
1114
- it("executes job using convenience function", async () => {
1115
- const messages: SDKMessage[] = [
1116
- { type: "system", content: "Init" },
1117
- { type: "assistant", content: "Done" },
1118
- ];
1119
-
1120
- const result = await executeJob(
1121
- createMockSDKQuery(messages),
1122
- {
1123
- agent: createTestAgent({ name: "convenience-agent" }),
1124
- prompt: "Test prompt",
1125
- stateDir,
1126
- },
1127
- { logger: createMockLogger() }
1128
- );
1129
-
1130
- expect(result.success).toBe(true);
1131
- expect(result.jobId).toBeDefined();
1132
-
1133
- const job = await getJob(join(stateDir, "jobs"), result.jobId);
1134
- expect(job?.agent).toBe("convenience-agent");
1135
- });
1136
-
1137
- it("passes executor options", async () => {
1138
- const messages: SDKMessage[] = [{ type: "assistant", content: "Done" }];
1139
- const logger = createMockLogger();
1140
-
1141
- await executeJob(
1142
- createMockSDKQuery(messages),
1143
- {
1144
- agent: createTestAgent(),
1145
- prompt: "Test prompt",
1146
- stateDir,
1147
- },
1148
- { logger }
1149
- );
1150
-
1151
- // Logger should have been used (even though messages are suppressed)
1152
- });
1153
- });
1154
-
1155
- // =============================================================================
1156
- // Edge cases
1157
- // =============================================================================
1158
-
1159
- describe("edge cases", () => {
1160
- let tempDir: string;
1161
- let stateDir: string;
1162
-
1163
- beforeEach(async () => {
1164
- tempDir = await createTempDir();
1165
- stateDir = join(tempDir, ".herdctl");
1166
- await initStateDirectory({ path: stateDir });
1167
- });
1168
-
1169
- afterEach(async () => {
1170
- await rm(tempDir, { recursive: true, force: true });
1171
- });
1172
-
1173
- it("handles empty message stream", async () => {
1174
- const executor = new JobExecutor(createMockSDKQuery([]), {
1175
- logger: createMockLogger(),
1176
- });
1177
-
1178
- const result = await executor.execute({
1179
- agent: createTestAgent(),
1180
- prompt: "Test prompt",
1181
- stateDir,
1182
- });
1183
-
1184
- expect(result.success).toBe(true);
1185
-
1186
- const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
1187
- expect(output).toHaveLength(0);
1188
- });
1189
-
1190
- it("handles very long content in messages", async () => {
1191
- const longContent = "x".repeat(100000);
1192
- const messages: SDKMessage[] = [
1193
- { type: "assistant", content: longContent },
1194
- ];
1195
-
1196
- const executor = new JobExecutor(createMockSDKQuery(messages), {
1197
- logger: createMockLogger(),
1198
- });
1199
-
1200
- const result = await executor.execute({
1201
- agent: createTestAgent(),
1202
- prompt: "Test prompt",
1203
- stateDir,
1204
- });
1205
-
1206
- const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
1207
- if (output[0].type === "assistant") {
1208
- expect(output[0].content).toHaveLength(100000);
1209
- }
1210
- });
1211
-
1212
- it("handles unicode content", async () => {
1213
- const messages: SDKMessage[] = [
1214
- { type: "assistant", content: "Hello 世界! 🌍 Γεια σου κόσμε" },
1215
- ];
1216
-
1217
- const executor = new JobExecutor(createMockSDKQuery(messages), {
1218
- logger: createMockLogger(),
1219
- });
1220
-
1221
- const result = await executor.execute({
1222
- agent: createTestAgent(),
1223
- prompt: "Test prompt",
1224
- stateDir,
1225
- });
1226
-
1227
- const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
1228
- if (output[0].type === "assistant") {
1229
- expect(output[0].content).toBe("Hello 世界! 🌍 Γεια σου κόσμε");
1230
- }
1231
- });
1232
-
1233
- it("handles special characters in content", async () => {
1234
- const messages: SDKMessage[] = [
1235
- {
1236
- type: "assistant",
1237
- content: 'Content with "quotes", \\backslashes\\, and\nnewlines',
1238
- },
1239
- ];
1240
-
1241
- const executor = new JobExecutor(createMockSDKQuery(messages), {
1242
- logger: createMockLogger(),
1243
- });
1244
-
1245
- const result = await executor.execute({
1246
- agent: createTestAgent(),
1247
- prompt: "Test prompt",
1248
- stateDir,
1249
- });
1250
-
1251
- const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
1252
- if (output[0].type === "assistant") {
1253
- expect(output[0].content).toBe(
1254
- 'Content with "quotes", \\backslashes\\, and\nnewlines'
1255
- );
1256
- }
1257
- });
1258
-
1259
- it("handles rapid message stream", async () => {
1260
- const messages: SDKMessage[] = Array(100)
1261
- .fill(null)
1262
- .map((_, i) => ({
1263
- type: "assistant" as const,
1264
- content: `Message ${i}`,
1265
- }));
1266
-
1267
- const executor = new JobExecutor(createMockSDKQuery(messages), {
1268
- logger: createMockLogger(),
1269
- });
1270
-
1271
- const result = await executor.execute({
1272
- agent: createTestAgent(),
1273
- prompt: "Test prompt",
1274
- stateDir,
1275
- });
1276
-
1277
- const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
1278
- expect(output).toHaveLength(100);
1279
- });
1280
- });
1281
-
1282
- // =============================================================================
1283
- // Enhanced error handling tests (US-7)
1284
- // =============================================================================
1285
-
1286
- describe("error handling (US-7)", () => {
1287
- let tempDir: string;
1288
- let stateDir: string;
1289
-
1290
- beforeEach(async () => {
1291
- tempDir = await createTempDir();
1292
- stateDir = join(tempDir, ".herdctl");
1293
- await initStateDirectory({ path: stateDir });
1294
- });
1295
-
1296
- afterEach(async () => {
1297
- await rm(tempDir, { recursive: true, force: true });
1298
- });
1299
-
1300
- describe("SDK initialization errors", () => {
1301
- it("catches SDK initialization errors (e.g., missing API key)", async () => {
1302
- // Simulates SDK throwing immediately when query is created
1303
- const sdkQuery: SDKQueryFunction = () => {
1304
- throw new Error("ANTHROPIC_API_KEY environment variable is not set");
1305
- };
1306
-
1307
- const executor = new JobExecutor(sdkQuery, {
1308
- logger: createMockLogger(),
1309
- });
1310
-
1311
- const result = await executor.execute({
1312
- agent: createTestAgent({ name: "api-key-agent" }),
1313
- prompt: "Test prompt",
1314
- stateDir,
1315
- });
1316
-
1317
- expect(result.success).toBe(false);
1318
- expect(result.error?.message).toContain("ANTHROPIC_API_KEY");
1319
- expect(result.errorDetails?.type).toBe("initialization");
1320
- });
1321
-
1322
- it("provides context (job ID, agent name) in initialization error", async () => {
1323
- const sdkQuery: SDKQueryFunction = () => {
1324
- throw new Error("SDK init failed");
1325
- };
1326
-
1327
- const executor = new JobExecutor(sdkQuery, {
1328
- logger: createMockLogger(),
1329
- });
1330
-
1331
- const result = await executor.execute({
1332
- agent: createTestAgent({ name: "context-agent" }),
1333
- prompt: "Test prompt",
1334
- stateDir,
1335
- });
1336
-
1337
- expect(result.error?.message).toContain("context-agent");
1338
- expect(result.error?.message).toContain(result.jobId);
1339
- });
1340
- });
1341
-
1342
- describe("SDK streaming errors", () => {
1343
- it("catches SDK streaming errors during execution", async () => {
1344
- const sdkQuery: SDKQueryFunction = async function* () {
1345
- yield { type: "system", content: "Init" };
1346
- yield { type: "assistant", content: "Working..." };
1347
- throw new Error("Connection reset by peer");
1348
- };
1349
-
1350
- const executor = new JobExecutor(sdkQuery, {
1351
- logger: createMockLogger(),
1352
- });
1353
-
1354
- const result = await executor.execute({
1355
- agent: createTestAgent({ name: "streaming-agent" }),
1356
- prompt: "Test prompt",
1357
- stateDir,
1358
- });
1359
-
1360
- expect(result.success).toBe(false);
1361
- expect(result.error?.message).toContain("Connection reset");
1362
- expect(result.errorDetails?.type).toBe("streaming");
1363
- });
1364
-
1365
- it("tracks messages received before streaming error", async () => {
1366
- const sdkQuery: SDKQueryFunction = async function* () {
1367
- yield { type: "system", content: "Init" };
1368
- yield { type: "assistant", content: "Message 1" };
1369
- yield { type: "assistant", content: "Message 2" };
1370
- throw new Error("Stream interrupted");
1371
- };
1372
-
1373
- const executor = new JobExecutor(sdkQuery, {
1374
- logger: createMockLogger(),
1375
- });
1376
-
1377
- const result = await executor.execute({
1378
- agent: createTestAgent(),
1379
- prompt: "Test prompt",
1380
- stateDir,
1381
- });
1382
-
1383
- expect(result.success).toBe(false);
1384
- expect(result.errorDetails?.messagesReceived).toBe(3);
1385
- });
1386
-
1387
- it("identifies recoverable errors (rate limit)", async () => {
1388
- const sdkQuery: SDKQueryFunction = async function* () {
1389
- yield { type: "system", content: "Init" };
1390
- throw new Error("Rate limit exceeded, please retry");
1391
- };
1392
-
1393
- const executor = new JobExecutor(sdkQuery, {
1394
- logger: createMockLogger(),
1395
- });
1396
-
1397
- const result = await executor.execute({
1398
- agent: createTestAgent(),
1399
- prompt: "Test prompt",
1400
- stateDir,
1401
- });
1402
-
1403
- expect(result.errorDetails?.recoverable).toBe(true);
1404
- });
1405
-
1406
- it("identifies non-recoverable errors", async () => {
1407
- const sdkQuery: SDKQueryFunction = async function* () {
1408
- yield { type: "system", content: "Init" };
1409
- throw new Error("Invalid request format");
1410
- };
1411
-
1412
- const executor = new JobExecutor(sdkQuery, {
1413
- logger: createMockLogger(),
1414
- });
1415
-
1416
- const result = await executor.execute({
1417
- agent: createTestAgent(),
1418
- prompt: "Test prompt",
1419
- stateDir,
1420
- });
1421
-
1422
- expect(result.errorDetails?.recoverable).toBe(false);
1423
- });
1424
- });
1425
-
1426
- describe("error logging to job output", () => {
1427
- it("logs error messages to job output as error type messages", async () => {
1428
- const executor = new JobExecutor(
1429
- createErrorSDKQuery(new Error("Test error for logging")),
1430
- { logger: createMockLogger() }
1431
- );
1432
-
1433
- const result = await executor.execute({
1434
- agent: createTestAgent(),
1435
- prompt: "Test prompt",
1436
- stateDir,
1437
- });
1438
-
1439
- const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
1440
- const errorMessages = output.filter((m) => m.type === "error");
1441
-
1442
- expect(errorMessages.length).toBeGreaterThanOrEqual(1);
1443
- expect(errorMessages[0].type).toBe("error");
1444
- if (errorMessages[0].type === "error") {
1445
- expect(errorMessages[0].message).toContain("Test error for logging");
1446
- }
1447
- });
1448
-
1449
- it("includes error code in job output when available", async () => {
1450
- const errorWithCode = new Error("Network error") as NodeJS.ErrnoException;
1451
- errorWithCode.code = "ECONNRESET";
1452
-
1453
- const executor = new JobExecutor(createErrorSDKQuery(errorWithCode), {
1454
- logger: createMockLogger(),
1455
- });
1456
-
1457
- const result = await executor.execute({
1458
- agent: createTestAgent(),
1459
- prompt: "Test prompt",
1460
- stateDir,
1461
- });
1462
-
1463
- const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
1464
- const errorMsg = output.find((m) => m.type === "error");
1465
-
1466
- if (errorMsg?.type === "error") {
1467
- expect(errorMsg.code).toBe("ECONNRESET");
1468
- }
1469
- });
1470
-
1471
- it("includes stack trace in job output", async () => {
1472
- const error = new Error("Stack trace test");
1473
-
1474
- const executor = new JobExecutor(createErrorSDKQuery(error), {
1475
- logger: createMockLogger(),
1476
- });
1477
-
1478
- const result = await executor.execute({
1479
- agent: createTestAgent(),
1480
- prompt: "Test prompt",
1481
- stateDir,
1482
- });
1483
-
1484
- const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
1485
- const errorMsg = output.find((m) => m.type === "error");
1486
-
1487
- if (errorMsg?.type === "error") {
1488
- expect(errorMsg.stack).toBeDefined();
1489
- expect(errorMsg.stack).toContain("Stack trace test");
1490
- }
1491
- });
1492
- });
1493
-
1494
- describe("job status updates", () => {
1495
- it("updates job status to failed with error exit_reason", async () => {
1496
- const executor = new JobExecutor(
1497
- createErrorSDKQuery(new Error("Failure")),
1498
- { logger: createMockLogger() }
1499
- );
1500
-
1501
- const result = await executor.execute({
1502
- agent: createTestAgent(),
1503
- prompt: "Test prompt",
1504
- stateDir,
1505
- });
1506
-
1507
- const job = await getJob(join(stateDir, "jobs"), result.jobId);
1508
- expect(job?.status).toBe("failed");
1509
- expect(job?.exit_reason).toBe("error");
1510
- });
1511
-
1512
- it("sets exit_reason to timeout for timeout errors", async () => {
1513
- const timeoutError = new Error("Request timed out");
1514
-
1515
- const executor = new JobExecutor(createErrorSDKQuery(timeoutError), {
1516
- logger: createMockLogger(),
1517
- });
1518
-
1519
- const result = await executor.execute({
1520
- agent: createTestAgent(),
1521
- prompt: "Test prompt",
1522
- stateDir,
1523
- });
1524
-
1525
- const job = await getJob(join(stateDir, "jobs"), result.jobId);
1526
- expect(job?.exit_reason).toBe("timeout");
1527
- });
1528
-
1529
- it("sets exit_reason to cancelled for abort errors", async () => {
1530
- const abortError = new Error("Operation aborted by user");
1531
-
1532
- const executor = new JobExecutor(createErrorSDKQuery(abortError), {
1533
- logger: createMockLogger(),
1534
- });
1535
-
1536
- const result = await executor.execute({
1537
- agent: createTestAgent(),
1538
- prompt: "Test prompt",
1539
- stateDir,
1540
- });
1541
-
1542
- const job = await getJob(join(stateDir, "jobs"), result.jobId);
1543
- expect(job?.exit_reason).toBe("cancelled");
1544
- });
1545
-
1546
- it("sets exit_reason to max_turns for turn limit errors", async () => {
1547
- const maxTurnsError = new Error("Maximum turns exceeded");
1548
-
1549
- const executor = new JobExecutor(createErrorSDKQuery(maxTurnsError), {
1550
- logger: createMockLogger(),
1551
- });
1552
-
1553
- const result = await executor.execute({
1554
- agent: createTestAgent(),
1555
- prompt: "Test prompt",
1556
- stateDir,
1557
- });
1558
-
1559
- const job = await getJob(join(stateDir, "jobs"), result.jobId);
1560
- expect(job?.exit_reason).toBe("max_turns");
1561
- });
1562
- });
1563
-
1564
- describe("error details in RunnerResult", () => {
1565
- it("provides descriptive error message with context", async () => {
1566
- const executor = new JobExecutor(
1567
- createErrorSDKQuery(new Error("API connection failed")),
1568
- { logger: createMockLogger() }
1569
- );
1570
-
1571
- const result = await executor.execute({
1572
- agent: createTestAgent({ name: "descriptive-agent" }),
1573
- prompt: "Test prompt",
1574
- stateDir,
1575
- });
1576
-
1577
- expect(result.error).toBeDefined();
1578
- expect(result.error?.message).toContain("API connection failed");
1579
- expect(result.error?.message).toContain("descriptive-agent");
1580
- expect(result.error?.message).toContain(result.jobId);
1581
- });
1582
-
1583
- it("returns error details in RunnerResult", async () => {
1584
- const errorWithCode = new Error("Network timeout") as NodeJS.ErrnoException;
1585
- errorWithCode.code = "ETIMEDOUT";
1586
-
1587
- const executor = new JobExecutor(createErrorSDKQuery(errorWithCode), {
1588
- logger: createMockLogger(),
1589
- });
1590
-
1591
- const result = await executor.execute({
1592
- agent: createTestAgent(),
1593
- prompt: "Test prompt",
1594
- stateDir,
1595
- });
1596
-
1597
- expect(result.errorDetails).toBeDefined();
1598
- expect(result.errorDetails?.message).toContain("Network timeout");
1599
- expect(result.errorDetails?.code).toBe("ETIMEDOUT");
1600
- expect(result.errorDetails?.stack).toBeDefined();
1601
- });
1602
- });
1603
-
1604
- describe("malformed SDK responses", () => {
1605
- it("does not crash on malformed SDK messages", async () => {
1606
- const sdkQuery: SDKQueryFunction = async function* () {
1607
- yield { type: "system", content: "Init" };
1608
- // Yield a malformed message (null)
1609
- yield null as unknown as SDKMessage;
1610
- yield { type: "assistant", content: "Continuing after malformed" };
1611
- };
1612
-
1613
- const executor = new JobExecutor(sdkQuery, {
1614
- logger: createMockLogger(),
1615
- });
1616
-
1617
- const result = await executor.execute({
1618
- agent: createTestAgent(),
1619
- prompt: "Test prompt",
1620
- stateDir,
1621
- });
1622
-
1623
- // Should complete without crashing
1624
- expect(result.success).toBe(true);
1625
-
1626
- const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
1627
- // Should have processed all 3 messages (including malformed one logged as system)
1628
- expect(output.length).toBe(3);
1629
- // The malformed message should be logged as a system warning
1630
- const malformedMsg = output.find(
1631
- (m) => m.type === "system" && m.subtype === "malformed_message"
1632
- );
1633
- expect(malformedMsg).toBeDefined();
1634
- });
1635
-
1636
- it("handles messages with missing type field", async () => {
1637
- const sdkQuery: SDKQueryFunction = async function* () {
1638
- yield { type: "system", content: "Init" };
1639
- yield { content: "Missing type" } as unknown as SDKMessage;
1640
- yield { type: "assistant", content: "Done" };
1641
- };
1642
-
1643
- const executor = new JobExecutor(sdkQuery, {
1644
- logger: createMockLogger(),
1645
- });
1646
-
1647
- const result = await executor.execute({
1648
- agent: createTestAgent(),
1649
- prompt: "Test prompt",
1650
- stateDir,
1651
- });
1652
-
1653
- expect(result.success).toBe(true);
1654
-
1655
- const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
1656
- // Should contain a system message about unknown type
1657
- const unknownTypeMsg = output.find(
1658
- (m) => m.type === "system" && m.subtype === "unknown_type"
1659
- );
1660
- expect(unknownTypeMsg).toBeDefined();
1661
- });
1662
-
1663
- it("handles messages with unexpected type values", async () => {
1664
- const sdkQuery: SDKQueryFunction = async function* () {
1665
- yield { type: "system", content: "Init" };
1666
- yield { type: "unexpected_type", content: "Unknown" } as unknown as SDKMessage;
1667
- yield { type: "assistant", content: "Done" };
1668
- };
1669
-
1670
- const executor = new JobExecutor(sdkQuery, {
1671
- logger: createMockLogger(),
1672
- });
1673
-
1674
- const result = await executor.execute({
1675
- agent: createTestAgent(),
1676
- prompt: "Test prompt",
1677
- stateDir,
1678
- });
1679
-
1680
- expect(result.success).toBe(true);
1681
-
1682
- const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
1683
- // Should handle gracefully
1684
- expect(output.length).toBeGreaterThanOrEqual(2);
1685
- });
1686
-
1687
- it("handles SDK error message type gracefully", async () => {
1688
- const messages: SDKMessage[] = [
1689
- { type: "system", content: "Start" },
1690
- { type: "error", message: "SDK reported error", code: "SDK_ERR" },
1691
- ];
1692
-
1693
- const executor = new JobExecutor(createMockSDKQuery(messages), {
1694
- logger: createMockLogger(),
1695
- });
1696
-
1697
- const result = await executor.execute({
1698
- agent: createTestAgent(),
1699
- prompt: "Test prompt",
1700
- stateDir,
1701
- });
1702
-
1703
- expect(result.success).toBe(false);
1704
- expect(result.error?.message).toContain("SDK reported error");
1705
- expect(result.errorDetails?.code).toBe("SDK_ERR");
1706
- });
1707
- });
1708
- });