@desplega.ai/agent-swarm 1.10.3 → 1.10.8
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/.github/workflows/ci.yml +76 -0
- package/.github/workflows/{docker-publish.yml → docker-and-deploy.yml} +26 -1
- package/Dockerfile +6 -3
- package/README.md +8 -0
- package/assets/agent-swarm.mp4 +0 -0
- package/biome.json +4 -1
- package/deploy/prod-db.ts +1 -1
- package/package.json +2 -2
- package/pyproject.toml +9 -0
- package/src/be/db.ts +32 -2
- package/src/claude.ts +8 -8
- package/src/commands/runner.ts +123 -16
- package/src/http.ts +69 -3
- package/src/server.ts +2 -1
- package/src/slack/handlers.ts +0 -1
- package/src/tests/rest-api.test.ts +555 -0
- package/src/tests/runner-polling-api.test.ts +12 -12
- package/src/tools/poll-task.ts +0 -1
- package/thoughts/shared/plans/2025-12-23-worker-lead-spawn-triggers.md +568 -0
- package/tsconfig.json +1 -1
package/src/slack/handlers.ts
CHANGED
|
@@ -24,7 +24,6 @@ const userNameCache = new Map<string, string>();
|
|
|
24
24
|
|
|
25
25
|
async function getUserDisplayName(client: WebClient, userId: string): Promise<string> {
|
|
26
26
|
if (userNameCache.has(userId)) {
|
|
27
|
-
// biome-ignore lint: This is fine
|
|
28
27
|
return userNameCache.get(userId)!;
|
|
29
28
|
}
|
|
30
29
|
try {
|
|
@@ -0,0 +1,555 @@
|
|
|
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
|
+
createAgent,
|
|
7
|
+
createTaskExtended,
|
|
8
|
+
getAgentById,
|
|
9
|
+
getDb,
|
|
10
|
+
initDb,
|
|
11
|
+
updateAgentStatus,
|
|
12
|
+
} from "../be/db";
|
|
13
|
+
|
|
14
|
+
const TEST_DB_PATH = "./test-rest-api.sqlite";
|
|
15
|
+
const TEST_PORT = 13015;
|
|
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
|
+
function _parseQueryParams(url: string): URLSearchParams {
|
|
25
|
+
const queryIndex = url.indexOf("?");
|
|
26
|
+
if (queryIndex === -1) return new URLSearchParams();
|
|
27
|
+
return new URLSearchParams(url.slice(queryIndex + 1));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Minimal HTTP handler for REST API endpoints
|
|
31
|
+
async function handleRequest(
|
|
32
|
+
req: { method: string; url: string; headers: { get: (key: string) => string | null } },
|
|
33
|
+
_body: string,
|
|
34
|
+
): Promise<{ status: number; body: unknown }> {
|
|
35
|
+
const pathSegments = getPathSegments(req.url || "");
|
|
36
|
+
const myAgentId = req.headers.get("x-agent-id");
|
|
37
|
+
|
|
38
|
+
// GET /me - Get current agent info
|
|
39
|
+
if (req.method === "GET" && (req.url === "/me" || req.url?.startsWith("/me?"))) {
|
|
40
|
+
if (!myAgentId) {
|
|
41
|
+
return { status: 400, body: { error: "Missing X-Agent-ID header" } };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const agent = getAgentById(myAgentId);
|
|
45
|
+
|
|
46
|
+
if (!agent) {
|
|
47
|
+
return { status: 404, body: { error: "Agent not found" } };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { status: 200, body: agent };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// POST /ping - Update agent heartbeat
|
|
54
|
+
if (req.method === "POST" && req.url === "/ping") {
|
|
55
|
+
if (!myAgentId) {
|
|
56
|
+
return { status: 400, body: { error: "Missing X-Agent-ID header" } };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const tx = getDb().transaction(() => {
|
|
60
|
+
const agent = getAgentById(myAgentId);
|
|
61
|
+
|
|
62
|
+
if (!agent) {
|
|
63
|
+
return { error: true };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let status: "idle" | "busy" = "idle";
|
|
67
|
+
if (agent.status === "busy") {
|
|
68
|
+
status = "busy";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
updateAgentStatus(agent.id, status);
|
|
72
|
+
return { error: false };
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const result = tx();
|
|
76
|
+
if (result.error) {
|
|
77
|
+
return { status: 404, body: { error: "Agent not found" } };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { status: 204, body: "" };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// POST /close - Mark agent as offline
|
|
84
|
+
if (req.method === "POST" && req.url === "/close") {
|
|
85
|
+
if (!myAgentId) {
|
|
86
|
+
return { status: 400, body: { error: "Missing X-Agent-ID header" } };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const tx = getDb().transaction(() => {
|
|
90
|
+
const agent = getAgentById(myAgentId);
|
|
91
|
+
|
|
92
|
+
if (!agent) {
|
|
93
|
+
return { error: true };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
updateAgentStatus(agent.id, "offline");
|
|
97
|
+
return { error: false };
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const result = tx();
|
|
101
|
+
if (result.error) {
|
|
102
|
+
return { status: 404, body: { error: "Agent not found" } };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { status: 204, body: "" };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// GET /api/agents/:id - Get single agent
|
|
109
|
+
if (
|
|
110
|
+
req.method === "GET" &&
|
|
111
|
+
pathSegments[0] === "api" &&
|
|
112
|
+
pathSegments[1] === "agents" &&
|
|
113
|
+
pathSegments[2]
|
|
114
|
+
) {
|
|
115
|
+
const agentId = pathSegments[2];
|
|
116
|
+
const agent = getAgentById(agentId);
|
|
117
|
+
|
|
118
|
+
if (!agent) {
|
|
119
|
+
return { status: 404, body: { error: "Agent not found" } };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return { status: 200, body: agent };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// GET /api/tasks/:id - Get single task
|
|
126
|
+
if (
|
|
127
|
+
req.method === "GET" &&
|
|
128
|
+
pathSegments[0] === "api" &&
|
|
129
|
+
pathSegments[1] === "tasks" &&
|
|
130
|
+
pathSegments[2] &&
|
|
131
|
+
!pathSegments[3]
|
|
132
|
+
) {
|
|
133
|
+
const taskId = pathSegments[2];
|
|
134
|
+
const task = getDb().query("SELECT * FROM agent_tasks WHERE id = ?").get(taskId) as unknown;
|
|
135
|
+
|
|
136
|
+
if (!task) {
|
|
137
|
+
return { status: 404, body: { error: "Task not found" } };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return { status: 200, body: task };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// GET /api/stats - Dashboard summary stats
|
|
144
|
+
if (req.method === "GET" && pathSegments[0] === "api" && pathSegments[1] === "stats") {
|
|
145
|
+
const agents = getDb().query("SELECT * FROM agents").all() as Array<{ status: string }>;
|
|
146
|
+
const tasks = getDb().query("SELECT * FROM agent_tasks").all() as Array<{ status: string }>;
|
|
147
|
+
|
|
148
|
+
const stats = {
|
|
149
|
+
agents: {
|
|
150
|
+
total: agents.length,
|
|
151
|
+
idle: agents.filter((a) => a.status === "idle").length,
|
|
152
|
+
busy: agents.filter((a) => a.status === "busy").length,
|
|
153
|
+
offline: agents.filter((a) => a.status === "offline").length,
|
|
154
|
+
},
|
|
155
|
+
tasks: {
|
|
156
|
+
total: tasks.length,
|
|
157
|
+
pending: tasks.filter((t) => t.status === "pending").length,
|
|
158
|
+
in_progress: tasks.filter((t) => t.status === "in_progress").length,
|
|
159
|
+
completed: tasks.filter((t) => t.status === "completed").length,
|
|
160
|
+
failed: tasks.filter((t) => t.status === "failed").length,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
return { status: 200, body: stats };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { status: 404, body: { error: "Not found" } };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Create test HTTP server
|
|
171
|
+
function createTestServer(): Server {
|
|
172
|
+
return createHttpServer(async (req, res) => {
|
|
173
|
+
res.setHeader("Content-Type", "application/json");
|
|
174
|
+
|
|
175
|
+
const chunks: Buffer[] = [];
|
|
176
|
+
for await (const chunk of req) {
|
|
177
|
+
chunks.push(chunk);
|
|
178
|
+
}
|
|
179
|
+
const body = Buffer.concat(chunks).toString();
|
|
180
|
+
|
|
181
|
+
const headers = {
|
|
182
|
+
get: (key: string) => req.headers[key.toLowerCase()] as string | null,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const result = await handleRequest(
|
|
186
|
+
{ method: req.method || "GET", url: req.url || "/", headers },
|
|
187
|
+
body,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
res.writeHead(result.status);
|
|
191
|
+
res.end(JSON.stringify(result.body));
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
describe("REST API Endpoints", () => {
|
|
196
|
+
let server: Server;
|
|
197
|
+
const baseUrl = `http://localhost:${TEST_PORT}`;
|
|
198
|
+
|
|
199
|
+
beforeAll(async () => {
|
|
200
|
+
// Clean up any existing test database
|
|
201
|
+
try {
|
|
202
|
+
await unlink(TEST_DB_PATH);
|
|
203
|
+
} catch {
|
|
204
|
+
// File doesn't exist, that's fine
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Initialize test database
|
|
208
|
+
initDb(TEST_DB_PATH);
|
|
209
|
+
|
|
210
|
+
// Start test server
|
|
211
|
+
server = createTestServer();
|
|
212
|
+
await new Promise<void>((resolve) => {
|
|
213
|
+
server.listen(TEST_PORT, () => {
|
|
214
|
+
console.log(`Test server listening on port ${TEST_PORT}`);
|
|
215
|
+
resolve();
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
afterAll(async () => {
|
|
221
|
+
// Close server
|
|
222
|
+
await new Promise<void>((resolve) => {
|
|
223
|
+
server.close(() => resolve());
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Close database
|
|
227
|
+
closeDb();
|
|
228
|
+
|
|
229
|
+
// Clean up test database file
|
|
230
|
+
try {
|
|
231
|
+
await unlink(TEST_DB_PATH);
|
|
232
|
+
await unlink(`${TEST_DB_PATH}-wal`);
|
|
233
|
+
await unlink(`${TEST_DB_PATH}-shm`);
|
|
234
|
+
} catch {
|
|
235
|
+
// Files may not exist
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe("GET /me", () => {
|
|
240
|
+
test("should return 400 if X-Agent-ID header is missing", async () => {
|
|
241
|
+
const response = await fetch(`${baseUrl}/me`);
|
|
242
|
+
|
|
243
|
+
expect(response.status).toBe(400);
|
|
244
|
+
const data = (await response.json()) as any;
|
|
245
|
+
expect(data.error).toContain("X-Agent-ID");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("should return 404 if agent does not exist", async () => {
|
|
249
|
+
const response = await fetch(`${baseUrl}/me`, {
|
|
250
|
+
headers: {
|
|
251
|
+
"X-Agent-ID": "non-existent-agent",
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
expect(response.status).toBe(404);
|
|
256
|
+
const data = (await response.json()) as any;
|
|
257
|
+
expect(data.error).toContain("not found");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("should return agent info for existing agent", async () => {
|
|
261
|
+
const agentId = "test-agent-me";
|
|
262
|
+
createAgent({
|
|
263
|
+
id: agentId,
|
|
264
|
+
name: "Test Agent Me",
|
|
265
|
+
isLead: false,
|
|
266
|
+
status: "idle",
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const response = await fetch(`${baseUrl}/me`, {
|
|
270
|
+
headers: {
|
|
271
|
+
"X-Agent-ID": agentId,
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
expect(response.status).toBe(200);
|
|
276
|
+
const data = (await response.json()) as any;
|
|
277
|
+
expect(data.id).toBe(agentId);
|
|
278
|
+
expect(data.name).toBe("Test Agent Me");
|
|
279
|
+
expect(data.status).toBe("idle");
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe("POST /ping", () => {
|
|
284
|
+
test("should return 400 if X-Agent-ID header is missing", async () => {
|
|
285
|
+
const response = await fetch(`${baseUrl}/ping`, {
|
|
286
|
+
method: "POST",
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
expect(response.status).toBe(400);
|
|
290
|
+
const data = (await response.json()) as any;
|
|
291
|
+
expect(data.error).toContain("X-Agent-ID");
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("should return 404 if agent does not exist", async () => {
|
|
295
|
+
const response = await fetch(`${baseUrl}/ping`, {
|
|
296
|
+
method: "POST",
|
|
297
|
+
headers: {
|
|
298
|
+
"X-Agent-ID": "non-existent-agent",
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
expect(response.status).toBe(404);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("should update agent heartbeat for existing agent", async () => {
|
|
306
|
+
const agentId = "test-agent-ping";
|
|
307
|
+
createAgent({
|
|
308
|
+
id: agentId,
|
|
309
|
+
name: "Test Agent Ping",
|
|
310
|
+
isLead: false,
|
|
311
|
+
status: "offline",
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const response = await fetch(`${baseUrl}/ping`, {
|
|
315
|
+
method: "POST",
|
|
316
|
+
headers: {
|
|
317
|
+
"X-Agent-ID": agentId,
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
expect(response.status).toBe(204);
|
|
322
|
+
|
|
323
|
+
// Verify agent status was updated to idle
|
|
324
|
+
const agent = getAgentById(agentId);
|
|
325
|
+
expect(agent?.status).toBe("idle");
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("should preserve busy status when pinging", async () => {
|
|
329
|
+
const agentId = "test-agent-ping-busy";
|
|
330
|
+
createAgent({
|
|
331
|
+
id: agentId,
|
|
332
|
+
name: "Test Agent Ping Busy",
|
|
333
|
+
isLead: false,
|
|
334
|
+
status: "busy",
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const response = await fetch(`${baseUrl}/ping`, {
|
|
338
|
+
method: "POST",
|
|
339
|
+
headers: {
|
|
340
|
+
"X-Agent-ID": agentId,
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
expect(response.status).toBe(204);
|
|
345
|
+
|
|
346
|
+
// Verify agent status remains busy
|
|
347
|
+
const agent = getAgentById(agentId);
|
|
348
|
+
expect(agent?.status).toBe("busy");
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
describe("POST /close", () => {
|
|
353
|
+
test("should return 400 if X-Agent-ID header is missing", async () => {
|
|
354
|
+
const response = await fetch(`${baseUrl}/close`, {
|
|
355
|
+
method: "POST",
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
expect(response.status).toBe(400);
|
|
359
|
+
const data = (await response.json()) as any;
|
|
360
|
+
expect(data.error).toContain("X-Agent-ID");
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test("should return 404 if agent does not exist", async () => {
|
|
364
|
+
const response = await fetch(`${baseUrl}/close`, {
|
|
365
|
+
method: "POST",
|
|
366
|
+
headers: {
|
|
367
|
+
"X-Agent-ID": "non-existent-agent",
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
expect(response.status).toBe(404);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("should mark agent as offline", async () => {
|
|
375
|
+
const agentId = "test-agent-close";
|
|
376
|
+
createAgent({
|
|
377
|
+
id: agentId,
|
|
378
|
+
name: "Test Agent Close",
|
|
379
|
+
isLead: false,
|
|
380
|
+
status: "idle",
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
const response = await fetch(`${baseUrl}/close`, {
|
|
384
|
+
method: "POST",
|
|
385
|
+
headers: {
|
|
386
|
+
"X-Agent-ID": agentId,
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
expect(response.status).toBe(204);
|
|
391
|
+
|
|
392
|
+
// Verify agent status was updated to offline
|
|
393
|
+
const agent = getAgentById(agentId);
|
|
394
|
+
expect(agent?.status).toBe("offline");
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
describe("GET /api/agents/:id", () => {
|
|
399
|
+
test("should return 404 if agent does not exist", async () => {
|
|
400
|
+
const response = await fetch(`${baseUrl}/api/agents/non-existent-agent`);
|
|
401
|
+
|
|
402
|
+
expect(response.status).toBe(404);
|
|
403
|
+
const data = (await response.json()) as any;
|
|
404
|
+
expect(data.error).toContain("not found");
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test("should return agent details for existing agent", async () => {
|
|
408
|
+
const agentId = "test-agent-get";
|
|
409
|
+
createAgent({
|
|
410
|
+
id: agentId,
|
|
411
|
+
name: "Test Agent Get",
|
|
412
|
+
isLead: true,
|
|
413
|
+
status: "idle",
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
const response = await fetch(`${baseUrl}/api/agents/${agentId}`);
|
|
417
|
+
|
|
418
|
+
expect(response.status).toBe(200);
|
|
419
|
+
const data = (await response.json()) as any;
|
|
420
|
+
expect(data.id).toBe(agentId);
|
|
421
|
+
expect(data.name).toBe("Test Agent Get");
|
|
422
|
+
expect(data.isLead).toBe(true);
|
|
423
|
+
expect(data.status).toBe("idle");
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test("should return agent with profile fields", async () => {
|
|
427
|
+
const agentId = "test-agent-with-profile";
|
|
428
|
+
|
|
429
|
+
// First create agent, then update its profile
|
|
430
|
+
createAgent({
|
|
431
|
+
id: agentId,
|
|
432
|
+
name: "Agent with Profile",
|
|
433
|
+
isLead: false,
|
|
434
|
+
status: "idle",
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// Update profile fields via SQL since createAgent doesn't accept them
|
|
438
|
+
getDb().run("UPDATE agents SET description = ?, role = ?, capabilities = ? WHERE id = ?", [
|
|
439
|
+
"Test description",
|
|
440
|
+
"Test role",
|
|
441
|
+
JSON.stringify(["test-cap-1", "test-cap-2"]),
|
|
442
|
+
agentId,
|
|
443
|
+
]);
|
|
444
|
+
|
|
445
|
+
const response = await fetch(`${baseUrl}/api/agents/${agentId}`);
|
|
446
|
+
|
|
447
|
+
expect(response.status).toBe(200);
|
|
448
|
+
const data = (await response.json()) as any;
|
|
449
|
+
expect(data.id).toBe(agentId);
|
|
450
|
+
expect(data.description).toBe("Test description");
|
|
451
|
+
expect(data.role).toBe("Test role");
|
|
452
|
+
expect(data.capabilities).toEqual(["test-cap-1", "test-cap-2"]);
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
describe("GET /api/tasks/:id", () => {
|
|
457
|
+
test("should return 404 if task does not exist", async () => {
|
|
458
|
+
const response = await fetch(`${baseUrl}/api/tasks/non-existent-task`);
|
|
459
|
+
|
|
460
|
+
expect(response.status).toBe(404);
|
|
461
|
+
const data = (await response.json()) as any;
|
|
462
|
+
expect(data.error).toContain("not found");
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test("should return task details for existing task", async () => {
|
|
466
|
+
const task = createTaskExtended("Test task for GET endpoint", {
|
|
467
|
+
creatorAgentId: "test-agent-get",
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
const response = await fetch(`${baseUrl}/api/tasks/${task.id}`);
|
|
471
|
+
|
|
472
|
+
expect(response.status).toBe(200);
|
|
473
|
+
const data = (await response.json()) as any;
|
|
474
|
+
expect(data.id).toBe(task.id);
|
|
475
|
+
expect(data.task).toBe("Test task for GET endpoint");
|
|
476
|
+
expect(data.status).toBe("unassigned");
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
describe("GET /api/stats", () => {
|
|
481
|
+
test("should return dashboard statistics", async () => {
|
|
482
|
+
// Create some test data
|
|
483
|
+
createAgent({
|
|
484
|
+
id: "stats-agent-1",
|
|
485
|
+
name: "Stats Agent 1",
|
|
486
|
+
isLead: false,
|
|
487
|
+
status: "idle",
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
createAgent({
|
|
491
|
+
id: "stats-agent-2",
|
|
492
|
+
name: "Stats Agent 2",
|
|
493
|
+
isLead: false,
|
|
494
|
+
status: "busy",
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
createAgent({
|
|
498
|
+
id: "stats-agent-3",
|
|
499
|
+
name: "Stats Agent 3",
|
|
500
|
+
isLead: false,
|
|
501
|
+
status: "offline",
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
createTaskExtended("Stats task 1", {
|
|
505
|
+
creatorAgentId: "stats-agent-1",
|
|
506
|
+
agentId: "stats-agent-1",
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
createTaskExtended("Stats task 2", {
|
|
510
|
+
creatorAgentId: "stats-agent-1",
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
const response = await fetch(`${baseUrl}/api/stats`);
|
|
514
|
+
|
|
515
|
+
expect(response.status).toBe(200);
|
|
516
|
+
const data = (await response.json()) as any;
|
|
517
|
+
|
|
518
|
+
expect(data.agents).toBeDefined();
|
|
519
|
+
expect(data.agents.total).toBeGreaterThanOrEqual(3);
|
|
520
|
+
expect(data.agents.idle).toBeGreaterThanOrEqual(1);
|
|
521
|
+
expect(data.agents.busy).toBeGreaterThanOrEqual(1);
|
|
522
|
+
expect(data.agents.offline).toBeGreaterThanOrEqual(1);
|
|
523
|
+
|
|
524
|
+
expect(data.tasks).toBeDefined();
|
|
525
|
+
expect(data.tasks.total).toBeGreaterThanOrEqual(2);
|
|
526
|
+
expect(data.tasks.pending).toBeGreaterThanOrEqual(1);
|
|
527
|
+
expect(data.tasks.unassigned).toBeUndefined(); // Check that invalid status isn't counted
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
test("should return empty stats for empty database", async () => {
|
|
531
|
+
// Clean up the database for this test
|
|
532
|
+
closeDb();
|
|
533
|
+
await unlink(TEST_DB_PATH);
|
|
534
|
+
await unlink(`${TEST_DB_PATH}-wal`).catch(() => {});
|
|
535
|
+
await unlink(`${TEST_DB_PATH}-shm`).catch(() => {});
|
|
536
|
+
initDb(TEST_DB_PATH);
|
|
537
|
+
|
|
538
|
+
const response = await fetch(`${baseUrl}/api/stats`);
|
|
539
|
+
|
|
540
|
+
expect(response.status).toBe(200);
|
|
541
|
+
const data = (await response.json()) as any;
|
|
542
|
+
|
|
543
|
+
expect(data.agents.total).toBe(0);
|
|
544
|
+
expect(data.agents.idle).toBe(0);
|
|
545
|
+
expect(data.agents.busy).toBe(0);
|
|
546
|
+
expect(data.agents.offline).toBe(0);
|
|
547
|
+
|
|
548
|
+
expect(data.tasks.total).toBe(0);
|
|
549
|
+
expect(data.tasks.pending).toBe(0);
|
|
550
|
+
expect(data.tasks.in_progress).toBe(0);
|
|
551
|
+
expect(data.tasks.completed).toBe(0);
|
|
552
|
+
expect(data.tasks.failed).toBe(0);
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
});
|
|
@@ -225,7 +225,7 @@ describe("Runner-Level Polling API", () => {
|
|
|
225
225
|
});
|
|
226
226
|
|
|
227
227
|
expect(response.status).toBe(201);
|
|
228
|
-
const data = await response.json();
|
|
228
|
+
const data = (await response.json()) as any;
|
|
229
229
|
expect(data.id).toBe(agentId);
|
|
230
230
|
expect(data.name).toBe("Test Agent 1");
|
|
231
231
|
expect(data.status).toBe("idle");
|
|
@@ -243,7 +243,7 @@ describe("Runner-Level Polling API", () => {
|
|
|
243
243
|
});
|
|
244
244
|
|
|
245
245
|
expect(response.status).toBe(200); // Not 201 since it exists
|
|
246
|
-
const data = await response.json();
|
|
246
|
+
const data = (await response.json()) as any;
|
|
247
247
|
expect(data.id).toBe(agentId);
|
|
248
248
|
expect(data.name).toBe("Test Agent 1"); // Original name preserved
|
|
249
249
|
});
|
|
@@ -258,7 +258,7 @@ describe("Runner-Level Polling API", () => {
|
|
|
258
258
|
});
|
|
259
259
|
|
|
260
260
|
expect(response.status).toBe(201);
|
|
261
|
-
const data = await response.json();
|
|
261
|
+
const data = (await response.json()) as any;
|
|
262
262
|
expect(data.id).toBeDefined();
|
|
263
263
|
expect(data.id.length).toBe(36); // UUID format
|
|
264
264
|
expect(data.name).toBe("Auto-ID Agent");
|
|
@@ -275,7 +275,7 @@ describe("Runner-Level Polling API", () => {
|
|
|
275
275
|
});
|
|
276
276
|
|
|
277
277
|
expect(response.status).toBe(400);
|
|
278
|
-
const data = await response.json();
|
|
278
|
+
const data = (await response.json()) as any;
|
|
279
279
|
expect(data.error).toContain("name");
|
|
280
280
|
});
|
|
281
281
|
|
|
@@ -291,7 +291,7 @@ describe("Runner-Level Polling API", () => {
|
|
|
291
291
|
});
|
|
292
292
|
|
|
293
293
|
expect(response.status).toBe(201);
|
|
294
|
-
const data = await response.json();
|
|
294
|
+
const data = (await response.json()) as any;
|
|
295
295
|
expect(data.id).toBe(agentId);
|
|
296
296
|
expect(data.isLead).toBe(true);
|
|
297
297
|
});
|
|
@@ -302,7 +302,7 @@ describe("Runner-Level Polling API", () => {
|
|
|
302
302
|
const response = await fetch(`${baseUrl}/api/poll`);
|
|
303
303
|
|
|
304
304
|
expect(response.status).toBe(400);
|
|
305
|
-
const data = await response.json();
|
|
305
|
+
const data = (await response.json()) as any;
|
|
306
306
|
expect(data.error).toContain("X-Agent-ID");
|
|
307
307
|
});
|
|
308
308
|
|
|
@@ -314,7 +314,7 @@ describe("Runner-Level Polling API", () => {
|
|
|
314
314
|
});
|
|
315
315
|
|
|
316
316
|
expect(response.status).toBe(404);
|
|
317
|
-
const data = await response.json();
|
|
317
|
+
const data = (await response.json()) as any;
|
|
318
318
|
expect(data.error).toContain("not found");
|
|
319
319
|
});
|
|
320
320
|
|
|
@@ -326,7 +326,7 @@ describe("Runner-Level Polling API", () => {
|
|
|
326
326
|
});
|
|
327
327
|
|
|
328
328
|
expect(response.status).toBe(200);
|
|
329
|
-
const data = await response.json();
|
|
329
|
+
const data = (await response.json()) as any;
|
|
330
330
|
expect(data.trigger).toBeNull();
|
|
331
331
|
});
|
|
332
332
|
|
|
@@ -356,7 +356,7 @@ describe("Runner-Level Polling API", () => {
|
|
|
356
356
|
});
|
|
357
357
|
|
|
358
358
|
expect(response.status).toBe(200);
|
|
359
|
-
const data = await response.json();
|
|
359
|
+
const data = (await response.json()) as any;
|
|
360
360
|
expect(data.trigger).not.toBeNull();
|
|
361
361
|
expect(data.trigger.type).toBe("task_assigned");
|
|
362
362
|
expect(data.trigger.taskId).toBe(task.id);
|
|
@@ -388,7 +388,7 @@ describe("Runner-Level Polling API", () => {
|
|
|
388
388
|
});
|
|
389
389
|
|
|
390
390
|
expect(response.status).toBe(200);
|
|
391
|
-
const data = await response.json();
|
|
391
|
+
const data = (await response.json()) as any;
|
|
392
392
|
expect(data.trigger).not.toBeNull();
|
|
393
393
|
expect(data.trigger.type).toBe("task_offered");
|
|
394
394
|
expect(data.trigger.taskId).toBe(task.id);
|
|
@@ -420,7 +420,7 @@ describe("Runner-Level Polling API", () => {
|
|
|
420
420
|
});
|
|
421
421
|
|
|
422
422
|
expect(response.status).toBe(200);
|
|
423
|
-
const data = await response.json();
|
|
423
|
+
const data = (await response.json()) as any;
|
|
424
424
|
expect(data.trigger).not.toBeNull();
|
|
425
425
|
expect(data.trigger.type).toBe("pool_tasks_available");
|
|
426
426
|
expect(data.trigger.count).toBeGreaterThan(0);
|
|
@@ -448,7 +448,7 @@ describe("Runner-Level Polling API", () => {
|
|
|
448
448
|
});
|
|
449
449
|
|
|
450
450
|
expect(response.status).toBe(200);
|
|
451
|
-
const data = await response.json();
|
|
451
|
+
const data = (await response.json()) as any;
|
|
452
452
|
// Worker should NOT see pool tasks
|
|
453
453
|
expect(data.trigger).toBeNull();
|
|
454
454
|
});
|
package/src/tools/poll-task.ts
CHANGED
|
@@ -107,7 +107,6 @@ export const registerPollTaskTool = (server: McpServer) => {
|
|
|
107
107
|
while (new Date() < maxTime) {
|
|
108
108
|
// Fetch and update in a single transaction to avoid race conditions
|
|
109
109
|
const startedTask = getDb().transaction(() => {
|
|
110
|
-
// biome-ignore lint/style/noNonNullAssertion: agent existence verified above
|
|
111
110
|
const agentNow = getAgentById(agentId)!;
|
|
112
111
|
|
|
113
112
|
if (agentNow.status !== "busy") {
|