@herdctl/core 1.3.1 → 2.0.1
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 +17 -17
- 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/hooks/__tests__/discord-runner.test.js +16 -16
- package/dist/hooks/__tests__/discord-runner.test.js.map +1 -1
- package/dist/hooks/runners/discord.d.ts.map +1 -1
- package/dist/hooks/runners/discord.js +15 -12
- package/dist/hooks/runners/discord.js.map +1 -1
- package/dist/runner/__tests__/job-executor.test.js +461 -126
- package/dist/runner/__tests__/job-executor.test.js.map +1 -1
- package/dist/runner/__tests__/message-processor.test.js +12 -35
- package/dist/runner/__tests__/message-processor.test.js.map +1 -1
- package/dist/runner/__tests__/sdk-adapter.test.js +137 -2
- 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 +280 -133
- package/dist/runner/job-executor.js.map +1 -1
- package/dist/runner/message-processor.d.ts +5 -2
- package/dist/runner/message-processor.d.ts.map +1 -1
- package/dist/runner/message-processor.js +9 -18
- package/dist/runner/message-processor.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 +341 -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.map +1 -1
- package/dist/runner/sdk-adapter.js +28 -10
- package/dist/runner/sdk-adapter.js.map +1 -1
- package/dist/runner/types.d.ts +11 -1
- 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 +227 -0
- package/dist/state/session-validation.d.ts.map +1 -0
- package/dist/state/session-validation.js +448 -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({
|
|
@@ -690,13 +702,13 @@ describe("JobExecutor", () => {
|
|
|
690
702
|
});
|
|
691
703
|
expect(result.summary).toBeUndefined();
|
|
692
704
|
});
|
|
693
|
-
it("returns
|
|
694
|
-
const longContent = "x".repeat(
|
|
705
|
+
it("returns full summary for long assistant messages (no truncation)", async () => {
|
|
706
|
+
const longContent = "x".repeat(5000);
|
|
695
707
|
const messages = [
|
|
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({
|
|
@@ -704,14 +716,17 @@ describe("JobExecutor", () => {
|
|
|
704
716
|
prompt: "Test prompt",
|
|
705
717
|
stateDir,
|
|
706
718
|
});
|
|
707
|
-
|
|
719
|
+
// No truncation at this layer - downstream consumers (Discord) handle their own limits
|
|
720
|
+
expect(result.summary).toBeDefined();
|
|
721
|
+
expect(result.summary).toBe(longContent);
|
|
722
|
+
expect(result.summary?.length).toBe(5000);
|
|
708
723
|
});
|
|
709
|
-
it("
|
|
710
|
-
const longSummary = "x".repeat(
|
|
724
|
+
it("returns full explicit summary (no truncation)", async () => {
|
|
725
|
+
const longSummary = "x".repeat(5000);
|
|
711
726
|
const messages = [
|
|
712
727
|
{ type: "assistant", content: "Done!", summary: longSummary },
|
|
713
728
|
];
|
|
714
|
-
const executor = new JobExecutor(
|
|
729
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
715
730
|
logger: createMockLogger(),
|
|
716
731
|
});
|
|
717
732
|
const result = await executor.execute({
|
|
@@ -719,11 +734,12 @@ describe("JobExecutor", () => {
|
|
|
719
734
|
prompt: "Test prompt",
|
|
720
735
|
stateDir,
|
|
721
736
|
});
|
|
737
|
+
// No truncation at this layer
|
|
722
738
|
expect(result.summary).toBeDefined();
|
|
723
|
-
expect(result.summary
|
|
724
|
-
expect(result.summary.
|
|
739
|
+
expect(result.summary).toBe(longSummary);
|
|
740
|
+
expect(result.summary.length).toBe(5000);
|
|
725
741
|
const job = await getJob(join(stateDir, "jobs"), result.jobId);
|
|
726
|
-
expect(job?.summary
|
|
742
|
+
expect(job?.summary).toBe(longSummary);
|
|
727
743
|
});
|
|
728
744
|
it("uses latest summary when multiple messages have summaries", async () => {
|
|
729
745
|
const messages = [
|
|
@@ -731,7 +747,7 @@ describe("JobExecutor", () => {
|
|
|
731
747
|
{ type: "assistant", content: "Second", summary: "Second summary" },
|
|
732
748
|
{ type: "assistant", content: "Third", summary: "Final summary" },
|
|
733
749
|
];
|
|
734
|
-
const executor = new JobExecutor(
|
|
750
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
735
751
|
logger: createMockLogger(),
|
|
736
752
|
});
|
|
737
753
|
const result = await executor.execute({
|
|
@@ -743,7 +759,7 @@ describe("JobExecutor", () => {
|
|
|
743
759
|
});
|
|
744
760
|
it("handles empty message stream with undefined summary", async () => {
|
|
745
761
|
const messages = [];
|
|
746
|
-
const executor = new JobExecutor(
|
|
762
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
747
763
|
logger: createMockLogger(),
|
|
748
764
|
});
|
|
749
765
|
const result = await executor.execute({
|
|
@@ -765,7 +781,7 @@ describe("JobExecutor", () => {
|
|
|
765
781
|
const onMessage = vi.fn((msg) => {
|
|
766
782
|
receivedMessages.push(msg);
|
|
767
783
|
});
|
|
768
|
-
const executor = new JobExecutor(
|
|
784
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
769
785
|
logger: createMockLogger(),
|
|
770
786
|
});
|
|
771
787
|
await executor.execute({
|
|
@@ -788,7 +804,7 @@ describe("JobExecutor", () => {
|
|
|
788
804
|
const onMessage = vi.fn(() => {
|
|
789
805
|
throw new Error("Callback error");
|
|
790
806
|
});
|
|
791
|
-
const executor = new JobExecutor(
|
|
807
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
792
808
|
logger: createMockLogger(),
|
|
793
809
|
});
|
|
794
810
|
const result = await executor.execute({
|
|
@@ -808,7 +824,7 @@ describe("JobExecutor", () => {
|
|
|
808
824
|
const onMessage = vi.fn(async () => {
|
|
809
825
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
810
826
|
});
|
|
811
|
-
const executor = new JobExecutor(
|
|
827
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
812
828
|
logger: createMockLogger(),
|
|
813
829
|
});
|
|
814
830
|
await executor.execute({
|
|
@@ -823,7 +839,7 @@ describe("JobExecutor", () => {
|
|
|
823
839
|
describe("trigger types", () => {
|
|
824
840
|
it("sets trigger type to manual by default", async () => {
|
|
825
841
|
const messages = [{ type: "assistant", content: "Done" }];
|
|
826
|
-
const executor = new JobExecutor(
|
|
842
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
827
843
|
logger: createMockLogger(),
|
|
828
844
|
});
|
|
829
845
|
const result = await executor.execute({
|
|
@@ -836,7 +852,7 @@ describe("JobExecutor", () => {
|
|
|
836
852
|
});
|
|
837
853
|
it("sets trigger type from options", async () => {
|
|
838
854
|
const messages = [{ type: "assistant", content: "Done" }];
|
|
839
|
-
const executor = new JobExecutor(
|
|
855
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
840
856
|
logger: createMockLogger(),
|
|
841
857
|
});
|
|
842
858
|
const result = await executor.execute({
|
|
@@ -857,7 +873,7 @@ describe("JobExecutor", () => {
|
|
|
857
873
|
{ type: "system", content: "Start" },
|
|
858
874
|
{ type: "assistant", content: "End" },
|
|
859
875
|
];
|
|
860
|
-
const executor = new JobExecutor(
|
|
876
|
+
const executor = new JobExecutor(createDelayedMockRuntime(messages, 50), { logger: createMockLogger() });
|
|
861
877
|
const result = await executor.execute({
|
|
862
878
|
agent: createTestAgent(),
|
|
863
879
|
prompt: "Test prompt",
|
|
@@ -888,7 +904,7 @@ describe("executeJob", () => {
|
|
|
888
904
|
{ type: "system", content: "Init" },
|
|
889
905
|
{ type: "assistant", content: "Done" },
|
|
890
906
|
];
|
|
891
|
-
const result = await executeJob(
|
|
907
|
+
const result = await executeJob(createMockRuntimeWithMessages(messages), {
|
|
892
908
|
agent: createTestAgent({ name: "convenience-agent" }),
|
|
893
909
|
prompt: "Test prompt",
|
|
894
910
|
stateDir,
|
|
@@ -901,7 +917,7 @@ describe("executeJob", () => {
|
|
|
901
917
|
it("passes executor options", async () => {
|
|
902
918
|
const messages = [{ type: "assistant", content: "Done" }];
|
|
903
919
|
const logger = createMockLogger();
|
|
904
|
-
await executeJob(
|
|
920
|
+
await executeJob(createMockRuntimeWithMessages(messages), {
|
|
905
921
|
agent: createTestAgent(),
|
|
906
922
|
prompt: "Test prompt",
|
|
907
923
|
stateDir,
|
|
@@ -924,7 +940,7 @@ describe("edge cases", () => {
|
|
|
924
940
|
await rm(tempDir, { recursive: true, force: true });
|
|
925
941
|
});
|
|
926
942
|
it("handles empty message stream", async () => {
|
|
927
|
-
const executor = new JobExecutor(
|
|
943
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages([]), {
|
|
928
944
|
logger: createMockLogger(),
|
|
929
945
|
});
|
|
930
946
|
const result = await executor.execute({
|
|
@@ -941,7 +957,7 @@ describe("edge cases", () => {
|
|
|
941
957
|
const messages = [
|
|
942
958
|
{ type: "assistant", content: longContent },
|
|
943
959
|
];
|
|
944
|
-
const executor = new JobExecutor(
|
|
960
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
945
961
|
logger: createMockLogger(),
|
|
946
962
|
});
|
|
947
963
|
const result = await executor.execute({
|
|
@@ -958,7 +974,7 @@ describe("edge cases", () => {
|
|
|
958
974
|
const messages = [
|
|
959
975
|
{ type: "assistant", content: "Hello 世界! 🌍 Γεια σου κόσμε" },
|
|
960
976
|
];
|
|
961
|
-
const executor = new JobExecutor(
|
|
977
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
962
978
|
logger: createMockLogger(),
|
|
963
979
|
});
|
|
964
980
|
const result = await executor.execute({
|
|
@@ -978,7 +994,7 @@ describe("edge cases", () => {
|
|
|
978
994
|
content: 'Content with "quotes", \\backslashes\\, and\nnewlines',
|
|
979
995
|
},
|
|
980
996
|
];
|
|
981
|
-
const executor = new JobExecutor(
|
|
997
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
982
998
|
logger: createMockLogger(),
|
|
983
999
|
});
|
|
984
1000
|
const result = await executor.execute({
|
|
@@ -998,7 +1014,7 @@ describe("edge cases", () => {
|
|
|
998
1014
|
type: "assistant",
|
|
999
1015
|
content: `Message ${i}`,
|
|
1000
1016
|
}));
|
|
1001
|
-
const executor = new JobExecutor(
|
|
1017
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
1002
1018
|
logger: createMockLogger(),
|
|
1003
1019
|
});
|
|
1004
1020
|
const result = await executor.execute({
|
|
@@ -1026,11 +1042,11 @@ describe("error handling (US-7)", () => {
|
|
|
1026
1042
|
});
|
|
1027
1043
|
describe("SDK initialization errors", () => {
|
|
1028
1044
|
it("catches SDK initialization errors (e.g., missing API key)", async () => {
|
|
1029
|
-
// Simulates SDK throwing immediately when
|
|
1030
|
-
const
|
|
1045
|
+
// Simulates SDK throwing immediately when execute is called
|
|
1046
|
+
const runtime = createMockRuntime(() => {
|
|
1031
1047
|
throw new Error("ANTHROPIC_API_KEY environment variable is not set");
|
|
1032
|
-
};
|
|
1033
|
-
const executor = new JobExecutor(
|
|
1048
|
+
});
|
|
1049
|
+
const executor = new JobExecutor(runtime, {
|
|
1034
1050
|
logger: createMockLogger(),
|
|
1035
1051
|
});
|
|
1036
1052
|
const result = await executor.execute({
|
|
@@ -1043,10 +1059,10 @@ describe("error handling (US-7)", () => {
|
|
|
1043
1059
|
expect(result.errorDetails?.type).toBe("initialization");
|
|
1044
1060
|
});
|
|
1045
1061
|
it("provides context (job ID, agent name) in initialization error", async () => {
|
|
1046
|
-
const
|
|
1062
|
+
const runtime = createMockRuntime(() => {
|
|
1047
1063
|
throw new Error("SDK init failed");
|
|
1048
|
-
};
|
|
1049
|
-
const executor = new JobExecutor(
|
|
1064
|
+
});
|
|
1065
|
+
const executor = new JobExecutor(runtime, {
|
|
1050
1066
|
logger: createMockLogger(),
|
|
1051
1067
|
});
|
|
1052
1068
|
const result = await executor.execute({
|
|
@@ -1060,12 +1076,12 @@ describe("error handling (US-7)", () => {
|
|
|
1060
1076
|
});
|
|
1061
1077
|
describe("SDK streaming errors", () => {
|
|
1062
1078
|
it("catches SDK streaming errors during execution", async () => {
|
|
1063
|
-
const
|
|
1079
|
+
const runtime = createMockRuntime(async function* (options) {
|
|
1064
1080
|
yield { type: "system", content: "Init" };
|
|
1065
1081
|
yield { type: "assistant", content: "Working..." };
|
|
1066
1082
|
throw new Error("Connection reset by peer");
|
|
1067
|
-
};
|
|
1068
|
-
const executor = new JobExecutor(
|
|
1083
|
+
});
|
|
1084
|
+
const executor = new JobExecutor(runtime, {
|
|
1069
1085
|
logger: createMockLogger(),
|
|
1070
1086
|
});
|
|
1071
1087
|
const result = await executor.execute({
|
|
@@ -1078,13 +1094,13 @@ describe("error handling (US-7)", () => {
|
|
|
1078
1094
|
expect(result.errorDetails?.type).toBe("streaming");
|
|
1079
1095
|
});
|
|
1080
1096
|
it("tracks messages received before streaming error", async () => {
|
|
1081
|
-
const
|
|
1097
|
+
const runtime = createMockRuntime(async function* (options) {
|
|
1082
1098
|
yield { type: "system", content: "Init" };
|
|
1083
1099
|
yield { type: "assistant", content: "Message 1" };
|
|
1084
1100
|
yield { type: "assistant", content: "Message 2" };
|
|
1085
1101
|
throw new Error("Stream interrupted");
|
|
1086
|
-
};
|
|
1087
|
-
const executor = new JobExecutor(
|
|
1102
|
+
});
|
|
1103
|
+
const executor = new JobExecutor(runtime, {
|
|
1088
1104
|
logger: createMockLogger(),
|
|
1089
1105
|
});
|
|
1090
1106
|
const result = await executor.execute({
|
|
@@ -1096,11 +1112,11 @@ describe("error handling (US-7)", () => {
|
|
|
1096
1112
|
expect(result.errorDetails?.messagesReceived).toBe(3);
|
|
1097
1113
|
});
|
|
1098
1114
|
it("identifies recoverable errors (rate limit)", async () => {
|
|
1099
|
-
const
|
|
1115
|
+
const runtime = createMockRuntime(async function* (options) {
|
|
1100
1116
|
yield { type: "system", content: "Init" };
|
|
1101
1117
|
throw new Error("Rate limit exceeded, please retry");
|
|
1102
|
-
};
|
|
1103
|
-
const executor = new JobExecutor(
|
|
1118
|
+
});
|
|
1119
|
+
const executor = new JobExecutor(runtime, {
|
|
1104
1120
|
logger: createMockLogger(),
|
|
1105
1121
|
});
|
|
1106
1122
|
const result = await executor.execute({
|
|
@@ -1111,11 +1127,11 @@ describe("error handling (US-7)", () => {
|
|
|
1111
1127
|
expect(result.errorDetails?.recoverable).toBe(true);
|
|
1112
1128
|
});
|
|
1113
1129
|
it("identifies non-recoverable errors", async () => {
|
|
1114
|
-
const
|
|
1130
|
+
const runtime = createMockRuntime(async function* (options) {
|
|
1115
1131
|
yield { type: "system", content: "Init" };
|
|
1116
1132
|
throw new Error("Invalid request format");
|
|
1117
|
-
};
|
|
1118
|
-
const executor = new JobExecutor(
|
|
1133
|
+
});
|
|
1134
|
+
const executor = new JobExecutor(runtime, {
|
|
1119
1135
|
logger: createMockLogger(),
|
|
1120
1136
|
});
|
|
1121
1137
|
const result = await executor.execute({
|
|
@@ -1128,7 +1144,7 @@ describe("error handling (US-7)", () => {
|
|
|
1128
1144
|
});
|
|
1129
1145
|
describe("error logging to job output", () => {
|
|
1130
1146
|
it("logs error messages to job output as error type messages", async () => {
|
|
1131
|
-
const executor = new JobExecutor(
|
|
1147
|
+
const executor = new JobExecutor(createErrorMockRuntime(new Error("Test error for logging")), { logger: createMockLogger() });
|
|
1132
1148
|
const result = await executor.execute({
|
|
1133
1149
|
agent: createTestAgent(),
|
|
1134
1150
|
prompt: "Test prompt",
|
|
@@ -1145,7 +1161,7 @@ describe("error handling (US-7)", () => {
|
|
|
1145
1161
|
it("includes error code in job output when available", async () => {
|
|
1146
1162
|
const errorWithCode = new Error("Network error");
|
|
1147
1163
|
errorWithCode.code = "ECONNRESET";
|
|
1148
|
-
const executor = new JobExecutor(
|
|
1164
|
+
const executor = new JobExecutor(createErrorMockRuntime(errorWithCode), {
|
|
1149
1165
|
logger: createMockLogger(),
|
|
1150
1166
|
});
|
|
1151
1167
|
const result = await executor.execute({
|
|
@@ -1161,7 +1177,7 @@ describe("error handling (US-7)", () => {
|
|
|
1161
1177
|
});
|
|
1162
1178
|
it("includes stack trace in job output", async () => {
|
|
1163
1179
|
const error = new Error("Stack trace test");
|
|
1164
|
-
const executor = new JobExecutor(
|
|
1180
|
+
const executor = new JobExecutor(createErrorMockRuntime(error), {
|
|
1165
1181
|
logger: createMockLogger(),
|
|
1166
1182
|
});
|
|
1167
1183
|
const result = await executor.execute({
|
|
@@ -1179,7 +1195,7 @@ describe("error handling (US-7)", () => {
|
|
|
1179
1195
|
});
|
|
1180
1196
|
describe("job status updates", () => {
|
|
1181
1197
|
it("updates job status to failed with error exit_reason", async () => {
|
|
1182
|
-
const executor = new JobExecutor(
|
|
1198
|
+
const executor = new JobExecutor(createErrorMockRuntime(new Error("Failure")), { logger: createMockLogger() });
|
|
1183
1199
|
const result = await executor.execute({
|
|
1184
1200
|
agent: createTestAgent(),
|
|
1185
1201
|
prompt: "Test prompt",
|
|
@@ -1191,7 +1207,7 @@ describe("error handling (US-7)", () => {
|
|
|
1191
1207
|
});
|
|
1192
1208
|
it("sets exit_reason to timeout for timeout errors", async () => {
|
|
1193
1209
|
const timeoutError = new Error("Request timed out");
|
|
1194
|
-
const executor = new JobExecutor(
|
|
1210
|
+
const executor = new JobExecutor(createErrorMockRuntime(timeoutError), {
|
|
1195
1211
|
logger: createMockLogger(),
|
|
1196
1212
|
});
|
|
1197
1213
|
const result = await executor.execute({
|
|
@@ -1204,7 +1220,7 @@ describe("error handling (US-7)", () => {
|
|
|
1204
1220
|
});
|
|
1205
1221
|
it("sets exit_reason to cancelled for abort errors", async () => {
|
|
1206
1222
|
const abortError = new Error("Operation aborted by user");
|
|
1207
|
-
const executor = new JobExecutor(
|
|
1223
|
+
const executor = new JobExecutor(createErrorMockRuntime(abortError), {
|
|
1208
1224
|
logger: createMockLogger(),
|
|
1209
1225
|
});
|
|
1210
1226
|
const result = await executor.execute({
|
|
@@ -1217,7 +1233,7 @@ describe("error handling (US-7)", () => {
|
|
|
1217
1233
|
});
|
|
1218
1234
|
it("sets exit_reason to max_turns for turn limit errors", async () => {
|
|
1219
1235
|
const maxTurnsError = new Error("Maximum turns exceeded");
|
|
1220
|
-
const executor = new JobExecutor(
|
|
1236
|
+
const executor = new JobExecutor(createErrorMockRuntime(maxTurnsError), {
|
|
1221
1237
|
logger: createMockLogger(),
|
|
1222
1238
|
});
|
|
1223
1239
|
const result = await executor.execute({
|
|
@@ -1231,7 +1247,7 @@ describe("error handling (US-7)", () => {
|
|
|
1231
1247
|
});
|
|
1232
1248
|
describe("error details in RunnerResult", () => {
|
|
1233
1249
|
it("provides descriptive error message with context", async () => {
|
|
1234
|
-
const executor = new JobExecutor(
|
|
1250
|
+
const executor = new JobExecutor(createErrorMockRuntime(new Error("API connection failed")), { logger: createMockLogger() });
|
|
1235
1251
|
const result = await executor.execute({
|
|
1236
1252
|
agent: createTestAgent({ name: "descriptive-agent" }),
|
|
1237
1253
|
prompt: "Test prompt",
|
|
@@ -1245,7 +1261,7 @@ describe("error handling (US-7)", () => {
|
|
|
1245
1261
|
it("returns error details in RunnerResult", async () => {
|
|
1246
1262
|
const errorWithCode = new Error("Network timeout");
|
|
1247
1263
|
errorWithCode.code = "ETIMEDOUT";
|
|
1248
|
-
const executor = new JobExecutor(
|
|
1264
|
+
const executor = new JobExecutor(createErrorMockRuntime(errorWithCode), {
|
|
1249
1265
|
logger: createMockLogger(),
|
|
1250
1266
|
});
|
|
1251
1267
|
const result = await executor.execute({
|
|
@@ -1261,13 +1277,13 @@ describe("error handling (US-7)", () => {
|
|
|
1261
1277
|
});
|
|
1262
1278
|
describe("malformed SDK responses", () => {
|
|
1263
1279
|
it("does not crash on malformed SDK messages", async () => {
|
|
1264
|
-
const
|
|
1280
|
+
const runtime = createMockRuntime(async function* (options) {
|
|
1265
1281
|
yield { type: "system", content: "Init" };
|
|
1266
1282
|
// Yield a malformed message (null)
|
|
1267
1283
|
yield null;
|
|
1268
1284
|
yield { type: "assistant", content: "Continuing after malformed" };
|
|
1269
|
-
};
|
|
1270
|
-
const executor = new JobExecutor(
|
|
1285
|
+
});
|
|
1286
|
+
const executor = new JobExecutor(runtime, {
|
|
1271
1287
|
logger: createMockLogger(),
|
|
1272
1288
|
});
|
|
1273
1289
|
const result = await executor.execute({
|
|
@@ -1285,12 +1301,12 @@ describe("error handling (US-7)", () => {
|
|
|
1285
1301
|
expect(malformedMsg).toBeDefined();
|
|
1286
1302
|
});
|
|
1287
1303
|
it("handles messages with missing type field", async () => {
|
|
1288
|
-
const
|
|
1304
|
+
const runtime = createMockRuntime(async function* (options) {
|
|
1289
1305
|
yield { type: "system", content: "Init" };
|
|
1290
1306
|
yield { content: "Missing type" };
|
|
1291
1307
|
yield { type: "assistant", content: "Done" };
|
|
1292
|
-
};
|
|
1293
|
-
const executor = new JobExecutor(
|
|
1308
|
+
});
|
|
1309
|
+
const executor = new JobExecutor(runtime, {
|
|
1294
1310
|
logger: createMockLogger(),
|
|
1295
1311
|
});
|
|
1296
1312
|
const result = await executor.execute({
|
|
@@ -1305,12 +1321,12 @@ describe("error handling (US-7)", () => {
|
|
|
1305
1321
|
expect(unknownTypeMsg).toBeDefined();
|
|
1306
1322
|
});
|
|
1307
1323
|
it("handles messages with unexpected type values", async () => {
|
|
1308
|
-
const
|
|
1324
|
+
const runtime = createMockRuntime(async function* (options) {
|
|
1309
1325
|
yield { type: "system", content: "Init" };
|
|
1310
1326
|
yield { type: "unexpected_type", content: "Unknown" };
|
|
1311
1327
|
yield { type: "assistant", content: "Done" };
|
|
1312
|
-
};
|
|
1313
|
-
const executor = new JobExecutor(
|
|
1328
|
+
});
|
|
1329
|
+
const executor = new JobExecutor(runtime, {
|
|
1314
1330
|
logger: createMockLogger(),
|
|
1315
1331
|
});
|
|
1316
1332
|
const result = await executor.execute({
|
|
@@ -1328,7 +1344,7 @@ describe("error handling (US-7)", () => {
|
|
|
1328
1344
|
{ type: "system", content: "Start" },
|
|
1329
1345
|
{ type: "error", message: "SDK reported error", code: "SDK_ERR" },
|
|
1330
1346
|
];
|
|
1331
|
-
const executor = new JobExecutor(
|
|
1347
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
1332
1348
|
logger: createMockLogger(),
|
|
1333
1349
|
});
|
|
1334
1350
|
const result = await executor.execute({
|
|
@@ -1361,7 +1377,7 @@ describe("outputToFile (US-9)", () => {
|
|
|
1361
1377
|
{ type: "system", content: "Init" },
|
|
1362
1378
|
{ type: "assistant", content: "Hello" },
|
|
1363
1379
|
];
|
|
1364
|
-
const executor = new JobExecutor(
|
|
1380
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
1365
1381
|
logger: createMockLogger(),
|
|
1366
1382
|
});
|
|
1367
1383
|
const result = await executor.execute({
|
|
@@ -1380,7 +1396,7 @@ describe("outputToFile (US-9)", () => {
|
|
|
1380
1396
|
{ type: "system", content: "Init" },
|
|
1381
1397
|
{ type: "assistant", content: "Hello world" },
|
|
1382
1398
|
];
|
|
1383
|
-
const executor = new JobExecutor(
|
|
1399
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
1384
1400
|
logger: createMockLogger(),
|
|
1385
1401
|
});
|
|
1386
1402
|
const result = await executor.execute({
|
|
@@ -1400,7 +1416,7 @@ describe("outputToFile (US-9)", () => {
|
|
|
1400
1416
|
{ type: "system", content: "Init" },
|
|
1401
1417
|
{ type: "assistant", content: "Hello" },
|
|
1402
1418
|
];
|
|
1403
|
-
const executor = new JobExecutor(
|
|
1419
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
1404
1420
|
logger: createMockLogger(),
|
|
1405
1421
|
});
|
|
1406
1422
|
const result = await executor.execute({
|
|
@@ -1424,7 +1440,7 @@ describe("outputToFile (US-9)", () => {
|
|
|
1424
1440
|
{ type: "system", content: "Init" },
|
|
1425
1441
|
{ type: "assistant", content: "Hello" },
|
|
1426
1442
|
];
|
|
1427
|
-
const executor = new JobExecutor(
|
|
1443
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
1428
1444
|
logger: createMockLogger(),
|
|
1429
1445
|
});
|
|
1430
1446
|
const result = await executor.execute({
|
|
@@ -1451,7 +1467,7 @@ describe("outputToFile (US-9)", () => {
|
|
|
1451
1467
|
{ type: "tool_result", result: "localhost", success: true },
|
|
1452
1468
|
{ type: "error", message: "An error occurred" },
|
|
1453
1469
|
];
|
|
1454
|
-
const executor = new JobExecutor(
|
|
1470
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
1455
1471
|
logger: createMockLogger(),
|
|
1456
1472
|
});
|
|
1457
1473
|
const result = await executor.execute({
|
|
@@ -1472,7 +1488,7 @@ describe("outputToFile (US-9)", () => {
|
|
|
1472
1488
|
const messages = [
|
|
1473
1489
|
{ type: "assistant", content: "Hello" },
|
|
1474
1490
|
];
|
|
1475
|
-
const executor = new JobExecutor(
|
|
1491
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
1476
1492
|
logger: createMockLogger(),
|
|
1477
1493
|
});
|
|
1478
1494
|
const result = await executor.execute({
|
|
@@ -1491,7 +1507,7 @@ describe("outputToFile (US-9)", () => {
|
|
|
1491
1507
|
{ type: "system", content: "Init" },
|
|
1492
1508
|
{ type: "assistant", content: "Response" },
|
|
1493
1509
|
];
|
|
1494
|
-
const executor = new JobExecutor(
|
|
1510
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
1495
1511
|
logger: createMockLogger(),
|
|
1496
1512
|
});
|
|
1497
1513
|
const result = await executor.execute({
|
|
@@ -1510,7 +1526,7 @@ describe("outputToFile (US-9)", () => {
|
|
|
1510
1526
|
const messages = [
|
|
1511
1527
|
{ type: "tool_result", result: "File not found", success: false },
|
|
1512
1528
|
];
|
|
1513
|
-
const executor = new JobExecutor(
|
|
1529
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
1514
1530
|
logger: createMockLogger(),
|
|
1515
1531
|
});
|
|
1516
1532
|
const result = await executor.execute({
|
|
@@ -1532,7 +1548,7 @@ describe("outputToFile (US-9)", () => {
|
|
|
1532
1548
|
{ type: "system", content: "Init" },
|
|
1533
1549
|
{ type: "assistant", content: "Hello" },
|
|
1534
1550
|
];
|
|
1535
|
-
const executor = new JobExecutor(
|
|
1551
|
+
const executor = new JobExecutor(createMockRuntimeWithMessages(messages), {
|
|
1536
1552
|
logger: createMockLogger(),
|
|
1537
1553
|
});
|
|
1538
1554
|
await executor.execute({
|
|
@@ -1547,4 +1563,323 @@ describe("outputToFile (US-9)", () => {
|
|
|
1547
1563
|
expect(receivedMessages).toHaveLength(2);
|
|
1548
1564
|
});
|
|
1549
1565
|
});
|
|
1566
|
+
// =============================================================================
|
|
1567
|
+
// Session expiration handling tests (fixes unexpected logout bug)
|
|
1568
|
+
// =============================================================================
|
|
1569
|
+
describe("session expiration handling", () => {
|
|
1570
|
+
let tempDir;
|
|
1571
|
+
let stateDir;
|
|
1572
|
+
beforeEach(async () => {
|
|
1573
|
+
tempDir = await createTempDir();
|
|
1574
|
+
stateDir = join(tempDir, ".herdctl");
|
|
1575
|
+
await initStateDirectory({ path: stateDir });
|
|
1576
|
+
});
|
|
1577
|
+
afterEach(async () => {
|
|
1578
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
1579
|
+
});
|
|
1580
|
+
it("starts fresh session when existing session is expired", async () => {
|
|
1581
|
+
const sessionsDir = join(stateDir, "sessions");
|
|
1582
|
+
// Create an expired session (last used 25 hours ago, default timeout is 24h)
|
|
1583
|
+
const expiredLastUsed = new Date(Date.now() - 25 * 60 * 60 * 1000).toISOString();
|
|
1584
|
+
await updateSessionInfo(sessionsDir, "expiry-test-agent", {
|
|
1585
|
+
session_id: "old-expired-session",
|
|
1586
|
+
mode: "autonomous",
|
|
1587
|
+
});
|
|
1588
|
+
// Manually update last_used_at to make it expired
|
|
1589
|
+
const { writeFile } = await import("node:fs/promises");
|
|
1590
|
+
const sessionPath = join(sessionsDir, "expiry-test-agent.json");
|
|
1591
|
+
const sessionData = {
|
|
1592
|
+
agent_name: "expiry-test-agent",
|
|
1593
|
+
session_id: "old-expired-session",
|
|
1594
|
+
created_at: expiredLastUsed,
|
|
1595
|
+
last_used_at: expiredLastUsed,
|
|
1596
|
+
job_count: 1,
|
|
1597
|
+
mode: "autonomous",
|
|
1598
|
+
};
|
|
1599
|
+
await writeFile(sessionPath, JSON.stringify(sessionData));
|
|
1600
|
+
let receivedOptions;
|
|
1601
|
+
const runtime = createMockRuntime(async function* (options) {
|
|
1602
|
+
receivedOptions = options;
|
|
1603
|
+
yield { type: "system", content: "Init", subtype: "init", session_id: "new-fresh-session" };
|
|
1604
|
+
yield { type: "assistant", content: "Started fresh" };
|
|
1605
|
+
});
|
|
1606
|
+
const executor = new JobExecutor(runtime, {
|
|
1607
|
+
logger: createMockLogger(),
|
|
1608
|
+
});
|
|
1609
|
+
const result = await executor.execute({
|
|
1610
|
+
agent: createTestAgent({ name: "expiry-test-agent" }),
|
|
1611
|
+
prompt: "Test prompt",
|
|
1612
|
+
stateDir,
|
|
1613
|
+
resume: "old-expired-session", // Try to resume expired session
|
|
1614
|
+
});
|
|
1615
|
+
expect(result.success).toBe(true);
|
|
1616
|
+
// Should NOT have passed resume option since session was expired
|
|
1617
|
+
expect(receivedOptions?.resume).toBeUndefined();
|
|
1618
|
+
// New session should be created
|
|
1619
|
+
expect(result.sessionId).toBe("new-fresh-session");
|
|
1620
|
+
});
|
|
1621
|
+
it("resumes valid session that is not expired", async () => {
|
|
1622
|
+
const sessionsDir = join(stateDir, "sessions");
|
|
1623
|
+
// Create a valid session (last used 1 hour ago, default timeout is 24h)
|
|
1624
|
+
await updateSessionInfo(sessionsDir, "valid-session-agent", {
|
|
1625
|
+
session_id: "valid-session-id",
|
|
1626
|
+
mode: "autonomous",
|
|
1627
|
+
});
|
|
1628
|
+
let receivedOptions;
|
|
1629
|
+
const runtime = createMockRuntime(async function* (options) {
|
|
1630
|
+
receivedOptions = options;
|
|
1631
|
+
yield { type: "assistant", content: "Resumed successfully" };
|
|
1632
|
+
});
|
|
1633
|
+
const executor = new JobExecutor(runtime, {
|
|
1634
|
+
logger: createMockLogger(),
|
|
1635
|
+
});
|
|
1636
|
+
await executor.execute({
|
|
1637
|
+
agent: createTestAgent({ name: "valid-session-agent" }),
|
|
1638
|
+
prompt: "Test prompt",
|
|
1639
|
+
stateDir,
|
|
1640
|
+
resume: "valid-session-id",
|
|
1641
|
+
});
|
|
1642
|
+
// Should have passed resume option since session is valid
|
|
1643
|
+
expect(receivedOptions?.resume).toBe("valid-session-id");
|
|
1644
|
+
});
|
|
1645
|
+
it("uses stored session_id from disk, not the options.resume value", async () => {
|
|
1646
|
+
const sessionsDir = join(stateDir, "sessions");
|
|
1647
|
+
// Create a valid session with a DIFFERENT session_id than what options.resume will provide
|
|
1648
|
+
// This simulates the case where the stored session has been updated but the caller
|
|
1649
|
+
// is using an outdated or generic resume value (like a boolean or old session ID)
|
|
1650
|
+
await updateSessionInfo(sessionsDir, "session-mismatch-agent", {
|
|
1651
|
+
session_id: "stored-session-abc123", // The actual session ID stored on disk
|
|
1652
|
+
mode: "autonomous",
|
|
1653
|
+
});
|
|
1654
|
+
let receivedOptions;
|
|
1655
|
+
const runtime = createMockRuntime(async function* (options) {
|
|
1656
|
+
receivedOptions = options;
|
|
1657
|
+
yield { type: "assistant", content: "Resumed with correct session" };
|
|
1658
|
+
});
|
|
1659
|
+
const executor = new JobExecutor(runtime, {
|
|
1660
|
+
logger: createMockLogger(),
|
|
1661
|
+
});
|
|
1662
|
+
await executor.execute({
|
|
1663
|
+
agent: createTestAgent({ name: "session-mismatch-agent" }),
|
|
1664
|
+
prompt: "Test prompt",
|
|
1665
|
+
stateDir,
|
|
1666
|
+
// Pass a DIFFERENT value than what's stored - this could be an old session ID
|
|
1667
|
+
// or any truthy value indicating "please resume"
|
|
1668
|
+
resume: "outdated-session-xyz789",
|
|
1669
|
+
});
|
|
1670
|
+
// CRITICAL: Should use the session_id from the stored session file, NOT the options.resume value
|
|
1671
|
+
// This is the fix for the unexpected logout bug - we must use the actual stored session ID
|
|
1672
|
+
expect(receivedOptions?.resume).toBe("stored-session-abc123");
|
|
1673
|
+
});
|
|
1674
|
+
it("respects custom session timeout from agent config", async () => {
|
|
1675
|
+
const sessionsDir = join(stateDir, "sessions");
|
|
1676
|
+
// Create a session that is 2 hours old
|
|
1677
|
+
const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString();
|
|
1678
|
+
const { writeFile } = await import("node:fs/promises");
|
|
1679
|
+
const sessionPath = join(sessionsDir, "custom-timeout-agent.json");
|
|
1680
|
+
const sessionData = {
|
|
1681
|
+
agent_name: "custom-timeout-agent",
|
|
1682
|
+
session_id: "two-hour-old-session",
|
|
1683
|
+
created_at: twoHoursAgo,
|
|
1684
|
+
last_used_at: twoHoursAgo,
|
|
1685
|
+
job_count: 1,
|
|
1686
|
+
mode: "autonomous",
|
|
1687
|
+
};
|
|
1688
|
+
await writeFile(sessionPath, JSON.stringify(sessionData));
|
|
1689
|
+
let receivedOptions;
|
|
1690
|
+
const runtime = createMockRuntime(async function* (options) {
|
|
1691
|
+
receivedOptions = options;
|
|
1692
|
+
yield { type: "assistant", content: "Done" };
|
|
1693
|
+
});
|
|
1694
|
+
const executor = new JobExecutor(runtime, {
|
|
1695
|
+
logger: createMockLogger(),
|
|
1696
|
+
});
|
|
1697
|
+
// With 1 hour timeout, 2-hour-old session should be expired
|
|
1698
|
+
await executor.execute({
|
|
1699
|
+
agent: createTestAgent({
|
|
1700
|
+
name: "custom-timeout-agent",
|
|
1701
|
+
session: { timeout: "1h" },
|
|
1702
|
+
}),
|
|
1703
|
+
prompt: "Test prompt",
|
|
1704
|
+
stateDir,
|
|
1705
|
+
resume: "two-hour-old-session",
|
|
1706
|
+
});
|
|
1707
|
+
// Should NOT have passed resume since session exceeded custom timeout
|
|
1708
|
+
expect(receivedOptions?.resume).toBeUndefined();
|
|
1709
|
+
});
|
|
1710
|
+
it("writes system message to job output when session expires", async () => {
|
|
1711
|
+
const sessionsDir = join(stateDir, "sessions");
|
|
1712
|
+
// Create an expired session
|
|
1713
|
+
const expiredLastUsed = new Date(Date.now() - 25 * 60 * 60 * 1000).toISOString();
|
|
1714
|
+
const { writeFile } = await import("node:fs/promises");
|
|
1715
|
+
const sessionPath = join(sessionsDir, "output-test-agent.json");
|
|
1716
|
+
const sessionData = {
|
|
1717
|
+
agent_name: "output-test-agent",
|
|
1718
|
+
session_id: "expired-session",
|
|
1719
|
+
created_at: expiredLastUsed,
|
|
1720
|
+
last_used_at: expiredLastUsed,
|
|
1721
|
+
job_count: 1,
|
|
1722
|
+
mode: "autonomous",
|
|
1723
|
+
};
|
|
1724
|
+
await writeFile(sessionPath, JSON.stringify(sessionData));
|
|
1725
|
+
const runtime = createMockRuntime(async function* () {
|
|
1726
|
+
yield { type: "assistant", content: "Fresh start" };
|
|
1727
|
+
});
|
|
1728
|
+
const executor = new JobExecutor(runtime, {
|
|
1729
|
+
logger: createMockLogger(),
|
|
1730
|
+
});
|
|
1731
|
+
const result = await executor.execute({
|
|
1732
|
+
agent: createTestAgent({ name: "output-test-agent" }),
|
|
1733
|
+
prompt: "Test prompt",
|
|
1734
|
+
stateDir,
|
|
1735
|
+
resume: "expired-session",
|
|
1736
|
+
});
|
|
1737
|
+
// Check job output for session expiry message
|
|
1738
|
+
const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
|
|
1739
|
+
const systemMsg = output.find((m) => m.type === "system" && m.content?.includes("session"));
|
|
1740
|
+
expect(systemMsg).toBeDefined();
|
|
1741
|
+
});
|
|
1742
|
+
it("clears expired session file when detected", async () => {
|
|
1743
|
+
const sessionsDir = join(stateDir, "sessions");
|
|
1744
|
+
// Create an expired session
|
|
1745
|
+
const expiredLastUsed = new Date(Date.now() - 25 * 60 * 60 * 1000).toISOString();
|
|
1746
|
+
const { writeFile } = await import("node:fs/promises");
|
|
1747
|
+
const sessionPath = join(sessionsDir, "clear-test-agent.json");
|
|
1748
|
+
const sessionData = {
|
|
1749
|
+
agent_name: "clear-test-agent",
|
|
1750
|
+
session_id: "expired-to-clear",
|
|
1751
|
+
created_at: expiredLastUsed,
|
|
1752
|
+
last_used_at: expiredLastUsed,
|
|
1753
|
+
job_count: 1,
|
|
1754
|
+
mode: "autonomous",
|
|
1755
|
+
};
|
|
1756
|
+
await writeFile(sessionPath, JSON.stringify(sessionData));
|
|
1757
|
+
// Verify session exists
|
|
1758
|
+
let sessionBefore = await getSessionInfo(sessionsDir, "clear-test-agent");
|
|
1759
|
+
expect(sessionBefore).not.toBeNull();
|
|
1760
|
+
const runtime = createMockRuntime(async function* () {
|
|
1761
|
+
yield { type: "system", content: "Init", subtype: "init", session_id: "new-session" };
|
|
1762
|
+
yield { type: "assistant", content: "Fresh" };
|
|
1763
|
+
});
|
|
1764
|
+
const executor = new JobExecutor(runtime, {
|
|
1765
|
+
logger: createMockLogger(),
|
|
1766
|
+
});
|
|
1767
|
+
await executor.execute({
|
|
1768
|
+
agent: createTestAgent({ name: "clear-test-agent" }),
|
|
1769
|
+
prompt: "Test prompt",
|
|
1770
|
+
stateDir,
|
|
1771
|
+
resume: "expired-to-clear",
|
|
1772
|
+
});
|
|
1773
|
+
// Session should now have the new ID (old one was cleared and new one created)
|
|
1774
|
+
const sessionAfter = await getSessionInfo(sessionsDir, "clear-test-agent");
|
|
1775
|
+
expect(sessionAfter?.session_id).toBe("new-session");
|
|
1776
|
+
});
|
|
1777
|
+
it("retries with fresh session when server-side session expiration detected", async () => {
|
|
1778
|
+
const sessionsDir = join(stateDir, "sessions");
|
|
1779
|
+
// Create a valid session (not locally expired)
|
|
1780
|
+
await updateSessionInfo(sessionsDir, "server-expiry-agent", {
|
|
1781
|
+
session_id: "valid-local-session",
|
|
1782
|
+
mode: "autonomous",
|
|
1783
|
+
});
|
|
1784
|
+
let attemptCount = 0;
|
|
1785
|
+
let lastResumeValue;
|
|
1786
|
+
// First attempt throws session expired error, second succeeds
|
|
1787
|
+
const runtime = createMockRuntime(async function* (options) {
|
|
1788
|
+
attemptCount++;
|
|
1789
|
+
lastResumeValue = options.resume;
|
|
1790
|
+
if (attemptCount === 1 && options.resume) {
|
|
1791
|
+
// First attempt with resume - server says session expired
|
|
1792
|
+
throw new Error("Session expired on server");
|
|
1793
|
+
}
|
|
1794
|
+
// Second attempt or fresh session - succeed
|
|
1795
|
+
yield { type: "system", content: "Init", subtype: "init", session_id: "new-server-session" };
|
|
1796
|
+
yield { type: "assistant", content: "Success after retry" };
|
|
1797
|
+
});
|
|
1798
|
+
const executor = new JobExecutor(runtime, {
|
|
1799
|
+
logger: createMockLogger(),
|
|
1800
|
+
});
|
|
1801
|
+
const result = await executor.execute({
|
|
1802
|
+
agent: createTestAgent({ name: "server-expiry-agent" }),
|
|
1803
|
+
prompt: "Test prompt",
|
|
1804
|
+
stateDir,
|
|
1805
|
+
resume: "valid-local-session",
|
|
1806
|
+
});
|
|
1807
|
+
// Should have succeeded after retry
|
|
1808
|
+
expect(result.success).toBe(true);
|
|
1809
|
+
expect(result.sessionId).toBe("new-server-session");
|
|
1810
|
+
// Should have attempted twice: first with resume, then fresh
|
|
1811
|
+
expect(attemptCount).toBe(2);
|
|
1812
|
+
// Second attempt should NOT have resume (fresh session)
|
|
1813
|
+
expect(lastResumeValue).toBeUndefined();
|
|
1814
|
+
// Check job output includes retry message
|
|
1815
|
+
const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
|
|
1816
|
+
const retryMsg = output.find((m) => m.type === "system" && m.content?.includes("Retrying with fresh session"));
|
|
1817
|
+
expect(retryMsg).toBeDefined();
|
|
1818
|
+
});
|
|
1819
|
+
it("does not retry infinitely on persistent server errors", async () => {
|
|
1820
|
+
const sessionsDir = join(stateDir, "sessions");
|
|
1821
|
+
await updateSessionInfo(sessionsDir, "no-infinite-retry-agent", {
|
|
1822
|
+
session_id: "some-session",
|
|
1823
|
+
mode: "autonomous",
|
|
1824
|
+
});
|
|
1825
|
+
let attemptCount = 0;
|
|
1826
|
+
// Always throw session expired error
|
|
1827
|
+
const runtime = createMockRuntime(async function* (options) {
|
|
1828
|
+
attemptCount++;
|
|
1829
|
+
throw new Error("Session expired on server");
|
|
1830
|
+
});
|
|
1831
|
+
const executor = new JobExecutor(runtime, {
|
|
1832
|
+
logger: createMockLogger(),
|
|
1833
|
+
});
|
|
1834
|
+
const result = await executor.execute({
|
|
1835
|
+
agent: createTestAgent({ name: "no-infinite-retry-agent" }),
|
|
1836
|
+
prompt: "Test prompt",
|
|
1837
|
+
stateDir,
|
|
1838
|
+
resume: "some-session",
|
|
1839
|
+
});
|
|
1840
|
+
// Should fail after at most 2 attempts (initial + 1 retry)
|
|
1841
|
+
expect(result.success).toBe(false);
|
|
1842
|
+
expect(attemptCount).toBeLessThanOrEqual(2);
|
|
1843
|
+
expect(result.error?.message).toContain("Session expired");
|
|
1844
|
+
});
|
|
1845
|
+
it("updates last_used_at before execution to prevent mid-job session expiry", async () => {
|
|
1846
|
+
const sessionsDir = join(stateDir, "sessions");
|
|
1847
|
+
// Create a session that is 23 hours old (close to default 24h expiry)
|
|
1848
|
+
const twentyThreeHoursAgo = new Date(Date.now() - 23 * 60 * 60 * 1000).toISOString();
|
|
1849
|
+
const { writeFile } = await import("node:fs/promises");
|
|
1850
|
+
const sessionPath = join(sessionsDir, "refresh-test-agent.json");
|
|
1851
|
+
const sessionData = {
|
|
1852
|
+
agent_name: "refresh-test-agent",
|
|
1853
|
+
session_id: "almost-expired-session",
|
|
1854
|
+
created_at: twentyThreeHoursAgo,
|
|
1855
|
+
last_used_at: twentyThreeHoursAgo,
|
|
1856
|
+
job_count: 5,
|
|
1857
|
+
mode: "autonomous",
|
|
1858
|
+
};
|
|
1859
|
+
await writeFile(sessionPath, JSON.stringify(sessionData));
|
|
1860
|
+
// Record when the test starts
|
|
1861
|
+
const testStartTime = Date.now();
|
|
1862
|
+
const runtime = createMockRuntime(async function* () {
|
|
1863
|
+
// Simulate a delay to ensure last_used_at was updated BEFORE we got here
|
|
1864
|
+
yield { type: "assistant", content: "Working..." };
|
|
1865
|
+
});
|
|
1866
|
+
const executor = new JobExecutor(runtime, {
|
|
1867
|
+
logger: createMockLogger(),
|
|
1868
|
+
});
|
|
1869
|
+
await executor.execute({
|
|
1870
|
+
agent: createTestAgent({ name: "refresh-test-agent" }),
|
|
1871
|
+
prompt: "Test prompt",
|
|
1872
|
+
stateDir,
|
|
1873
|
+
resume: "almost-expired-session",
|
|
1874
|
+
});
|
|
1875
|
+
// Check that last_used_at was updated to a recent time (not 23 hours ago)
|
|
1876
|
+
const sessionAfter = await getSessionInfo(sessionsDir, "refresh-test-agent");
|
|
1877
|
+
expect(sessionAfter).not.toBeNull();
|
|
1878
|
+
const lastUsedMs = new Date(sessionAfter.last_used_at).getTime();
|
|
1879
|
+
// Should be updated to approximately now (within last few seconds)
|
|
1880
|
+
expect(lastUsedMs).toBeGreaterThanOrEqual(testStartTime - 1000);
|
|
1881
|
+
// And not still 23 hours ago
|
|
1882
|
+
expect(lastUsedMs).toBeGreaterThan(new Date(twentyThreeHoursAgo).getTime());
|
|
1883
|
+
});
|
|
1884
|
+
});
|
|
1550
1885
|
//# sourceMappingURL=job-executor.test.js.map
|