@herdctl/core 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. package/dist/config/__tests__/agent.test.js +31 -13
  2. package/dist/config/__tests__/agent.test.js.map +1 -1
  3. package/dist/config/__tests__/merge.test.js +9 -2
  4. package/dist/config/__tests__/merge.test.js.map +1 -1
  5. package/dist/config/__tests__/schema.test.js +350 -1
  6. package/dist/config/__tests__/schema.test.js.map +1 -1
  7. package/dist/config/index.d.ts +1 -1
  8. package/dist/config/index.d.ts.map +1 -1
  9. package/dist/config/index.js +3 -1
  10. package/dist/config/index.js.map +1 -1
  11. package/dist/config/schema.d.ts +828 -24
  12. package/dist/config/schema.d.ts.map +1 -1
  13. package/dist/config/schema.js +118 -6
  14. package/dist/config/schema.js.map +1 -1
  15. package/dist/fleet-manager/__tests__/coverage.test.js +11 -332
  16. package/dist/fleet-manager/__tests__/coverage.test.js.map +1 -1
  17. package/dist/fleet-manager/__tests__/errors.test.js +1 -49
  18. package/dist/fleet-manager/__tests__/errors.test.js.map +1 -1
  19. package/dist/fleet-manager/__tests__/integration.test.js +109 -0
  20. package/dist/fleet-manager/__tests__/integration.test.js.map +1 -1
  21. package/dist/fleet-manager/__tests__/reload.test.js +1 -1
  22. package/dist/fleet-manager/__tests__/reload.test.js.map +1 -1
  23. package/dist/fleet-manager/config-reload.d.ts +164 -0
  24. package/dist/fleet-manager/config-reload.d.ts.map +1 -0
  25. package/dist/fleet-manager/config-reload.js +445 -0
  26. package/dist/fleet-manager/config-reload.js.map +1 -0
  27. package/dist/fleet-manager/context.d.ts +76 -0
  28. package/dist/fleet-manager/context.d.ts.map +1 -0
  29. package/dist/fleet-manager/context.js +11 -0
  30. package/dist/fleet-manager/context.js.map +1 -0
  31. package/dist/fleet-manager/errors.d.ts +0 -25
  32. package/dist/fleet-manager/errors.d.ts.map +1 -1
  33. package/dist/fleet-manager/errors.js +0 -38
  34. package/dist/fleet-manager/errors.js.map +1 -1
  35. package/dist/fleet-manager/event-emitters.d.ts +123 -0
  36. package/dist/fleet-manager/event-emitters.d.ts.map +1 -0
  37. package/dist/fleet-manager/event-emitters.js +136 -0
  38. package/dist/fleet-manager/event-emitters.js.map +1 -0
  39. package/dist/fleet-manager/event-types.d.ts +0 -15
  40. package/dist/fleet-manager/event-types.d.ts.map +1 -1
  41. package/dist/fleet-manager/fleet-manager.d.ts +40 -653
  42. package/dist/fleet-manager/fleet-manager.d.ts.map +1 -1
  43. package/dist/fleet-manager/fleet-manager.js +95 -1720
  44. package/dist/fleet-manager/fleet-manager.js.map +1 -1
  45. package/dist/fleet-manager/index.d.ts +13 -2
  46. package/dist/fleet-manager/index.d.ts.map +1 -1
  47. package/dist/fleet-manager/index.js +19 -6
  48. package/dist/fleet-manager/index.js.map +1 -1
  49. package/dist/fleet-manager/job-control.d.ts +64 -0
  50. package/dist/fleet-manager/job-control.d.ts.map +1 -0
  51. package/dist/fleet-manager/job-control.js +296 -0
  52. package/dist/fleet-manager/job-control.js.map +1 -0
  53. package/dist/fleet-manager/log-streaming.d.ts +171 -0
  54. package/dist/fleet-manager/log-streaming.d.ts.map +1 -0
  55. package/dist/fleet-manager/log-streaming.js +503 -0
  56. package/dist/fleet-manager/log-streaming.js.map +1 -0
  57. package/dist/fleet-manager/schedule-executor.d.ts +63 -0
  58. package/dist/fleet-manager/schedule-executor.d.ts.map +1 -0
  59. package/dist/fleet-manager/schedule-executor.js +209 -0
  60. package/dist/fleet-manager/schedule-executor.js.map +1 -0
  61. package/dist/fleet-manager/schedule-management.d.ts +71 -0
  62. package/dist/fleet-manager/schedule-management.d.ts.map +1 -0
  63. package/dist/fleet-manager/schedule-management.js +171 -0
  64. package/dist/fleet-manager/schedule-management.js.map +1 -0
  65. package/dist/fleet-manager/status-queries.d.ts +105 -0
  66. package/dist/fleet-manager/status-queries.d.ts.map +1 -0
  67. package/dist/fleet-manager/status-queries.js +247 -0
  68. package/dist/fleet-manager/status-queries.js.map +1 -0
  69. package/dist/fleet-manager/types.d.ts +0 -39
  70. package/dist/fleet-manager/types.d.ts.map +1 -1
  71. package/dist/runner/__tests__/job-executor.test.js +206 -1
  72. package/dist/runner/__tests__/job-executor.test.js.map +1 -1
  73. package/dist/runner/job-executor.d.ts +9 -0
  74. package/dist/runner/job-executor.d.ts.map +1 -1
  75. package/dist/runner/job-executor.js +78 -4
  76. package/dist/runner/job-executor.js.map +1 -1
  77. package/dist/runner/types.d.ts +2 -0
  78. package/dist/runner/types.d.ts.map +1 -1
  79. package/dist/scheduler/__tests__/cron.test.d.ts +2 -0
  80. package/dist/scheduler/__tests__/cron.test.d.ts.map +1 -0
  81. package/dist/scheduler/__tests__/cron.test.js +867 -0
  82. package/dist/scheduler/__tests__/cron.test.js.map +1 -0
  83. package/dist/scheduler/__tests__/scheduler.test.js +164 -5
  84. package/dist/scheduler/__tests__/scheduler.test.js.map +1 -1
  85. package/dist/scheduler/cron.d.ts +126 -0
  86. package/dist/scheduler/cron.d.ts.map +1 -0
  87. package/dist/scheduler/cron.js +390 -0
  88. package/dist/scheduler/cron.js.map +1 -0
  89. package/dist/scheduler/errors.d.ts +81 -1
  90. package/dist/scheduler/errors.d.ts.map +1 -1
  91. package/dist/scheduler/errors.js +81 -6
  92. package/dist/scheduler/errors.js.map +1 -1
  93. package/dist/scheduler/index.d.ts +1 -0
  94. package/dist/scheduler/index.d.ts.map +1 -1
  95. package/dist/scheduler/index.js +2 -0
  96. package/dist/scheduler/index.js.map +1 -1
  97. package/dist/scheduler/schedule-runner.d.ts +2 -2
  98. package/dist/scheduler/schedule-runner.d.ts.map +1 -1
  99. package/dist/scheduler/schedule-runner.js +20 -8
  100. package/dist/scheduler/schedule-runner.js.map +1 -1
  101. package/dist/scheduler/scheduler.d.ts +4 -4
  102. package/dist/scheduler/scheduler.d.ts.map +1 -1
  103. package/dist/scheduler/scheduler.js +86 -20
  104. package/dist/scheduler/scheduler.js.map +1 -1
  105. package/dist/scheduler/types.d.ts +1 -1
  106. package/dist/scheduler/types.d.ts.map +1 -1
  107. package/dist/state/schemas/job-metadata.d.ts +2 -2
  108. package/package.json +33 -8
  109. package/.turbo/turbo-build.log +0 -4
  110. package/.turbo/turbo-test.log +0 -219
  111. package/.turbo/turbo-typecheck.log +0 -4
  112. package/coverage/base.css +0 -224
  113. package/coverage/block-navigation.js +0 -87
  114. package/coverage/coverage-final.json +0 -51
  115. package/coverage/favicon.png +0 -0
  116. package/coverage/index.html +0 -251
  117. package/coverage/prettify.css +0 -1
  118. package/coverage/prettify.js +0 -2
  119. package/coverage/sort-arrow-sprite.png +0 -0
  120. package/coverage/sorter.js +0 -210
  121. package/coverage/src/config/index.html +0 -191
  122. package/coverage/src/config/index.ts.html +0 -442
  123. package/coverage/src/config/interpolate.ts.html +0 -652
  124. package/coverage/src/config/loader.ts.html +0 -1501
  125. package/coverage/src/config/merge.ts.html +0 -823
  126. package/coverage/src/config/parser.ts.html +0 -1213
  127. package/coverage/src/config/schema.ts.html +0 -1123
  128. package/coverage/src/fleet-manager/errors.ts.html +0 -2326
  129. package/coverage/src/fleet-manager/event-types.ts.html +0 -1219
  130. package/coverage/src/fleet-manager/fleet-manager.ts.html +0 -7030
  131. package/coverage/src/fleet-manager/index.html +0 -206
  132. package/coverage/src/fleet-manager/index.ts.html +0 -469
  133. package/coverage/src/fleet-manager/job-manager.ts.html +0 -2074
  134. package/coverage/src/fleet-manager/job-queue.ts.html +0 -2479
  135. package/coverage/src/fleet-manager/types.ts.html +0 -2602
  136. package/coverage/src/index.html +0 -116
  137. package/coverage/src/index.ts.html +0 -181
  138. package/coverage/src/runner/errors.ts.html +0 -1006
  139. package/coverage/src/runner/index.html +0 -191
  140. package/coverage/src/runner/index.ts.html +0 -256
  141. package/coverage/src/runner/job-executor.ts.html +0 -1429
  142. package/coverage/src/runner/message-processor.ts.html +0 -1150
  143. package/coverage/src/runner/sdk-adapter.ts.html +0 -658
  144. package/coverage/src/runner/types.ts.html +0 -559
  145. package/coverage/src/scheduler/errors.ts.html +0 -388
  146. package/coverage/src/scheduler/index.html +0 -206
  147. package/coverage/src/scheduler/index.ts.html +0 -244
  148. package/coverage/src/scheduler/interval.ts.html +0 -652
  149. package/coverage/src/scheduler/schedule-runner.ts.html +0 -1411
  150. package/coverage/src/scheduler/schedule-state.ts.html +0 -718
  151. package/coverage/src/scheduler/scheduler.ts.html +0 -1795
  152. package/coverage/src/scheduler/types.ts.html +0 -733
  153. package/coverage/src/state/directory.ts.html +0 -736
  154. package/coverage/src/state/errors.ts.html +0 -376
  155. package/coverage/src/state/fleet-state.ts.html +0 -937
  156. package/coverage/src/state/index.html +0 -221
  157. package/coverage/src/state/index.ts.html +0 -322
  158. package/coverage/src/state/job-metadata.ts.html +0 -1420
  159. package/coverage/src/state/job-output.ts.html +0 -1033
  160. package/coverage/src/state/schemas/fleet-state.ts.html +0 -445
  161. package/coverage/src/state/schemas/index.html +0 -176
  162. package/coverage/src/state/schemas/index.ts.html +0 -286
  163. package/coverage/src/state/schemas/job-metadata.ts.html +0 -628
  164. package/coverage/src/state/schemas/job-output.ts.html +0 -616
  165. package/coverage/src/state/schemas/session-info.ts.html +0 -361
  166. package/coverage/src/state/session.ts.html +0 -844
  167. package/coverage/src/state/types.ts.html +0 -262
  168. package/coverage/src/state/utils/atomic.ts.html +0 -748
  169. package/coverage/src/state/utils/index.html +0 -146
  170. package/coverage/src/state/utils/index.ts.html +0 -103
  171. package/coverage/src/state/utils/reads.ts.html +0 -1621
  172. package/coverage/src/work-sources/adapters/github.ts.html +0 -3583
  173. package/coverage/src/work-sources/adapters/index.html +0 -131
  174. package/coverage/src/work-sources/adapters/index.ts.html +0 -277
  175. package/coverage/src/work-sources/errors.ts.html +0 -298
  176. package/coverage/src/work-sources/index.html +0 -176
  177. package/coverage/src/work-sources/index.ts.html +0 -529
  178. package/coverage/src/work-sources/manager.ts.html +0 -1324
  179. package/coverage/src/work-sources/registry.ts.html +0 -619
  180. package/coverage/src/work-sources/types.ts.html +0 -568
  181. package/dist/fleet-manager/__tests__/event-helpers.test.d.ts +0 -7
  182. package/dist/fleet-manager/__tests__/event-helpers.test.d.ts.map +0 -1
  183. package/dist/fleet-manager/__tests__/event-helpers.test.js +0 -368
  184. package/dist/fleet-manager/__tests__/event-helpers.test.js.map +0 -1
  185. package/src/config/__tests__/agent.test.ts +0 -864
  186. package/src/config/__tests__/interpolate.test.ts +0 -644
  187. package/src/config/__tests__/loader.test.ts +0 -784
  188. package/src/config/__tests__/merge.test.ts +0 -751
  189. package/src/config/__tests__/parser.test.ts +0 -533
  190. package/src/config/__tests__/schema.test.ts +0 -873
  191. package/src/config/index.ts +0 -119
  192. package/src/config/interpolate.ts +0 -189
  193. package/src/config/loader.ts +0 -472
  194. package/src/config/merge.ts +0 -246
  195. package/src/config/parser.ts +0 -376
  196. package/src/config/schema.ts +0 -346
  197. package/src/fleet-manager/__tests__/coverage.test.ts +0 -2869
  198. package/src/fleet-manager/__tests__/errors.test.ts +0 -660
  199. package/src/fleet-manager/__tests__/event-helpers.test.ts +0 -448
  200. package/src/fleet-manager/__tests__/integration.test.ts +0 -1209
  201. package/src/fleet-manager/__tests__/job-control.test.ts +0 -283
  202. package/src/fleet-manager/__tests__/job-manager.test.ts +0 -869
  203. package/src/fleet-manager/__tests__/job-queue.test.ts +0 -401
  204. package/src/fleet-manager/__tests__/reload.test.ts +0 -751
  205. package/src/fleet-manager/__tests__/status-queries.test.ts +0 -595
  206. package/src/fleet-manager/__tests__/trigger.test.ts +0 -601
  207. package/src/fleet-manager/errors.ts +0 -747
  208. package/src/fleet-manager/event-types.ts +0 -378
  209. package/src/fleet-manager/fleet-manager.ts +0 -2315
  210. package/src/fleet-manager/index.ts +0 -128
  211. package/src/fleet-manager/job-manager.ts +0 -663
  212. package/src/fleet-manager/job-queue.ts +0 -798
  213. package/src/fleet-manager/types.ts +0 -839
  214. package/src/index.ts +0 -32
  215. package/src/runner/__tests__/errors.test.ts +0 -382
  216. package/src/runner/__tests__/job-executor.test.ts +0 -1708
  217. package/src/runner/__tests__/message-processor.test.ts +0 -960
  218. package/src/runner/__tests__/sdk-adapter.test.ts +0 -626
  219. package/src/runner/errors.ts +0 -307
  220. package/src/runner/index.ts +0 -57
  221. package/src/runner/job-executor.ts +0 -448
  222. package/src/runner/message-processor.ts +0 -355
  223. package/src/runner/sdk-adapter.ts +0 -191
  224. package/src/runner/types.ts +0 -158
  225. package/src/scheduler/__tests__/errors.test.ts +0 -159
  226. package/src/scheduler/__tests__/interval.test.ts +0 -515
  227. package/src/scheduler/__tests__/schedule-runner.test.ts +0 -798
  228. package/src/scheduler/__tests__/schedule-state.test.ts +0 -671
  229. package/src/scheduler/__tests__/scheduler.test.ts +0 -1280
  230. package/src/scheduler/errors.ts +0 -101
  231. package/src/scheduler/index.ts +0 -53
  232. package/src/scheduler/interval.ts +0 -189
  233. package/src/scheduler/schedule-runner.ts +0 -442
  234. package/src/scheduler/schedule-state.ts +0 -211
  235. package/src/scheduler/scheduler.ts +0 -570
  236. package/src/scheduler/types.ts +0 -216
  237. package/src/state/__tests__/directory.test.ts +0 -595
  238. package/src/state/__tests__/fleet-state.test.ts +0 -868
  239. package/src/state/__tests__/job-metadata-schema.test.ts +0 -414
  240. package/src/state/__tests__/job-metadata.test.ts +0 -831
  241. package/src/state/__tests__/job-output.test.ts +0 -856
  242. package/src/state/__tests__/session-schema.test.ts +0 -378
  243. package/src/state/__tests__/session.test.ts +0 -604
  244. package/src/state/directory.ts +0 -217
  245. package/src/state/errors.ts +0 -97
  246. package/src/state/fleet-state.ts +0 -284
  247. package/src/state/index.ts +0 -79
  248. package/src/state/job-metadata.ts +0 -445
  249. package/src/state/job-output.ts +0 -316
  250. package/src/state/schemas/__tests__/job-output.test.ts +0 -338
  251. package/src/state/schemas/fleet-state.ts +0 -120
  252. package/src/state/schemas/index.ts +0 -67
  253. package/src/state/schemas/job-metadata.ts +0 -181
  254. package/src/state/schemas/job-output.ts +0 -177
  255. package/src/state/schemas/session-info.ts +0 -92
  256. package/src/state/session.ts +0 -253
  257. package/src/state/types.ts +0 -59
  258. package/src/state/utils/__tests__/atomic.test.ts +0 -723
  259. package/src/state/utils/__tests__/reads.test.ts +0 -1071
  260. package/src/state/utils/atomic.ts +0 -221
  261. package/src/state/utils/index.ts +0 -6
  262. package/src/state/utils/reads.ts +0 -512
  263. package/src/work-sources/__tests__/github.test.ts +0 -1800
  264. package/src/work-sources/__tests__/manager.test.ts +0 -529
  265. package/src/work-sources/__tests__/registry.test.ts +0 -477
  266. package/src/work-sources/__tests__/types.test.ts +0 -479
  267. package/src/work-sources/adapters/github.ts +0 -1166
  268. package/src/work-sources/adapters/index.ts +0 -64
  269. package/src/work-sources/errors.ts +0 -71
  270. package/src/work-sources/index.ts +0 -148
  271. package/src/work-sources/manager.ts +0 -413
  272. package/src/work-sources/registry.ts +0 -178
  273. package/src/work-sources/types.ts +0 -161
  274. package/tsconfig.json +0 -9
  275. package/vitest.config.ts +0 -19
@@ -1,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
- });