@desplega.ai/agent-swarm 2.2.0 → 2.3.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/{acp-adapter-k0vwfyvn.js → acp-adapter-v85a2bqy.js} +1 -1
- package/dist/{actions-w4pnb8vz.js → actions-zgcxfspj.js} +15 -15
- package/dist/{amp-adapter-0cmxb2t5.js → amp-adapter-jw9xfz42.js} +2 -2
- package/dist/{app-zneem7g9.js → app-4e1s4675.js} +3 -3
- package/dist/{assistant-wbzd2rnr.js → assistant-v238n9dk.js} +16 -16
- package/dist/{boot-reembed-4kx69j3g.js → boot-reembed-p666cw5n.js} +1 -1
- package/dist/{boot-reembed-db0wwgy1.js → boot-reembed-w6pm64j4.js} +1 -1
- package/dist/{boot-scrub-sweep-tfv4d3mm.js → boot-scrub-sweep-jcyjagn3.js} +2 -2
- package/dist/{boot-seeds-w2hmkxn5.js → boot-seeds-gbedmamp.js} +4 -4
- package/dist/{claude-adapter-5qrc2paq.js → claude-adapter-q7wspzt8.js} +2 -2
- package/dist/{claude-managed-setup-7ra32em4.js → claude-managed-setup-wza784fb.js} +1 -1
- package/dist/{claude-sdk-session-cd02gc76.js → claude-sdk-session-yf400ptr.js} +2 -2
- package/dist/{cli-p46xdr1m.js → cli-0rh20bp3.js} +1 -1
- package/dist/{cli-e5y285rg.js → cli-1v8w782f.js} +1 -1
- package/dist/{cli-8c1m9mhy.js → cli-30m5q8mc.js} +5 -5
- package/dist/{cli-xnp2vkx7.js → cli-4hmaheb6.js} +6 -6
- package/dist/{cli-xhgtay7w.js → cli-4s1kqrqg.js} +1 -1
- package/dist/{cli-2jm992rm.js → cli-5d1m1qca.js} +2 -2
- package/dist/{cli-mvsfbbgq.js → cli-65q70v2v.js} +3 -3
- package/dist/{cli-th8krqbf.js → cli-6p7ddg72.js} +2 -2
- package/dist/{cli-6q7pyd3k.js → cli-6vcyw6v7.js} +1 -1
- package/dist/{cli-bddxamg7.js → cli-8qsx817b.js} +23 -22
- package/dist/{cli-ztcxv5nv.js → cli-9e9kjqsj.js} +1 -1
- package/dist/{cli-jrk8mj0t.js → cli-a17tzmn0.js} +1 -1
- package/dist/{cli-raejy0mp.js → cli-d43xqzbe.js} +1 -1
- package/dist/{cli-tba8fxf9.js → cli-dxwh4j32.js} +11 -11
- package/dist/{cli-c9vrwcpb.js → cli-ea7chd26.js} +19 -19
- package/dist/{cli-vmbakbq0.js → cli-fhvqt2fp.js} +1 -1
- package/dist/{cli-z3dk189k.js → cli-fp4xbw07.js} +6 -6
- package/dist/{cli-xx13gqf3.js → cli-g6p7c2my.js} +1 -1
- package/dist/{cli-c0dpzwnx.js → cli-hn3pftj2.js} +2 -2
- package/dist/{cli-h23mr2s6.js → cli-hz64bcnj.js} +2 -2
- package/dist/{cli-dw0njd9q.js → cli-jhmjwqcg.js} +5 -5
- package/dist/{cli-0k9rcwfy.js → cli-ka01m5rb.js} +28 -12
- package/dist/{cli-jhhb2v9n.js → cli-kqypty34.js} +2 -2
- package/dist/{cli-hwc4a9xm.js → cli-q1812jbw.js} +4 -4
- package/dist/{cli-8w65vwee.js → cli-qrwp40st.js} +1 -1
- package/dist/{cli-4c7k75rq.js → cli-re4wkbzx.js} +54 -41
- package/dist/{cli-z1hh99j0.js → cli-tr24eeny.js} +2 -2
- package/dist/{cli-q6gw45wj.js → cli-wtg6edvd.js} +1 -1
- package/dist/{cli-mnry16kh.js → cli-xgn4rwyv.js} +4 -4
- package/dist/{cli-5ks6x36q.js → cli-y3dvz6ge.js} +3 -3
- package/dist/{cli-ksrphveb.js → cli-yv0vgscc.js} +1 -0
- package/dist/{cli-ak4c12mh.js → cli-yv3n83x8.js} +1 -1
- package/dist/{cli-9ac0mkg5.js → cli-zes4tsbb.js} +1 -1
- package/dist/cli.js +14 -14
- package/dist/{codex-adapter-m0fx3v3s.js → codex-adapter-nhjj2veb.js} +1 -1
- package/dist/{codex-session-runner-j9m2gfyy.js → codex-session-runner-6hggrjbx.js} +1 -1
- package/dist/{commands-ktnqwrv4.js → commands-mrw1ph2w.js} +2 -2
- package/dist/{controller-nex2he8v.js → controller-pt1zam5w.js} +2 -2
- package/dist/{controllers-connect-6yhwqv9v.js → controllers-connect-ymw5541d.js} +16 -16
- package/dist/{cursor-adapter-02s5ekva.js → cursor-adapter-cf4zh5x0.js} +1 -1
- package/dist/{db-19tw9r5s.js → db-hhpy6m4x.js} +2 -2
- package/dist/{db-jvr86v7f.js → db-hy80219q.js} +1 -1
- package/dist/{dispatcher-d3mnsg2y.js → dispatcher-6pdjtkr1.js} +14 -14
- package/dist/{dsh-adapter-63rvej88.js → dsh-adapter-fbx06re2.js} +1 -1
- package/dist/{external-channel-44cwer32.js → external-channel-j6vb6ya1.js} +2 -2
- package/dist/{grok-adapter-ysp907f6.js → grok-adapter-1193sprp.js} +2 -2
- package/dist/{handlers-t5pbabwf.js → handlers-tb3xgjnm.js} +17 -17
- package/dist/{heartbeat-2etacsqq.js → heartbeat-h1w1ktce.js} +17 -17
- package/dist/{hook-3kvmax6d.js → hook-ejja4bpe.js} +1 -1
- package/dist/{hook-mzxxs5n2.js → hook-skrbja18.js} +1 -1
- package/dist/{http-fn978m0g.js → http-mzh41mfj.js} +108 -52
- package/dist/{index-039x2sag.js → index-6rmnn89d.js} +40 -12
- package/dist/{index-w2wr89xd.js → index-epr2gnbw.js} +1 -1
- package/dist/{index-qyjbb7br.js → index-nyfcdpky.js} +17 -17
- package/dist/{index-e4tsa1h7.js → index-vjn3wbzf.js} +14 -14
- package/dist/{index-1r8hw4vd.js → index-xmmkyx82.js} +14 -14
- package/dist/{keepalive-d4nzs76x.js → keepalive-4djvbm2j.js} +3 -3
- package/dist/{lead-8h5n9k5f.js → lead-61kqp604.js} +23 -23
- package/dist/{maintenance-ea6jfw42.js → maintenance-f3xhnjsm.js} +1 -1
- package/dist/{oauth-refresh-sweep-3f25djq3.js → oauth-refresh-sweep-fpd6x6rw.js} +2 -2
- package/dist/{onboard-pvk7hdb2.js → onboard-196tjgyn.js} +2 -2
- package/dist/{otel-impl-qb7nhgdp.js → otel-impl-g8fh4c21.js} +1 -1
- package/dist/{pricing-refresh-6ehwkcmp.js → pricing-refresh-sdcf5yaz.js} +3 -3
- package/dist/{rbac-roles-9648nh00.js → rbac-roles-2x4g1wb5.js} +2 -2
- package/dist/{rbac-roles-km0f9cmm.js → rbac-roles-bsp5657v.js} +1 -1
- package/dist/{render-v2-xkze1wef.js → render-v2-0pyb39bb.js} +3 -3
- package/dist/{resume-tq0bbvt5.js → resume-a86tkj6r.js} +5 -5
- package/dist/{setup-rnrazxvd.js → setup-ypbzcapd.js} +1 -1
- package/dist/{work-objects-e75q2rqs.js → work-objects-yjp8de12.js} +17 -17
- package/dist/{worker-4ww4hjef.js → worker-4ptnn1ge.js} +23 -23
- package/openapi.json +168 -6
- package/package.json +2 -2
- package/src/be/asset-key-audit.ts +4 -13
- package/src/be/db.ts +57 -33
- package/src/be/migrations/227_inbox_item_state_last_read_at.sql +1 -0
- package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +22 -4
- package/src/heartbeat/heartbeat.ts +25 -7
- package/src/http/assets.ts +26 -4
- package/src/http/inbox-state.ts +58 -8
- package/src/kv-reserved-namespaces.ts +1 -0
- package/src/providers/amp-adapter.ts +1 -1
- package/src/tests/asset-key-api.test.ts +128 -0
- package/src/tests/asset-key-invariants.test.ts +179 -3
- package/src/tests/asset-key-migration.test.ts +35 -0
- package/src/tests/docs-index-footprint.test.js +2 -2
- package/src/tests/heartbeat-checklist.test.ts +88 -0
- package/src/tests/heartbeat-jev-gate.test.ts +394 -10
- package/src/tests/inbox-state.test.ts +167 -0
- package/src/tests/kv-tool.test.ts +15 -0
- package/src/tests/script-connections.test.ts +2 -0
- package/src/types.ts +2 -0
- package/templates/skills/code-quality/content.md +2 -2
- package/templates/skills/pages/content.md +4 -0
- package/templates/skills/swarm-scripts/SKILL.md +6 -0
- package/templates/skills/swarm-scripts/content.md +6 -0
|
@@ -4,6 +4,7 @@ import { closeDb, createTaskExtended, createUser, getDb, getDbClient, initDb } f
|
|
|
4
4
|
const FRESH_DB = "./test-asset-key-migration-fresh.sqlite";
|
|
5
5
|
const HISTORICAL_DB = "./test-asset-key-migration-historical.sqlite";
|
|
6
6
|
const HISTORY_REBUILD_DB = "./test-asset-key-migration-history-rebuild.sqlite";
|
|
7
|
+
const INBOX_UPGRADE_DB = "./test-asset-key-migration-inbox-upgrade.sqlite";
|
|
7
8
|
const LEGACY_STATUS_DB = "./test-asset-key-migration-legacy-status.sqlite";
|
|
8
9
|
|
|
9
10
|
const globals = globalThis as typeof globalThis & {
|
|
@@ -97,6 +98,7 @@ beforeAll(async () => {
|
|
|
97
98
|
await removeDb(HISTORICAL_DB);
|
|
98
99
|
await removeDb(HISTORY_REBUILD_DB);
|
|
99
100
|
await removeDb(LEGACY_STATUS_DB);
|
|
101
|
+
await removeDb(INBOX_UPGRADE_DB);
|
|
100
102
|
initDb(FRESH_DB);
|
|
101
103
|
});
|
|
102
104
|
|
|
@@ -108,6 +110,7 @@ afterAll(async () => {
|
|
|
108
110
|
await removeDb(HISTORICAL_DB);
|
|
109
111
|
await removeDb(HISTORY_REBUILD_DB);
|
|
110
112
|
await removeDb(LEGACY_STATUS_DB);
|
|
113
|
+
await removeDb(INBOX_UPGRADE_DB);
|
|
111
114
|
});
|
|
112
115
|
|
|
113
116
|
describe("asset namespace key migrations", () => {
|
|
@@ -173,6 +176,38 @@ describe("asset namespace key migrations", () => {
|
|
|
173
176
|
expect(historySchema).toContain("'script'");
|
|
174
177
|
});
|
|
175
178
|
|
|
179
|
+
test("inbox read cursor is nullable on fresh databases and has no backfill on upgrade", async () => {
|
|
180
|
+
const column = await getDbClient().get<{ notnull: number; dflt_value: string | null }>(
|
|
181
|
+
"SELECT \"notnull\", dflt_value FROM pragma_table_info('inbox_item_state') WHERE name = 'lastReadAt'",
|
|
182
|
+
);
|
|
183
|
+
expect(column).toEqual({ notnull: 0, dflt_value: null });
|
|
184
|
+
closeDb();
|
|
185
|
+
initDb(INBOX_UPGRADE_DB);
|
|
186
|
+
try {
|
|
187
|
+
const user = await createUser({ name: "Upgrade User", email: "inbox-upgrade@example.com" });
|
|
188
|
+
const readAt = "2026-01-01T00:00:00.000Z";
|
|
189
|
+
await getDbClient().run(
|
|
190
|
+
`INSERT INTO inbox_item_state (userId, itemType, itemId, status, readAt)
|
|
191
|
+
VALUES (?, 'to_read', 'existing-item', 'done', ?)`,
|
|
192
|
+
[user.id, readAt],
|
|
193
|
+
);
|
|
194
|
+
await getDbClient().run("ALTER TABLE inbox_item_state DROP COLUMN lastReadAt");
|
|
195
|
+
await getDbClient().run("DELETE FROM _migrations WHERE version = 227");
|
|
196
|
+
closeDb();
|
|
197
|
+
initDb(INBOX_UPGRADE_DB);
|
|
198
|
+
expect(
|
|
199
|
+
await getDbClient().get(
|
|
200
|
+
"SELECT status, readAt, lastReadAt FROM inbox_item_state WHERE itemId = 'existing-item'",
|
|
201
|
+
),
|
|
202
|
+
).toEqual({ status: "done", readAt, lastReadAt: null });
|
|
203
|
+
closeDb();
|
|
204
|
+
expect(() => initDb(INBOX_UPGRADE_DB)).not.toThrow();
|
|
205
|
+
} finally {
|
|
206
|
+
closeDb();
|
|
207
|
+
initDb(FRESH_DB);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
176
211
|
test("old insert statements may omit key and repeated shared namespaces are valid", () => {
|
|
177
212
|
const now = new Date().toISOString();
|
|
178
213
|
const taskId = crypto.randomUUID();
|
|
@@ -103,8 +103,8 @@ describe("docs API index footprint", () => {
|
|
|
103
103
|
expect(spec.paths["/api/agents/{id}/runtime"]?.get).toBeDefined();
|
|
104
104
|
|
|
105
105
|
const generatedKeys = await generatedOperationKeys();
|
|
106
|
-
expect(operationKeys).toHaveLength(
|
|
107
|
-
expect(generatedKeys).toHaveLength(
|
|
106
|
+
expect(operationKeys).toHaveLength(431);
|
|
107
|
+
expect(generatedKeys).toHaveLength(431);
|
|
108
108
|
expect(new Set(generatedKeys)).toEqual(new Set(operationKeys));
|
|
109
109
|
});
|
|
110
110
|
|
|
@@ -13,10 +13,14 @@ import {
|
|
|
13
13
|
closeDb,
|
|
14
14
|
createAgent,
|
|
15
15
|
createTaskExtended,
|
|
16
|
+
deletePromptTemplate,
|
|
16
17
|
getDbClient,
|
|
18
|
+
getKv,
|
|
19
|
+
getRecentFailedTasks,
|
|
17
20
|
initDb,
|
|
18
21
|
startTask,
|
|
19
22
|
updateAgentProfile,
|
|
23
|
+
upsertPromptTemplate,
|
|
20
24
|
upsertSwarmConfig,
|
|
21
25
|
} from "../be/db";
|
|
22
26
|
import { getScript, upsertScriptByName } from "../be/scripts/db";
|
|
@@ -240,6 +244,89 @@ describe("Heartbeat Checklist", () => {
|
|
|
240
244
|
expect(tasks[0]!.priority).toBe(60);
|
|
241
245
|
});
|
|
242
246
|
|
|
247
|
+
test("receipt stores exactly the ten rendered full IDs from twelve failures", async () => {
|
|
248
|
+
const lead = await createAgent({ name: "lead", isLead: true, status: "idle" });
|
|
249
|
+
await updateAgentProfile(lead.id, { heartbeatMd: "- Check failures" });
|
|
250
|
+
for (let i = 0; i < 12; i++) {
|
|
251
|
+
const task = await createTaskExtended(`receipt failure ${i}`, { agentId: lead.id });
|
|
252
|
+
await getDbClient().run(
|
|
253
|
+
"UPDATE agent_tasks SET status = 'failed', failureReason = ?, finishedAt = ? WHERE id = ?",
|
|
254
|
+
[`receipt reason ${i}`, new Date(Date.now() - (i + 1) * 60e3).toISOString(), task.id],
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
const failures = await getRecentFailedTasks(6);
|
|
258
|
+
const before = Date.now();
|
|
259
|
+
await checkHeartbeatChecklist();
|
|
260
|
+
const checklist = await getDbClient().get<{ id: string; task: string }>(
|
|
261
|
+
"SELECT id, task FROM agent_tasks WHERE taskType = 'heartbeat-checklist'",
|
|
262
|
+
);
|
|
263
|
+
expect(checklist).toBeDefined();
|
|
264
|
+
const receipt = await getKv("heartbeat-checklist:reported-failures", checklist!.id);
|
|
265
|
+
expect(receipt?.value).toEqual(failures.slice(0, 10).map((task) => task.id));
|
|
266
|
+
expect(receipt?.expiresAt).toBeGreaterThanOrEqual(before + 7 * 24 * 3600e3);
|
|
267
|
+
expect(receipt?.expiresAt).toBeLessThanOrEqual(Date.now() + 7 * 24 * 3600e3);
|
|
268
|
+
for (const failure of failures.slice(0, 10)) {
|
|
269
|
+
expect(checklist!.task).toContain(`[${failure.id.slice(0, 8)}]`);
|
|
270
|
+
}
|
|
271
|
+
for (const failure of failures.slice(10)) {
|
|
272
|
+
expect(checklist!.task).not.toContain(`[${failure.id.slice(0, 8)}]`);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("receipt excludes failures when the global checklist template omits system status", async () => {
|
|
277
|
+
const lead = await createAgent({ name: "lead", isLead: true, status: "idle" });
|
|
278
|
+
await updateAgentProfile(lead.id, { heartbeatMd: "- Check standing orders" });
|
|
279
|
+
const failure = await createTaskExtended("Unrendered failure", { agentId: lead.id });
|
|
280
|
+
await getDbClient().run(
|
|
281
|
+
"UPDATE agent_tasks SET status = 'failed', failureReason = ?, finishedAt = ? WHERE id = ?",
|
|
282
|
+
["boom", new Date(Date.now() - 1000).toISOString(), failure.id],
|
|
283
|
+
);
|
|
284
|
+
const template = upsertPromptTemplate({
|
|
285
|
+
eventType: "heartbeat.checklist",
|
|
286
|
+
scope: "global",
|
|
287
|
+
state: "enabled",
|
|
288
|
+
body: "Review standing orders:\n{{heartbeat_content}}",
|
|
289
|
+
});
|
|
290
|
+
try {
|
|
291
|
+
await checkHeartbeatChecklist();
|
|
292
|
+
const checklist = await getDbClient().get<{ id: string; task: string }>(
|
|
293
|
+
"SELECT id, task FROM agent_tasks WHERE taskType = 'heartbeat-checklist'",
|
|
294
|
+
);
|
|
295
|
+
expect(checklist?.task).toBe("Review standing orders:\n- Check standing orders");
|
|
296
|
+
expect(
|
|
297
|
+
(await getKv("heartbeat-checklist:reported-failures", checklist!.id))?.value,
|
|
298
|
+
).toEqual([]);
|
|
299
|
+
} finally {
|
|
300
|
+
await deletePromptTemplate(template.id);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("receipt write errors log and preserve the created checklist", async () => {
|
|
305
|
+
const lead = await createAgent({ name: "lead", isLead: true, status: "idle" });
|
|
306
|
+
await updateAgentProfile(lead.id, { heartbeatMd: "- Check failures" });
|
|
307
|
+
await getDbClient().run(`CREATE TRIGGER reject_heartbeat_receipt
|
|
308
|
+
BEFORE INSERT ON kv_entries
|
|
309
|
+
WHEN NEW.namespace = 'heartbeat-checklist:reported-failures'
|
|
310
|
+
BEGIN SELECT RAISE(FAIL, 'receipt unavailable'); END`);
|
|
311
|
+
const log = spyOn(console, "error").mockImplementation(() => {});
|
|
312
|
+
try {
|
|
313
|
+
await checkHeartbeatChecklist();
|
|
314
|
+
const checklist = await getDbClient().get<{ id: string }>(
|
|
315
|
+
"SELECT id FROM agent_tasks WHERE taskType = 'heartbeat-checklist'",
|
|
316
|
+
);
|
|
317
|
+
expect(checklist).toBeDefined();
|
|
318
|
+
expect(log).toHaveBeenCalledWith(
|
|
319
|
+
expect.stringContaining(
|
|
320
|
+
`Failed to save reported failures for checklist ${checklist!.id}`,
|
|
321
|
+
),
|
|
322
|
+
expect.any(Error),
|
|
323
|
+
);
|
|
324
|
+
} finally {
|
|
325
|
+
log.mockRestore();
|
|
326
|
+
await getDbClient().run("DROP TRIGGER reject_heartbeat_receipt");
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
243
330
|
test("dedup: skips when active heartbeat-checklist task exists for lead", async () => {
|
|
244
331
|
const lead = await createAgent({ name: "lead", isLead: true, status: "idle" });
|
|
245
332
|
await updateAgentProfile(lead.id, {
|
|
@@ -725,6 +812,7 @@ describe("Heartbeat Checklist", () => {
|
|
|
725
812
|
expect(tasks.length).toBe(1);
|
|
726
813
|
expect(tasks[0]!.agentId).toBe(lead.id);
|
|
727
814
|
expect(tasks[0]!.priority).toBe(70);
|
|
815
|
+
expect(await getKv("heartbeat-checklist:reported-failures", tasks[0]!.id)).toBeNull();
|
|
728
816
|
});
|
|
729
817
|
|
|
730
818
|
test("boot-triage task includes reboot context", async () => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
1
2
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
4
|
import {
|
|
@@ -19,7 +20,9 @@ type Fixture = {
|
|
|
19
20
|
configs: Record<string, string>;
|
|
20
21
|
heartbeatMd?: string;
|
|
21
22
|
prevStatus?: string;
|
|
23
|
+
reportedFailures?: unknown;
|
|
22
24
|
prevOutput?: string | null;
|
|
25
|
+
prevCreatedAt?: string | null;
|
|
23
26
|
failed?: Array<Record<string, unknown>>;
|
|
24
27
|
approvalStatus?: string;
|
|
25
28
|
/** Stands in for `ctx.swarm.systemOne_decide`; returns the bridge result. */
|
|
@@ -33,6 +36,7 @@ function fakeCtx(f: Fixture) {
|
|
|
33
36
|
kvSets: 0,
|
|
34
37
|
decides: [] as Array<Record<string, any>>,
|
|
35
38
|
approvalReads: 0,
|
|
39
|
+
previousReads: 0,
|
|
36
40
|
};
|
|
37
41
|
const table = (columns: string[], rows: unknown[][]) => ({
|
|
38
42
|
success: true,
|
|
@@ -56,17 +60,48 @@ function fakeCtx(f: Fixture) {
|
|
|
56
60
|
},
|
|
57
61
|
};
|
|
58
62
|
},
|
|
59
|
-
db_query: async ({ sql }: { sql: string }) => {
|
|
63
|
+
db_query: async ({ sql, params = [] }: { sql: string; params?: any[] }) => {
|
|
60
64
|
if (sql.includes("FROM approval_requests")) {
|
|
61
65
|
calls.approvalReads++;
|
|
62
66
|
return table(["status"], f.approvalStatus ? [[f.approvalStatus]] : []);
|
|
63
67
|
}
|
|
64
68
|
if (sql.includes("heartbeatMd")) return table(["heartbeatMd"], [[f.heartbeatMd ?? ""]]);
|
|
65
|
-
if (sql.includes("'heartbeat-checklist'")) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
if (sql.includes("'heartbeat-checklist'") || sql.includes("finishedAt <= ?")) {
|
|
70
|
+
if (sql.includes("'heartbeat-checklist'")) calls.previousReads++;
|
|
71
|
+
// Execute historical selection against real rows, including its bounds and limit.
|
|
72
|
+
const db = new Database(":memory:");
|
|
73
|
+
try {
|
|
74
|
+
db.run(`CREATE TABLE agent_tasks (
|
|
75
|
+
id TEXT, output TEXT, task TEXT, agentId TEXT, taskType TEXT,
|
|
76
|
+
status TEXT, createdAt TEXT, finishedAt TEXT
|
|
77
|
+
)`);
|
|
78
|
+
if (f.prevCreatedAt !== null)
|
|
79
|
+
db.run("INSERT INTO agent_tasks VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [
|
|
80
|
+
"prev-id",
|
|
81
|
+
f.prevOutput ?? null,
|
|
82
|
+
f.prevStatus ?? "",
|
|
83
|
+
LEAD,
|
|
84
|
+
"heartbeat-checklist",
|
|
85
|
+
"completed",
|
|
86
|
+
f.prevCreatedAt ?? "2026-10-15T13:00:00.000Z",
|
|
87
|
+
null,
|
|
88
|
+
]);
|
|
89
|
+
for (const failure of f.failed ?? []) {
|
|
90
|
+
db.run("INSERT INTO agent_tasks VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [
|
|
91
|
+
failure.id,
|
|
92
|
+
null,
|
|
93
|
+
failure.task,
|
|
94
|
+
failure.agentId,
|
|
95
|
+
"test",
|
|
96
|
+
"failed",
|
|
97
|
+
"2026-10-15T06:00:00.000Z",
|
|
98
|
+
failure.finishedAt,
|
|
99
|
+
]);
|
|
100
|
+
}
|
|
101
|
+
return { data: { rows: db.query(sql).all(...params) } };
|
|
102
|
+
} finally {
|
|
103
|
+
db.close();
|
|
104
|
+
}
|
|
70
105
|
}
|
|
71
106
|
if (sql.includes("completed24")) {
|
|
72
107
|
return table(
|
|
@@ -75,7 +110,10 @@ function fakeCtx(f: Fixture) {
|
|
|
75
110
|
);
|
|
76
111
|
}
|
|
77
112
|
if (sql.includes("status = 'failed' AND finishedAt")) {
|
|
78
|
-
const rows = f.failed ?? []
|
|
113
|
+
const rows = (f.failed ?? [])
|
|
114
|
+
.filter((r) => String(r.finishedAt) > params[0])
|
|
115
|
+
.sort((a, b) => String(b.finishedAt).localeCompare(String(a.finishedAt)))
|
|
116
|
+
.slice(0, 10);
|
|
79
117
|
return table(
|
|
80
118
|
["id", "agentId", "task", "failureReason", "finishedAt"],
|
|
81
119
|
rows.map((r) => [r.id, r.agentId, r.task, r.failureReason, r.finishedAt]),
|
|
@@ -93,8 +131,13 @@ function fakeCtx(f: Fixture) {
|
|
|
93
131
|
}
|
|
94
132
|
throw new Error(`unexpected sql: ${sql}`);
|
|
95
133
|
},
|
|
96
|
-
kv_getOrNull: async ({ key }: { key: string }) =>
|
|
97
|
-
|
|
134
|
+
kv_getOrNull: async ({ namespace, key }: { namespace: string; key: string }) => {
|
|
135
|
+
if (namespace === "heartbeat-checklist:reported-failures") {
|
|
136
|
+
expect(key).toBe("prev-id");
|
|
137
|
+
return f.reportedFailures === undefined ? null : { key, value: f.reportedFailures };
|
|
138
|
+
}
|
|
139
|
+
return kv.has(key) ? { key, value: kv.get(key) } : null;
|
|
140
|
+
},
|
|
98
141
|
kv_set: async ({ key, value }: { key: string; value: unknown }) => {
|
|
99
142
|
calls.kvSets++;
|
|
100
143
|
kv.set(key, value);
|
|
@@ -168,6 +211,7 @@ describe("heartbeat-jev-gate with a provider", () => {
|
|
|
168
211
|
test("wake at or above the threshold, with the new failures and due items in summary", async () => {
|
|
169
212
|
const { ctx } = fakeCtx({
|
|
170
213
|
configs: { TYPESAFE_API_KEY: "x" },
|
|
214
|
+
reportedFailures: ["aaaaaaaa-0000"],
|
|
171
215
|
heartbeatMd: [
|
|
172
216
|
"# Standing orders",
|
|
173
217
|
`- Watch: release on ${MMDD} — lift after ship`,
|
|
@@ -176,7 +220,7 @@ describe("heartbeat-jev-gate with a provider", () => {
|
|
|
176
220
|
].join("\n"),
|
|
177
221
|
// The previous checklist already listed aaaaaaaa, so only bbbbbbbb is new.
|
|
178
222
|
prevStatus:
|
|
179
|
-
'## Recent Failures (last 6h) [auto-generated]\n- [aaaaaaaa] "old failure" — agent: 12345678, reason: boom, at: 2026-10-15T12:00:00Z\n',
|
|
223
|
+
'## Current System Status [auto-generated]\n## Recent Failures (last 6h) [auto-generated]\n- [aaaaaaaa] "old failure" — agent: 12345678, reason: boom, at: 2026-10-15T12:00:00Z\n',
|
|
180
224
|
prevOutput: "All clear.\n\nNothing to do.",
|
|
181
225
|
failed: [
|
|
182
226
|
{
|
|
@@ -217,6 +261,346 @@ describe("heartbeat-jev-gate with a provider", () => {
|
|
|
217
261
|
});
|
|
218
262
|
});
|
|
219
263
|
|
|
264
|
+
for (const ordersFirst of [true, false]) {
|
|
265
|
+
test(`previous failures are not new with standing orders ${ordersFirst ? "before" : "after"} status`, async () => {
|
|
266
|
+
const failures =
|
|
267
|
+
'## Recent Failures (last 6h) [auto-generated]\n- [aaaaaaaa] "old failure" — agent: 12345678, reason: boom, at: 2026-10-15T12:00:00Z\n';
|
|
268
|
+
const orders = "## Your Standing Orders\n- Already triaged.\n";
|
|
269
|
+
const status = `## Current System Status [auto-generated]\n${failures}`;
|
|
270
|
+
const { ctx } = fakeCtx({
|
|
271
|
+
configs: { TYPESAFE_API_KEY: "x" },
|
|
272
|
+
reportedFailures: ["aaaaaaaa-0000"],
|
|
273
|
+
prevStatus: ordersFirst ? orders + status : status + orders,
|
|
274
|
+
failed: [
|
|
275
|
+
{
|
|
276
|
+
id: "aaaaaaaa-0000",
|
|
277
|
+
agentId: "12345678-bbbb",
|
|
278
|
+
task: "old failure",
|
|
279
|
+
failureReason: "boom",
|
|
280
|
+
finishedAt: "2026-10-15T12:00:00Z",
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
id: "bbbbbbbb-0000",
|
|
284
|
+
agentId: "12345678-bbbb",
|
|
285
|
+
task: "new failure",
|
|
286
|
+
failureReason: "exit 1",
|
|
287
|
+
finishedAt: "2026-10-15T13:01:00.000Z",
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
jev: jevSays(0.9),
|
|
291
|
+
});
|
|
292
|
+
const verdict = await run(ctx, { dryRun: true });
|
|
293
|
+
expect(verdict.state.new_failures_since_previous_heartbeat).toEqual([
|
|
294
|
+
"bbbbbbbb: new failure | exit 1",
|
|
295
|
+
]);
|
|
296
|
+
expect(verdict.state.failures_last_6h_total).toBe(2);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
test.each([
|
|
301
|
+
"## Recent Failures (last 6h) [auto-generated]\nNone.\n",
|
|
302
|
+
"",
|
|
303
|
+
])("failures outside the previous Recent Failures section stay new (%s)", async (section) => {
|
|
304
|
+
const { ctx } = fakeCtx({
|
|
305
|
+
configs: { TYPESAFE_API_KEY: "x" },
|
|
306
|
+
prevStatus: `## Current System Status [auto-generated]\n${section}## Gate Findings [auto-generated]\n- [aaaaaaaa] "quoted failure" — agent: 12345678, reason: boom, at: 2026-10-15T12:00:00Z\n`,
|
|
307
|
+
failed: [
|
|
308
|
+
{
|
|
309
|
+
id: "aaaaaaaa-0000",
|
|
310
|
+
agentId: "12345678-bbbb",
|
|
311
|
+
task: "quoted failure",
|
|
312
|
+
failureReason: "boom",
|
|
313
|
+
finishedAt: "2026-10-15T13:01:00.000Z",
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
jev: jevSays(0.9),
|
|
317
|
+
});
|
|
318
|
+
expect((await run(ctx, { dryRun: true })).state.new_failures_since_previous_heartbeat).toEqual([
|
|
319
|
+
"aaaaaaaa: quoted failure | boom",
|
|
320
|
+
]);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test.each([
|
|
324
|
+
"- Item 544: fix `## Recent Failures` parsing.\n",
|
|
325
|
+
"## Recent Failures\nSnapshot notes.\n",
|
|
326
|
+
'## Recent Failures (last 6h) [auto-generated]\n- [bbbbbbbb] "snapshot quote" — agent: 12345678, reason: boom, at: 2026-10-15T12:00:00Z\n',
|
|
327
|
+
])("snapshot failure headings cannot hijack generated status (%s)", async (snapshot) => {
|
|
328
|
+
const { ctx, calls } = fakeCtx({
|
|
329
|
+
configs: { TYPESAFE_API_KEY: "x" },
|
|
330
|
+
reportedFailures: ["aaaaaaaa-0000"],
|
|
331
|
+
prevStatus: [
|
|
332
|
+
"## Your Standing Orders (snapshot from HEARTBEAT.md)",
|
|
333
|
+
snapshot,
|
|
334
|
+
"## Instructions",
|
|
335
|
+
"Review the generated status.",
|
|
336
|
+
"## Current System Status [auto-generated]",
|
|
337
|
+
"## Recent Failures (last 6h) [auto-generated]",
|
|
338
|
+
'- [aaaaaaaa] "old failure" — agent: 12345678, reason: boom, at: 2026-10-15T12:00:00Z',
|
|
339
|
+
].join("\n"),
|
|
340
|
+
failed: [
|
|
341
|
+
{
|
|
342
|
+
id: "aaaaaaaa-0000",
|
|
343
|
+
agentId: "12345678-bbbb",
|
|
344
|
+
task: "old failure",
|
|
345
|
+
failureReason: "boom",
|
|
346
|
+
finishedAt: "2026-10-15T12:00:00Z",
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
id: "bbbbbbbb-0000",
|
|
350
|
+
agentId: "12345678-bbbb",
|
|
351
|
+
task: "snapshot quote",
|
|
352
|
+
failureReason: "boom",
|
|
353
|
+
finishedAt: "2026-10-15T13:01:00.000Z",
|
|
354
|
+
},
|
|
355
|
+
],
|
|
356
|
+
jev: jevSays(0.9),
|
|
357
|
+
});
|
|
358
|
+
expect((await run(ctx, { dryRun: true })).state.new_failures_since_previous_heartbeat).toEqual([
|
|
359
|
+
"bbbbbbbb: snapshot quote | boom",
|
|
360
|
+
]);
|
|
361
|
+
expect(calls.previousReads).toBe(1);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("an absent generated failures section cannot use a trailing snapshot heading", async () => {
|
|
365
|
+
const { ctx, calls } = fakeCtx({
|
|
366
|
+
configs: { TYPESAFE_API_KEY: "x" },
|
|
367
|
+
prevStatus: [
|
|
368
|
+
"## Current System Status [auto-generated]",
|
|
369
|
+
"## Task Overview [auto-generated]",
|
|
370
|
+
"- Failed (24h): 0",
|
|
371
|
+
"## Agent Status [auto-generated]",
|
|
372
|
+
"- Online: 6",
|
|
373
|
+
"## Your Standing Orders (snapshot from HEARTBEAT.md)",
|
|
374
|
+
"## Recent Failures (last 6h) [auto-generated]",
|
|
375
|
+
'- [aaaaaaaa] "snapshot quote" — agent: 12345678, reason: boom, at: 2026-10-15T12:00:00Z',
|
|
376
|
+
].join("\n"),
|
|
377
|
+
failed: [
|
|
378
|
+
{
|
|
379
|
+
id: "aaaaaaaa-0000",
|
|
380
|
+
agentId: "12345678-bbbb",
|
|
381
|
+
task: "snapshot quote",
|
|
382
|
+
failureReason: "boom",
|
|
383
|
+
finishedAt: "2026-10-15T13:01:00.000Z",
|
|
384
|
+
},
|
|
385
|
+
],
|
|
386
|
+
jev: jevSays(0.9),
|
|
387
|
+
});
|
|
388
|
+
expect((await run(ctx, { dryRun: true })).state.new_failures_since_previous_heartbeat).toEqual([
|
|
389
|
+
"aaaaaaaa: snapshot quote | boom",
|
|
390
|
+
]);
|
|
391
|
+
expect(calls.previousReads).toBe(1);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test("a failed-task title heading does not hide later failures", async () => {
|
|
395
|
+
const { ctx, calls } = fakeCtx({
|
|
396
|
+
configs: { TYPESAFE_API_KEY: "x" },
|
|
397
|
+
reportedFailures: ["aaaaaaaa-0000", "bbbbbbbb-0000"],
|
|
398
|
+
prevStatus: [
|
|
399
|
+
"## Current System Status [auto-generated]",
|
|
400
|
+
"## Recent Failures (last 6h) [auto-generated]",
|
|
401
|
+
'- [aaaaaaaa] "Fix the task\n## Requirements" — agent: 12345678, reason: boom, at: 2026-10-15T12:00:00Z',
|
|
402
|
+
'- [bbbbbbbb] "later failure" — agent: 12345678, reason: boom, at: 2026-10-15T12:00:00Z',
|
|
403
|
+
"## Gate Findings [auto-generated]",
|
|
404
|
+
'- [cccccccc] "outside failures" — agent: 12345678, reason: boom, at: 2026-10-15T12:00:00Z',
|
|
405
|
+
].join("\n"),
|
|
406
|
+
failed: [
|
|
407
|
+
{
|
|
408
|
+
id: "aaaaaaaa-0000",
|
|
409
|
+
agentId: "12345678-bbbb",
|
|
410
|
+
task: "Fix the task\n## Requirements",
|
|
411
|
+
failureReason: "boom",
|
|
412
|
+
finishedAt: "2026-10-15T12:00:00Z",
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
id: "bbbbbbbb-0000",
|
|
416
|
+
agentId: "12345678-bbbb",
|
|
417
|
+
task: "later failure",
|
|
418
|
+
failureReason: "boom",
|
|
419
|
+
finishedAt: "2026-10-15T12:00:00Z",
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
id: "cccccccc-0000",
|
|
423
|
+
agentId: "12345678-bbbb",
|
|
424
|
+
task: "outside failures",
|
|
425
|
+
failureReason: "boom",
|
|
426
|
+
finishedAt: "2026-10-15T13:01:00.000Z",
|
|
427
|
+
},
|
|
428
|
+
],
|
|
429
|
+
jev: jevSays(0.9),
|
|
430
|
+
});
|
|
431
|
+
const verdict = await run(ctx, { dryRun: true });
|
|
432
|
+
expect(verdict.state.new_failures_since_previous_heartbeat).toEqual([
|
|
433
|
+
"cccccccc: outside failures | boom",
|
|
434
|
+
]);
|
|
435
|
+
expect(verdict.state.failures_last_6h_total).toBe(3);
|
|
436
|
+
expect(calls.previousReads).toBe(1);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test("code-fenced snapshot system and agent markers cannot hide previous failures", async () => {
|
|
440
|
+
const { ctx, calls } = fakeCtx({
|
|
441
|
+
configs: { TYPESAFE_API_KEY: "x" },
|
|
442
|
+
reportedFailures: ["aaaaaaaa-0000"],
|
|
443
|
+
prevStatus: [
|
|
444
|
+
"## Your Standing Orders (snapshot from HEARTBEAT.md)",
|
|
445
|
+
"```md",
|
|
446
|
+
"## Current System Status [auto-generated]",
|
|
447
|
+
"## Agent Status [auto-generated]",
|
|
448
|
+
"```",
|
|
449
|
+
"## Current System Status [auto-generated]",
|
|
450
|
+
"## Recent Failures (last 6h) [auto-generated]",
|
|
451
|
+
'- [aaaaaaaa] "old failure" — agent: 12345678, reason: boom, at: 2026-10-15T12:00:00.000Z',
|
|
452
|
+
"## Agent Status [auto-generated]",
|
|
453
|
+
].join("\n"),
|
|
454
|
+
failed: [
|
|
455
|
+
{
|
|
456
|
+
id: "aaaaaaaa-0000",
|
|
457
|
+
agentId: "12345678-bbbb",
|
|
458
|
+
task: "old failure",
|
|
459
|
+
failureReason: "boom",
|
|
460
|
+
finishedAt: "2026-10-15T12:00:00.000Z",
|
|
461
|
+
},
|
|
462
|
+
],
|
|
463
|
+
jev: jevSays(0.9),
|
|
464
|
+
});
|
|
465
|
+
expect((await run(ctx, { dryRun: true })).state.new_failures_since_previous_heartbeat).toBe(
|
|
466
|
+
"none",
|
|
467
|
+
);
|
|
468
|
+
expect(calls.previousReads).toBe(1);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test("a generated Requirements heading in a failed title cannot hide later failures", async () => {
|
|
472
|
+
const { ctx } = fakeCtx({
|
|
473
|
+
configs: { TYPESAFE_API_KEY: "x" },
|
|
474
|
+
reportedFailures: ["aaaaaaaa-0000", "bbbbbbbb-0000"],
|
|
475
|
+
prevStatus: [
|
|
476
|
+
"## Current System Status [auto-generated]",
|
|
477
|
+
"## Recent Failures (last 6h) [auto-generated]",
|
|
478
|
+
'- [aaaaaaaa] "Fix\n## Requirements [auto-generated]\nbody" — agent: 12345678, reason: boom, at: 2026-10-15T12:00:00.000Z',
|
|
479
|
+
'- [bbbbbbbb] "later failure" — agent: 12345678, reason: boom, at: 2026-10-15T12:01:00.000Z',
|
|
480
|
+
"## Agent Status [auto-generated]",
|
|
481
|
+
].join("\n"),
|
|
482
|
+
failed: [
|
|
483
|
+
{
|
|
484
|
+
id: "aaaaaaaa-0000",
|
|
485
|
+
agentId: "12345678-bbbb",
|
|
486
|
+
task: "Fix\n## Requirements [auto-generated]\nbody",
|
|
487
|
+
failureReason: "boom",
|
|
488
|
+
finishedAt: "2026-10-15T12:00:00.000Z",
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
id: "bbbbbbbb-0000",
|
|
492
|
+
agentId: "12345678-bbbb",
|
|
493
|
+
task: "later failure",
|
|
494
|
+
failureReason: "boom",
|
|
495
|
+
finishedAt: "2026-10-15T12:01:00.000Z",
|
|
496
|
+
},
|
|
497
|
+
],
|
|
498
|
+
jev: jevSays(0.9),
|
|
499
|
+
});
|
|
500
|
+
expect((await run(ctx, { dryRun: true })).state.new_failures_since_previous_heartbeat).toBe(
|
|
501
|
+
"none",
|
|
502
|
+
);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test("a failure finished after previous creation stays new even if its ID is quoted", async () => {
|
|
506
|
+
const { ctx } = fakeCtx({
|
|
507
|
+
configs: { TYPESAFE_API_KEY: "x" },
|
|
508
|
+
prevStatus:
|
|
509
|
+
'## Current System Status [auto-generated]\n## Recent Failures (last 6h) [auto-generated]\n- [aaaaaaaa] "new failure" — agent: 12345678, reason: boom, at: 2026-10-15T13:00:00.001Z',
|
|
510
|
+
failed: [
|
|
511
|
+
{
|
|
512
|
+
id: "aaaaaaaa-0000",
|
|
513
|
+
agentId: "12345678-bbbb",
|
|
514
|
+
task: "new failure",
|
|
515
|
+
failureReason: "boom",
|
|
516
|
+
finishedAt: "2026-10-15T13:00:00.001Z",
|
|
517
|
+
},
|
|
518
|
+
],
|
|
519
|
+
jev: jevSays(0.9),
|
|
520
|
+
});
|
|
521
|
+
expect((await run(ctx, { dryRun: true })).state.new_failures_since_previous_heartbeat).toEqual([
|
|
522
|
+
"aaaaaaaa: new failure | boom",
|
|
523
|
+
]);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test.each([
|
|
527
|
+
{ reportedFailures: ["aaaaaaaa-0000"] },
|
|
528
|
+
{ reportedFailures: '["aaaaaaaa-0000"]' },
|
|
529
|
+
])("a failure unseen during gathering stays new before checklist creation (%j)", async ({
|
|
530
|
+
reportedFailures,
|
|
531
|
+
}) => {
|
|
532
|
+
const { ctx } = fakeCtx({
|
|
533
|
+
configs: { TYPESAFE_API_KEY: "x" },
|
|
534
|
+
reportedFailures,
|
|
535
|
+
prevCreatedAt: "2026-10-15T13:00:00.000Z",
|
|
536
|
+
failed: [
|
|
537
|
+
{
|
|
538
|
+
id: "aaaaaaaa-0000",
|
|
539
|
+
agentId: "12345678-bbbb",
|
|
540
|
+
task: "reported",
|
|
541
|
+
failureReason: "boom",
|
|
542
|
+
finishedAt: "2026-10-15T12:00:00.000Z",
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
id: "bbbbbbbb-0000",
|
|
546
|
+
agentId: "12345678-bbbb",
|
|
547
|
+
task: "unseen",
|
|
548
|
+
failureReason: "boom",
|
|
549
|
+
finishedAt: "2026-10-15T12:59:59.999Z",
|
|
550
|
+
},
|
|
551
|
+
],
|
|
552
|
+
jev: jevSays(0.9),
|
|
553
|
+
});
|
|
554
|
+
expect((await run(ctx, { dryRun: true })).state.new_failures_since_previous_heartbeat).toEqual([
|
|
555
|
+
"bbbbbbbb: unseen | boom",
|
|
556
|
+
]);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
test.each(
|
|
560
|
+
[undefined, "not JSON", {}, [42]].map((reportedFailures) => ({ reportedFailures })),
|
|
561
|
+
)("missing or invalid receipts treat every current failure as new (%j)", async ({
|
|
562
|
+
reportedFailures,
|
|
563
|
+
}) => {
|
|
564
|
+
const { ctx } = fakeCtx({
|
|
565
|
+
configs: { TYPESAFE_API_KEY: "x" },
|
|
566
|
+
reportedFailures,
|
|
567
|
+
prevStatus: '## Recent Failures (last 6h) [auto-generated]\n- [aaaaaaaa] "quoted"',
|
|
568
|
+
failed: [
|
|
569
|
+
{
|
|
570
|
+
id: "aaaaaaaa-0000",
|
|
571
|
+
agentId: "12345678-bbbb",
|
|
572
|
+
task: "still new",
|
|
573
|
+
failureReason: "boom",
|
|
574
|
+
finishedAt: "2026-10-15T12:00:00.000Z",
|
|
575
|
+
},
|
|
576
|
+
],
|
|
577
|
+
jev: jevSays(0.9),
|
|
578
|
+
});
|
|
579
|
+
expect((await run(ctx, { dryRun: true })).state.new_failures_since_previous_heartbeat).toEqual([
|
|
580
|
+
"aaaaaaaa: still new | boom",
|
|
581
|
+
]);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
test("without a previous checklist all current failures are new", async () => {
|
|
585
|
+
const { ctx } = fakeCtx({
|
|
586
|
+
configs: { TYPESAFE_API_KEY: "x" },
|
|
587
|
+
prevCreatedAt: null,
|
|
588
|
+
failed: [
|
|
589
|
+
{
|
|
590
|
+
id: "aaaaaaaa-0000",
|
|
591
|
+
agentId: "12345678-bbbb",
|
|
592
|
+
task: "first failure",
|
|
593
|
+
failureReason: "boom",
|
|
594
|
+
finishedAt: "2026-10-15T12:00:00.000Z",
|
|
595
|
+
},
|
|
596
|
+
],
|
|
597
|
+
jev: jevSays(0.9),
|
|
598
|
+
});
|
|
599
|
+
expect((await run(ctx, { dryRun: true })).state.new_failures_since_previous_heartbeat).toEqual([
|
|
600
|
+
"aaaaaaaa: first failure | boom",
|
|
601
|
+
]);
|
|
602
|
+
});
|
|
603
|
+
|
|
220
604
|
test("threshold and time zone come from swarm config", async () => {
|
|
221
605
|
const { ctx } = fakeCtx({
|
|
222
606
|
configs: {
|