@desplega.ai/agent-swarm 1.52.1 → 1.53.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/openapi.json +1517 -488
- package/package.json +5 -2
- package/src/be/db.ts +530 -0
- package/src/be/events.ts +322 -0
- package/src/be/migrations/021_events.sql +24 -0
- package/src/be/migrations/022_context_usage.sql +34 -0
- package/src/be/migrations/023_mcp_servers.sql +44 -0
- package/src/commands/runner.ts +348 -1
- package/src/http/context.ts +118 -0
- package/src/http/events.ts +188 -0
- package/src/http/index.ts +6 -0
- package/src/http/mcp-servers.ts +364 -0
- package/src/http/tasks.ts +33 -0
- package/src/linear/outbound.ts +8 -1
- package/src/linear/sync.ts +3 -0
- package/src/oauth/ensure-token.ts +50 -0
- package/src/prompts/base-prompt.ts +7 -0
- package/src/providers/claude-adapter.ts +156 -15
- package/src/providers/pi-mono-adapter.ts +68 -0
- package/src/providers/pi-mono-extension.ts +56 -2
- package/src/providers/pi-mono-mcp-client.ts +10 -1
- package/src/providers/types.ts +14 -1
- package/src/server.ts +19 -0
- package/src/tests/context-window.test.ts +66 -0
- package/src/tests/ensure-token.test.ts +170 -0
- package/src/tests/events-db.test.ts +314 -0
- package/src/tests/events-http.test.ts +267 -0
- package/src/tests/prompt-template-remaining.test.ts +5 -5
- package/src/tests/tool-annotations.test.ts +2 -2
- package/src/tests/vcs-tracking.test.ts +176 -0
- package/src/tests/workflow-executors.test.ts +8 -1
- package/src/tools/mcp-servers/index.ts +7 -0
- package/src/tools/mcp-servers/mcp-server-create.ts +138 -0
- package/src/tools/mcp-servers/mcp-server-delete.ts +72 -0
- package/src/tools/mcp-servers/mcp-server-get.ts +80 -0
- package/src/tools/mcp-servers/mcp-server-install.ts +110 -0
- package/src/tools/mcp-servers/mcp-server-list.ts +67 -0
- package/src/tools/mcp-servers/mcp-server-uninstall.ts +71 -0
- package/src/tools/mcp-servers/mcp-server-update.ts +120 -0
- package/src/tools/tool-config.ts +9 -0
- package/src/types.ts +153 -0
- package/src/utils/context-window.ts +41 -0
- package/src/workflows/executors/base.ts +9 -1
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import { closeDb, initDb } from "../be/db";
|
|
4
|
+
import {
|
|
5
|
+
deleteOAuthTokens,
|
|
6
|
+
getOAuthTokens,
|
|
7
|
+
storeOAuthTokens,
|
|
8
|
+
upsertOAuthApp,
|
|
9
|
+
} from "../be/db-queries/oauth";
|
|
10
|
+
import { ensureToken } from "../oauth/ensure-token";
|
|
11
|
+
|
|
12
|
+
const TEST_DB_PATH = "./test-ensure-token.sqlite";
|
|
13
|
+
|
|
14
|
+
const testApp = {
|
|
15
|
+
clientId: "test-client-id",
|
|
16
|
+
clientSecret: "test-client-secret",
|
|
17
|
+
authorizeUrl: "https://example.com/oauth/authorize",
|
|
18
|
+
tokenUrl: "https://example.com/oauth/token",
|
|
19
|
+
redirectUri: "http://localhost:3013/callback",
|
|
20
|
+
scopes: "read,write",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const originalFetch = globalThis.fetch;
|
|
24
|
+
|
|
25
|
+
beforeAll(() => {
|
|
26
|
+
initDb(TEST_DB_PATH);
|
|
27
|
+
upsertOAuthApp("test-provider", testApp);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
deleteOAuthTokens("test-provider");
|
|
32
|
+
globalThis.fetch = originalFetch;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterAll(async () => {
|
|
36
|
+
globalThis.fetch = originalFetch;
|
|
37
|
+
closeDb();
|
|
38
|
+
await unlink(TEST_DB_PATH).catch(() => {});
|
|
39
|
+
await unlink(`${TEST_DB_PATH}-wal`).catch(() => {});
|
|
40
|
+
await unlink(`${TEST_DB_PATH}-shm`).catch(() => {});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("ensureToken", () => {
|
|
44
|
+
test("does nothing when token is not expiring", async () => {
|
|
45
|
+
storeOAuthTokens("test-provider", {
|
|
46
|
+
accessToken: "valid-token",
|
|
47
|
+
refreshToken: "refresh-token",
|
|
48
|
+
expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString(), // 1 hour from now
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const fetchSpy = mock(() => Promise.resolve(new Response()));
|
|
52
|
+
globalThis.fetch = fetchSpy;
|
|
53
|
+
|
|
54
|
+
await ensureToken("test-provider");
|
|
55
|
+
|
|
56
|
+
// No fetch call should have been made — token is still valid
|
|
57
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
58
|
+
|
|
59
|
+
// Token should be unchanged
|
|
60
|
+
const tokens = getOAuthTokens("test-provider");
|
|
61
|
+
expect(tokens?.accessToken).toBe("valid-token");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("refreshes token when expiring soon", async () => {
|
|
65
|
+
storeOAuthTokens("test-provider", {
|
|
66
|
+
accessToken: "old-token",
|
|
67
|
+
refreshToken: "refresh-token",
|
|
68
|
+
expiresAt: new Date(Date.now() + 2 * 60 * 1000).toISOString(), // 2 minutes (within 5-min buffer)
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const fetchSpy = mock(() =>
|
|
72
|
+
Promise.resolve(
|
|
73
|
+
new Response(
|
|
74
|
+
JSON.stringify({
|
|
75
|
+
access_token: "new-access-token",
|
|
76
|
+
token_type: "Bearer",
|
|
77
|
+
expires_in: 3600,
|
|
78
|
+
refresh_token: "new-refresh-token",
|
|
79
|
+
}),
|
|
80
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
81
|
+
),
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
globalThis.fetch = fetchSpy;
|
|
85
|
+
|
|
86
|
+
await ensureToken("test-provider");
|
|
87
|
+
|
|
88
|
+
// Should have called the token endpoint
|
|
89
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
90
|
+
const [url, init] = fetchSpy.mock.calls[0] as [string, RequestInit];
|
|
91
|
+
expect(url).toBe("https://example.com/oauth/token");
|
|
92
|
+
expect(init.method).toBe("POST");
|
|
93
|
+
expect(init.body).toContain("grant_type=refresh_token");
|
|
94
|
+
expect(init.body).toContain("refresh_token=refresh-token");
|
|
95
|
+
|
|
96
|
+
// Token should be updated in DB
|
|
97
|
+
const tokens = getOAuthTokens("test-provider");
|
|
98
|
+
expect(tokens?.accessToken).toBe("new-access-token");
|
|
99
|
+
expect(tokens?.refreshToken).toBe("new-refresh-token");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("handles gracefully when no tokens exist", async () => {
|
|
103
|
+
// No tokens stored — isTokenExpiringSoon returns true but no refresh token available
|
|
104
|
+
deleteOAuthTokens("test-provider");
|
|
105
|
+
|
|
106
|
+
const fetchSpy = mock(() => Promise.resolve(new Response()));
|
|
107
|
+
globalThis.fetch = fetchSpy;
|
|
108
|
+
|
|
109
|
+
// Should not throw
|
|
110
|
+
await ensureToken("test-provider");
|
|
111
|
+
|
|
112
|
+
// No fetch call — can't refresh without a refresh token
|
|
113
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("handles gracefully when no OAuth app is configured", async () => {
|
|
117
|
+
// Store expiring token for the configured provider, but query a nonexistent one
|
|
118
|
+
const fetchSpy = mock(() => Promise.resolve(new Response()));
|
|
119
|
+
globalThis.fetch = fetchSpy;
|
|
120
|
+
|
|
121
|
+
// Should not throw for unconfigured provider
|
|
122
|
+
await ensureToken("nonexistent-provider");
|
|
123
|
+
|
|
124
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("handles refresh failure gracefully", async () => {
|
|
128
|
+
storeOAuthTokens("test-provider", {
|
|
129
|
+
accessToken: "old-token",
|
|
130
|
+
refreshToken: "refresh-token",
|
|
131
|
+
expiresAt: new Date(Date.now() + 60 * 1000).toISOString(), // 1 minute from now
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const fetchSpy = mock(() =>
|
|
135
|
+
Promise.resolve(
|
|
136
|
+
new Response('{"error":"invalid_grant"}', {
|
|
137
|
+
status: 400,
|
|
138
|
+
headers: { "Content-Type": "application/json" },
|
|
139
|
+
}),
|
|
140
|
+
),
|
|
141
|
+
);
|
|
142
|
+
globalThis.fetch = fetchSpy;
|
|
143
|
+
|
|
144
|
+
// Should not throw — error is caught and logged
|
|
145
|
+
await ensureToken("test-provider");
|
|
146
|
+
|
|
147
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
148
|
+
|
|
149
|
+
// Original token should still be in DB (refresh failed)
|
|
150
|
+
const tokens = getOAuthTokens("test-provider");
|
|
151
|
+
expect(tokens?.accessToken).toBe("old-token");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("handles token with no refresh token", async () => {
|
|
155
|
+
storeOAuthTokens("test-provider", {
|
|
156
|
+
accessToken: "old-token",
|
|
157
|
+
refreshToken: null,
|
|
158
|
+
expiresAt: new Date(Date.now() + 60 * 1000).toISOString(), // 1 minute from now
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const fetchSpy = mock(() => Promise.resolve(new Response()));
|
|
162
|
+
globalThis.fetch = fetchSpy;
|
|
163
|
+
|
|
164
|
+
// Should not throw
|
|
165
|
+
await ensureToken("test-provider");
|
|
166
|
+
|
|
167
|
+
// No fetch — can't refresh without a refresh token
|
|
168
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import { closeDb, createAgent, initDb } from "../be/db";
|
|
4
|
+
import {
|
|
5
|
+
createEvent,
|
|
6
|
+
createEventsBatch,
|
|
7
|
+
getAllEvents,
|
|
8
|
+
getEventCounts,
|
|
9
|
+
getEventCountsFiltered,
|
|
10
|
+
getEventCountsForAgent,
|
|
11
|
+
getEventsByAgentId,
|
|
12
|
+
getEventsByCategory,
|
|
13
|
+
getEventsByEvent,
|
|
14
|
+
getEventsBySessionId,
|
|
15
|
+
getEventsByTaskId,
|
|
16
|
+
getEventsFiltered,
|
|
17
|
+
} from "../be/events";
|
|
18
|
+
import type { SwarmEvent } from "../types";
|
|
19
|
+
|
|
20
|
+
const TEST_DB_PATH = "./test-events-db.sqlite";
|
|
21
|
+
|
|
22
|
+
let testAgent: { id: string };
|
|
23
|
+
|
|
24
|
+
beforeAll(async () => {
|
|
25
|
+
try {
|
|
26
|
+
await unlink(TEST_DB_PATH);
|
|
27
|
+
} catch {}
|
|
28
|
+
initDb(TEST_DB_PATH);
|
|
29
|
+
testAgent = createAgent({ name: "Events Test Agent", isLead: false, status: "idle" });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterAll(async () => {
|
|
33
|
+
closeDb();
|
|
34
|
+
try {
|
|
35
|
+
await unlink(TEST_DB_PATH);
|
|
36
|
+
await unlink(`${TEST_DB_PATH}-wal`);
|
|
37
|
+
await unlink(`${TEST_DB_PATH}-shm`);
|
|
38
|
+
} catch {}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("createEvent", () => {
|
|
42
|
+
test("creates a single event with required fields", () => {
|
|
43
|
+
const evt = createEvent({
|
|
44
|
+
category: "tool",
|
|
45
|
+
event: "tool.start",
|
|
46
|
+
source: "worker",
|
|
47
|
+
});
|
|
48
|
+
expect(evt.id).toBeDefined();
|
|
49
|
+
expect(evt.category).toBe("tool");
|
|
50
|
+
expect(evt.event).toBe("tool.start");
|
|
51
|
+
expect(evt.status).toBe("ok");
|
|
52
|
+
expect(evt.source).toBe("worker");
|
|
53
|
+
expect(evt.createdAt).toBeDefined();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("creates event with all optional fields", () => {
|
|
57
|
+
const evt = createEvent({
|
|
58
|
+
category: "tool",
|
|
59
|
+
event: "tool.start",
|
|
60
|
+
source: "worker",
|
|
61
|
+
status: "error",
|
|
62
|
+
agentId: testAgent.id,
|
|
63
|
+
taskId: "task-123",
|
|
64
|
+
sessionId: "session-456",
|
|
65
|
+
parentEventId: "parent-789",
|
|
66
|
+
numericValue: 42.5,
|
|
67
|
+
durationMs: 1500,
|
|
68
|
+
data: { toolName: "Read", filePath: "/tmp/test.txt" },
|
|
69
|
+
});
|
|
70
|
+
expect(evt.status).toBe("error");
|
|
71
|
+
expect(evt.agentId).toBe(testAgent.id);
|
|
72
|
+
expect(evt.taskId).toBe("task-123");
|
|
73
|
+
expect(evt.sessionId).toBe("session-456");
|
|
74
|
+
expect(evt.parentEventId).toBe("parent-789");
|
|
75
|
+
expect(evt.numericValue).toBe(42.5);
|
|
76
|
+
expect(evt.durationMs).toBe(1500);
|
|
77
|
+
expect(evt.data).toEqual({ toolName: "Read", filePath: "/tmp/test.txt" });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("defaults status to ok", () => {
|
|
81
|
+
const evt = createEvent({ category: "system", event: "system.boot", source: "api" });
|
|
82
|
+
expect(evt.status).toBe("ok");
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("createEventsBatch", () => {
|
|
87
|
+
test("inserts multiple events in a transaction", () => {
|
|
88
|
+
const before = getAllEvents(1000).length;
|
|
89
|
+
const count = createEventsBatch([
|
|
90
|
+
{ category: "tool", event: "tool.start", source: "worker", agentId: testAgent.id },
|
|
91
|
+
{ category: "tool", event: "tool.end", source: "worker", agentId: testAgent.id },
|
|
92
|
+
{ category: "skill", event: "skill.invoke", source: "worker", agentId: testAgent.id },
|
|
93
|
+
]);
|
|
94
|
+
expect(count).toBe(3);
|
|
95
|
+
const after = getAllEvents(1000).length;
|
|
96
|
+
expect(after - before).toBe(3);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("returns 0 for empty batch", () => {
|
|
100
|
+
const count = createEventsBatch([]);
|
|
101
|
+
expect(count).toBe(0);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("query functions", () => {
|
|
106
|
+
const sessionId = `query-test-session-${Date.now()}`;
|
|
107
|
+
const taskId = `query-test-task-${Date.now()}`;
|
|
108
|
+
|
|
109
|
+
beforeAll(() => {
|
|
110
|
+
// Seed events for query tests
|
|
111
|
+
createEventsBatch([
|
|
112
|
+
{
|
|
113
|
+
category: "tool",
|
|
114
|
+
event: "tool.start",
|
|
115
|
+
source: "worker",
|
|
116
|
+
agentId: testAgent.id,
|
|
117
|
+
sessionId,
|
|
118
|
+
taskId,
|
|
119
|
+
data: { toolName: "Read" },
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
category: "tool",
|
|
123
|
+
event: "tool.start",
|
|
124
|
+
source: "worker",
|
|
125
|
+
agentId: testAgent.id,
|
|
126
|
+
sessionId,
|
|
127
|
+
taskId,
|
|
128
|
+
data: { toolName: "Bash" },
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
category: "skill",
|
|
132
|
+
event: "skill.invoke",
|
|
133
|
+
source: "worker",
|
|
134
|
+
agentId: testAgent.id,
|
|
135
|
+
sessionId,
|
|
136
|
+
taskId,
|
|
137
|
+
data: { skillName: "commit" },
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
category: "session",
|
|
141
|
+
event: "session.start",
|
|
142
|
+
source: "worker",
|
|
143
|
+
agentId: testAgent.id,
|
|
144
|
+
sessionId,
|
|
145
|
+
taskId,
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
category: "api",
|
|
149
|
+
event: "api.request",
|
|
150
|
+
source: "api",
|
|
151
|
+
data: { method: "GET", path: "/health" },
|
|
152
|
+
},
|
|
153
|
+
]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("getEventsByCategory filters by category", () => {
|
|
157
|
+
const tools = getEventsByCategory("tool");
|
|
158
|
+
expect(tools.length).toBeGreaterThanOrEqual(2);
|
|
159
|
+
for (const evt of tools) {
|
|
160
|
+
expect(evt.category).toBe("tool");
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("getEventsByEvent filters by event name", () => {
|
|
165
|
+
const starts = getEventsByEvent("tool.start");
|
|
166
|
+
expect(starts.length).toBeGreaterThanOrEqual(2);
|
|
167
|
+
for (const evt of starts) {
|
|
168
|
+
expect(evt.event).toBe("tool.start");
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("getEventsByAgentId filters by agentId", () => {
|
|
173
|
+
const agentEvents = getEventsByAgentId(testAgent.id);
|
|
174
|
+
expect(agentEvents.length).toBeGreaterThanOrEqual(4);
|
|
175
|
+
for (const evt of agentEvents) {
|
|
176
|
+
expect(evt.agentId).toBe(testAgent.id);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("getEventsByTaskId filters by taskId", () => {
|
|
181
|
+
const taskEvents = getEventsByTaskId(taskId);
|
|
182
|
+
expect(taskEvents.length).toBeGreaterThanOrEqual(4);
|
|
183
|
+
for (const evt of taskEvents) {
|
|
184
|
+
expect(evt.taskId).toBe(taskId);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("getEventsBySessionId filters by sessionId", () => {
|
|
189
|
+
const sessionEvents = getEventsBySessionId(sessionId);
|
|
190
|
+
expect(sessionEvents.length).toBeGreaterThanOrEqual(4);
|
|
191
|
+
for (const evt of sessionEvents) {
|
|
192
|
+
expect(evt.sessionId).toBe(sessionId);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("getAllEvents respects limit", () => {
|
|
197
|
+
const events = getAllEvents(2);
|
|
198
|
+
expect(events.length).toBe(2);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("getAllEvents returns descending createdAt order", () => {
|
|
202
|
+
const events = getAllEvents(10);
|
|
203
|
+
for (let i = 1; i < events.length; i++) {
|
|
204
|
+
expect(events[i - 1].createdAt >= events[i].createdAt).toBe(true);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("getEventCounts groups by event name", () => {
|
|
209
|
+
const counts = getEventCounts();
|
|
210
|
+
expect(counts.length).toBeGreaterThanOrEqual(1);
|
|
211
|
+
const toolStart = counts.find((c) => c.event === "tool.start");
|
|
212
|
+
expect(toolStart).toBeDefined();
|
|
213
|
+
expect(toolStart!.count).toBeGreaterThanOrEqual(2);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("getEventCountsForAgent scopes to agent", () => {
|
|
217
|
+
const counts = getEventCountsForAgent(testAgent.id);
|
|
218
|
+
expect(counts.length).toBeGreaterThanOrEqual(1);
|
|
219
|
+
// All counted events should belong to this agent
|
|
220
|
+
const total = counts.reduce((sum, c) => sum + c.count, 0);
|
|
221
|
+
const agentEvents = getEventsByAgentId(testAgent.id);
|
|
222
|
+
expect(total).toBe(agentEvents.length);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe("getEventsFiltered", () => {
|
|
227
|
+
test("filters by multiple criteria", () => {
|
|
228
|
+
const events = getEventsFiltered({
|
|
229
|
+
category: "tool",
|
|
230
|
+
source: "worker",
|
|
231
|
+
agentId: testAgent.id,
|
|
232
|
+
limit: 5,
|
|
233
|
+
});
|
|
234
|
+
for (const evt of events) {
|
|
235
|
+
expect(evt.category).toBe("tool");
|
|
236
|
+
expect(evt.source).toBe("worker");
|
|
237
|
+
expect(evt.agentId).toBe(testAgent.id);
|
|
238
|
+
}
|
|
239
|
+
expect(events.length).toBeLessThanOrEqual(5);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("filters by status", () => {
|
|
243
|
+
createEvent({
|
|
244
|
+
category: "api",
|
|
245
|
+
event: "api.error",
|
|
246
|
+
source: "api",
|
|
247
|
+
status: "error",
|
|
248
|
+
data: { message: "timeout" },
|
|
249
|
+
});
|
|
250
|
+
const errors = getEventsFiltered({ status: "error" });
|
|
251
|
+
expect(errors.length).toBeGreaterThanOrEqual(1);
|
|
252
|
+
for (const evt of errors) {
|
|
253
|
+
expect(evt.status).toBe("error");
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("defaults limit to 100", () => {
|
|
258
|
+
const events = getEventsFiltered({});
|
|
259
|
+
expect(events.length).toBeLessThanOrEqual(100);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe("getEventCountsFiltered", () => {
|
|
264
|
+
test("filters counts by category", () => {
|
|
265
|
+
const counts = getEventCountsFiltered({ category: "tool" });
|
|
266
|
+
for (const c of counts) {
|
|
267
|
+
// Each counted event name should be a tool event
|
|
268
|
+
expect(c.event.startsWith("tool.")).toBe(true);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("filters counts by source", () => {
|
|
273
|
+
const counts = getEventCountsFiltered({ source: "api" });
|
|
274
|
+
expect(counts.length).toBeGreaterThanOrEqual(1);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("returns empty for non-matching filters", () => {
|
|
278
|
+
const counts = getEventCountsFiltered({ agentId: "nonexistent-agent-id" });
|
|
279
|
+
expect(counts.length).toBe(0);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe("data JSON round-trip", () => {
|
|
284
|
+
test("preserves nested objects in data field", () => {
|
|
285
|
+
const data = {
|
|
286
|
+
toolName: "Read",
|
|
287
|
+
nested: { deep: { value: 42 } },
|
|
288
|
+
array: [1, "two", { three: true }],
|
|
289
|
+
};
|
|
290
|
+
const evt = createEvent({
|
|
291
|
+
category: "tool",
|
|
292
|
+
event: "tool.start",
|
|
293
|
+
source: "worker",
|
|
294
|
+
data,
|
|
295
|
+
});
|
|
296
|
+
// Re-fetch from DB to verify round-trip
|
|
297
|
+
const fetched = getEventsFiltered({ limit: 1 });
|
|
298
|
+
const found = fetched.find((e) => e.id === evt.id);
|
|
299
|
+
expect(found).toBeDefined();
|
|
300
|
+
expect(found!.data).toEqual(data);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("handles null data gracefully", () => {
|
|
304
|
+
const evt = createEvent({
|
|
305
|
+
category: "system",
|
|
306
|
+
event: "system.boot",
|
|
307
|
+
source: "api",
|
|
308
|
+
});
|
|
309
|
+
const fetched = getEventsFiltered({ limit: 1000 });
|
|
310
|
+
const found = fetched.find((e) => e.id === evt.id);
|
|
311
|
+
expect(found).toBeDefined();
|
|
312
|
+
expect(found!.data).toBeUndefined();
|
|
313
|
+
});
|
|
314
|
+
});
|