runtimeuse 0.2.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/.env.example +4 -0
- package/README.md +222 -0
- package/dist/agent-handler.d.ts +26 -0
- package/dist/agent-handler.d.ts.map +1 -0
- package/dist/agent-handler.js +2 -0
- package/dist/agent-handler.js.map +1 -0
- package/dist/artifact-manager.d.ts +27 -0
- package/dist/artifact-manager.d.ts.map +1 -0
- package/dist/artifact-manager.js +125 -0
- package/dist/artifact-manager.js.map +1 -0
- package/dist/artifact-manager.test.d.ts +2 -0
- package/dist/artifact-manager.test.d.ts.map +1 -0
- package/dist/artifact-manager.test.js +251 -0
- package/dist/artifact-manager.test.js.map +1 -0
- package/dist/claude-handler.d.ts +3 -0
- package/dist/claude-handler.d.ts.map +1 -0
- package/dist/claude-handler.js +76 -0
- package/dist/claude-handler.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +87 -0
- package/dist/cli.js.map +1 -0
- package/dist/command-handler.d.ts +22 -0
- package/dist/command-handler.d.ts.map +1 -0
- package/dist/command-handler.js +75 -0
- package/dist/command-handler.js.map +1 -0
- package/dist/command-handler.test.d.ts +2 -0
- package/dist/command-handler.test.d.ts.map +1 -0
- package/dist/command-handler.test.js +267 -0
- package/dist/command-handler.test.js.map +1 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +13 -0
- package/dist/constants.js.map +1 -0
- package/dist/default-handler.d.ts +3 -0
- package/dist/default-handler.d.ts.map +1 -0
- package/dist/default-handler.js +76 -0
- package/dist/default-handler.js.map +1 -0
- package/dist/download-handler.d.ts +8 -0
- package/dist/download-handler.d.ts.map +1 -0
- package/dist/download-handler.js +36 -0
- package/dist/download-handler.js.map +1 -0
- package/dist/download-handler.test.d.ts +2 -0
- package/dist/download-handler.test.d.ts.map +1 -0
- package/dist/download-handler.test.js +123 -0
- package/dist/download-handler.test.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +8 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +14 -0
- package/dist/logger.js.map +1 -0
- package/dist/openai-handler.d.ts +3 -0
- package/dist/openai-handler.d.ts.map +1 -0
- package/dist/openai-handler.js +86 -0
- package/dist/openai-handler.js.map +1 -0
- package/dist/server.d.ts +21 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +52 -0
- package/dist/server.js.map +1 -0
- package/dist/session.d.ts +29 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +244 -0
- package/dist/session.js.map +1 -0
- package/dist/session.test.d.ts +2 -0
- package/dist/session.test.d.ts.map +1 -0
- package/dist/session.test.js +339 -0
- package/dist/session.test.js.map +1 -0
- package/dist/storage.d.ts +3 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +21 -0
- package/dist/storage.js.map +1 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/upload-tracker.d.ts +10 -0
- package/dist/upload-tracker.d.ts.map +1 -0
- package/dist/upload-tracker.js +27 -0
- package/dist/upload-tracker.js.map +1 -0
- package/dist/upload-tracker.test.d.ts +2 -0
- package/dist/upload-tracker.test.d.ts.map +1 -0
- package/dist/upload-tracker.test.js +89 -0
- package/dist/upload-tracker.test.js.map +1 -0
- package/dist/utils.d.ts +7 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +32 -0
- package/dist/utils.js.map +1 -0
- package/dist/utils.test.d.ts +2 -0
- package/dist/utils.test.d.ts.map +1 -0
- package/dist/utils.test.js +92 -0
- package/dist/utils.test.js.map +1 -0
- package/package.json +40 -0
- package/scripts/dev-publish.sh +45 -0
- package/src/agent-handler.ts +26 -0
- package/src/artifact-manager.test.ts +320 -0
- package/src/artifact-manager.ts +170 -0
- package/src/claude-handler.ts +95 -0
- package/src/cli.ts +107 -0
- package/src/command-handler.test.ts +507 -0
- package/src/command-handler.ts +102 -0
- package/src/constants.ts +12 -0
- package/src/download-handler.test.ts +183 -0
- package/src/download-handler.ts +45 -0
- package/src/index.ts +59 -0
- package/src/logger.ts +20 -0
- package/src/openai-handler.ts +120 -0
- package/src/server.ts +68 -0
- package/src/session.test.ts +448 -0
- package/src/session.ts +319 -0
- package/src/storage.ts +28 -0
- package/src/types.ts +101 -0
- package/src/upload-tracker.test.ts +112 -0
- package/src/upload-tracker.ts +30 -0
- package/src/utils.test.ts +120 -0
- package/src/utils.ts +35 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { EventEmitter } from "events";
|
|
3
|
+
import { WebSocket } from "ws";
|
|
4
|
+
|
|
5
|
+
const mockArtifactManager = {
|
|
6
|
+
setLogger: vi.fn(),
|
|
7
|
+
handleUploadResponse: vi.fn().mockResolvedValue(undefined),
|
|
8
|
+
waitForPendingRequests: vi.fn().mockResolvedValue(undefined),
|
|
9
|
+
stopWatching: vi.fn().mockResolvedValue(undefined),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
vi.mock("./artifact-manager.js", () => ({
|
|
13
|
+
ArtifactManager: vi.fn().mockImplementation(function () {
|
|
14
|
+
return mockArtifactManager;
|
|
15
|
+
}),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
const mockDownloadHandler = {
|
|
19
|
+
download: vi.fn().mockResolvedValue(undefined),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
vi.mock("./download-handler.js", () => ({
|
|
23
|
+
default: vi.fn().mockImplementation(function () {
|
|
24
|
+
return mockDownloadHandler;
|
|
25
|
+
}),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
const mockCommandExecute = vi.fn();
|
|
29
|
+
|
|
30
|
+
vi.mock("./command-handler.js", () => ({
|
|
31
|
+
default: vi.fn().mockImplementation(function () {
|
|
32
|
+
return { execute: mockCommandExecute };
|
|
33
|
+
}),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
vi.mock("fs", async () => {
|
|
37
|
+
const actual = await vi.importActual<typeof import("fs")>("fs");
|
|
38
|
+
return {
|
|
39
|
+
...actual,
|
|
40
|
+
default: {
|
|
41
|
+
...actual,
|
|
42
|
+
createWriteStream: vi.fn(() => ({ write: vi.fn(), end: vi.fn(), close: vi.fn() })),
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
import { WebSocketSession, type SessionConfig } from "./session.js";
|
|
48
|
+
import { UploadTracker } from "./upload-tracker.js";
|
|
49
|
+
import type { AgentHandler, AgentInvocation, AgentResult, MessageSender } from "./agent-handler.js";
|
|
50
|
+
import CommandHandler from "./command-handler.js";
|
|
51
|
+
|
|
52
|
+
const mockHandlerRun = vi.fn<(invocation: AgentInvocation, sender: MessageSender) => Promise<AgentResult>>();
|
|
53
|
+
|
|
54
|
+
const mockHandler: AgentHandler = {
|
|
55
|
+
run: mockHandlerRun,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
class MockWebSocket extends EventEmitter {
|
|
59
|
+
readyState: number = WebSocket.OPEN;
|
|
60
|
+
send = vi.fn();
|
|
61
|
+
close = vi.fn(() => {
|
|
62
|
+
if (this.readyState !== WebSocket.CLOSED) {
|
|
63
|
+
this.readyState = WebSocket.CLOSED;
|
|
64
|
+
this.emit("close");
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function createSession() {
|
|
70
|
+
const ws = new MockWebSocket();
|
|
71
|
+
const uploadTracker = new UploadTracker();
|
|
72
|
+
const config: SessionConfig = {
|
|
73
|
+
handler: mockHandler,
|
|
74
|
+
uploadTracker,
|
|
75
|
+
};
|
|
76
|
+
const session = new WebSocketSession(ws as any, config);
|
|
77
|
+
return { session, ws, config, uploadTracker };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function sendMessage(ws: MockWebSocket, message: unknown) {
|
|
81
|
+
ws.emit("message", Buffer.from(JSON.stringify(message)));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const INVOCATION_MSG = {
|
|
85
|
+
message_type: "invocation_message" as const,
|
|
86
|
+
source_id: "test-source-id",
|
|
87
|
+
system_prompt: "You are a tester",
|
|
88
|
+
user_prompt: "Test the login flow",
|
|
89
|
+
secrets_to_redact: ["secret123"],
|
|
90
|
+
output_format_json_schema_str: JSON.stringify({
|
|
91
|
+
type: "json_schema",
|
|
92
|
+
schema: { type: "object" },
|
|
93
|
+
}),
|
|
94
|
+
preferred_model: "test-model",
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
describe("WebSocketSession", () => {
|
|
98
|
+
beforeEach(() => {
|
|
99
|
+
vi.clearAllMocks();
|
|
100
|
+
|
|
101
|
+
mockHandlerRun.mockResolvedValue({
|
|
102
|
+
structuredOutput: { success: true },
|
|
103
|
+
metadata: { duration_ms: 1000 },
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
mockArtifactManager.handleUploadResponse.mockResolvedValue(undefined);
|
|
107
|
+
mockArtifactManager.waitForPendingRequests.mockResolvedValue(undefined);
|
|
108
|
+
mockArtifactManager.stopWatching.mockResolvedValue(undefined);
|
|
109
|
+
mockDownloadHandler.download.mockResolvedValue(undefined);
|
|
110
|
+
mockCommandExecute.mockResolvedValue({ exitCode: 0 });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("lifecycle", () => {
|
|
114
|
+
it("resolves when invocation finishes", async () => {
|
|
115
|
+
const { session, ws } = createSession();
|
|
116
|
+
const done = session.run();
|
|
117
|
+
sendMessage(ws, INVOCATION_MSG);
|
|
118
|
+
await done;
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("message routing", () => {
|
|
123
|
+
it("rejects non-invocation messages before invocation", async () => {
|
|
124
|
+
const { session, ws } = createSession();
|
|
125
|
+
session.run();
|
|
126
|
+
|
|
127
|
+
sendMessage(ws, { message_type: "cancel_message" });
|
|
128
|
+
await tick();
|
|
129
|
+
|
|
130
|
+
expectSentError(ws, "non-invocation message before invocation");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("rejects duplicate invocation messages", async () => {
|
|
134
|
+
let resolveAgent!: () => void;
|
|
135
|
+
mockHandlerRun.mockImplementation(
|
|
136
|
+
() =>
|
|
137
|
+
new Promise((r) => {
|
|
138
|
+
resolveAgent = () =>
|
|
139
|
+
r({ structuredOutput: { success: true } });
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const { session, ws } = createSession();
|
|
144
|
+
session.run();
|
|
145
|
+
|
|
146
|
+
sendMessage(ws, INVOCATION_MSG);
|
|
147
|
+
await tick();
|
|
148
|
+
sendMessage(ws, INVOCATION_MSG);
|
|
149
|
+
await tick();
|
|
150
|
+
|
|
151
|
+
expectSentError(ws, "multiple invocation messages");
|
|
152
|
+
|
|
153
|
+
resolveAgent();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("delegates artifact upload responses to ArtifactManager", async () => {
|
|
157
|
+
let resolveAgent!: () => void;
|
|
158
|
+
mockHandlerRun.mockImplementation(
|
|
159
|
+
() =>
|
|
160
|
+
new Promise((r) => {
|
|
161
|
+
resolveAgent = () =>
|
|
162
|
+
r({ structuredOutput: { success: true } });
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const { session, ws } = createSession();
|
|
167
|
+
const done = session.run();
|
|
168
|
+
|
|
169
|
+
sendMessage(ws, INVOCATION_MSG);
|
|
170
|
+
await tick();
|
|
171
|
+
|
|
172
|
+
const uploadResponse = {
|
|
173
|
+
message_type: "artifact_upload_response_message",
|
|
174
|
+
filename: "shot.png",
|
|
175
|
+
filepath: "/artifacts/shot.png",
|
|
176
|
+
presigned_url: "https://s3.example.com/upload",
|
|
177
|
+
};
|
|
178
|
+
sendMessage(ws, uploadResponse);
|
|
179
|
+
await tick();
|
|
180
|
+
|
|
181
|
+
expect(mockArtifactManager.handleUploadResponse).toHaveBeenCalledWith(
|
|
182
|
+
uploadResponse,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
resolveAgent();
|
|
186
|
+
await done;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("aborts and closes on cancel message", async () => {
|
|
190
|
+
let resolveAgent!: () => void;
|
|
191
|
+
mockHandlerRun.mockImplementation(
|
|
192
|
+
() =>
|
|
193
|
+
new Promise((r) => {
|
|
194
|
+
resolveAgent = () => r({ structuredOutput: {} });
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const { session, ws } = createSession();
|
|
199
|
+
const done = session.run();
|
|
200
|
+
|
|
201
|
+
sendMessage(ws, INVOCATION_MSG);
|
|
202
|
+
await tick();
|
|
203
|
+
|
|
204
|
+
sendMessage(ws, { message_type: "cancel_message" });
|
|
205
|
+
await tick();
|
|
206
|
+
|
|
207
|
+
expect(ws.close).toHaveBeenCalled();
|
|
208
|
+
|
|
209
|
+
resolveAgent();
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("invocation", () => {
|
|
214
|
+
it("calls handler.run with correct arguments", async () => {
|
|
215
|
+
const { session, ws } = createSession();
|
|
216
|
+
const done = session.run();
|
|
217
|
+
sendMessage(ws, INVOCATION_MSG);
|
|
218
|
+
await done;
|
|
219
|
+
|
|
220
|
+
expect(mockHandlerRun).toHaveBeenCalledWith(
|
|
221
|
+
expect.objectContaining({
|
|
222
|
+
systemPrompt: "You are a tester",
|
|
223
|
+
userPrompt: "Test the login flow",
|
|
224
|
+
outputFormat: { type: "json_schema", schema: { type: "object" } },
|
|
225
|
+
model: "test-model",
|
|
226
|
+
secrets: ["secret123"],
|
|
227
|
+
}),
|
|
228
|
+
expect.objectContaining({
|
|
229
|
+
sendAssistantMessage: expect.any(Function),
|
|
230
|
+
sendErrorMessage: expect.any(Function),
|
|
231
|
+
}),
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("sends result_message from handler result", async () => {
|
|
236
|
+
mockHandlerRun.mockResolvedValue({
|
|
237
|
+
structuredOutput: { success: true, steps: ["step1"] },
|
|
238
|
+
metadata: { duration_ms: 1000 },
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const { session, ws } = createSession();
|
|
242
|
+
const done = session.run();
|
|
243
|
+
sendMessage(ws, INVOCATION_MSG);
|
|
244
|
+
await done;
|
|
245
|
+
|
|
246
|
+
const sent = parseSentMessages(ws);
|
|
247
|
+
const result = sent.find((m) => m.message_type === "result_message");
|
|
248
|
+
expect(result).toBeDefined();
|
|
249
|
+
expect(result!.structured_output.success).toBe(true);
|
|
250
|
+
expect(result!.metadata).toMatchObject({ duration_ms: 1000 });
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("sends error message when agent throws", async () => {
|
|
254
|
+
mockHandlerRun.mockRejectedValueOnce(
|
|
255
|
+
new Error("agent crashed"),
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const { session, ws } = createSession();
|
|
259
|
+
const done = session.run();
|
|
260
|
+
sendMessage(ws, INVOCATION_MSG);
|
|
261
|
+
await done;
|
|
262
|
+
|
|
263
|
+
expectSentError(ws, "agent crashed");
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe("finalization", () => {
|
|
268
|
+
it("stops the artifact watcher", async () => {
|
|
269
|
+
const { session, ws } = createSession();
|
|
270
|
+
const done = session.run();
|
|
271
|
+
sendMessage(ws, INVOCATION_MSG);
|
|
272
|
+
await done;
|
|
273
|
+
|
|
274
|
+
expect(mockArtifactManager.stopWatching).toHaveBeenCalled();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("waits for pending artifact requests", async () => {
|
|
278
|
+
const { session, ws } = createSession();
|
|
279
|
+
const done = session.run();
|
|
280
|
+
sendMessage(ws, INVOCATION_MSG);
|
|
281
|
+
await done;
|
|
282
|
+
|
|
283
|
+
expect(mockArtifactManager.waitForPendingRequests).toHaveBeenCalledWith(
|
|
284
|
+
60_000,
|
|
285
|
+
);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("closes the websocket after finalization", async () => {
|
|
289
|
+
const { session, ws } = createSession();
|
|
290
|
+
const done = session.run();
|
|
291
|
+
sendMessage(ws, INVOCATION_MSG);
|
|
292
|
+
await done;
|
|
293
|
+
|
|
294
|
+
expect(ws.close).toHaveBeenCalled();
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe("runtime environment downloadables", () => {
|
|
299
|
+
it("downloads all runtime environment downloadables before running agent", async () => {
|
|
300
|
+
const { session, ws } = createSession();
|
|
301
|
+
const done = session.run();
|
|
302
|
+
sendMessage(ws, {
|
|
303
|
+
...INVOCATION_MSG,
|
|
304
|
+
runtime_environment_downloadables: [
|
|
305
|
+
{
|
|
306
|
+
download_url: "https://example.com/test.zip",
|
|
307
|
+
working_dir: "/tmp/test",
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
download_url: "https://example.com/data.tar.gz",
|
|
311
|
+
working_dir: "/tmp/data",
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
});
|
|
315
|
+
await done;
|
|
316
|
+
|
|
317
|
+
expect(mockDownloadHandler.download).toHaveBeenCalledTimes(2);
|
|
318
|
+
expect(mockDownloadHandler.download).toHaveBeenCalledWith(
|
|
319
|
+
"https://example.com/test.zip",
|
|
320
|
+
"/tmp/test",
|
|
321
|
+
);
|
|
322
|
+
expect(mockDownloadHandler.download).toHaveBeenCalledWith(
|
|
323
|
+
"https://example.com/data.tar.gz",
|
|
324
|
+
"/tmp/data",
|
|
325
|
+
);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("does not download when no downloadables are provided", async () => {
|
|
329
|
+
const { session, ws } = createSession();
|
|
330
|
+
const done = session.run();
|
|
331
|
+
sendMessage(ws, INVOCATION_MSG);
|
|
332
|
+
await done;
|
|
333
|
+
|
|
334
|
+
expect(mockDownloadHandler.download).not.toHaveBeenCalled();
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe("pre-agent invocation commands", () => {
|
|
339
|
+
it("executes pre-agent commands and sends success result", async () => {
|
|
340
|
+
mockCommandExecute.mockResolvedValueOnce({ exitCode: 0 });
|
|
341
|
+
|
|
342
|
+
const { session, ws } = createSession();
|
|
343
|
+
const done = session.run();
|
|
344
|
+
sendMessage(ws, {
|
|
345
|
+
...INVOCATION_MSG,
|
|
346
|
+
pre_agent_invocation_commands: [
|
|
347
|
+
{ command: "npm test", cwd: "/app", env: { CI: "true" } },
|
|
348
|
+
],
|
|
349
|
+
});
|
|
350
|
+
await done;
|
|
351
|
+
|
|
352
|
+
expect(CommandHandler).toHaveBeenCalledWith(
|
|
353
|
+
{ command: "npm test", cwd: "/app", env: { CI: "true" } },
|
|
354
|
+
expect.any(Object),
|
|
355
|
+
expect.any(Object),
|
|
356
|
+
expect.any(Object),
|
|
357
|
+
expect.any(AbortController),
|
|
358
|
+
expect.any(Function),
|
|
359
|
+
expect.any(Function),
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
const sent = parseSentMessages(ws);
|
|
363
|
+
const result = sent.find((m) => m.message_type === "result_message");
|
|
364
|
+
expect(result).toBeDefined();
|
|
365
|
+
expect(result!.structured_output.success).toBe(true);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("does not call handler.run when pre-agent command succeeds", async () => {
|
|
369
|
+
mockCommandExecute.mockResolvedValueOnce({ exitCode: 0 });
|
|
370
|
+
|
|
371
|
+
const { session, ws } = createSession();
|
|
372
|
+
const done = session.run();
|
|
373
|
+
sendMessage(ws, {
|
|
374
|
+
...INVOCATION_MSG,
|
|
375
|
+
pre_agent_invocation_commands: [{ command: "npm test", cwd: "/app" }],
|
|
376
|
+
});
|
|
377
|
+
await done;
|
|
378
|
+
|
|
379
|
+
expect(mockHandlerRun).not.toHaveBeenCalled();
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it("sends failure result when pre-agent command exits with non-zero code", async () => {
|
|
383
|
+
mockCommandExecute.mockResolvedValueOnce({
|
|
384
|
+
exitCode: 1,
|
|
385
|
+
error: new Error("test failed"),
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
const { session, ws } = createSession();
|
|
389
|
+
const done = session.run();
|
|
390
|
+
sendMessage(ws, {
|
|
391
|
+
...INVOCATION_MSG,
|
|
392
|
+
pre_agent_invocation_commands: [{ command: "npm test", cwd: "/app" }],
|
|
393
|
+
});
|
|
394
|
+
await done;
|
|
395
|
+
|
|
396
|
+
const sent = parseSentMessages(ws);
|
|
397
|
+
const result = sent.find((m) => m.message_type === "result_message");
|
|
398
|
+
expect(result).toBeDefined();
|
|
399
|
+
expect(result!.structured_output.success).toBe(false);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("sends error and result messages when command execution throws", async () => {
|
|
403
|
+
mockCommandExecute.mockRejectedValueOnce(new Error("spawn failed"));
|
|
404
|
+
|
|
405
|
+
const { session, ws } = createSession();
|
|
406
|
+
const done = session.run();
|
|
407
|
+
sendMessage(ws, {
|
|
408
|
+
...INVOCATION_MSG,
|
|
409
|
+
pre_agent_invocation_commands: [{ command: "bad-cmd" }],
|
|
410
|
+
});
|
|
411
|
+
await done;
|
|
412
|
+
|
|
413
|
+
const sent = parseSentMessages(ws);
|
|
414
|
+
const errors = sent.filter((m) => m.message_type === "error_message");
|
|
415
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
416
|
+
|
|
417
|
+
const result = sent.find((m) => m.message_type === "result_message");
|
|
418
|
+
expect(result).toBeDefined();
|
|
419
|
+
expect(result!.structured_output.success).toBe(false);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it("runs agent normally when no pre-agent commands are provided", async () => {
|
|
423
|
+
const { session, ws } = createSession();
|
|
424
|
+
const done = session.run();
|
|
425
|
+
sendMessage(ws, INVOCATION_MSG);
|
|
426
|
+
await done;
|
|
427
|
+
|
|
428
|
+
expect(CommandHandler).not.toHaveBeenCalled();
|
|
429
|
+
expect(mockHandlerRun).toHaveBeenCalled();
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
async function tick() {
|
|
435
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function parseSentMessages(ws: MockWebSocket): Record<string, any>[] {
|
|
439
|
+
return ws.send.mock.calls.map((c: unknown[]) => JSON.parse(c[0] as string));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function expectSentError(ws: MockWebSocket, substring: string) {
|
|
443
|
+
const sent = parseSentMessages(ws);
|
|
444
|
+
const errors = sent.filter((m) => m.message_type === "error_message");
|
|
445
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
446
|
+
const match = errors.some((m) => m.error.includes(substring));
|
|
447
|
+
expect(match).toBe(true);
|
|
448
|
+
}
|