@herdctl/core 1.3.0 → 2.0.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.
- package/dist/config/__tests__/agent.test.js +12 -12
- package/dist/config/__tests__/agent.test.js.map +1 -1
- package/dist/config/__tests__/loader.test.js +201 -4
- package/dist/config/__tests__/loader.test.js.map +1 -1
- package/dist/config/__tests__/merge.test.js +29 -4
- package/dist/config/__tests__/merge.test.js.map +1 -1
- package/dist/config/__tests__/parser.test.js +13 -13
- package/dist/config/__tests__/parser.test.js.map +1 -1
- package/dist/config/__tests__/schema.test.js +10 -10
- package/dist/config/__tests__/schema.test.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +2 -2
- package/dist/config/index.js.map +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +71 -0
- package/dist/config/loader.js.map +1 -1
- package/dist/config/merge.d.ts +4 -1
- package/dist/config/merge.d.ts.map +1 -1
- package/dist/config/merge.js +16 -0
- package/dist/config/merge.js.map +1 -1
- package/dist/config/schema.d.ts +906 -89
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +109 -7
- package/dist/config/schema.js.map +1 -1
- package/dist/fleet-manager/__tests__/coverage.test.js +25 -24
- package/dist/fleet-manager/__tests__/coverage.test.js.map +1 -1
- package/dist/fleet-manager/__tests__/discord-manager.test.js +9 -2
- package/dist/fleet-manager/__tests__/discord-manager.test.js.map +1 -1
- package/dist/fleet-manager/__tests__/integration.test.js +27 -0
- package/dist/fleet-manager/__tests__/integration.test.js.map +1 -1
- package/dist/fleet-manager/__tests__/job-control.test.js +66 -0
- package/dist/fleet-manager/__tests__/job-control.test.js.map +1 -1
- package/dist/fleet-manager/__tests__/status-queries.test.js +12 -11
- package/dist/fleet-manager/__tests__/status-queries.test.js.map +1 -1
- package/dist/fleet-manager/config-reload.js +9 -9
- package/dist/fleet-manager/config-reload.js.map +1 -1
- package/dist/fleet-manager/discord-manager.d.ts.map +1 -1
- package/dist/fleet-manager/discord-manager.js +27 -4
- package/dist/fleet-manager/discord-manager.js.map +1 -1
- package/dist/fleet-manager/fleet-manager.d.ts +11 -0
- package/dist/fleet-manager/fleet-manager.d.ts.map +1 -1
- package/dist/fleet-manager/fleet-manager.js +27 -0
- package/dist/fleet-manager/fleet-manager.js.map +1 -1
- package/dist/fleet-manager/job-control.d.ts +1 -1
- package/dist/fleet-manager/job-control.d.ts.map +1 -1
- package/dist/fleet-manager/job-control.js +36 -14
- package/dist/fleet-manager/job-control.js.map +1 -1
- package/dist/fleet-manager/schedule-executor.d.ts +1 -1
- package/dist/fleet-manager/schedule-executor.d.ts.map +1 -1
- package/dist/fleet-manager/schedule-executor.js +11 -14
- package/dist/fleet-manager/schedule-executor.js.map +1 -1
- package/dist/fleet-manager/status-queries.js +7 -7
- package/dist/fleet-manager/status-queries.js.map +1 -1
- package/dist/fleet-manager/types.d.ts +10 -2
- package/dist/fleet-manager/types.d.ts.map +1 -1
- package/dist/fleet-manager/working-directory-helper.d.ts +29 -0
- package/dist/fleet-manager/working-directory-helper.d.ts.map +1 -0
- package/dist/fleet-manager/working-directory-helper.js +36 -0
- package/dist/fleet-manager/working-directory-helper.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/runner/__tests__/job-executor.test.js +449 -118
- package/dist/runner/__tests__/job-executor.test.js.map +1 -1
- package/dist/runner/__tests__/sdk-adapter.test.js +147 -23
- package/dist/runner/__tests__/sdk-adapter.test.js.map +1 -1
- package/dist/runner/index.d.ts +2 -0
- package/dist/runner/index.d.ts.map +1 -1
- package/dist/runner/index.js +1 -0
- package/dist/runner/index.js.map +1 -1
- package/dist/runner/job-executor.d.ts +12 -8
- package/dist/runner/job-executor.d.ts.map +1 -1
- package/dist/runner/job-executor.js +257 -126
- package/dist/runner/job-executor.js.map +1 -1
- package/dist/runner/runtime/__tests__/cli-session-path.test.d.ts +2 -0
- package/dist/runner/runtime/__tests__/cli-session-path.test.d.ts.map +1 -0
- package/dist/runner/runtime/__tests__/cli-session-path.test.js +150 -0
- package/dist/runner/runtime/__tests__/cli-session-path.test.js.map +1 -0
- package/dist/runner/runtime/__tests__/docker-config.test.d.ts +2 -0
- package/dist/runner/runtime/__tests__/docker-config.test.d.ts.map +1 -0
- package/dist/runner/runtime/__tests__/docker-config.test.js +352 -0
- package/dist/runner/runtime/__tests__/docker-config.test.js.map +1 -0
- package/dist/runner/runtime/__tests__/docker-security.test.d.ts +2 -0
- package/dist/runner/runtime/__tests__/docker-security.test.d.ts.map +1 -0
- package/dist/runner/runtime/__tests__/docker-security.test.js +384 -0
- package/dist/runner/runtime/__tests__/docker-security.test.js.map +1 -0
- package/dist/runner/runtime/__tests__/factory.test.d.ts +2 -0
- package/dist/runner/runtime/__tests__/factory.test.d.ts.map +1 -0
- package/dist/runner/runtime/__tests__/factory.test.js +149 -0
- package/dist/runner/runtime/__tests__/factory.test.js.map +1 -0
- package/dist/runner/runtime/__tests__/integration.test.d.ts +2 -0
- package/dist/runner/runtime/__tests__/integration.test.d.ts.map +1 -0
- package/dist/runner/runtime/__tests__/integration.test.js +274 -0
- package/dist/runner/runtime/__tests__/integration.test.js.map +1 -0
- package/dist/runner/runtime/cli-runtime.d.ts +107 -0
- package/dist/runner/runtime/cli-runtime.d.ts.map +1 -0
- package/dist/runner/runtime/cli-runtime.js +335 -0
- package/dist/runner/runtime/cli-runtime.js.map +1 -0
- package/dist/runner/runtime/cli-session-path.d.ts +108 -0
- package/dist/runner/runtime/cli-session-path.d.ts.map +1 -0
- package/dist/runner/runtime/cli-session-path.js +173 -0
- package/dist/runner/runtime/cli-session-path.js.map +1 -0
- package/dist/runner/runtime/cli-session-watcher.d.ts +55 -0
- package/dist/runner/runtime/cli-session-watcher.d.ts.map +1 -0
- package/dist/runner/runtime/cli-session-watcher.js +187 -0
- package/dist/runner/runtime/cli-session-watcher.js.map +1 -0
- package/dist/runner/runtime/container-manager.d.ts +76 -0
- package/dist/runner/runtime/container-manager.d.ts.map +1 -0
- package/dist/runner/runtime/container-manager.js +229 -0
- package/dist/runner/runtime/container-manager.js.map +1 -0
- package/dist/runner/runtime/container-runner.d.ts +62 -0
- package/dist/runner/runtime/container-runner.d.ts.map +1 -0
- package/dist/runner/runtime/container-runner.js +235 -0
- package/dist/runner/runtime/container-runner.js.map +1 -0
- package/dist/runner/runtime/docker-config.d.ts +100 -0
- package/dist/runner/runtime/docker-config.d.ts.map +1 -0
- package/dist/runner/runtime/docker-config.js +98 -0
- package/dist/runner/runtime/docker-config.js.map +1 -0
- package/dist/runner/runtime/factory.d.ts +63 -0
- package/dist/runner/runtime/factory.d.ts.map +1 -0
- package/dist/runner/runtime/factory.js +68 -0
- package/dist/runner/runtime/factory.js.map +1 -0
- package/dist/runner/runtime/index.d.ts +20 -0
- package/dist/runner/runtime/index.d.ts.map +1 -0
- package/dist/runner/runtime/index.js +21 -0
- package/dist/runner/runtime/index.js.map +1 -0
- package/dist/runner/runtime/interface.d.ts +59 -0
- package/dist/runner/runtime/interface.d.ts.map +1 -0
- package/dist/runner/runtime/interface.js +12 -0
- package/dist/runner/runtime/interface.js.map +1 -0
- package/dist/runner/runtime/sdk-runtime.d.ts +46 -0
- package/dist/runner/runtime/sdk-runtime.d.ts.map +1 -0
- package/dist/runner/runtime/sdk-runtime.js +63 -0
- package/dist/runner/runtime/sdk-runtime.js.map +1 -0
- package/dist/runner/sdk-adapter.d.ts +4 -0
- package/dist/runner/sdk-adapter.d.ts.map +1 -1
- package/dist/runner/sdk-adapter.js +35 -16
- package/dist/runner/sdk-adapter.js.map +1 -1
- package/dist/runner/types.d.ts +12 -6
- package/dist/runner/types.d.ts.map +1 -1
- package/dist/scheduler/__tests__/schedule-runner.test.js +61 -50
- package/dist/scheduler/__tests__/schedule-runner.test.js.map +1 -1
- package/dist/scheduler/schedule-runner.d.ts +1 -4
- package/dist/scheduler/schedule-runner.d.ts.map +1 -1
- package/dist/scheduler/schedule-runner.js +40 -8
- package/dist/scheduler/schedule-runner.js.map +1 -1
- package/dist/state/__tests__/session-schema.test.js +4 -0
- package/dist/state/__tests__/session-schema.test.js.map +1 -1
- package/dist/state/__tests__/session-validation.test.d.ts +2 -0
- package/dist/state/__tests__/session-validation.test.d.ts.map +1 -0
- package/dist/state/__tests__/session-validation.test.js +446 -0
- package/dist/state/__tests__/session-validation.test.js.map +1 -0
- package/dist/state/__tests__/session.test.js +68 -0
- package/dist/state/__tests__/session.test.js.map +1 -1
- package/dist/state/__tests__/working-directory-validation.test.d.ts +5 -0
- package/dist/state/__tests__/working-directory-validation.test.d.ts.map +1 -0
- package/dist/state/__tests__/working-directory-validation.test.js +101 -0
- package/dist/state/__tests__/working-directory-validation.test.js.map +1 -0
- package/dist/state/index.d.ts +2 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +4 -0
- package/dist/state/index.js.map +1 -1
- package/dist/state/schemas/session-info.d.ts +32 -0
- package/dist/state/schemas/session-info.d.ts.map +1 -1
- package/dist/state/schemas/session-info.js +22 -0
- package/dist/state/schemas/session-info.js.map +1 -1
- package/dist/state/session-validation.d.ts +202 -0
- package/dist/state/session-validation.d.ts.map +1 -0
- package/dist/state/session-validation.js +407 -0
- package/dist/state/session-validation.js.map +1 -0
- package/dist/state/session.d.ts +23 -3
- package/dist/state/session.d.ts.map +1 -1
- package/dist/state/session.js +41 -6
- package/dist/state/session.js.map +1 -1
- package/dist/state/working-directory-validation.d.ts +52 -0
- package/dist/state/working-directory-validation.d.ts.map +1 -0
- package/dist/state/working-directory-validation.js +81 -0
- package/dist/state/working-directory-validation.js.map +1 -0
- package/package.json +7 -2
|
@@ -3,7 +3,7 @@ import { mkdir, rm, realpath, readFile, stat } from "node:fs/promises";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { JobExecutor, executeJob } from "../job-executor.js";
|
|
6
|
-
import { getJob, readJobOutputAll, initStateDirectory, getSessionInfo, } from "../../state/index.js";
|
|
6
|
+
import { getJob, readJobOutputAll, initStateDirectory, getSessionInfo, updateSessionInfo, } from "../../state/index.js";
|
|
7
7
|
// =============================================================================
|
|
8
8
|
// Test Helpers
|
|
9
9
|
// =============================================================================
|
|
@@ -35,28 +35,34 @@ function createMockLogger() {
|
|
|
35
35
|
},
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
|
-
// Helper to create
|
|
39
|
-
function
|
|
40
|
-
return
|
|
38
|
+
// Helper to create mock RuntimeInterface
|
|
39
|
+
function createMockRuntime(handler) {
|
|
40
|
+
return {
|
|
41
|
+
execute: handler,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// Helper to create a mock runtime with predefined messages
|
|
45
|
+
function createMockRuntimeWithMessages(messages) {
|
|
46
|
+
return createMockRuntime(async function* () {
|
|
41
47
|
for (const message of messages) {
|
|
42
48
|
yield message;
|
|
43
49
|
}
|
|
44
|
-
};
|
|
50
|
+
});
|
|
45
51
|
}
|
|
46
|
-
// Helper to create a mock
|
|
47
|
-
function
|
|
48
|
-
return async function*
|
|
52
|
+
// Helper to create a mock runtime that yields messages with delays
|
|
53
|
+
function createDelayedMockRuntime(messages, delayMs = 10) {
|
|
54
|
+
return createMockRuntime(async function* () {
|
|
49
55
|
for (const message of messages) {
|
|
50
56
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
51
57
|
yield message;
|
|
52
58
|
}
|
|
53
|
-
};
|
|
59
|
+
});
|
|
54
60
|
}
|
|
55
|
-
// Helper to create a mock
|
|
56
|
-
function
|
|
57
|
-
return async function*
|
|
61
|
+
// Helper to create a mock runtime that throws an error
|
|
62
|
+
function createErrorMockRuntime(error) {
|
|
63
|
+
return createMockRuntime(async function* () {
|
|
58
64
|
throw error;
|
|
59
|
-
};
|
|
65
|
+
});
|
|
60
66
|
}
|
|
61
67
|
// =============================================================================
|
|
62
68
|
// JobExecutor tests
|
|
@@ -78,7 +84,7 @@ describe("JobExecutor", () => {
|
|
|
78
84
|
{ type: "system", content: "Initialized" },
|
|
79
85
|
{ type: "assistant", content: "Done" },
|
|
80
86
|
];
|
|
81
|
-
const executor = new JobExecutor(
|
|
87
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
82
88
|
logger: createMockLogger(),
|
|
83
89
|
});
|
|
84
90
|
const result = await executor.execute({
|
|
@@ -93,11 +99,11 @@ describe("JobExecutor", () => {
|
|
|
93
99
|
});
|
|
94
100
|
it("updates job status to running", async () => {
|
|
95
101
|
let jobIdDuringExecution;
|
|
96
|
-
const
|
|
102
|
+
const runtime = createMockRuntime(async function* (options) {
|
|
97
103
|
// During execution, we can check the job status
|
|
98
104
|
yield { type: "system", content: "Running" };
|
|
99
|
-
};
|
|
100
|
-
const executor = new JobExecutor(
|
|
105
|
+
});
|
|
106
|
+
const executor = new JobExecutor(runtime, {
|
|
101
107
|
logger: createMockLogger(),
|
|
102
108
|
});
|
|
103
109
|
const result = await executor.execute({
|
|
@@ -114,7 +120,7 @@ describe("JobExecutor", () => {
|
|
|
114
120
|
{ type: "system", content: "Start" },
|
|
115
121
|
{ type: "assistant", content: "Task completed!" },
|
|
116
122
|
];
|
|
117
|
-
const executor = new JobExecutor(
|
|
123
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
118
124
|
logger: createMockLogger(),
|
|
119
125
|
});
|
|
120
126
|
const result = await executor.execute({
|
|
@@ -134,7 +140,7 @@ describe("JobExecutor", () => {
|
|
|
134
140
|
{ type: "system", content: "Start" },
|
|
135
141
|
{ type: "error", message: "Something went wrong" },
|
|
136
142
|
];
|
|
137
|
-
const executor = new JobExecutor(
|
|
143
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
138
144
|
logger: createMockLogger(),
|
|
139
145
|
});
|
|
140
146
|
const result = await executor.execute({
|
|
@@ -149,7 +155,7 @@ describe("JobExecutor", () => {
|
|
|
149
155
|
expect(job?.exit_reason).toBe("error");
|
|
150
156
|
});
|
|
151
157
|
it("handles SDK query throwing an error", async () => {
|
|
152
|
-
const executor = new JobExecutor(
|
|
158
|
+
const executor = new JobExecutor(createErrorMockRuntime(new Error("SDK error")), { logger: createMockLogger() });
|
|
153
159
|
const result = await executor.execute({
|
|
154
160
|
agent: createTestAgent(),
|
|
155
161
|
prompt: "Test prompt",
|
|
@@ -170,7 +176,7 @@ describe("JobExecutor", () => {
|
|
|
170
176
|
{ type: "tool_result", result: "file1\nfile2", success: true },
|
|
171
177
|
{ type: "assistant", content: "Done" },
|
|
172
178
|
];
|
|
173
|
-
const executor = new JobExecutor(
|
|
179
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
174
180
|
logger: createMockLogger(),
|
|
175
181
|
});
|
|
176
182
|
const result = await executor.execute({
|
|
@@ -189,14 +195,14 @@ describe("JobExecutor", () => {
|
|
|
189
195
|
it("writes output immediately without buffering", async () => {
|
|
190
196
|
let outputCountDuringExecution = 0;
|
|
191
197
|
const jobsDir = join(stateDir, "jobs");
|
|
192
|
-
const
|
|
198
|
+
const runtime = createMockRuntime(async function* (options) {
|
|
193
199
|
yield { type: "system", content: "First" };
|
|
194
200
|
yield { type: "assistant", content: "Second" };
|
|
195
201
|
yield { type: "assistant", content: "Third" };
|
|
196
|
-
};
|
|
202
|
+
});
|
|
197
203
|
// We can't easily verify real-time writing in a unit test,
|
|
198
204
|
// but we can verify all messages are written
|
|
199
|
-
const executor = new JobExecutor(
|
|
205
|
+
const executor = new JobExecutor(runtime, {
|
|
200
206
|
logger: createMockLogger(),
|
|
201
207
|
});
|
|
202
208
|
const result = await executor.execute({
|
|
@@ -228,7 +234,7 @@ describe("JobExecutor", () => {
|
|
|
228
234
|
success: true,
|
|
229
235
|
},
|
|
230
236
|
];
|
|
231
|
-
const executor = new JobExecutor(
|
|
237
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
232
238
|
logger: createMockLogger(),
|
|
233
239
|
});
|
|
234
240
|
const result = await executor.execute({
|
|
@@ -266,7 +272,7 @@ describe("JobExecutor", () => {
|
|
|
266
272
|
}
|
|
267
273
|
});
|
|
268
274
|
it("writes error message to output when SDK throws", async () => {
|
|
269
|
-
const executor = new JobExecutor(
|
|
275
|
+
const executor = new JobExecutor(createErrorMockRuntime(new Error("Connection failed")), { logger: createMockLogger() });
|
|
270
276
|
const result = await executor.execute({
|
|
271
277
|
agent: createTestAgent(),
|
|
272
278
|
prompt: "Test prompt",
|
|
@@ -289,7 +295,7 @@ describe("JobExecutor", () => {
|
|
|
289
295
|
{ type: "tool_result", result: "output", success: true },
|
|
290
296
|
{ type: "error", message: "Error message", code: "ERR_TEST" },
|
|
291
297
|
];
|
|
292
|
-
const executor = new JobExecutor(
|
|
298
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
293
299
|
logger: createMockLogger(),
|
|
294
300
|
});
|
|
295
301
|
const result = await executor.execute({
|
|
@@ -310,7 +316,7 @@ describe("JobExecutor", () => {
|
|
|
310
316
|
{ type: "assistant", content: "Part 1... Part 2...", partial: true },
|
|
311
317
|
{ type: "assistant", content: "Part 1... Part 2... Done!", partial: false },
|
|
312
318
|
];
|
|
313
|
-
const executor = new JobExecutor(
|
|
319
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
314
320
|
logger: createMockLogger(),
|
|
315
321
|
});
|
|
316
322
|
const result = await executor.execute({
|
|
@@ -332,7 +338,7 @@ describe("JobExecutor", () => {
|
|
|
332
338
|
{ type: "tool_use", tool_name: "read_file", input: { path: "/nope" } },
|
|
333
339
|
{ type: "tool_result", success: false, error: "File not found" },
|
|
334
340
|
];
|
|
335
|
-
const executor = new JobExecutor(
|
|
341
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
336
342
|
logger: createMockLogger(),
|
|
337
343
|
});
|
|
338
344
|
const result = await executor.execute({
|
|
@@ -353,7 +359,7 @@ describe("JobExecutor", () => {
|
|
|
353
359
|
{ type: "system", content: "Init", subtype: "init", session_id: "session-abc123" },
|
|
354
360
|
{ type: "assistant", content: "Done" },
|
|
355
361
|
];
|
|
356
|
-
const executor = new JobExecutor(
|
|
362
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
357
363
|
logger: createMockLogger(),
|
|
358
364
|
});
|
|
359
365
|
const result = await executor.execute({
|
|
@@ -370,7 +376,7 @@ describe("JobExecutor", () => {
|
|
|
370
376
|
{ type: "system", content: "Progress", subtype: "progress", session_id: "should-ignore" },
|
|
371
377
|
{ type: "assistant", content: "Done" },
|
|
372
378
|
];
|
|
373
|
-
const executor = new JobExecutor(
|
|
379
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
374
380
|
logger: createMockLogger(),
|
|
375
381
|
});
|
|
376
382
|
const result = await executor.execute({
|
|
@@ -387,7 +393,7 @@ describe("JobExecutor", () => {
|
|
|
387
393
|
{ type: "system", content: "Init", subtype: "init", session_id: "session-persist-123" },
|
|
388
394
|
{ type: "assistant", content: "Done" },
|
|
389
395
|
];
|
|
390
|
-
const executor = new JobExecutor(
|
|
396
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
391
397
|
logger: createMockLogger(),
|
|
392
398
|
});
|
|
393
399
|
await executor.execute({
|
|
@@ -408,7 +414,7 @@ describe("JobExecutor", () => {
|
|
|
408
414
|
{ type: "system", content: "Init", subtype: "init", session_id: "session-multi-123" },
|
|
409
415
|
{ type: "assistant", content: "Done" },
|
|
410
416
|
];
|
|
411
|
-
const executor = new JobExecutor(
|
|
417
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
412
418
|
logger: createMockLogger(),
|
|
413
419
|
});
|
|
414
420
|
// Run twice
|
|
@@ -431,7 +437,7 @@ describe("JobExecutor", () => {
|
|
|
431
437
|
{ type: "system", content: "Init", subtype: "init", session_id: "session-ts-123" },
|
|
432
438
|
{ type: "assistant", content: "Done" },
|
|
433
439
|
];
|
|
434
|
-
const executor = new JobExecutor(
|
|
440
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
435
441
|
logger: createMockLogger(),
|
|
436
442
|
});
|
|
437
443
|
await executor.execute({
|
|
@@ -451,7 +457,7 @@ describe("JobExecutor", () => {
|
|
|
451
457
|
{ type: "system", content: "Init", subtype: "init", session_id: "session-result-123" },
|
|
452
458
|
{ type: "assistant", content: "Done" },
|
|
453
459
|
];
|
|
454
|
-
const executor = new JobExecutor(
|
|
460
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
455
461
|
logger: createMockLogger(),
|
|
456
462
|
});
|
|
457
463
|
const result = await executor.execute({
|
|
@@ -463,12 +469,18 @@ describe("JobExecutor", () => {
|
|
|
463
469
|
expect(result.sessionId).toBe("session-result-123");
|
|
464
470
|
});
|
|
465
471
|
it("passes resume option to SDK", async () => {
|
|
472
|
+
// Create a valid session so resume isn't cleared
|
|
473
|
+
const sessionsDir = join(stateDir, "sessions");
|
|
474
|
+
await updateSessionInfo(sessionsDir, "test-agent", {
|
|
475
|
+
session_id: "session-to-resume",
|
|
476
|
+
mode: "autonomous",
|
|
477
|
+
});
|
|
466
478
|
let receivedOptions;
|
|
467
|
-
const
|
|
468
|
-
receivedOptions =
|
|
479
|
+
const runtime = createMockRuntime(async function* (options) {
|
|
480
|
+
receivedOptions = options;
|
|
469
481
|
yield { type: "assistant", content: "Resumed" };
|
|
470
|
-
};
|
|
471
|
-
const executor = new JobExecutor(
|
|
482
|
+
});
|
|
483
|
+
const executor = new JobExecutor(runtime, {
|
|
472
484
|
logger: createMockLogger(),
|
|
473
485
|
});
|
|
474
486
|
await executor.execute({
|
|
@@ -481,11 +493,11 @@ describe("JobExecutor", () => {
|
|
|
481
493
|
});
|
|
482
494
|
it("passes fork option to SDK", async () => {
|
|
483
495
|
let receivedOptions;
|
|
484
|
-
const
|
|
485
|
-
receivedOptions =
|
|
496
|
+
const runtime = createMockRuntime(async function* (options) {
|
|
497
|
+
receivedOptions = options;
|
|
486
498
|
yield { type: "assistant", content: "Forked" };
|
|
487
|
-
};
|
|
488
|
-
const executor = new JobExecutor(
|
|
499
|
+
});
|
|
500
|
+
const executor = new JobExecutor(runtime, {
|
|
489
501
|
logger: createMockLogger(),
|
|
490
502
|
});
|
|
491
503
|
await executor.execute({
|
|
@@ -494,14 +506,14 @@ describe("JobExecutor", () => {
|
|
|
494
506
|
stateDir,
|
|
495
507
|
fork: "session-to-fork",
|
|
496
508
|
});
|
|
497
|
-
expect(receivedOptions?.
|
|
509
|
+
expect(receivedOptions?.fork).toBe(true);
|
|
498
510
|
});
|
|
499
511
|
it("creates job with trigger_type 'fork' and forked_from when forking", async () => {
|
|
500
512
|
const messages = [
|
|
501
513
|
{ type: "system", content: "Init", subtype: "init", session_id: "forked-session-123" },
|
|
502
514
|
{ type: "assistant", content: "Forked session started" },
|
|
503
515
|
];
|
|
504
|
-
const executor = new JobExecutor(
|
|
516
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
505
517
|
logger: createMockLogger(),
|
|
506
518
|
});
|
|
507
519
|
const result = await executor.execute({
|
|
@@ -520,7 +532,7 @@ describe("JobExecutor", () => {
|
|
|
520
532
|
const messages = [
|
|
521
533
|
{ type: "assistant", content: "Forked" },
|
|
522
534
|
];
|
|
523
|
-
const executor = new JobExecutor(
|
|
535
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
524
536
|
logger: createMockLogger(),
|
|
525
537
|
});
|
|
526
538
|
const result = await executor.execute({
|
|
@@ -537,7 +549,7 @@ describe("JobExecutor", () => {
|
|
|
537
549
|
const messages = [
|
|
538
550
|
{ type: "assistant", content: "Normal run" },
|
|
539
551
|
];
|
|
540
|
-
const executor = new JobExecutor(
|
|
552
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
541
553
|
logger: createMockLogger(),
|
|
542
554
|
});
|
|
543
555
|
const result = await executor.execute({
|
|
@@ -554,7 +566,7 @@ describe("JobExecutor", () => {
|
|
|
554
566
|
{ type: "system", content: "Init", subtype: "init", session_id: "resume-session-123" },
|
|
555
567
|
{ type: "assistant", content: "Resumed" },
|
|
556
568
|
];
|
|
557
|
-
const executor = new JobExecutor(
|
|
569
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
558
570
|
logger: createMockLogger(),
|
|
559
571
|
});
|
|
560
572
|
// First run to establish the session
|
|
@@ -582,7 +594,7 @@ describe("JobExecutor", () => {
|
|
|
582
594
|
{ type: "system", content: "Init", subtype: "init", session_id: "fork-session-123" },
|
|
583
595
|
{ type: "assistant", content: "Forked" },
|
|
584
596
|
];
|
|
585
|
-
const executor = new JobExecutor(
|
|
597
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
586
598
|
logger: createMockLogger(),
|
|
587
599
|
});
|
|
588
600
|
// First run to establish a session
|
|
@@ -611,7 +623,7 @@ describe("JobExecutor", () => {
|
|
|
611
623
|
{ type: "system", content: "No session" },
|
|
612
624
|
{ type: "assistant", content: "Done" },
|
|
613
625
|
];
|
|
614
|
-
const executor = new JobExecutor(
|
|
626
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
615
627
|
logger: createMockLogger(),
|
|
616
628
|
});
|
|
617
629
|
await executor.execute({
|
|
@@ -630,7 +642,7 @@ describe("JobExecutor", () => {
|
|
|
630
642
|
{ type: "assistant", content: "Working..." },
|
|
631
643
|
{ type: "assistant", content: "Done!", summary: "Task completed" },
|
|
632
644
|
];
|
|
633
|
-
const executor = new JobExecutor(
|
|
645
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
634
646
|
logger: createMockLogger(),
|
|
635
647
|
});
|
|
636
648
|
const result = await executor.execute({
|
|
@@ -647,7 +659,7 @@ describe("JobExecutor", () => {
|
|
|
647
659
|
{ type: "assistant", content: "Working..." },
|
|
648
660
|
{ type: "assistant", content: "All tasks finished successfully." },
|
|
649
661
|
];
|
|
650
|
-
const executor = new JobExecutor(
|
|
662
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
651
663
|
logger: createMockLogger(),
|
|
652
664
|
});
|
|
653
665
|
const result = await executor.execute({
|
|
@@ -663,7 +675,7 @@ describe("JobExecutor", () => {
|
|
|
663
675
|
{ type: "tool_use", tool_name: "bash", input: { command: "ls" } },
|
|
664
676
|
{ type: "tool_result", result: "file1\nfile2", success: true },
|
|
665
677
|
];
|
|
666
|
-
const executor = new JobExecutor(
|
|
678
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
667
679
|
logger: createMockLogger(),
|
|
668
680
|
});
|
|
669
681
|
const result = await executor.execute({
|
|
@@ -680,7 +692,7 @@ describe("JobExecutor", () => {
|
|
|
680
692
|
{ type: "assistant", content: "Partial 1...", partial: true },
|
|
681
693
|
{ type: "assistant", content: "Partial 2...", partial: true },
|
|
682
694
|
];
|
|
683
|
-
const executor = new JobExecutor(
|
|
695
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
684
696
|
logger: createMockLogger(),
|
|
685
697
|
});
|
|
686
698
|
const result = await executor.execute({
|
|
@@ -696,7 +708,7 @@ describe("JobExecutor", () => {
|
|
|
696
708
|
{ type: "assistant", content: longContent },
|
|
697
709
|
{ type: "assistant", content: longContent },
|
|
698
710
|
];
|
|
699
|
-
const executor = new JobExecutor(
|
|
711
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
700
712
|
logger: createMockLogger(),
|
|
701
713
|
});
|
|
702
714
|
const result = await executor.execute({
|
|
@@ -711,7 +723,7 @@ describe("JobExecutor", () => {
|
|
|
711
723
|
const messages = [
|
|
712
724
|
{ type: "assistant", content: "Done!", summary: longSummary },
|
|
713
725
|
];
|
|
714
|
-
const executor = new JobExecutor(
|
|
726
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
715
727
|
logger: createMockLogger(),
|
|
716
728
|
});
|
|
717
729
|
const result = await executor.execute({
|
|
@@ -731,7 +743,7 @@ describe("JobExecutor", () => {
|
|
|
731
743
|
{ type: "assistant", content: "Second", summary: "Second summary" },
|
|
732
744
|
{ type: "assistant", content: "Third", summary: "Final summary" },
|
|
733
745
|
];
|
|
734
|
-
const executor = new JobExecutor(
|
|
746
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
735
747
|
logger: createMockLogger(),
|
|
736
748
|
});
|
|
737
749
|
const result = await executor.execute({
|
|
@@ -743,7 +755,7 @@ describe("JobExecutor", () => {
|
|
|
743
755
|
});
|
|
744
756
|
it("handles empty message stream with undefined summary", async () => {
|
|
745
757
|
const messages = [];
|
|
746
|
-
const executor = new JobExecutor(
|
|
758
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
747
759
|
logger: createMockLogger(),
|
|
748
760
|
});
|
|
749
761
|
const result = await executor.execute({
|
|
@@ -765,7 +777,7 @@ describe("JobExecutor", () => {
|
|
|
765
777
|
const onMessage = vi.fn((msg) => {
|
|
766
778
|
receivedMessages.push(msg);
|
|
767
779
|
});
|
|
768
|
-
const executor = new JobExecutor(
|
|
780
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
769
781
|
logger: createMockLogger(),
|
|
770
782
|
});
|
|
771
783
|
await executor.execute({
|
|
@@ -788,7 +800,7 @@ describe("JobExecutor", () => {
|
|
|
788
800
|
const onMessage = vi.fn(() => {
|
|
789
801
|
throw new Error("Callback error");
|
|
790
802
|
});
|
|
791
|
-
const executor = new JobExecutor(
|
|
803
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
792
804
|
logger: createMockLogger(),
|
|
793
805
|
});
|
|
794
806
|
const result = await executor.execute({
|
|
@@ -808,7 +820,7 @@ describe("JobExecutor", () => {
|
|
|
808
820
|
const onMessage = vi.fn(async () => {
|
|
809
821
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
810
822
|
});
|
|
811
|
-
const executor = new JobExecutor(
|
|
823
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
812
824
|
logger: createMockLogger(),
|
|
813
825
|
});
|
|
814
826
|
await executor.execute({
|
|
@@ -823,7 +835,7 @@ describe("JobExecutor", () => {
|
|
|
823
835
|
describe("trigger types", () => {
|
|
824
836
|
it("sets trigger type to manual by default", async () => {
|
|
825
837
|
const messages = [{ type: "assistant", content: "Done" }];
|
|
826
|
-
const executor = new JobExecutor(
|
|
838
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
827
839
|
logger: createMockLogger(),
|
|
828
840
|
});
|
|
829
841
|
const result = await executor.execute({
|
|
@@ -836,7 +848,7 @@ describe("JobExecutor", () => {
|
|
|
836
848
|
});
|
|
837
849
|
it("sets trigger type from options", async () => {
|
|
838
850
|
const messages = [{ type: "assistant", content: "Done" }];
|
|
839
|
-
const executor = new JobExecutor(
|
|
851
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
840
852
|
logger: createMockLogger(),
|
|
841
853
|
});
|
|
842
854
|
const result = await executor.execute({
|
|
@@ -857,7 +869,7 @@ describe("JobExecutor", () => {
|
|
|
857
869
|
{ type: "system", content: "Start" },
|
|
858
870
|
{ type: "assistant", content: "End" },
|
|
859
871
|
];
|
|
860
|
-
const executor = new JobExecutor(
|
|
872
|
+
const executor = new JobExecutor(createDelayedMockRuntime(messages, 50), { logger: createMockLogger() });
|
|
861
873
|
const result = await executor.execute({
|
|
862
874
|
agent: createTestAgent(),
|
|
863
875
|
prompt: "Test prompt",
|
|
@@ -888,7 +900,7 @@ describe("executeJob", () => {
|
|
|
888
900
|
{ type: "system", content: "Init" },
|
|
889
901
|
{ type: "assistant", content: "Done" },
|
|
890
902
|
];
|
|
891
|
-
const result = await executeJob(
|
|
903
|
+
const result = await executeJob(createMockRuntimeWithMessages(messages), {
|
|
892
904
|
agent: createTestAgent({ name: "convenience-agent" }),
|
|
893
905
|
prompt: "Test prompt",
|
|
894
906
|
stateDir,
|
|
@@ -901,7 +913,7 @@ describe("executeJob", () => {
|
|
|
901
913
|
it("passes executor options", async () => {
|
|
902
914
|
const messages = [{ type: "assistant", content: "Done" }];
|
|
903
915
|
const logger = createMockLogger();
|
|
904
|
-
await executeJob(
|
|
916
|
+
await executeJob(createMockRuntimeWithMessages(messages), {
|
|
905
917
|
agent: createTestAgent(),
|
|
906
918
|
prompt: "Test prompt",
|
|
907
919
|
stateDir,
|
|
@@ -924,7 +936,7 @@ describe("edge cases", () => {
|
|
|
924
936
|
await rm(tempDir, { recursive: true, force: true });
|
|
925
937
|
});
|
|
926
938
|
it("handles empty message stream", async () => {
|
|
927
|
-
const executor = new JobExecutor(
|
|
939
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages([]), {
|
|
928
940
|
logger: createMockLogger(),
|
|
929
941
|
});
|
|
930
942
|
const result = await executor.execute({
|
|
@@ -941,7 +953,7 @@ describe("edge cases", () => {
|
|
|
941
953
|
const messages = [
|
|
942
954
|
{ type: "assistant", content: longContent },
|
|
943
955
|
];
|
|
944
|
-
const executor = new JobExecutor(
|
|
956
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
945
957
|
logger: createMockLogger(),
|
|
946
958
|
});
|
|
947
959
|
const result = await executor.execute({
|
|
@@ -958,7 +970,7 @@ describe("edge cases", () => {
|
|
|
958
970
|
const messages = [
|
|
959
971
|
{ type: "assistant", content: "Hello 世界! 🌍 Γεια σου κόσμε" },
|
|
960
972
|
];
|
|
961
|
-
const executor = new JobExecutor(
|
|
973
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
962
974
|
logger: createMockLogger(),
|
|
963
975
|
});
|
|
964
976
|
const result = await executor.execute({
|
|
@@ -978,7 +990,7 @@ describe("edge cases", () => {
|
|
|
978
990
|
content: 'Content with "quotes", \\backslashes\\, and\nnewlines',
|
|
979
991
|
},
|
|
980
992
|
];
|
|
981
|
-
const executor = new JobExecutor(
|
|
993
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
982
994
|
logger: createMockLogger(),
|
|
983
995
|
});
|
|
984
996
|
const result = await executor.execute({
|
|
@@ -998,7 +1010,7 @@ describe("edge cases", () => {
|
|
|
998
1010
|
type: "assistant",
|
|
999
1011
|
content: `Message ${i}`,
|
|
1000
1012
|
}));
|
|
1001
|
-
const executor = new JobExecutor(
|
|
1013
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
1002
1014
|
logger: createMockLogger(),
|
|
1003
1015
|
});
|
|
1004
1016
|
const result = await executor.execute({
|
|
@@ -1026,11 +1038,11 @@ describe("error handling (US-7)", () => {
|
|
|
1026
1038
|
});
|
|
1027
1039
|
describe("SDK initialization errors", () => {
|
|
1028
1040
|
it("catches SDK initialization errors (e.g., missing API key)", async () => {
|
|
1029
|
-
// Simulates SDK throwing immediately when
|
|
1030
|
-
const
|
|
1041
|
+
// Simulates SDK throwing immediately when execute is called
|
|
1042
|
+
const runtime = createMockRuntime(() => {
|
|
1031
1043
|
throw new Error("ANTHROPIC_API_KEY environment variable is not set");
|
|
1032
|
-
};
|
|
1033
|
-
const executor = new JobExecutor(
|
|
1044
|
+
});
|
|
1045
|
+
const executor = new JobExecutor(runtime, {
|
|
1034
1046
|
logger: createMockLogger(),
|
|
1035
1047
|
});
|
|
1036
1048
|
const result = await executor.execute({
|
|
@@ -1043,10 +1055,10 @@ describe("error handling (US-7)", () => {
|
|
|
1043
1055
|
expect(result.errorDetails?.type).toBe("initialization");
|
|
1044
1056
|
});
|
|
1045
1057
|
it("provides context (job ID, agent name) in initialization error", async () => {
|
|
1046
|
-
const
|
|
1058
|
+
const runtime = createMockRuntime(() => {
|
|
1047
1059
|
throw new Error("SDK init failed");
|
|
1048
|
-
};
|
|
1049
|
-
const executor = new JobExecutor(
|
|
1060
|
+
});
|
|
1061
|
+
const executor = new JobExecutor(runtime, {
|
|
1050
1062
|
logger: createMockLogger(),
|
|
1051
1063
|
});
|
|
1052
1064
|
const result = await executor.execute({
|
|
@@ -1060,12 +1072,12 @@ describe("error handling (US-7)", () => {
|
|
|
1060
1072
|
});
|
|
1061
1073
|
describe("SDK streaming errors", () => {
|
|
1062
1074
|
it("catches SDK streaming errors during execution", async () => {
|
|
1063
|
-
const
|
|
1075
|
+
const runtime = createMockRuntime(async function* (options) {
|
|
1064
1076
|
yield { type: "system", content: "Init" };
|
|
1065
1077
|
yield { type: "assistant", content: "Working..." };
|
|
1066
1078
|
throw new Error("Connection reset by peer");
|
|
1067
|
-
};
|
|
1068
|
-
const executor = new JobExecutor(
|
|
1079
|
+
});
|
|
1080
|
+
const executor = new JobExecutor(runtime, {
|
|
1069
1081
|
logger: createMockLogger(),
|
|
1070
1082
|
});
|
|
1071
1083
|
const result = await executor.execute({
|
|
@@ -1078,13 +1090,13 @@ describe("error handling (US-7)", () => {
|
|
|
1078
1090
|
expect(result.errorDetails?.type).toBe("streaming");
|
|
1079
1091
|
});
|
|
1080
1092
|
it("tracks messages received before streaming error", async () => {
|
|
1081
|
-
const
|
|
1093
|
+
const runtime = createMockRuntime(async function* (options) {
|
|
1082
1094
|
yield { type: "system", content: "Init" };
|
|
1083
1095
|
yield { type: "assistant", content: "Message 1" };
|
|
1084
1096
|
yield { type: "assistant", content: "Message 2" };
|
|
1085
1097
|
throw new Error("Stream interrupted");
|
|
1086
|
-
};
|
|
1087
|
-
const executor = new JobExecutor(
|
|
1098
|
+
});
|
|
1099
|
+
const executor = new JobExecutor(runtime, {
|
|
1088
1100
|
logger: createMockLogger(),
|
|
1089
1101
|
});
|
|
1090
1102
|
const result = await executor.execute({
|
|
@@ -1096,11 +1108,11 @@ describe("error handling (US-7)", () => {
|
|
|
1096
1108
|
expect(result.errorDetails?.messagesReceived).toBe(3);
|
|
1097
1109
|
});
|
|
1098
1110
|
it("identifies recoverable errors (rate limit)", async () => {
|
|
1099
|
-
const
|
|
1111
|
+
const runtime = createMockRuntime(async function* (options) {
|
|
1100
1112
|
yield { type: "system", content: "Init" };
|
|
1101
1113
|
throw new Error("Rate limit exceeded, please retry");
|
|
1102
|
-
};
|
|
1103
|
-
const executor = new JobExecutor(
|
|
1114
|
+
});
|
|
1115
|
+
const executor = new JobExecutor(runtime, {
|
|
1104
1116
|
logger: createMockLogger(),
|
|
1105
1117
|
});
|
|
1106
1118
|
const result = await executor.execute({
|
|
@@ -1111,11 +1123,11 @@ describe("error handling (US-7)", () => {
|
|
|
1111
1123
|
expect(result.errorDetails?.recoverable).toBe(true);
|
|
1112
1124
|
});
|
|
1113
1125
|
it("identifies non-recoverable errors", async () => {
|
|
1114
|
-
const
|
|
1126
|
+
const runtime = createMockRuntime(async function* (options) {
|
|
1115
1127
|
yield { type: "system", content: "Init" };
|
|
1116
1128
|
throw new Error("Invalid request format");
|
|
1117
|
-
};
|
|
1118
|
-
const executor = new JobExecutor(
|
|
1129
|
+
});
|
|
1130
|
+
const executor = new JobExecutor(runtime, {
|
|
1119
1131
|
logger: createMockLogger(),
|
|
1120
1132
|
});
|
|
1121
1133
|
const result = await executor.execute({
|
|
@@ -1128,7 +1140,7 @@ describe("error handling (US-7)", () => {
|
|
|
1128
1140
|
});
|
|
1129
1141
|
describe("error logging to job output", () => {
|
|
1130
1142
|
it("logs error messages to job output as error type messages", async () => {
|
|
1131
|
-
const executor = new JobExecutor(
|
|
1143
|
+
const executor = new JobExecutor(createErrorMockRuntime(new Error("Test error for logging")), { logger: createMockLogger() });
|
|
1132
1144
|
const result = await executor.execute({
|
|
1133
1145
|
agent: createTestAgent(),
|
|
1134
1146
|
prompt: "Test prompt",
|
|
@@ -1145,7 +1157,7 @@ describe("error handling (US-7)", () => {
|
|
|
1145
1157
|
it("includes error code in job output when available", async () => {
|
|
1146
1158
|
const errorWithCode = new Error("Network error");
|
|
1147
1159
|
errorWithCode.code = "ECONNRESET";
|
|
1148
|
-
const executor = new JobExecutor(
|
|
1160
|
+
const executor = new JobExecutor(createErrorMockRuntime(errorWithCode), {
|
|
1149
1161
|
logger: createMockLogger(),
|
|
1150
1162
|
});
|
|
1151
1163
|
const result = await executor.execute({
|
|
@@ -1161,7 +1173,7 @@ describe("error handling (US-7)", () => {
|
|
|
1161
1173
|
});
|
|
1162
1174
|
it("includes stack trace in job output", async () => {
|
|
1163
1175
|
const error = new Error("Stack trace test");
|
|
1164
|
-
const executor = new JobExecutor(
|
|
1176
|
+
const executor = new JobExecutor(createErrorMockRuntime(error), {
|
|
1165
1177
|
logger: createMockLogger(),
|
|
1166
1178
|
});
|
|
1167
1179
|
const result = await executor.execute({
|
|
@@ -1179,7 +1191,7 @@ describe("error handling (US-7)", () => {
|
|
|
1179
1191
|
});
|
|
1180
1192
|
describe("job status updates", () => {
|
|
1181
1193
|
it("updates job status to failed with error exit_reason", async () => {
|
|
1182
|
-
const executor = new JobExecutor(
|
|
1194
|
+
const executor = new JobExecutor(createErrorMockRuntime(new Error("Failure")), { logger: createMockLogger() });
|
|
1183
1195
|
const result = await executor.execute({
|
|
1184
1196
|
agent: createTestAgent(),
|
|
1185
1197
|
prompt: "Test prompt",
|
|
@@ -1191,7 +1203,7 @@ describe("error handling (US-7)", () => {
|
|
|
1191
1203
|
});
|
|
1192
1204
|
it("sets exit_reason to timeout for timeout errors", async () => {
|
|
1193
1205
|
const timeoutError = new Error("Request timed out");
|
|
1194
|
-
const executor = new JobExecutor(
|
|
1206
|
+
const executor = new JobExecutor(createErrorMockRuntime(timeoutError), {
|
|
1195
1207
|
logger: createMockLogger(),
|
|
1196
1208
|
});
|
|
1197
1209
|
const result = await executor.execute({
|
|
@@ -1204,7 +1216,7 @@ describe("error handling (US-7)", () => {
|
|
|
1204
1216
|
});
|
|
1205
1217
|
it("sets exit_reason to cancelled for abort errors", async () => {
|
|
1206
1218
|
const abortError = new Error("Operation aborted by user");
|
|
1207
|
-
const executor = new JobExecutor(
|
|
1219
|
+
const executor = new JobExecutor(createErrorMockRuntime(abortError), {
|
|
1208
1220
|
logger: createMockLogger(),
|
|
1209
1221
|
});
|
|
1210
1222
|
const result = await executor.execute({
|
|
@@ -1217,7 +1229,7 @@ describe("error handling (US-7)", () => {
|
|
|
1217
1229
|
});
|
|
1218
1230
|
it("sets exit_reason to max_turns for turn limit errors", async () => {
|
|
1219
1231
|
const maxTurnsError = new Error("Maximum turns exceeded");
|
|
1220
|
-
const executor = new JobExecutor(
|
|
1232
|
+
const executor = new JobExecutor(createErrorMockRuntime(maxTurnsError), {
|
|
1221
1233
|
logger: createMockLogger(),
|
|
1222
1234
|
});
|
|
1223
1235
|
const result = await executor.execute({
|
|
@@ -1231,7 +1243,7 @@ describe("error handling (US-7)", () => {
|
|
|
1231
1243
|
});
|
|
1232
1244
|
describe("error details in RunnerResult", () => {
|
|
1233
1245
|
it("provides descriptive error message with context", async () => {
|
|
1234
|
-
const executor = new JobExecutor(
|
|
1246
|
+
const executor = new JobExecutor(createErrorMockRuntime(new Error("API connection failed")), { logger: createMockLogger() });
|
|
1235
1247
|
const result = await executor.execute({
|
|
1236
1248
|
agent: createTestAgent({ name: "descriptive-agent" }),
|
|
1237
1249
|
prompt: "Test prompt",
|
|
@@ -1245,7 +1257,7 @@ describe("error handling (US-7)", () => {
|
|
|
1245
1257
|
it("returns error details in RunnerResult", async () => {
|
|
1246
1258
|
const errorWithCode = new Error("Network timeout");
|
|
1247
1259
|
errorWithCode.code = "ETIMEDOUT";
|
|
1248
|
-
const executor = new JobExecutor(
|
|
1260
|
+
const executor = new JobExecutor(createErrorMockRuntime(errorWithCode), {
|
|
1249
1261
|
logger: createMockLogger(),
|
|
1250
1262
|
});
|
|
1251
1263
|
const result = await executor.execute({
|
|
@@ -1261,13 +1273,13 @@ describe("error handling (US-7)", () => {
|
|
|
1261
1273
|
});
|
|
1262
1274
|
describe("malformed SDK responses", () => {
|
|
1263
1275
|
it("does not crash on malformed SDK messages", async () => {
|
|
1264
|
-
const
|
|
1276
|
+
const runtime = createMockRuntime(async function* (options) {
|
|
1265
1277
|
yield { type: "system", content: "Init" };
|
|
1266
1278
|
// Yield a malformed message (null)
|
|
1267
1279
|
yield null;
|
|
1268
1280
|
yield { type: "assistant", content: "Continuing after malformed" };
|
|
1269
|
-
};
|
|
1270
|
-
const executor = new JobExecutor(
|
|
1281
|
+
});
|
|
1282
|
+
const executor = new JobExecutor(runtime, {
|
|
1271
1283
|
logger: createMockLogger(),
|
|
1272
1284
|
});
|
|
1273
1285
|
const result = await executor.execute({
|
|
@@ -1285,12 +1297,12 @@ describe("error handling (US-7)", () => {
|
|
|
1285
1297
|
expect(malformedMsg).toBeDefined();
|
|
1286
1298
|
});
|
|
1287
1299
|
it("handles messages with missing type field", async () => {
|
|
1288
|
-
const
|
|
1300
|
+
const runtime = createMockRuntime(async function* (options) {
|
|
1289
1301
|
yield { type: "system", content: "Init" };
|
|
1290
1302
|
yield { content: "Missing type" };
|
|
1291
1303
|
yield { type: "assistant", content: "Done" };
|
|
1292
|
-
};
|
|
1293
|
-
const executor = new JobExecutor(
|
|
1304
|
+
});
|
|
1305
|
+
const executor = new JobExecutor(runtime, {
|
|
1294
1306
|
logger: createMockLogger(),
|
|
1295
1307
|
});
|
|
1296
1308
|
const result = await executor.execute({
|
|
@@ -1305,12 +1317,12 @@ describe("error handling (US-7)", () => {
|
|
|
1305
1317
|
expect(unknownTypeMsg).toBeDefined();
|
|
1306
1318
|
});
|
|
1307
1319
|
it("handles messages with unexpected type values", async () => {
|
|
1308
|
-
const
|
|
1320
|
+
const runtime = createMockRuntime(async function* (options) {
|
|
1309
1321
|
yield { type: "system", content: "Init" };
|
|
1310
1322
|
yield { type: "unexpected_type", content: "Unknown" };
|
|
1311
1323
|
yield { type: "assistant", content: "Done" };
|
|
1312
|
-
};
|
|
1313
|
-
const executor = new JobExecutor(
|
|
1324
|
+
});
|
|
1325
|
+
const executor = new JobExecutor(runtime, {
|
|
1314
1326
|
logger: createMockLogger(),
|
|
1315
1327
|
});
|
|
1316
1328
|
const result = await executor.execute({
|
|
@@ -1328,7 +1340,7 @@ describe("error handling (US-7)", () => {
|
|
|
1328
1340
|
{ type: "system", content: "Start" },
|
|
1329
1341
|
{ type: "error", message: "SDK reported error", code: "SDK_ERR" },
|
|
1330
1342
|
];
|
|
1331
|
-
const executor = new JobExecutor(
|
|
1343
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
1332
1344
|
logger: createMockLogger(),
|
|
1333
1345
|
});
|
|
1334
1346
|
const result = await executor.execute({
|
|
@@ -1361,7 +1373,7 @@ describe("outputToFile (US-9)", () => {
|
|
|
1361
1373
|
{ type: "system", content: "Init" },
|
|
1362
1374
|
{ type: "assistant", content: "Hello" },
|
|
1363
1375
|
];
|
|
1364
|
-
const executor = new JobExecutor(
|
|
1376
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
1365
1377
|
logger: createMockLogger(),
|
|
1366
1378
|
});
|
|
1367
1379
|
const result = await executor.execute({
|
|
@@ -1380,7 +1392,7 @@ describe("outputToFile (US-9)", () => {
|
|
|
1380
1392
|
{ type: "system", content: "Init" },
|
|
1381
1393
|
{ type: "assistant", content: "Hello world" },
|
|
1382
1394
|
];
|
|
1383
|
-
const executor = new JobExecutor(
|
|
1395
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
1384
1396
|
logger: createMockLogger(),
|
|
1385
1397
|
});
|
|
1386
1398
|
const result = await executor.execute({
|
|
@@ -1400,7 +1412,7 @@ describe("outputToFile (US-9)", () => {
|
|
|
1400
1412
|
{ type: "system", content: "Init" },
|
|
1401
1413
|
{ type: "assistant", content: "Hello" },
|
|
1402
1414
|
];
|
|
1403
|
-
const executor = new JobExecutor(
|
|
1415
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
1404
1416
|
logger: createMockLogger(),
|
|
1405
1417
|
});
|
|
1406
1418
|
const result = await executor.execute({
|
|
@@ -1424,7 +1436,7 @@ describe("outputToFile (US-9)", () => {
|
|
|
1424
1436
|
{ type: "system", content: "Init" },
|
|
1425
1437
|
{ type: "assistant", content: "Hello" },
|
|
1426
1438
|
];
|
|
1427
|
-
const executor = new JobExecutor(
|
|
1439
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
1428
1440
|
logger: createMockLogger(),
|
|
1429
1441
|
});
|
|
1430
1442
|
const result = await executor.execute({
|
|
@@ -1451,7 +1463,7 @@ describe("outputToFile (US-9)", () => {
|
|
|
1451
1463
|
{ type: "tool_result", result: "localhost", success: true },
|
|
1452
1464
|
{ type: "error", message: "An error occurred" },
|
|
1453
1465
|
];
|
|
1454
|
-
const executor = new JobExecutor(
|
|
1466
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
1455
1467
|
logger: createMockLogger(),
|
|
1456
1468
|
});
|
|
1457
1469
|
const result = await executor.execute({
|
|
@@ -1472,7 +1484,7 @@ describe("outputToFile (US-9)", () => {
|
|
|
1472
1484
|
const messages = [
|
|
1473
1485
|
{ type: "assistant", content: "Hello" },
|
|
1474
1486
|
];
|
|
1475
|
-
const executor = new JobExecutor(
|
|
1487
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
1476
1488
|
logger: createMockLogger(),
|
|
1477
1489
|
});
|
|
1478
1490
|
const result = await executor.execute({
|
|
@@ -1491,7 +1503,7 @@ describe("outputToFile (US-9)", () => {
|
|
|
1491
1503
|
{ type: "system", content: "Init" },
|
|
1492
1504
|
{ type: "assistant", content: "Response" },
|
|
1493
1505
|
];
|
|
1494
|
-
const executor = new JobExecutor(
|
|
1506
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
1495
1507
|
logger: createMockLogger(),
|
|
1496
1508
|
});
|
|
1497
1509
|
const result = await executor.execute({
|
|
@@ -1510,7 +1522,7 @@ describe("outputToFile (US-9)", () => {
|
|
|
1510
1522
|
const messages = [
|
|
1511
1523
|
{ type: "tool_result", result: "File not found", success: false },
|
|
1512
1524
|
];
|
|
1513
|
-
const executor = new JobExecutor(
|
|
1525
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
1514
1526
|
logger: createMockLogger(),
|
|
1515
1527
|
});
|
|
1516
1528
|
const result = await executor.execute({
|
|
@@ -1532,7 +1544,7 @@ describe("outputToFile (US-9)", () => {
|
|
|
1532
1544
|
{ type: "system", content: "Init" },
|
|
1533
1545
|
{ type: "assistant", content: "Hello" },
|
|
1534
1546
|
];
|
|
1535
|
-
const executor = new JobExecutor(
|
|
1547
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
1536
1548
|
logger: createMockLogger(),
|
|
1537
1549
|
});
|
|
1538
1550
|
await executor.execute({
|
|
@@ -1547,4 +1559,323 @@ describe("outputToFile (US-9)", () => {
|
|
|
1547
1559
|
expect(receivedMessages).toHaveLength(2);
|
|
1548
1560
|
});
|
|
1549
1561
|
});
|
|
1562
|
+
// =============================================================================
|
|
1563
|
+
// Session expiration handling tests (fixes unexpected logout bug)
|
|
1564
|
+
// =============================================================================
|
|
1565
|
+
describe("session expiration handling", () => {
|
|
1566
|
+
let tempDir;
|
|
1567
|
+
let stateDir;
|
|
1568
|
+
beforeEach(async () => {
|
|
1569
|
+
tempDir = await createTempDir();
|
|
1570
|
+
stateDir = join(tempDir, ".herdctl");
|
|
1571
|
+
await initStateDirectory({ path: stateDir });
|
|
1572
|
+
});
|
|
1573
|
+
afterEach(async () => {
|
|
1574
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
1575
|
+
});
|
|
1576
|
+
it("starts fresh session when existing session is expired", async () => {
|
|
1577
|
+
const sessionsDir = join(stateDir, "sessions");
|
|
1578
|
+
// Create an expired session (last used 25 hours ago, default timeout is 24h)
|
|
1579
|
+
const expiredLastUsed = new Date(Date.now() - 25 * 60 * 60 * 1000).toISOString();
|
|
1580
|
+
await updateSessionInfo(sessionsDir, "expiry-test-agent", {
|
|
1581
|
+
session_id: "old-expired-session",
|
|
1582
|
+
mode: "autonomous",
|
|
1583
|
+
});
|
|
1584
|
+
// Manually update last_used_at to make it expired
|
|
1585
|
+
const { writeFile } = await import("node:fs/promises");
|
|
1586
|
+
const sessionPath = join(sessionsDir, "expiry-test-agent.json");
|
|
1587
|
+
const sessionData = {
|
|
1588
|
+
agent_name: "expiry-test-agent",
|
|
1589
|
+
session_id: "old-expired-session",
|
|
1590
|
+
created_at: expiredLastUsed,
|
|
1591
|
+
last_used_at: expiredLastUsed,
|
|
1592
|
+
job_count: 1,
|
|
1593
|
+
mode: "autonomous",
|
|
1594
|
+
};
|
|
1595
|
+
await writeFile(sessionPath, JSON.stringify(sessionData));
|
|
1596
|
+
let receivedOptions;
|
|
1597
|
+
const runtime = createMockRuntime(async function* (options) {
|
|
1598
|
+
receivedOptions = options;
|
|
1599
|
+
yield { type: "system", content: "Init", subtype: "init", session_id: "new-fresh-session" };
|
|
1600
|
+
yield { type: "assistant", content: "Started fresh" };
|
|
1601
|
+
});
|
|
1602
|
+
const executor = new JobExecutor(runtime, {
|
|
1603
|
+
logger: createMockLogger(),
|
|
1604
|
+
});
|
|
1605
|
+
const result = await executor.execute({
|
|
1606
|
+
agent: createTestAgent({ name: "expiry-test-agent" }),
|
|
1607
|
+
prompt: "Test prompt",
|
|
1608
|
+
stateDir,
|
|
1609
|
+
resume: "old-expired-session", // Try to resume expired session
|
|
1610
|
+
});
|
|
1611
|
+
expect(result.success).toBe(true);
|
|
1612
|
+
// Should NOT have passed resume option since session was expired
|
|
1613
|
+
expect(receivedOptions?.resume).toBeUndefined();
|
|
1614
|
+
// New session should be created
|
|
1615
|
+
expect(result.sessionId).toBe("new-fresh-session");
|
|
1616
|
+
});
|
|
1617
|
+
it("resumes valid session that is not expired", async () => {
|
|
1618
|
+
const sessionsDir = join(stateDir, "sessions");
|
|
1619
|
+
// Create a valid session (last used 1 hour ago, default timeout is 24h)
|
|
1620
|
+
await updateSessionInfo(sessionsDir, "valid-session-agent", {
|
|
1621
|
+
session_id: "valid-session-id",
|
|
1622
|
+
mode: "autonomous",
|
|
1623
|
+
});
|
|
1624
|
+
let receivedOptions;
|
|
1625
|
+
const runtime = createMockRuntime(async function* (options) {
|
|
1626
|
+
receivedOptions = options;
|
|
1627
|
+
yield { type: "assistant", content: "Resumed successfully" };
|
|
1628
|
+
});
|
|
1629
|
+
const executor = new JobExecutor(runtime, {
|
|
1630
|
+
logger: createMockLogger(),
|
|
1631
|
+
});
|
|
1632
|
+
await executor.execute({
|
|
1633
|
+
agent: createTestAgent({ name: "valid-session-agent" }),
|
|
1634
|
+
prompt: "Test prompt",
|
|
1635
|
+
stateDir,
|
|
1636
|
+
resume: "valid-session-id",
|
|
1637
|
+
});
|
|
1638
|
+
// Should have passed resume option since session is valid
|
|
1639
|
+
expect(receivedOptions?.resume).toBe("valid-session-id");
|
|
1640
|
+
});
|
|
1641
|
+
it("uses stored session_id from disk, not the options.resume value", async () => {
|
|
1642
|
+
const sessionsDir = join(stateDir, "sessions");
|
|
1643
|
+
// Create a valid session with a DIFFERENT session_id than what options.resume will provide
|
|
1644
|
+
// This simulates the case where the stored session has been updated but the caller
|
|
1645
|
+
// is using an outdated or generic resume value (like a boolean or old session ID)
|
|
1646
|
+
await updateSessionInfo(sessionsDir, "session-mismatch-agent", {
|
|
1647
|
+
session_id: "stored-session-abc123", // The actual session ID stored on disk
|
|
1648
|
+
mode: "autonomous",
|
|
1649
|
+
});
|
|
1650
|
+
let receivedOptions;
|
|
1651
|
+
const runtime = createMockRuntime(async function* (options) {
|
|
1652
|
+
receivedOptions = options;
|
|
1653
|
+
yield { type: "assistant", content: "Resumed with correct session" };
|
|
1654
|
+
});
|
|
1655
|
+
const executor = new JobExecutor(runtime, {
|
|
1656
|
+
logger: createMockLogger(),
|
|
1657
|
+
});
|
|
1658
|
+
await executor.execute({
|
|
1659
|
+
agent: createTestAgent({ name: "session-mismatch-agent" }),
|
|
1660
|
+
prompt: "Test prompt",
|
|
1661
|
+
stateDir,
|
|
1662
|
+
// Pass a DIFFERENT value than what's stored - this could be an old session ID
|
|
1663
|
+
// or any truthy value indicating "please resume"
|
|
1664
|
+
resume: "outdated-session-xyz789",
|
|
1665
|
+
});
|
|
1666
|
+
// CRITICAL: Should use the session_id from the stored session file, NOT the options.resume value
|
|
1667
|
+
// This is the fix for the unexpected logout bug - we must use the actual stored session ID
|
|
1668
|
+
expect(receivedOptions?.resume).toBe("stored-session-abc123");
|
|
1669
|
+
});
|
|
1670
|
+
it("respects custom session timeout from agent config", async () => {
|
|
1671
|
+
const sessionsDir = join(stateDir, "sessions");
|
|
1672
|
+
// Create a session that is 2 hours old
|
|
1673
|
+
const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString();
|
|
1674
|
+
const { writeFile } = await import("node:fs/promises");
|
|
1675
|
+
const sessionPath = join(sessionsDir, "custom-timeout-agent.json");
|
|
1676
|
+
const sessionData = {
|
|
1677
|
+
agent_name: "custom-timeout-agent",
|
|
1678
|
+
session_id: "two-hour-old-session",
|
|
1679
|
+
created_at: twoHoursAgo,
|
|
1680
|
+
last_used_at: twoHoursAgo,
|
|
1681
|
+
job_count: 1,
|
|
1682
|
+
mode: "autonomous",
|
|
1683
|
+
};
|
|
1684
|
+
await writeFile(sessionPath, JSON.stringify(sessionData));
|
|
1685
|
+
let receivedOptions;
|
|
1686
|
+
const runtime = createMockRuntime(async function* (options) {
|
|
1687
|
+
receivedOptions = options;
|
|
1688
|
+
yield { type: "assistant", content: "Done" };
|
|
1689
|
+
});
|
|
1690
|
+
const executor = new JobExecutor(runtime, {
|
|
1691
|
+
logger: createMockLogger(),
|
|
1692
|
+
});
|
|
1693
|
+
// With 1 hour timeout, 2-hour-old session should be expired
|
|
1694
|
+
await executor.execute({
|
|
1695
|
+
agent: createTestAgent({
|
|
1696
|
+
name: "custom-timeout-agent",
|
|
1697
|
+
session: { timeout: "1h" },
|
|
1698
|
+
}),
|
|
1699
|
+
prompt: "Test prompt",
|
|
1700
|
+
stateDir,
|
|
1701
|
+
resume: "two-hour-old-session",
|
|
1702
|
+
});
|
|
1703
|
+
// Should NOT have passed resume since session exceeded custom timeout
|
|
1704
|
+
expect(receivedOptions?.resume).toBeUndefined();
|
|
1705
|
+
});
|
|
1706
|
+
it("writes system message to job output when session expires", async () => {
|
|
1707
|
+
const sessionsDir = join(stateDir, "sessions");
|
|
1708
|
+
// Create an expired session
|
|
1709
|
+
const expiredLastUsed = new Date(Date.now() - 25 * 60 * 60 * 1000).toISOString();
|
|
1710
|
+
const { writeFile } = await import("node:fs/promises");
|
|
1711
|
+
const sessionPath = join(sessionsDir, "output-test-agent.json");
|
|
1712
|
+
const sessionData = {
|
|
1713
|
+
agent_name: "output-test-agent",
|
|
1714
|
+
session_id: "expired-session",
|
|
1715
|
+
created_at: expiredLastUsed,
|
|
1716
|
+
last_used_at: expiredLastUsed,
|
|
1717
|
+
job_count: 1,
|
|
1718
|
+
mode: "autonomous",
|
|
1719
|
+
};
|
|
1720
|
+
await writeFile(sessionPath, JSON.stringify(sessionData));
|
|
1721
|
+
const runtime = createMockRuntime(async function* () {
|
|
1722
|
+
yield { type: "assistant", content: "Fresh start" };
|
|
1723
|
+
});
|
|
1724
|
+
const executor = new JobExecutor(runtime, {
|
|
1725
|
+
logger: createMockLogger(),
|
|
1726
|
+
});
|
|
1727
|
+
const result = await executor.execute({
|
|
1728
|
+
agent: createTestAgent({ name: "output-test-agent" }),
|
|
1729
|
+
prompt: "Test prompt",
|
|
1730
|
+
stateDir,
|
|
1731
|
+
resume: "expired-session",
|
|
1732
|
+
});
|
|
1733
|
+
// Check job output for session expiry message
|
|
1734
|
+
const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
|
|
1735
|
+
const systemMsg = output.find((m) => m.type === "system" && m.content?.includes("session"));
|
|
1736
|
+
expect(systemMsg).toBeDefined();
|
|
1737
|
+
});
|
|
1738
|
+
it("clears expired session file when detected", async () => {
|
|
1739
|
+
const sessionsDir = join(stateDir, "sessions");
|
|
1740
|
+
// Create an expired session
|
|
1741
|
+
const expiredLastUsed = new Date(Date.now() - 25 * 60 * 60 * 1000).toISOString();
|
|
1742
|
+
const { writeFile } = await import("node:fs/promises");
|
|
1743
|
+
const sessionPath = join(sessionsDir, "clear-test-agent.json");
|
|
1744
|
+
const sessionData = {
|
|
1745
|
+
agent_name: "clear-test-agent",
|
|
1746
|
+
session_id: "expired-to-clear",
|
|
1747
|
+
created_at: expiredLastUsed,
|
|
1748
|
+
last_used_at: expiredLastUsed,
|
|
1749
|
+
job_count: 1,
|
|
1750
|
+
mode: "autonomous",
|
|
1751
|
+
};
|
|
1752
|
+
await writeFile(sessionPath, JSON.stringify(sessionData));
|
|
1753
|
+
// Verify session exists
|
|
1754
|
+
let sessionBefore = await getSessionInfo(sessionsDir, "clear-test-agent");
|
|
1755
|
+
expect(sessionBefore).not.toBeNull();
|
|
1756
|
+
const runtime = createMockRuntime(async function* () {
|
|
1757
|
+
yield { type: "system", content: "Init", subtype: "init", session_id: "new-session" };
|
|
1758
|
+
yield { type: "assistant", content: "Fresh" };
|
|
1759
|
+
});
|
|
1760
|
+
const executor = new JobExecutor(runtime, {
|
|
1761
|
+
logger: createMockLogger(),
|
|
1762
|
+
});
|
|
1763
|
+
await executor.execute({
|
|
1764
|
+
agent: createTestAgent({ name: "clear-test-agent" }),
|
|
1765
|
+
prompt: "Test prompt",
|
|
1766
|
+
stateDir,
|
|
1767
|
+
resume: "expired-to-clear",
|
|
1768
|
+
});
|
|
1769
|
+
// Session should now have the new ID (old one was cleared and new one created)
|
|
1770
|
+
const sessionAfter = await getSessionInfo(sessionsDir, "clear-test-agent");
|
|
1771
|
+
expect(sessionAfter?.session_id).toBe("new-session");
|
|
1772
|
+
});
|
|
1773
|
+
it("retries with fresh session when server-side session expiration detected", async () => {
|
|
1774
|
+
const sessionsDir = join(stateDir, "sessions");
|
|
1775
|
+
// Create a valid session (not locally expired)
|
|
1776
|
+
await updateSessionInfo(sessionsDir, "server-expiry-agent", {
|
|
1777
|
+
session_id: "valid-local-session",
|
|
1778
|
+
mode: "autonomous",
|
|
1779
|
+
});
|
|
1780
|
+
let attemptCount = 0;
|
|
1781
|
+
let lastResumeValue;
|
|
1782
|
+
// First attempt throws session expired error, second succeeds
|
|
1783
|
+
const runtime = createMockRuntime(async function* (options) {
|
|
1784
|
+
attemptCount++;
|
|
1785
|
+
lastResumeValue = options.resume;
|
|
1786
|
+
if (attemptCount === 1 && options.resume) {
|
|
1787
|
+
// First attempt with resume - server says session expired
|
|
1788
|
+
throw new Error("Session expired on server");
|
|
1789
|
+
}
|
|
1790
|
+
// Second attempt or fresh session - succeed
|
|
1791
|
+
yield { type: "system", content: "Init", subtype: "init", session_id: "new-server-session" };
|
|
1792
|
+
yield { type: "assistant", content: "Success after retry" };
|
|
1793
|
+
});
|
|
1794
|
+
const executor = new JobExecutor(runtime, {
|
|
1795
|
+
logger: createMockLogger(),
|
|
1796
|
+
});
|
|
1797
|
+
const result = await executor.execute({
|
|
1798
|
+
agent: createTestAgent({ name: "server-expiry-agent" }),
|
|
1799
|
+
prompt: "Test prompt",
|
|
1800
|
+
stateDir,
|
|
1801
|
+
resume: "valid-local-session",
|
|
1802
|
+
});
|
|
1803
|
+
// Should have succeeded after retry
|
|
1804
|
+
expect(result.success).toBe(true);
|
|
1805
|
+
expect(result.sessionId).toBe("new-server-session");
|
|
1806
|
+
// Should have attempted twice: first with resume, then fresh
|
|
1807
|
+
expect(attemptCount).toBe(2);
|
|
1808
|
+
// Second attempt should NOT have resume (fresh session)
|
|
1809
|
+
expect(lastResumeValue).toBeUndefined();
|
|
1810
|
+
// Check job output includes retry message
|
|
1811
|
+
const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
|
|
1812
|
+
const retryMsg = output.find((m) => m.type === "system" && m.content?.includes("Retrying with fresh session"));
|
|
1813
|
+
expect(retryMsg).toBeDefined();
|
|
1814
|
+
});
|
|
1815
|
+
it("does not retry infinitely on persistent server errors", async () => {
|
|
1816
|
+
const sessionsDir = join(stateDir, "sessions");
|
|
1817
|
+
await updateSessionInfo(sessionsDir, "no-infinite-retry-agent", {
|
|
1818
|
+
session_id: "some-session",
|
|
1819
|
+
mode: "autonomous",
|
|
1820
|
+
});
|
|
1821
|
+
let attemptCount = 0;
|
|
1822
|
+
// Always throw session expired error
|
|
1823
|
+
const runtime = createMockRuntime(async function* (options) {
|
|
1824
|
+
attemptCount++;
|
|
1825
|
+
throw new Error("Session expired on server");
|
|
1826
|
+
});
|
|
1827
|
+
const executor = new JobExecutor(runtime, {
|
|
1828
|
+
logger: createMockLogger(),
|
|
1829
|
+
});
|
|
1830
|
+
const result = await executor.execute({
|
|
1831
|
+
agent: createTestAgent({ name: "no-infinite-retry-agent" }),
|
|
1832
|
+
prompt: "Test prompt",
|
|
1833
|
+
stateDir,
|
|
1834
|
+
resume: "some-session",
|
|
1835
|
+
});
|
|
1836
|
+
// Should fail after at most 2 attempts (initial + 1 retry)
|
|
1837
|
+
expect(result.success).toBe(false);
|
|
1838
|
+
expect(attemptCount).toBeLessThanOrEqual(2);
|
|
1839
|
+
expect(result.error?.message).toContain("Session expired");
|
|
1840
|
+
});
|
|
1841
|
+
it("updates last_used_at before execution to prevent mid-job session expiry", async () => {
|
|
1842
|
+
const sessionsDir = join(stateDir, "sessions");
|
|
1843
|
+
// Create a session that is 23 hours old (close to default 24h expiry)
|
|
1844
|
+
const twentyThreeHoursAgo = new Date(Date.now() - 23 * 60 * 60 * 1000).toISOString();
|
|
1845
|
+
const { writeFile } = await import("node:fs/promises");
|
|
1846
|
+
const sessionPath = join(sessionsDir, "refresh-test-agent.json");
|
|
1847
|
+
const sessionData = {
|
|
1848
|
+
agent_name: "refresh-test-agent",
|
|
1849
|
+
session_id: "almost-expired-session",
|
|
1850
|
+
created_at: twentyThreeHoursAgo,
|
|
1851
|
+
last_used_at: twentyThreeHoursAgo,
|
|
1852
|
+
job_count: 5,
|
|
1853
|
+
mode: "autonomous",
|
|
1854
|
+
};
|
|
1855
|
+
await writeFile(sessionPath, JSON.stringify(sessionData));
|
|
1856
|
+
// Record when the test starts
|
|
1857
|
+
const testStartTime = Date.now();
|
|
1858
|
+
const runtime = createMockRuntime(async function* () {
|
|
1859
|
+
// Simulate a delay to ensure last_used_at was updated BEFORE we got here
|
|
1860
|
+
yield { type: "assistant", content: "Working..." };
|
|
1861
|
+
});
|
|
1862
|
+
const executor = new JobExecutor(runtime, {
|
|
1863
|
+
logger: createMockLogger(),
|
|
1864
|
+
});
|
|
1865
|
+
await executor.execute({
|
|
1866
|
+
agent: createTestAgent({ name: "refresh-test-agent" }),
|
|
1867
|
+
prompt: "Test prompt",
|
|
1868
|
+
stateDir,
|
|
1869
|
+
resume: "almost-expired-session",
|
|
1870
|
+
});
|
|
1871
|
+
// Check that last_used_at was updated to a recent time (not 23 hours ago)
|
|
1872
|
+
const sessionAfter = await getSessionInfo(sessionsDir, "refresh-test-agent");
|
|
1873
|
+
expect(sessionAfter).not.toBeNull();
|
|
1874
|
+
const lastUsedMs = new Date(sessionAfter.last_used_at).getTime();
|
|
1875
|
+
// Should be updated to approximately now (within last few seconds)
|
|
1876
|
+
expect(lastUsedMs).toBeGreaterThanOrEqual(testStartTime - 1000);
|
|
1877
|
+
// And not still 23 hours ago
|
|
1878
|
+
expect(lastUsedMs).toBeGreaterThan(new Date(twentyThreeHoursAgo).getTime());
|
|
1879
|
+
});
|
|
1880
|
+
});
|
|
1550
1881
|
//# sourceMappingURL=job-executor.test.js.map
|