@desplega.ai/agent-swarm 1.135.2 → 1.136.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/dist/{actions-3az2vwwn.js → actions-agcbc8nd.js} +6 -6
- package/dist/{app-vygnfvrh.js → app-g2s78xya.js} +3 -3
- package/dist/{assistant-2jr2frrm.js → assistant-yd3jkb9d.js} +8 -8
- package/dist/{boot-reembed-y7r9cxt0.js → boot-reembed-6spqw4q0.js} +3 -3
- package/dist/{boot-reembed-ncbg16rb.js → boot-reembed-b22s9w5x.js} +4 -4
- package/dist/{boot-scrub-logs-t82rwa0y.js → boot-scrub-logs-6h3cqkc0.js} +2 -2
- package/dist/{claude-adapter-vvx2aw3v.js → claude-adapter-ng5y8zd7.js} +4 -4
- package/dist/{cli-mfa2eaef.js → cli-35jknh9e.js} +5 -5
- package/dist/{cli-cv2vjg2y.js → cli-3y553gws.js} +1 -1
- package/dist/{cli-nhvym0d2.js → cli-4gzv54ss.js} +15 -6
- package/dist/{cli-4dakdn30.js → cli-4r4jkqep.js} +6 -6
- package/dist/{cli-83razmqg.js → cli-54ht104h.js} +3 -3
- package/dist/{cli-6290n4yz.js → cli-5bnz1t86.js} +1 -1
- package/dist/{cli-6bz2hzvw.js → cli-68vayryq.js} +2 -2
- package/dist/{cli-1z01xdqh.js → cli-6gwgzdgw.js} +2 -2
- package/dist/{cli-1dfatkzm.js → cli-84vrhesc.js} +1 -1
- package/dist/{cli-jfrh963q.js → cli-8v57p983.js} +10 -10
- package/dist/{cli-3f0tnh34.js → cli-ads0hfw2.js} +81 -23
- package/dist/{cli-fmha74ga.js → cli-bnq8h7ea.js} +3 -3
- package/dist/{cli-j3xnd5b4.js → cli-d5ygfv18.js} +7 -6
- package/dist/{cli-8f4h27gm.js → cli-edwrx42s.js} +1 -1
- package/dist/{cli-r5vwg1ma.js → cli-fpjmcbhz.js} +4 -4
- package/dist/{cli-w0ygcv43.js → cli-gpdtxzar.js} +6 -4
- package/dist/{cli-4rdjdddh.js → cli-hz5hp6wn.js} +1 -1
- package/dist/{cli-6mmwmy45.js → cli-j7qp8gtf.js} +1 -1
- package/dist/{cli-5a5nrq3g.js → cli-jjhnma34.js} +5 -5
- package/dist/{cli-ey09krp2.js → cli-m3z53bed.js} +1 -1
- package/dist/{cli-cf6vq6qa.js → cli-mgef08pc.js} +1 -1
- package/dist/{cli-0jhyxtdj.js → cli-qckgdqmr.js} +2 -2
- package/dist/{cli-a1zrc2g6.js → cli-qhfkxrpg.js} +108 -33
- package/dist/{cli-yw2z5kj9.js → cli-spf767vc.js} +1 -1
- package/dist/{cli-1857msnd.js → cli-tee8w7tq.js} +1 -1
- package/dist/{cli-qxn3g3yx.js → cli-tmn19cdz.js} +61 -6
- package/dist/{cli-7ccy0h4r.js → cli-y16cesaq.js} +2 -2
- package/dist/{cli-cm4z7w7w.js → cli-z6e8eqrm.js} +2 -2
- package/dist/cli.js +16 -16
- package/dist/{codex-adapter-6p290yq3.js → codex-adapter-kw11ps3j.js} +2 -2
- package/dist/{codex-session-runner-7b3dhpmx.js → codex-session-runner-sffb69v7.js} +2 -2
- package/dist/{commands-wxg1g4nq.js → commands-0xxp4tz8.js} +2 -2
- package/dist/{db-8fqeheve.js → db-0npp39zd.js} +6 -2
- package/dist/{handlers-gwtqp17c.js → handlers-5p7a1jx6.js} +15 -9
- package/dist/{hook-gd9fwfp0.js → hook-hpefr0a8.js} +2 -2
- package/dist/{hook-mrqygs7p.js → hook-jjpxn4se.js} +3 -3
- package/dist/{http-x4mc1mqg.js → http-wfp33653.js} +322 -61
- package/dist/{index-yd654kac.js → index-17h657rg.js} +12 -8
- package/dist/{index-pzgasj16.js → index-m2p7ctyh.js} +10 -10
- package/dist/{index-2mdqrd0n.js → index-racb4wfq.js} +9 -9
- package/dist/{index-4djjkp9z.js → index-zh66wm65.js} +8 -8
- package/dist/{keepalive-3tycspcy.js → keepalive-wxjd633a.js} +4 -4
- package/dist/{lead-js1h3z7d.js → lead-359ec2z0.js} +23 -23
- package/dist/{maintenance-y91ry8q4.js → maintenance-rr03qjtb.js} +4 -4
- package/dist/{mistral-conversations-hdmnkf6h.js → mistral-conversations-qdrnmrb8.js} +1 -1
- package/dist/{oauth-refresh-sweep-s33gtxrw.js → oauth-refresh-sweep-jwj22yb8.js} +3 -3
- package/dist/{onboard-vndj64mb.js → onboard-asnhchcv.js} +2 -2
- package/dist/{openai-completions-cwy2kkzg.js → openai-completions-yzftqrft.js} +33 -4
- package/dist/{otel-impl-d2p84k6e.js → otel-impl-54cra6mw.js} +18 -4
- package/dist/{pi-mono-adapter-1phspz2y.js → pi-mono-adapter-29g7axgj.js} +669 -335
- package/dist/{pricing-refresh-c2bwsyk5.js → pricing-refresh-mkyy7c2y.js} +4 -4
- package/dist/{rbac-roles-2pedz5e9.js → rbac-roles-ak56tr75.js} +2 -2
- package/dist/{rbac-roles-reef5y2d.js → rbac-roles-kb3afm95.js} +3 -3
- package/dist/{render-v2-zd5r5wgf.js → render-v2-434asg49.js} +3 -3
- package/dist/{seed-pricing-2kyk1vhg.js → seed-pricing-nbmd7ah8.js} +3 -3
- package/dist/{setup-5ys9pwyr.js → setup-wh4qmjs4.js} +2 -2
- package/dist/{worker-nb1f7k04.js → worker-j1d27tjv.js} +23 -23
- package/openapi.json +167 -2
- package/package.json +6 -6
- package/src/be/db-retention.ts +275 -0
- package/src/be/db.ts +82 -7
- package/src/be/swarm-config-guard.ts +30 -0
- package/src/heartbeat/heartbeat.ts +21 -0
- package/src/http/index.ts +33 -33
- package/src/http/route-def.ts +93 -4
- package/src/http/stats.ts +37 -1
- package/src/http/tasks.ts +93 -0
- package/src/http/utils.ts +94 -0
- package/src/otel-impl.ts +21 -3
- package/src/otel.ts +15 -4
- package/src/slack/blocks.ts +1 -0
- package/src/slack/handlers.ts +7 -0
- package/src/tasks/sibling-awareness.ts +1 -0
- package/src/tasks/sibling-block.ts +17 -4
- package/src/tests/artifact-sdk.test.ts +20 -15
- package/src/tests/db-retention.test.ts +344 -0
- package/src/tests/docs-index-footprint.test.js +4 -5
- package/src/tests/draft-task-dispatch-race.test.ts +189 -0
- package/src/tests/events-http.test.ts +70 -27
- package/src/tests/otel-abort-span.test.ts +156 -0
- package/src/tests/otel-span-kind.test.ts +59 -0
- package/src/tests/promote-draft-task-route.test.ts +175 -0
- package/src/tests/route-def-find-route.test.ts +90 -4
- package/src/tests/sibling-block.test.ts +29 -1
- package/src/tests/slack-assistant-comention-production.test.ts +7 -1
- package/src/tests/steering-enabled.test.ts +6 -0
- package/src/tests/swarm-config-reserved-keys.test.ts +27 -0
- package/src/types.ts +2 -1
- package/templates/skills/swarm-scripts/SKILL.md +4 -0
- package/templates/skills/swarm-scripts/content.md +4 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import { closeDb, getDbClient, initDb } from "../be/db";
|
|
4
|
+
import {
|
|
5
|
+
DB_RETENTION_TABLES,
|
|
6
|
+
getDbRetentionStats,
|
|
7
|
+
resetDbRetentionForTests,
|
|
8
|
+
runDbRetentionTick,
|
|
9
|
+
startDbRetention,
|
|
10
|
+
stopDbRetention,
|
|
11
|
+
} from "../be/db-retention";
|
|
12
|
+
import { MAX_DB_RETENTION_DAYS, validateConfigValue } from "../be/swarm-config-guard";
|
|
13
|
+
|
|
14
|
+
const TEST_DB_PATH = "./test-db-retention.sqlite";
|
|
15
|
+
const NOW = new Date("2026-08-23T12:00:00.000Z");
|
|
16
|
+
const RETENTION_KEYS = DB_RETENTION_TABLES.map((table) => table.envKey);
|
|
17
|
+
|
|
18
|
+
async function removeDbFiles(): Promise<void> {
|
|
19
|
+
for (const suffix of ["", "-wal", "-shm"])
|
|
20
|
+
await unlink(`${TEST_DB_PATH}${suffix}`).catch(() => undefined);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function insertRow(
|
|
24
|
+
table: (typeof DB_RETENTION_TABLES)[number]["table"],
|
|
25
|
+
id: string,
|
|
26
|
+
createdAt: string,
|
|
27
|
+
): Promise<void> {
|
|
28
|
+
const client = getDbClient();
|
|
29
|
+
if (table === "session_logs") {
|
|
30
|
+
await client.run(
|
|
31
|
+
"INSERT INTO session_logs (id, sessionId, iteration, cli, content, lineNumber, createdAt) VALUES (?, ?, 0, 'bun', 'log', 1, ?)",
|
|
32
|
+
[id, `session-${id}`, createdAt],
|
|
33
|
+
);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (table === "agent_log") {
|
|
37
|
+
await client.run("INSERT INTO agent_log (id, eventType, createdAt) VALUES (?, 'test', ?)", [
|
|
38
|
+
id,
|
|
39
|
+
createdAt,
|
|
40
|
+
]);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
await client.run(
|
|
44
|
+
"INSERT INTO events (id, category, event, source, createdAt) VALUES (?, 'test', 'retention.test', 'test', ?)",
|
|
45
|
+
[id, createdAt],
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function countRows(table: (typeof DB_RETENTION_TABLES)[number]["table"]): Promise<number> {
|
|
50
|
+
const row = await getDbClient().get<{ count: number }>(`SELECT COUNT(*) AS count FROM ${table}`);
|
|
51
|
+
return row?.count ?? 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
beforeAll(async () => {
|
|
55
|
+
closeDb();
|
|
56
|
+
await removeDbFiles();
|
|
57
|
+
initDb(TEST_DB_PATH);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterAll(async () => {
|
|
61
|
+
await resetDbRetentionForTests();
|
|
62
|
+
closeDb();
|
|
63
|
+
await removeDbFiles();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
beforeEach(async () => {
|
|
67
|
+
await resetDbRetentionForTests();
|
|
68
|
+
for (const table of DB_RETENTION_TABLES) await getDbClient().run(`DELETE FROM ${table.table}`);
|
|
69
|
+
for (const key of [...RETENTION_KEYS, "DB_RETENTION_DRY_RUN"]) delete process.env[key];
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
afterEach(async () => {
|
|
73
|
+
await stopDbRetention();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("DB retention", () => {
|
|
77
|
+
test("keeps the closed allowlist limited to the three approved tables", () => {
|
|
78
|
+
expect(DB_RETENTION_TABLES.map((table) => table.table)).toEqual([
|
|
79
|
+
"session_logs",
|
|
80
|
+
"agent_log",
|
|
81
|
+
"events",
|
|
82
|
+
]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("validates each retention setting as an integer of at least one day", () => {
|
|
86
|
+
for (const key of RETENTION_KEYS) {
|
|
87
|
+
expect(validateConfigValue(key, "30")).toBeNull();
|
|
88
|
+
expect(validateConfigValue(key, "0")).toContain("between 1");
|
|
89
|
+
expect(validateConfigValue(key, "-1")).toContain("integer");
|
|
90
|
+
expect(validateConfigValue(key, "abc")).toContain("integer");
|
|
91
|
+
expect(validateConfigValue(key, String(MAX_DB_RETENTION_DAYS))).toBeNull();
|
|
92
|
+
expect(validateConfigValue(key, String(MAX_DB_RETENTION_DAYS + 1))).toContain("between");
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("validates the dry-run setting as a strict boolean literal", () => {
|
|
97
|
+
for (const value of ["true", "false", "1", "0"]) {
|
|
98
|
+
expect(validateConfigValue("DB_RETENTION_DRY_RUN", value)).toBeNull();
|
|
99
|
+
}
|
|
100
|
+
expect(validateConfigValue("DB_RETENTION_DRY_RUN", "treu")).toContain(
|
|
101
|
+
"Invalid DB_RETENTION_DRY_RUN",
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("is opt-in and rejects invalid retention windows", async () => {
|
|
106
|
+
for (const table of DB_RETENTION_TABLES)
|
|
107
|
+
await insertRow(table.table, `${table.table}-old`, "2026-08-01T00:00:00.000Z");
|
|
108
|
+
process.env.SESSION_LOG_RETENTION_DAYS = "0";
|
|
109
|
+
process.env.AGENT_LOG_RETENTION_DAYS = "abc";
|
|
110
|
+
process.env.EVENTS_RETENTION_DAYS = "-1";
|
|
111
|
+
|
|
112
|
+
await runDbRetentionTick({ now: NOW });
|
|
113
|
+
|
|
114
|
+
for (const table of DB_RETENTION_TABLES) expect(await countRows(table.table)).toBe(1);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("sweeps only enabled tables and preserves rows at or after the ISO cutoff", async () => {
|
|
118
|
+
const cutoff = "2026-08-22T12:00:00.000Z";
|
|
119
|
+
for (const table of DB_RETENTION_TABLES) {
|
|
120
|
+
await insertRow(table.table, `${table.table}-old`, "2026-08-22T11:59:59.999Z");
|
|
121
|
+
await insertRow(table.table, `${table.table}-cutoff`, cutoff);
|
|
122
|
+
}
|
|
123
|
+
process.env.SESSION_LOG_RETENTION_DAYS = "1";
|
|
124
|
+
|
|
125
|
+
await runDbRetentionTick({ now: NOW });
|
|
126
|
+
|
|
127
|
+
expect(await countRows("session_logs")).toBe(1);
|
|
128
|
+
expect(await countRows("agent_log")).toBe(2);
|
|
129
|
+
expect(await countRows("events")).toBe(2);
|
|
130
|
+
expect(await getDbRetentionStats().sessionLogs).toMatchObject({
|
|
131
|
+
rowsDeleted: 1,
|
|
132
|
+
dryRun: false,
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("never deletes old rows from critical tables", async () => {
|
|
137
|
+
const old = "2020-01-01T00:00:00.000Z";
|
|
138
|
+
const client = getDbClient();
|
|
139
|
+
await client.run(
|
|
140
|
+
"INSERT INTO agents (id, name, status, createdAt, lastUpdatedAt) VALUES ('critical-agent', 'Critical', 'idle', ?, ?)",
|
|
141
|
+
[old, old],
|
|
142
|
+
);
|
|
143
|
+
await client.run(
|
|
144
|
+
"INSERT INTO agent_tasks (id, task, status, source, createdAt, lastUpdatedAt) VALUES ('critical-task', 'keep', 'completed', 'mcp', ?, ?)",
|
|
145
|
+
[old, old],
|
|
146
|
+
);
|
|
147
|
+
await client.run(
|
|
148
|
+
"INSERT INTO agent_memory (id, scope, name, content, source, createdAt, accessedAt) VALUES ('critical-memory', 'swarm', 'keep', 'keep', 'manual', ?, ?)",
|
|
149
|
+
[old, old],
|
|
150
|
+
);
|
|
151
|
+
await client.run(
|
|
152
|
+
"INSERT INTO permission_audit (id, ts, principalType, verb, decision, source) VALUES ('critical-audit', ?, 'operator', 'config.read', 'allow', 'http')",
|
|
153
|
+
[old],
|
|
154
|
+
);
|
|
155
|
+
for (const table of DB_RETENTION_TABLES) process.env[table.envKey] = "1";
|
|
156
|
+
|
|
157
|
+
await runDbRetentionTick({ now: NOW });
|
|
158
|
+
|
|
159
|
+
for (const table of ["agents", "agent_tasks", "agent_memory", "permission_audit"]) {
|
|
160
|
+
const row = await client.get<{ count: number }>(
|
|
161
|
+
`SELECT COUNT(*) AS count FROM ${table} WHERE id LIKE 'critical-%'`,
|
|
162
|
+
);
|
|
163
|
+
expect(row?.count).toBe(1);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("uses bounded batches and continues on the next tick", async () => {
|
|
168
|
+
process.env.SESSION_LOG_RETENTION_DAYS = "1";
|
|
169
|
+
for (let index = 0; index < 5; index++) {
|
|
170
|
+
await insertRow("session_logs", `old-${index}`, "2026-08-01T00:00:00.000Z");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
await runDbRetentionTick({ now: NOW, batchSize: 2, perTableBatchCap: 1 });
|
|
174
|
+
expect(await countRows("session_logs")).toBe(3);
|
|
175
|
+
await runDbRetentionTick({ now: NOW, batchSize: 2, perTableBatchCap: 1 });
|
|
176
|
+
expect(await countRows("session_logs")).toBe(1);
|
|
177
|
+
await runDbRetentionTick({ now: NOW, batchSize: 2, perTableBatchCap: 1 });
|
|
178
|
+
expect(await countRows("session_logs")).toBe(0);
|
|
179
|
+
expect(getDbRetentionStats().sessionLogs).toMatchObject({
|
|
180
|
+
rowsDeleted: 1,
|
|
181
|
+
batches: 1,
|
|
182
|
+
cumulativeRowsDeleted: 5,
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("dry run reports candidates without deleting any enabled table rows", async () => {
|
|
187
|
+
for (const table of DB_RETENTION_TABLES) {
|
|
188
|
+
process.env[table.envKey] = "1";
|
|
189
|
+
await insertRow(table.table, `${table.table}-old`, "2026-08-01T00:00:00.000Z");
|
|
190
|
+
}
|
|
191
|
+
process.env.DB_RETENTION_DRY_RUN = "true";
|
|
192
|
+
|
|
193
|
+
await runDbRetentionTick({ now: NOW });
|
|
194
|
+
|
|
195
|
+
for (const table of DB_RETENTION_TABLES) expect(await countRows(table.table)).toBe(1);
|
|
196
|
+
expect(getDbRetentionStats()).toMatchObject({
|
|
197
|
+
sessionLogs: { rowsDeleted: 1, dryRun: true },
|
|
198
|
+
agentLog: { rowsDeleted: 1, dryRun: true },
|
|
199
|
+
events: { rowsDeleted: 1, dryRun: true },
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("uses dry run when the deployed dry-run value is invalid", async () => {
|
|
204
|
+
process.env.SESSION_LOG_RETENTION_DAYS = "1";
|
|
205
|
+
process.env.DB_RETENTION_DRY_RUN = "treu";
|
|
206
|
+
await insertRow("session_logs", "invalid-dry-run-old", "2026-08-01T00:00:00.000Z");
|
|
207
|
+
|
|
208
|
+
await runDbRetentionTick({ now: NOW });
|
|
209
|
+
|
|
210
|
+
expect(await countRows("session_logs")).toBe(1);
|
|
211
|
+
expect(getDbRetentionStats().sessionLogs).toMatchObject({ rowsDeleted: 1, dryRun: true });
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("dry run reports an exact count above the former safety cap", async () => {
|
|
215
|
+
process.env.SESSION_LOG_RETENTION_DAYS = "1";
|
|
216
|
+
process.env.DB_RETENTION_DRY_RUN = "true";
|
|
217
|
+
await getDbClient().run(
|
|
218
|
+
`WITH RECURSIVE candidates(value) AS (
|
|
219
|
+
SELECT 1
|
|
220
|
+
UNION ALL
|
|
221
|
+
SELECT value + 1 FROM candidates WHERE value < 250001
|
|
222
|
+
)
|
|
223
|
+
INSERT INTO session_logs (id, sessionId, iteration, cli, content, lineNumber, createdAt)
|
|
224
|
+
SELECT 'dry-' || value, 'dry-session', 0, 'bun', 'log', value, '2026-08-01T00:00:00.000Z'
|
|
225
|
+
FROM candidates`,
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
await runDbRetentionTick({ now: NOW });
|
|
229
|
+
|
|
230
|
+
expect(getDbRetentionStats().sessionLogs?.rowsDeleted).toBe(250_001);
|
|
231
|
+
expect(await countRows("session_logs")).toBe(250_001);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("dry run scans sparse tables in bounded keyset pages when no rows are eligible", async () => {
|
|
235
|
+
process.env.SESSION_LOG_RETENTION_DAYS = "1";
|
|
236
|
+
process.env.DB_RETENTION_DRY_RUN = "true";
|
|
237
|
+
await getDbClient().run(
|
|
238
|
+
`WITH RECURSIVE recent(value) AS (
|
|
239
|
+
SELECT 1
|
|
240
|
+
UNION ALL
|
|
241
|
+
SELECT value + 1 FROM recent WHERE value < 10000
|
|
242
|
+
)
|
|
243
|
+
INSERT INTO session_logs (id, sessionId, iteration, cli, content, lineNumber, createdAt)
|
|
244
|
+
SELECT 'recent-' || printf('%05d', value), 'recent-session', 0, 'bun', 'log', value,
|
|
245
|
+
'2026-08-23T11:59:59.999Z'
|
|
246
|
+
FROM recent`,
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
await runDbRetentionTick({ now: NOW, batchSize: 100, wallClockCapMs: 1 });
|
|
250
|
+
|
|
251
|
+
expect(getDbRetentionStats().sessionLogs).toBeUndefined();
|
|
252
|
+
expect(await countRows("session_logs")).toBe(10_000);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("bounds pages fetched on a non-dry sweep of a sparse table with no eligible rows", async () => {
|
|
256
|
+
process.env.SESSION_LOG_RETENTION_DAYS = "1";
|
|
257
|
+
await getDbClient().run(
|
|
258
|
+
`WITH RECURSIVE recent(value) AS (
|
|
259
|
+
SELECT 1
|
|
260
|
+
UNION ALL
|
|
261
|
+
SELECT value + 1 FROM recent WHERE value < 10000
|
|
262
|
+
)
|
|
263
|
+
INSERT INTO session_logs (id, sessionId, iteration, cli, content, lineNumber, createdAt)
|
|
264
|
+
SELECT 'recent-' || printf('%05d', value), 'recent-session', 0, 'bun', 'log', value,
|
|
265
|
+
'2026-08-23T11:59:59.999Z'
|
|
266
|
+
FROM recent`,
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
const startedAt = Date.now();
|
|
270
|
+
await runDbRetentionTick({ now: NOW, batchSize: 1, perTableBatchCap: 1 });
|
|
271
|
+
|
|
272
|
+
// A page counter must cap the loop even when zero rows are eligible for
|
|
273
|
+
// deletion, since batches only increments on a page with a match. Without
|
|
274
|
+
// that cap this sweep walks all 10,000 pages (with a 5ms yield between
|
|
275
|
+
// each) instead of stopping after the one page the cap allows.
|
|
276
|
+
expect(Date.now() - startedAt).toBeLessThan(500);
|
|
277
|
+
expect(getDbRetentionStats().sessionLogs).toMatchObject({ rowsDeleted: 0, batches: 0 });
|
|
278
|
+
expect(await countRows("session_logs")).toBe(10_000);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("stops a dry-run count when its wall-clock budget expires", async () => {
|
|
282
|
+
process.env.SESSION_LOG_RETENTION_DAYS = "1";
|
|
283
|
+
process.env.DB_RETENTION_DRY_RUN = "true";
|
|
284
|
+
await getDbClient().run(
|
|
285
|
+
`WITH RECURSIVE candidates(value) AS (
|
|
286
|
+
SELECT 1
|
|
287
|
+
UNION ALL
|
|
288
|
+
SELECT value + 1 FROM candidates WHERE value < 1000
|
|
289
|
+
)
|
|
290
|
+
INSERT INTO session_logs (id, sessionId, iteration, cli, content, lineNumber, createdAt)
|
|
291
|
+
SELECT 'deadline-' || value, 'deadline-session', 0, 'bun', 'log', value, '2026-08-01T00:00:00.000Z'
|
|
292
|
+
FROM candidates`,
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
const startedAt = Date.now();
|
|
296
|
+
await runDbRetentionTick({ now: NOW, batchSize: 1, wallClockCapMs: 10 });
|
|
297
|
+
|
|
298
|
+
expect(Date.now() - startedAt).toBeLessThan(150);
|
|
299
|
+
expect(await countRows("session_logs")).toBe(1000);
|
|
300
|
+
expect(getDbRetentionStats().sessionLogs).toBeUndefined();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("rejects an excessive window without aborting later table sweeps", async () => {
|
|
304
|
+
process.env.SESSION_LOG_RETENTION_DAYS = String(MAX_DB_RETENTION_DAYS + 1);
|
|
305
|
+
process.env.AGENT_LOG_RETENTION_DAYS = "1";
|
|
306
|
+
await insertRow("session_logs", "oversized-window", "2026-08-01T00:00:00.000Z");
|
|
307
|
+
await insertRow("agent_log", "valid-window", "2026-08-01T00:00:00.000Z");
|
|
308
|
+
|
|
309
|
+
await runDbRetentionTick({ now: NOW });
|
|
310
|
+
|
|
311
|
+
expect(await countRows("session_logs")).toBe(1);
|
|
312
|
+
expect(await countRows("agent_log")).toBe(0);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("shutdown cancels an in-flight sweep between batches and waits for it", async () => {
|
|
316
|
+
process.env.SESSION_LOG_RETENTION_DAYS = "1";
|
|
317
|
+
for (let index = 0; index < 100; index++) {
|
|
318
|
+
await insertRow("session_logs", `shutdown-${index}`, "2026-08-01T00:00:00.000Z");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const tick = runDbRetentionTick({ now: NOW, batchSize: 1, perTableBatchCap: 100 });
|
|
322
|
+
while ((await countRows("session_logs")) === 100) {
|
|
323
|
+
await Bun.sleep(1);
|
|
324
|
+
}
|
|
325
|
+
await stopDbRetention();
|
|
326
|
+
await tick;
|
|
327
|
+
|
|
328
|
+
const remaining = await countRows("session_logs");
|
|
329
|
+
expect(remaining).toBeGreaterThan(0);
|
|
330
|
+
expect(remaining).toBeLessThan(100);
|
|
331
|
+
await Bun.sleep(20);
|
|
332
|
+
expect(await countRows("session_logs")).toBe(remaining);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("runs the first lifecycle tick immediately and can stop cleanly", async () => {
|
|
336
|
+
process.env.SESSION_LOG_RETENTION_DAYS = "1";
|
|
337
|
+
await insertRow("session_logs", "lifecycle-old", "2026-08-01T00:00:00.000Z");
|
|
338
|
+
|
|
339
|
+
await startDbRetention(60_000);
|
|
340
|
+
|
|
341
|
+
expect(await countRows("session_logs")).toBe(0);
|
|
342
|
+
await stopDbRetention();
|
|
343
|
+
});
|
|
344
|
+
});
|
|
@@ -78,7 +78,7 @@ describe("docs API index footprint", () => {
|
|
|
78
78
|
expect(tasks).toContain('"path":"/api/task-templates","method":"get"');
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
-
test("OpenAPI and generated pages keep all
|
|
81
|
+
test("OpenAPI and generated pages keep all 352 operations", async () => {
|
|
82
82
|
const spec = await Bun.file(resolve(repoRoot, "openapi.json")).json();
|
|
83
83
|
const operationKeys = [];
|
|
84
84
|
const tags = new Set();
|
|
@@ -102,18 +102,17 @@ describe("docs API index footprint", () => {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
const generatedKeys = await generatedOperationKeys();
|
|
105
|
-
expect(operationKeys).toHaveLength(
|
|
106
|
-
expect(generatedKeys).toHaveLength(
|
|
105
|
+
expect(operationKeys).toHaveLength(352);
|
|
106
|
+
expect(generatedKeys).toHaveLength(352);
|
|
107
107
|
expect(new Set(generatedKeys)).toEqual(new Set(operationKeys));
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
-
test("keeps retired routes out of the
|
|
110
|
+
test("keeps retired routes out of the sitemap source inventory", async () => {
|
|
111
111
|
const urls = contentRoutes().map((route) => `${baseUrl}${route}`);
|
|
112
112
|
const sitemapSource = await Bun.file(resolve(repoRoot, "docs-site/app/sitemap.ts")).text();
|
|
113
113
|
|
|
114
114
|
expect(sitemapSource).toContain("source.getPages().map((page) => ({");
|
|
115
115
|
expect(sitemapSource).toMatch(/url:\s*`\$\{baseUrl\}\$\{page\.url\}`/);
|
|
116
|
-
expect(urls).toHaveLength(135);
|
|
117
116
|
expect(new Set(urls).size).toBe(urls.length);
|
|
118
117
|
for (const route of retiredRoutes) {
|
|
119
118
|
expect(urls).not.toContain(`${baseUrl}${route}`);
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #1240 — a UI task with attachments is pool-claimable before its
|
|
3
|
+
* attachments exist. Covers the `draft` status that closes the race:
|
|
4
|
+
* created dispatch-ineligible, promoted once the upload batch settles (or
|
|
5
|
+
* times out via the heartbeat sweep for an abandoned draft).
|
|
6
|
+
*/
|
|
7
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
8
|
+
import { unlink } from "node:fs/promises";
|
|
9
|
+
import {
|
|
10
|
+
claimTask,
|
|
11
|
+
closeDb,
|
|
12
|
+
createAgent,
|
|
13
|
+
createTaskExtended,
|
|
14
|
+
getDbClient,
|
|
15
|
+
getPendingTaskForAgent,
|
|
16
|
+
getTaskById,
|
|
17
|
+
initDb,
|
|
18
|
+
promoteAbandonedDraftTasks,
|
|
19
|
+
promoteDraftTask,
|
|
20
|
+
} from "../be/db";
|
|
21
|
+
|
|
22
|
+
const TEST_DB_PATH = "./test-draft-task-dispatch-race.sqlite";
|
|
23
|
+
|
|
24
|
+
describe("Draft task dispatch race (#1240)", () => {
|
|
25
|
+
beforeAll(async () => {
|
|
26
|
+
try {
|
|
27
|
+
await unlink(TEST_DB_PATH);
|
|
28
|
+
} catch {
|
|
29
|
+
// File doesn't exist
|
|
30
|
+
}
|
|
31
|
+
closeDb();
|
|
32
|
+
initDb(TEST_DB_PATH);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterAll(async () => {
|
|
36
|
+
closeDb();
|
|
37
|
+
try {
|
|
38
|
+
await unlink(TEST_DB_PATH);
|
|
39
|
+
await unlink(`${TEST_DB_PATH}-wal`);
|
|
40
|
+
await unlink(`${TEST_DB_PATH}-shm`);
|
|
41
|
+
} catch {
|
|
42
|
+
// Files may not exist
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
beforeEach(async () => {
|
|
47
|
+
await getDbClient().run("DELETE FROM agent_tasks");
|
|
48
|
+
await getDbClient().run("DELETE FROM agents");
|
|
49
|
+
await getDbClient().run("DELETE FROM agent_log");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("status:'draft' wins over agentId — task lands in draft, not pending", async () => {
|
|
53
|
+
const agent = await createAgent({ name: "lead-1", isLead: true, status: "idle" });
|
|
54
|
+
const task = await createTaskExtended("Draft task with owner", {
|
|
55
|
+
agentId: agent.id,
|
|
56
|
+
status: "draft",
|
|
57
|
+
});
|
|
58
|
+
expect(task.status).toBe("draft");
|
|
59
|
+
expect(task.agentId).toBe(agent.id);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("status:'draft' wins over offeredTo — task lands in draft, not offered", async () => {
|
|
63
|
+
const agent = await createAgent({ name: "offeree-1", isLead: false, status: "idle" });
|
|
64
|
+
const task = await createTaskExtended("Draft task offered", {
|
|
65
|
+
offeredTo: agent.id,
|
|
66
|
+
status: "draft",
|
|
67
|
+
});
|
|
68
|
+
expect(task.status).toBe("draft");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("a draft task assigned to an agent is NOT returned by getPendingTaskForAgent", async () => {
|
|
72
|
+
const agent = await createAgent({ name: "lead-2", isLead: true, status: "idle" });
|
|
73
|
+
await createTaskExtended("Draft task, uploads in flight", {
|
|
74
|
+
agentId: agent.id,
|
|
75
|
+
status: "draft",
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const pending = await getPendingTaskForAgent(agent.id);
|
|
79
|
+
expect(pending).toBeNull();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("a draft task with no owner is NOT returned by pool claim", async () => {
|
|
83
|
+
const agent = await createAgent({ name: "worker-1", isLead: false, status: "idle" });
|
|
84
|
+
const task = await createTaskExtended("Draft task, no owner yet", {
|
|
85
|
+
status: "draft",
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const claimed = await claimTask(task.id, agent.id);
|
|
89
|
+
expect(claimed).toBeNull();
|
|
90
|
+
|
|
91
|
+
const stored = await getTaskById(task.id);
|
|
92
|
+
expect(stored?.status).toBe("draft");
|
|
93
|
+
expect(stored?.agentId).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("promoteDraftTask moves an owned draft to pending, then it dispatches normally", async () => {
|
|
97
|
+
const agent = await createAgent({ name: "lead-3", isLead: true, status: "idle" });
|
|
98
|
+
const task = await createTaskExtended("Draft task to promote", {
|
|
99
|
+
agentId: agent.id,
|
|
100
|
+
status: "draft",
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Not dispatch-eligible yet.
|
|
104
|
+
expect(await getPendingTaskForAgent(agent.id)).toBeNull();
|
|
105
|
+
|
|
106
|
+
const promoted = await promoteDraftTask(task.id);
|
|
107
|
+
expect(promoted?.status).toBe("pending");
|
|
108
|
+
expect(promoted?.agentId).toBe(agent.id);
|
|
109
|
+
|
|
110
|
+
// Now dispatch-eligible.
|
|
111
|
+
const pending = await getPendingTaskForAgent(agent.id);
|
|
112
|
+
expect(pending?.id).toBe(task.id);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("promoteDraftTask moves an unowned draft to unassigned, then pool claim succeeds", async () => {
|
|
116
|
+
const worker = await createAgent({ name: "worker-2", isLead: false, status: "idle" });
|
|
117
|
+
const task = await createTaskExtended("Draft task, no owner", { status: "draft" });
|
|
118
|
+
|
|
119
|
+
expect(await claimTask(task.id, worker.id)).toBeNull();
|
|
120
|
+
|
|
121
|
+
const promoted = await promoteDraftTask(task.id);
|
|
122
|
+
expect(promoted?.status).toBe("unassigned");
|
|
123
|
+
|
|
124
|
+
const claimed = await claimTask(task.id, worker.id);
|
|
125
|
+
expect(claimed?.status).toBe("in_progress");
|
|
126
|
+
expect(claimed?.agentId).toBe(worker.id);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("promoteDraftTask moves an offered draft to offered", async () => {
|
|
130
|
+
const agent = await createAgent({ name: "offeree-2", isLead: false, status: "idle" });
|
|
131
|
+
const task = await createTaskExtended("Draft task, offered", {
|
|
132
|
+
offeredTo: agent.id,
|
|
133
|
+
status: "draft",
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const promoted = await promoteDraftTask(task.id);
|
|
137
|
+
expect(promoted?.status).toBe("offered");
|
|
138
|
+
expect(promoted?.offeredTo).toBe(agent.id);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("promoteDraftTask is a no-op on a task that already left draft", async () => {
|
|
142
|
+
const task = await createTaskExtended("Already pending", {});
|
|
143
|
+
expect(task.status).toBe("unassigned");
|
|
144
|
+
|
|
145
|
+
const result = await promoteDraftTask(task.id);
|
|
146
|
+
expect(result).toBeNull();
|
|
147
|
+
|
|
148
|
+
const stored = await getTaskById(task.id);
|
|
149
|
+
expect(stored?.status).toBe("unassigned");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("promoteAbandonedDraftTasks leaves a fresh draft untouched", async () => {
|
|
153
|
+
const agent = await createAgent({ name: "lead-4", isLead: true, status: "idle" });
|
|
154
|
+
const task = await createTaskExtended("Fresh draft", {
|
|
155
|
+
agentId: agent.id,
|
|
156
|
+
status: "draft",
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const promotedCount = await promoteAbandonedDraftTasks(5);
|
|
160
|
+
expect(promotedCount).toBe(0);
|
|
161
|
+
|
|
162
|
+
const stored = await getTaskById(task.id);
|
|
163
|
+
expect(stored?.status).toBe("draft");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("promoteAbandonedDraftTasks promotes a draft past the timeout", async () => {
|
|
167
|
+
const agent = await createAgent({ name: "lead-5", isLead: true, status: "idle" });
|
|
168
|
+
const task = await createTaskExtended("Abandoned draft (tab closed)", {
|
|
169
|
+
agentId: agent.id,
|
|
170
|
+
status: "draft",
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Simulate an abandoned draft — back-date lastUpdatedAt past the timeout.
|
|
174
|
+
const staleTime = new Date(Date.now() - 10 * 60 * 1000).toISOString();
|
|
175
|
+
await getDbClient().run("UPDATE agent_tasks SET lastUpdatedAt = ? WHERE id = ?", [
|
|
176
|
+
staleTime,
|
|
177
|
+
task.id,
|
|
178
|
+
]);
|
|
179
|
+
|
|
180
|
+
const promotedCount = await promoteAbandonedDraftTasks(5);
|
|
181
|
+
expect(promotedCount).toBe(1);
|
|
182
|
+
|
|
183
|
+
const stored = await getTaskById(task.id);
|
|
184
|
+
expect(stored?.status).toBe("pending");
|
|
185
|
+
|
|
186
|
+
const pending = await getPendingTaskForAgent(agent.id);
|
|
187
|
+
expect(pending?.id).toBe(task.id);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
2
|
import { unlink } from "node:fs/promises";
|
|
3
3
|
import type { Subprocess } from "bun";
|
|
4
|
-
import { getFreePort, SERVER_BOOT_HOOK_TIMEOUT_MS
|
|
4
|
+
import { getFreePort, SERVER_BOOT_HOOK_TIMEOUT_MS } from "./test-net";
|
|
5
5
|
|
|
6
6
|
let TEST_PORT = 0;
|
|
7
7
|
const TEST_DB_PATH = `/tmp/test-events-http-${Date.now()}.sqlite`;
|
|
@@ -10,6 +10,44 @@ const TEST_API_KEY = "test-events-http-key";
|
|
|
10
10
|
|
|
11
11
|
let serverProc: Subprocess;
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Poll the child until the authenticated events route answers.
|
|
15
|
+
*
|
|
16
|
+
* A refused connection means the child is still booting: keep waiting. The
|
|
17
|
+
* ceiling matches waitForServer() in test-net.ts because a boot under
|
|
18
|
+
* --parallel load on a shared runner takes far longer than the ~1 s it takes
|
|
19
|
+
* idle, and restarting a slow child only resets its progress.
|
|
20
|
+
*
|
|
21
|
+
* Any HTTP response that is not a 200 events list comes from another server:
|
|
22
|
+
* a parallel test claimed the released getFreePort() socket first and the
|
|
23
|
+
* child's own listen failed with EADDRINUSE. Report "foreign" so the caller
|
|
24
|
+
* retries on a fresh port right away.
|
|
25
|
+
*/
|
|
26
|
+
async function waitForEventsApi(timeoutMs = 60_000): Promise<"ready" | "foreign"> {
|
|
27
|
+
const deadline = Date.now() + timeoutMs;
|
|
28
|
+
while (Date.now() < deadline) {
|
|
29
|
+
try {
|
|
30
|
+
const response = await fetch(`${BASE}/api/events`, {
|
|
31
|
+
headers: { Authorization: `Bearer ${TEST_API_KEY}` },
|
|
32
|
+
});
|
|
33
|
+
const body = (await response.json().catch(() => null)) as { events?: unknown } | null;
|
|
34
|
+
return response.status === 200 && Array.isArray(body?.events) ? "ready" : "foreign";
|
|
35
|
+
} catch {
|
|
36
|
+
// Connection refused: the child has not bound the port yet.
|
|
37
|
+
}
|
|
38
|
+
await Bun.sleep(50);
|
|
39
|
+
}
|
|
40
|
+
throw new Error(`Events API did not become ready on ${BASE} within ${timeoutMs}ms`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function cleanupTestDb(): Promise<void> {
|
|
44
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
45
|
+
try {
|
|
46
|
+
await unlink(`${TEST_DB_PATH}${suffix}`);
|
|
47
|
+
} catch {}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
13
51
|
async function api(
|
|
14
52
|
method: string,
|
|
15
53
|
path: string,
|
|
@@ -36,31 +74,40 @@ async function api(
|
|
|
36
74
|
const get = (p: string) => api("GET", p);
|
|
37
75
|
const post = (p: string, body?: unknown) => api("POST", p, { body });
|
|
38
76
|
|
|
77
|
+
const PORT_CLAIM_ATTEMPTS = 3;
|
|
78
|
+
|
|
39
79
|
beforeAll(async () => {
|
|
40
|
-
|
|
41
|
-
|
|
80
|
+
for (let attempt = 1; attempt <= PORT_CLAIM_ATTEMPTS; attempt++) {
|
|
81
|
+
TEST_PORT = await getFreePort();
|
|
82
|
+
BASE = `http://localhost:${TEST_PORT}`;
|
|
83
|
+
await cleanupTestDb();
|
|
42
84
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
85
|
+
serverProc = Bun.spawn(["bun", "src/http.ts"], {
|
|
86
|
+
cwd: `${import.meta.dir}/../..`,
|
|
87
|
+
env: {
|
|
88
|
+
...process.env,
|
|
89
|
+
PORT: String(TEST_PORT),
|
|
90
|
+
DATABASE_PATH: TEST_DB_PATH,
|
|
91
|
+
API_KEY: TEST_API_KEY,
|
|
92
|
+
CAPABILITIES: "core",
|
|
93
|
+
SLACK_BOT_TOKEN: "",
|
|
94
|
+
GITHUB_WEBHOOK_SECRET: "",
|
|
95
|
+
AGENTMAIL_API_KEY: "",
|
|
96
|
+
},
|
|
97
|
+
stdout: "ignore",
|
|
98
|
+
stderr: "ignore",
|
|
99
|
+
});
|
|
46
100
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
env: {
|
|
50
|
-
...process.env,
|
|
51
|
-
PORT: String(TEST_PORT),
|
|
52
|
-
DATABASE_PATH: TEST_DB_PATH,
|
|
53
|
-
API_KEY: TEST_API_KEY,
|
|
54
|
-
CAPABILITIES: "core",
|
|
55
|
-
SLACK_BOT_TOKEN: "",
|
|
56
|
-
GITHUB_WEBHOOK_SECRET: "",
|
|
57
|
-
AGENTMAIL_API_KEY: "",
|
|
58
|
-
},
|
|
59
|
-
stdout: "ignore",
|
|
60
|
-
stderr: "ignore",
|
|
61
|
-
});
|
|
101
|
+
// A slow boot throws here and fails the suite: afterAll still kills the child.
|
|
102
|
+
if ((await waitForEventsApi()) === "ready") return;
|
|
62
103
|
|
|
63
|
-
|
|
104
|
+
// Another server owns this port. Drop the child and try a fresh port.
|
|
105
|
+
serverProc.kill();
|
|
106
|
+
await serverProc.exited.catch(() => {});
|
|
107
|
+
}
|
|
108
|
+
throw new Error(
|
|
109
|
+
`another server claimed the events-http port on ${PORT_CLAIM_ATTEMPTS} consecutive attempts`,
|
|
110
|
+
);
|
|
64
111
|
}, SERVER_BOOT_HOOK_TIMEOUT_MS);
|
|
65
112
|
|
|
66
113
|
afterAll(async () => {
|
|
@@ -71,11 +118,7 @@ afterAll(async () => {
|
|
|
71
118
|
} catch {}
|
|
72
119
|
}
|
|
73
120
|
await Bun.sleep(50);
|
|
74
|
-
|
|
75
|
-
await unlink(TEST_DB_PATH);
|
|
76
|
-
await unlink(`${TEST_DB_PATH}-wal`);
|
|
77
|
-
await unlink(`${TEST_DB_PATH}-shm`);
|
|
78
|
-
} catch {}
|
|
121
|
+
await cleanupTestDb();
|
|
79
122
|
});
|
|
80
123
|
|
|
81
124
|
describe("POST /api/events — single event", () => {
|