@desplega.ai/agent-swarm 1.10.0 → 1.10.3

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.
@@ -0,0 +1,388 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import { createServer as createHttpServer, type Server } from "node:http";
4
+ import {
5
+ closeDb,
6
+ createSessionLogs,
7
+ createTaskExtended,
8
+ getSessionLogsBySession,
9
+ getSessionLogsByTaskId,
10
+ getTaskById,
11
+ initDb,
12
+ } from "../be/db";
13
+
14
+ const TEST_DB_PATH = "./test-session-logs.sqlite";
15
+ const TEST_PORT = 13014;
16
+
17
+ // Helper to parse path segments
18
+ function getPathSegments(url: string): string[] {
19
+ const pathEnd = url.indexOf("?");
20
+ const path = pathEnd === -1 ? url : url.slice(0, pathEnd);
21
+ return path.split("/").filter(Boolean);
22
+ }
23
+
24
+ // Minimal HTTP handler for session logs endpoints
25
+ async function handleRequest(
26
+ req: { method: string; url: string },
27
+ body: string,
28
+ ): Promise<{ status: number; body: unknown }> {
29
+ const pathSegments = getPathSegments(req.url || "");
30
+
31
+ // POST /api/session-logs - Store session logs (batch)
32
+ if (req.method === "POST" && pathSegments[0] === "api" && pathSegments[1] === "session-logs") {
33
+ const parsedBody = JSON.parse(body);
34
+
35
+ // Validate required fields
36
+ if (!parsedBody.sessionId || typeof parsedBody.sessionId !== "string") {
37
+ return { status: 400, body: { error: "Missing or invalid 'sessionId' field" } };
38
+ }
39
+
40
+ if (typeof parsedBody.iteration !== "number" || parsedBody.iteration < 1) {
41
+ return { status: 400, body: { error: "Missing or invalid 'iteration' field" } };
42
+ }
43
+
44
+ if (!Array.isArray(parsedBody.lines) || parsedBody.lines.length === 0) {
45
+ return { status: 400, body: { error: "Missing or invalid 'lines' array" } };
46
+ }
47
+
48
+ try {
49
+ createSessionLogs({
50
+ taskId: parsedBody.taskId || undefined,
51
+ sessionId: parsedBody.sessionId,
52
+ iteration: parsedBody.iteration,
53
+ cli: parsedBody.cli || "claude",
54
+ lines: parsedBody.lines,
55
+ });
56
+
57
+ return { status: 201, body: { success: true, count: parsedBody.lines.length } };
58
+ } catch (error) {
59
+ console.error("[TEST] Failed to create session logs:", error);
60
+ return { status: 500, body: { error: "Failed to store session logs" } };
61
+ }
62
+ }
63
+
64
+ // GET /api/tasks/:id/session-logs - Get session logs for a task
65
+ if (
66
+ req.method === "GET" &&
67
+ pathSegments[0] === "api" &&
68
+ pathSegments[1] === "tasks" &&
69
+ pathSegments[2] &&
70
+ pathSegments[3] === "session-logs"
71
+ ) {
72
+ const taskId = pathSegments[2];
73
+ const task = getTaskById(taskId);
74
+
75
+ if (!task) {
76
+ return { status: 404, body: { error: "Task not found" } };
77
+ }
78
+
79
+ const logs = getSessionLogsByTaskId(taskId);
80
+ return { status: 200, body: { logs } };
81
+ }
82
+
83
+ return { status: 404, body: { error: "Not found" } };
84
+ }
85
+
86
+ // Create test HTTP server
87
+ function createTestServer(): Server {
88
+ return createHttpServer(async (req, res) => {
89
+ res.setHeader("Content-Type", "application/json");
90
+
91
+ const chunks: Buffer[] = [];
92
+ for await (const chunk of req) {
93
+ chunks.push(chunk);
94
+ }
95
+ const body = Buffer.concat(chunks).toString();
96
+
97
+ const result = await handleRequest({ method: req.method || "GET", url: req.url || "/" }, body);
98
+
99
+ res.writeHead(result.status);
100
+ res.end(JSON.stringify(result.body));
101
+ });
102
+ }
103
+
104
+ describe("Session Logs API", () => {
105
+ let server: Server;
106
+ const baseUrl = `http://localhost:${TEST_PORT}`;
107
+
108
+ beforeAll(async () => {
109
+ // Clean up any existing test database
110
+ try {
111
+ await unlink(TEST_DB_PATH);
112
+ } catch {
113
+ // File doesn't exist, that's fine
114
+ }
115
+
116
+ // Initialize test database
117
+ initDb(TEST_DB_PATH);
118
+
119
+ // Start test server
120
+ server = createTestServer();
121
+ await new Promise<void>((resolve) => {
122
+ server.listen(TEST_PORT, () => {
123
+ console.log(`Test server listening on port ${TEST_PORT}`);
124
+ resolve();
125
+ });
126
+ });
127
+ });
128
+
129
+ afterAll(async () => {
130
+ // Close server
131
+ await new Promise<void>((resolve) => {
132
+ server.close(() => resolve());
133
+ });
134
+
135
+ // Close database
136
+ closeDb();
137
+
138
+ // Clean up test database file
139
+ try {
140
+ await unlink(TEST_DB_PATH);
141
+ await unlink(`${TEST_DB_PATH}-wal`);
142
+ await unlink(`${TEST_DB_PATH}-shm`);
143
+ } catch {
144
+ // Files may not exist
145
+ }
146
+ });
147
+
148
+ describe("Database Functions", () => {
149
+ test("should create and retrieve session logs by taskId", () => {
150
+ // Create a task first
151
+ const task = createTaskExtended("Test task for session logs");
152
+
153
+ // Create session logs for the task
154
+ createSessionLogs({
155
+ taskId: task.id,
156
+ sessionId: "test-session-1",
157
+ iteration: 1,
158
+ cli: "claude",
159
+ lines: ['{"type":"system"}', '{"type":"assistant","message":"Hello"}'],
160
+ });
161
+
162
+ // Retrieve logs
163
+ const logs = getSessionLogsByTaskId(task.id);
164
+
165
+ expect(logs.length).toBe(2);
166
+ expect(logs[0]?.content).toBe('{"type":"system"}');
167
+ expect(logs[1]?.content).toBe('{"type":"assistant","message":"Hello"}');
168
+ expect(logs[0]?.taskId).toBe(task.id);
169
+ expect(logs[0]?.sessionId).toBe("test-session-1");
170
+ expect(logs[0]?.iteration).toBe(1);
171
+ expect(logs[0]?.cli).toBe("claude");
172
+ expect(logs[0]?.lineNumber).toBe(0);
173
+ expect(logs[1]?.lineNumber).toBe(1);
174
+ });
175
+
176
+ test("should create session logs without taskId", () => {
177
+ createSessionLogs({
178
+ sessionId: "ai-loop-session",
179
+ iteration: 1,
180
+ cli: "claude",
181
+ lines: ['{"type":"system","subtype":"init"}'],
182
+ });
183
+
184
+ // Retrieve by session
185
+ const logs = getSessionLogsBySession("ai-loop-session", 1);
186
+
187
+ expect(logs.length).toBe(1);
188
+ expect(logs[0]?.taskId).toBeUndefined();
189
+ expect(logs[0]?.sessionId).toBe("ai-loop-session");
190
+ });
191
+
192
+ test("should order logs by iteration and lineNumber", () => {
193
+ const task = createTaskExtended("Task for ordering test");
194
+
195
+ // Create logs for multiple iterations
196
+ createSessionLogs({
197
+ taskId: task.id,
198
+ sessionId: "order-session",
199
+ iteration: 1,
200
+ cli: "claude",
201
+ lines: ["line1-iter1", "line2-iter1"],
202
+ });
203
+
204
+ createSessionLogs({
205
+ taskId: task.id,
206
+ sessionId: "order-session",
207
+ iteration: 2,
208
+ cli: "claude",
209
+ lines: ["line1-iter2", "line2-iter2"],
210
+ });
211
+
212
+ const logs = getSessionLogsByTaskId(task.id);
213
+
214
+ expect(logs.length).toBe(4);
215
+ // First iteration, first line
216
+ expect(logs[0]?.content).toBe("line1-iter1");
217
+ expect(logs[0]?.iteration).toBe(1);
218
+ expect(logs[0]?.lineNumber).toBe(0);
219
+ // First iteration, second line
220
+ expect(logs[1]?.content).toBe("line2-iter1");
221
+ expect(logs[1]?.iteration).toBe(1);
222
+ expect(logs[1]?.lineNumber).toBe(1);
223
+ // Second iteration, first line
224
+ expect(logs[2]?.content).toBe("line1-iter2");
225
+ expect(logs[2]?.iteration).toBe(2);
226
+ expect(logs[2]?.lineNumber).toBe(0);
227
+ // Second iteration, second line
228
+ expect(logs[3]?.content).toBe("line2-iter2");
229
+ expect(logs[3]?.iteration).toBe(2);
230
+ expect(logs[3]?.lineNumber).toBe(1);
231
+ });
232
+ });
233
+
234
+ describe("POST /api/session-logs", () => {
235
+ test("should return 400 if sessionId is missing", async () => {
236
+ const response = await fetch(`${baseUrl}/api/session-logs`, {
237
+ method: "POST",
238
+ headers: { "Content-Type": "application/json" },
239
+ body: JSON.stringify({ iteration: 1, lines: ["test"] }),
240
+ });
241
+
242
+ expect(response.status).toBe(400);
243
+ const data = (await response.json()) as { error: string };
244
+ expect(data.error).toContain("sessionId");
245
+ });
246
+
247
+ test("should return 400 if iteration is missing", async () => {
248
+ const response = await fetch(`${baseUrl}/api/session-logs`, {
249
+ method: "POST",
250
+ headers: { "Content-Type": "application/json" },
251
+ body: JSON.stringify({ sessionId: "test", lines: ["test"] }),
252
+ });
253
+
254
+ expect(response.status).toBe(400);
255
+ const data = (await response.json()) as { error: string };
256
+ expect(data.error).toContain("iteration");
257
+ });
258
+
259
+ test("should return 400 if iteration is less than 1", async () => {
260
+ const response = await fetch(`${baseUrl}/api/session-logs`, {
261
+ method: "POST",
262
+ headers: { "Content-Type": "application/json" },
263
+ body: JSON.stringify({ sessionId: "test", iteration: 0, lines: ["test"] }),
264
+ });
265
+
266
+ expect(response.status).toBe(400);
267
+ const data = (await response.json()) as { error: string };
268
+ expect(data.error).toContain("iteration");
269
+ });
270
+
271
+ test("should return 400 if lines is missing", async () => {
272
+ const response = await fetch(`${baseUrl}/api/session-logs`, {
273
+ method: "POST",
274
+ headers: { "Content-Type": "application/json" },
275
+ body: JSON.stringify({ sessionId: "test", iteration: 1 }),
276
+ });
277
+
278
+ expect(response.status).toBe(400);
279
+ const data = (await response.json()) as { error: string };
280
+ expect(data.error).toContain("lines");
281
+ });
282
+
283
+ test("should return 400 if lines is empty array", async () => {
284
+ const response = await fetch(`${baseUrl}/api/session-logs`, {
285
+ method: "POST",
286
+ headers: { "Content-Type": "application/json" },
287
+ body: JSON.stringify({ sessionId: "test", iteration: 1, lines: [] }),
288
+ });
289
+
290
+ expect(response.status).toBe(400);
291
+ const data = (await response.json()) as { error: string };
292
+ expect(data.error).toContain("lines");
293
+ });
294
+
295
+ test("should return 201 on successful POST", async () => {
296
+ const response = await fetch(`${baseUrl}/api/session-logs`, {
297
+ method: "POST",
298
+ headers: { "Content-Type": "application/json" },
299
+ body: JSON.stringify({
300
+ sessionId: "api-test-session",
301
+ iteration: 1,
302
+ cli: "claude",
303
+ lines: ['{"type":"system"}', '{"type":"result"}'],
304
+ }),
305
+ });
306
+
307
+ expect(response.status).toBe(201);
308
+ const data = (await response.json()) as { success: boolean; count: number };
309
+ expect(data.success).toBe(true);
310
+ expect(data.count).toBe(2);
311
+ });
312
+
313
+ test("should store logs with taskId", async () => {
314
+ const task = createTaskExtended("API test task");
315
+
316
+ const response = await fetch(`${baseUrl}/api/session-logs`, {
317
+ method: "POST",
318
+ headers: { "Content-Type": "application/json" },
319
+ body: JSON.stringify({
320
+ sessionId: "api-task-session",
321
+ iteration: 1,
322
+ taskId: task.id,
323
+ cli: "claude",
324
+ lines: ['{"type":"test"}'],
325
+ }),
326
+ });
327
+
328
+ expect(response.status).toBe(201);
329
+
330
+ // Verify it was stored correctly
331
+ const logs = getSessionLogsByTaskId(task.id);
332
+ expect(logs.length).toBe(1);
333
+ expect(logs[0]?.taskId).toBe(task.id);
334
+ });
335
+ });
336
+
337
+ describe("GET /api/tasks/:id/session-logs", () => {
338
+ test("should return 404 if task does not exist", async () => {
339
+ const response = await fetch(`${baseUrl}/api/tasks/non-existent-task/session-logs`);
340
+
341
+ expect(response.status).toBe(404);
342
+ const data = (await response.json()) as { error: string };
343
+ expect(data.error).toContain("Task not found");
344
+ });
345
+
346
+ test("should return empty logs array for task with no logs", async () => {
347
+ const task = createTaskExtended("Task without logs");
348
+
349
+ const response = await fetch(`${baseUrl}/api/tasks/${task.id}/session-logs`);
350
+
351
+ expect(response.status).toBe(200);
352
+ const data = (await response.json()) as { logs: unknown[] };
353
+ expect(data.logs).toEqual([]);
354
+ });
355
+
356
+ test("should return session logs for a task", async () => {
357
+ const task = createTaskExtended("Task with logs for GET test");
358
+
359
+ // Create some logs
360
+ createSessionLogs({
361
+ taskId: task.id,
362
+ sessionId: "get-test-session",
363
+ iteration: 1,
364
+ cli: "claude",
365
+ lines: ['{"type":"system"}', '{"type":"assistant"}'],
366
+ });
367
+
368
+ const response = await fetch(`${baseUrl}/api/tasks/${task.id}/session-logs`);
369
+
370
+ expect(response.status).toBe(200);
371
+ const data = (await response.json()) as {
372
+ logs: Array<{
373
+ id: string;
374
+ taskId: string;
375
+ sessionId: string;
376
+ iteration: number;
377
+ cli: string;
378
+ content: string;
379
+ lineNumber: number;
380
+ createdAt: string;
381
+ }>;
382
+ };
383
+ expect(data.logs.length).toBe(2);
384
+ expect(data.logs[0]?.content).toBe('{"type":"system"}');
385
+ expect(data.logs[1]?.content).toBe('{"type":"assistant"}');
386
+ });
387
+ });
388
+ });
package/src/types.ts CHANGED
@@ -171,3 +171,17 @@ export const AgentLogSchema = z.object({
171
171
 
172
172
  export type AgentLogEventType = z.infer<typeof AgentLogEventTypeSchema>;
173
173
  export type AgentLog = z.infer<typeof AgentLogSchema>;
174
+
175
+ // Session Log Types (raw CLI output)
176
+ export const SessionLogSchema = z.object({
177
+ id: z.uuid(),
178
+ taskId: z.uuid().optional(),
179
+ sessionId: z.string(),
180
+ iteration: z.number().int().min(1),
181
+ cli: z.string().default("claude"),
182
+ content: z.string(), // Raw JSON line
183
+ lineNumber: z.number().int().min(0),
184
+ createdAt: z.iso.datetime(),
185
+ });
186
+
187
+ export type SessionLog = z.infer<typeof SessionLogSchema>;