@desplega.ai/agent-swarm 2.2.0 → 2.4.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/README.md +1 -0
- package/dist/{acp-adapter-k0vwfyvn.js → acp-adapter-kedstxx6.js} +6 -6
- package/dist/{actions-w4pnb8vz.js → actions-vget6r1k.js} +18 -16
- package/dist/{amp-adapter-0cmxb2t5.js → amp-adapter-xeb5mktv.js} +2 -2
- package/dist/{app-zneem7g9.js → app-mqap8t8m.js} +3 -3
- package/dist/{assistant-wbzd2rnr.js → assistant-h0gv97mv.js} +20 -18
- package/dist/{boot-reembed-4kx69j3g.js → boot-reembed-qr8a894z.js} +1 -1
- package/dist/{boot-reembed-db0wwgy1.js → boot-reembed-rj85jjhw.js} +1 -1
- package/dist/{boot-scrub-sweep-tfv4d3mm.js → boot-scrub-sweep-eyfvn6v9.js} +3 -3
- package/dist/{boot-seeds-w2hmkxn5.js → boot-seeds-xs9a8dx1.js} +4 -4
- package/dist/{claude-adapter-5qrc2paq.js → claude-adapter-bm23cye7.js} +5 -5
- package/dist/{claude-managed-adapter-n7ahjyx6.js → claude-managed-adapter-1mwrsm9d.js} +2 -2
- package/dist/{claude-managed-setup-7ra32em4.js → claude-managed-setup-gkfgks4j.js} +1 -1
- package/dist/{claude-sdk-session-cd02gc76.js → claude-sdk-session-apb4ypz3.js} +5 -5
- package/dist/cli-02vhhssk.js +848 -0
- package/dist/{cli-4ync7m60.js → cli-0ebey9hg.js} +1 -1
- package/dist/{cli-665j3feb.js → cli-0xk505e8.js} +1 -1
- package/dist/{cli-8c1m9mhy.js → cli-1cevz46c.js} +5 -5
- package/dist/{cli-gzvb3d4a.js → cli-1jx0srjt.js} +3 -3
- package/dist/{cli-zj4d6afs.js → cli-1pa3rz69.js} +21 -6
- package/dist/{cli-vmbakbq0.js → cli-2rwet3r7.js} +1 -1
- package/dist/{cli-azs9bhc5.js → cli-3wjs22x3.js} +2 -2
- package/dist/{cli-tba8fxf9.js → cli-4fhyq711.js} +45 -36
- package/dist/{cli-ak4c12mh.js → cli-5ff2sjyt.js} +1 -1
- package/dist/{cli-6q7pyd3k.js → cli-5s5hx57y.js} +5 -3
- package/dist/{cli-h23mr2s6.js → cli-6ftrrwm1.js} +9 -371
- package/dist/{cli-9ac0mkg5.js → cli-8b9wvjps.js} +1 -1
- package/dist/cli-8cxkw1f5.js +382 -0
- package/dist/{cli-xhgtay7w.js → cli-8rwvksbc.js} +2 -2
- package/dist/{cli-e09hj50q.js → cli-8wkb09q5.js} +11 -11
- package/dist/{cli-ksrphveb.js → cli-a1esmmee.js} +14 -2
- package/dist/{cli-xnp2vkx7.js → cli-avmqhatn.js} +7 -7
- package/dist/{cli-xx13gqf3.js → cli-b80a1100.js} +1 -1
- package/dist/{cli-z1hh99j0.js → cli-bfc3k1x5.js} +3 -3
- package/dist/{cli-0k9rcwfy.js → cli-bhwa8rzc.js} +33 -15
- package/dist/{cli-8cvvytad.js → cli-bn26xbcs.js} +2 -2
- package/dist/{cli-k28p6xh7.js → cli-c8fwpgqb.js} +1 -1
- package/dist/{cli-rc3r0bnh.js → cli-ccn0eybf.js} +1 -1
- package/dist/{cli-9td5ss65.js → cli-dsgj3e7a.js} +2 -2
- package/dist/{cli-jrk8mj0t.js → cli-ecshf504.js} +2 -2
- package/dist/{cli-raejy0mp.js → cli-eqgfs202.js} +1 -1
- package/dist/{cli-ppapy34w.js → cli-f3nx1h8z.js} +2 -2
- package/dist/{cli-c9vrwcpb.js → cli-ffbrkh1r.js} +74 -35
- package/dist/{cli-mnry16kh.js → cli-kk2r80n5.js} +4 -4
- package/dist/{cli-c0dpzwnx.js → cli-kz116kvw.js} +3 -3
- package/dist/{cli-e5y285rg.js → cli-mvg2hf8p.js} +4 -4
- package/dist/{cli-2jm992rm.js → cli-mz1ezzzh.js} +2 -2
- package/dist/{cli-dw0njd9q.js → cli-nme7wery.js} +6 -6
- package/dist/{cli-mvsfbbgq.js → cli-nrrg0fvy.js} +18 -4
- package/dist/{cli-8jtrekpy.js → cli-p1m2b6gc.js} +7 -7
- package/dist/{cli-4c7k75rq.js → cli-rg7ntypm.js} +378 -84
- package/dist/{cli-z3dk189k.js → cli-rsd7ga7v.js} +10 -8
- package/dist/{cli-etvn5ej2.js → cli-sppc0cmw.js} +1 -1
- package/dist/{cli-8w65vwee.js → cli-tg9ksbn8.js} +1 -1
- package/dist/{cli-th8krqbf.js → cli-tgvpmnwz.js} +2 -2
- package/dist/{cli-hwc4a9xm.js → cli-tj4j2ts4.js} +6 -6
- package/dist/{cli-jhhb2v9n.js → cli-ttaz6c7m.js} +2 -2
- package/dist/{cli-w7z94e6c.js → cli-tx00c0h6.js} +4 -2
- package/dist/{cli-4g629sgr.js → cli-wrv09v7q.js} +20 -1
- package/dist/{cli-cby329ny.js → cli-wyrr38xm.js} +2 -2
- package/dist/{cli-5ks6x36q.js → cli-x1p5dhq2.js} +3 -3
- package/dist/{cli-bddxamg7.js → cli-x719j3vs.js} +220 -629
- package/dist/{cli-ztcxv5nv.js → cli-xfe63zx3.js} +1 -1
- package/dist/{cli-q6gw45wj.js → cli-ywyrffff.js} +1 -1
- package/dist/{cli-p46xdr1m.js → cli-zvze9rbm.js} +1 -1
- package/dist/cli.js +15 -15
- package/dist/{codex-adapter-m0fx3v3s.js → codex-adapter-pp2gq8gr.js} +3 -3
- package/dist/{codex-hook-x6tm6y5m.js → codex-hook-qwzccd64.js} +1 -1
- package/dist/{codex-session-runner-j9m2gfyy.js → codex-session-runner-0vzn09t8.js} +3 -3
- package/dist/{commands-ktnqwrv4.js → commands-psqhkttb.js} +2 -2
- package/dist/{controller-nex2he8v.js → controller-t20vz2k8.js} +2 -2
- package/dist/{controllers-connect-6yhwqv9v.js → controllers-connect-gkxmqw9d.js} +24 -20
- package/dist/{cursor-adapter-02s5ekva.js → cursor-adapter-3v542qe5.js} +1 -1
- package/dist/{db-jvr86v7f.js → db-2xbyenjr.js} +2 -2
- package/dist/{db-19tw9r5s.js → db-nc1rd869.js} +3 -2
- package/dist/{dispatcher-d3mnsg2y.js → dispatcher-ewceb8ts.js} +17 -15
- package/dist/{dsh-adapter-63rvej88.js → dsh-adapter-75xwh78t.js} +1 -1
- package/dist/{easter-egg-3d-6hg2r942.js → easter-egg-3d-xsc47jwf.js} +1 -1
- package/dist/{execute-bcqc69a5.js → execute-gzmw1mnw.js} +1 -1
- package/dist/{external-channel-44cwer32.js → external-channel-a5dsmxn4.js} +2 -2
- package/dist/{google-generative-ai-phpmwxvg.js → google-generative-ai-bmq8gkf3.js} +1 -1
- package/dist/{google-vertex-es9ybgdy.js → google-vertex-b1rvp52s.js} +1 -1
- package/dist/{grok-adapter-ysp907f6.js → grok-adapter-vj88xr23.js} +7 -7
- package/dist/handlers-zbs90bny.js +39 -0
- package/dist/{heartbeat-2etacsqq.js → heartbeat-t2m9r3wa.js} +20 -18
- package/dist/{hook-mzxxs5n2.js → hook-8femx1bg.js} +4 -4
- package/dist/{hook-3kvmax6d.js → hook-xa677f8r.js} +5 -5
- package/dist/{http-fn978m0g.js → http-xpmmq028.js} +940 -269
- package/dist/{index-e4tsa1h7.js → index-8pwc1jc8.js} +17 -15
- package/dist/{index-w2wr89xd.js → index-cdzyfrsp.js} +1 -1
- package/dist/{index-1r8hw4vd.js → index-dg2tmj99.js} +17 -15
- package/dist/index-nxa6me0q.js +41 -0
- package/dist/{index-039x2sag.js → index-s7ckdajq.js} +42 -14
- package/dist/{index-wgsw9xcz.js → index-syztqdt6.js} +1 -1
- package/dist/{index-x0q2a4f9.js → index-ysd45kgc.js} +1 -1
- package/dist/{index-g909y089.js → index-zgvqzjsw.js} +8 -8
- package/dist/{keepalive-d4nzs76x.js → keepalive-3nr4st3r.js} +3 -3
- package/dist/{lead-8h5n9k5f.js → lead-td8h2sfn.js} +33 -31
- package/dist/{maintenance-ea6jfw42.js → maintenance-verwpe2c.js} +1 -1
- package/dist/{oauth-refresh-sweep-3f25djq3.js → oauth-refresh-sweep-9q2t0y3q.js} +2 -2
- package/dist/{onboard-pvk7hdb2.js → onboard-aytqedmq.js} +2 -2
- package/dist/{opencode-adapter-eeesgfqk.js → opencode-adapter-f4hryjny.js} +4 -4
- package/dist/{otel-impl-qb7nhgdp.js → otel-impl-hckw4wt1.js} +1 -1
- package/dist/{pi-mono-adapter-zasp3ze3.js → pi-mono-adapter-hxby0ce1.js} +6 -6
- package/dist/{pricing-refresh-6ehwkcmp.js → pricing-refresh-5nvme3sm.js} +3 -3
- package/dist/{rbac-roles-km0f9cmm.js → rbac-roles-pd8c26m1.js} +5 -3
- package/dist/{rbac-roles-9648nh00.js → rbac-roles-vbpy21mg.js} +2 -2
- package/dist/{render-v2-xkze1wef.js → render-v2-7nqgvk2q.js} +3 -3
- package/dist/{resume-tq0bbvt5.js → resume-b6djcj3c.js} +6 -6
- package/dist/{runtime-ar8310ba.js → runtime-txrxc9za.js} +1 -1
- package/dist/session-access-fc3f9y12.js +25 -0
- package/dist/{setup-rnrazxvd.js → setup-n39szm6b.js} +1 -1
- package/dist/{virtual-modules-wzk6jf3a.js → virtual-modules-wyp2k6z3.js} +2 -2
- package/dist/{work-objects-e75q2rqs.js → work-objects-jem2fre0.js} +20 -18
- package/dist/{worker-4ww4hjef.js → worker-wr6xgfr2.js} +33 -31
- package/openapi.json +1011 -115
- package/package.json +2 -2
- package/src/apps/action-caller.ts +6 -0
- package/src/apps/script-types.ts +16 -3
- package/src/be/asset-key-audit.ts +4 -13
- package/src/be/db/active-sessions.ts +5 -0
- package/src/be/db/session-visibility.ts +29 -0
- package/src/be/db/tasks/read.ts +61 -3
- package/src/be/db/tasks/write.ts +15 -6
- package/src/be/db.ts +149 -66
- package/src/be/events.ts +11 -0
- package/src/be/migrations/227_inbox_item_state_last_read_at.sql +1 -0
- package/src/be/migrations/230_session_shares.sql +23 -0
- package/src/be/rbac-roles.ts +2 -0
- package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +22 -4
- package/src/be/session-shares.ts +244 -0
- package/src/be/swarm-config-guard.ts +23 -2
- package/src/commands/runner.ts +11 -0
- package/src/heartbeat/heartbeat.ts +25 -7
- package/src/http/active-sessions.ts +13 -2
- package/src/http/agents.ts +10 -3
- package/src/http/all-routes.ts +1 -0
- package/src/http/api-keys.ts +3 -1
- package/src/http/approval-requests.ts +2 -1
- package/src/http/apps.ts +36 -11
- package/src/http/assets.ts +26 -4
- package/src/http/config.ts +3 -0
- package/src/http/context.ts +2 -0
- package/src/http/core.ts +39 -2
- package/src/http/events.ts +6 -2
- package/src/http/favorite-owner.ts +1 -1
- package/src/http/fs.ts +38 -3
- package/src/http/inbox-state.ts +58 -8
- package/src/http/index.ts +3 -1
- package/src/http/memory.ts +3 -1
- package/src/http/ranking.ts +367 -0
- package/src/http/realtime.ts +26 -21
- package/src/http/route-def.ts +77 -11
- package/src/http/script-runs.ts +2 -1
- package/src/http/scripts.ts +8 -0
- package/src/http/session-access.ts +179 -0
- package/src/http/session-data.ts +10 -2
- package/src/http/sessions.ts +212 -12
- package/src/http/stats.ts +7 -3
- package/src/http/tasks.ts +83 -7
- package/src/http/used-by.ts +11 -3
- package/src/kv-reserved-namespaces.ts +1 -0
- package/src/prompts/session-templates.ts +3 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/ranking/telemetry-ranking.ts +147 -0
- package/src/ranking/usage.ts +57 -0
- package/src/rbac/admission.ts +22 -7
- package/src/rbac/legacy-policy.ts +2 -0
- package/src/rbac/permissions.ts +9 -0
- package/src/realtime/auth.ts +69 -0
- package/src/realtime/tickets.ts +4 -1
- package/src/realtime/transport.ts +77 -9
- package/src/slack/blocks.ts +18 -0
- package/src/slack/dm.ts +29 -0
- package/src/telemetry-contract/EVENTS.md +3 -0
- package/src/telemetry-contract/SOURCE.json +37 -4
- package/src/telemetry-contract/catalog.json +69 -0
- package/src/telemetry-contract/ranking/fixtures/expected.json +30 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/bad-category.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/bad-claim.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/blank-description.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/blank-name.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/description-too-long.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/domain-uppercase.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/domain-with-scheme.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/domain-with-www.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/http-source-url.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/missing-description.json +10 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/missing-version.json +10 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/name-not-string.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/name-too-long.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/not-object.json +1 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/null.json +1 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/other-with-subcategory.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/oversize.json +1 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/short-claim.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/subcategory-other-category.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/trailing-data.json +1 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/unknown-field.json +12 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/unsupported-version.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/updated-at-not-date.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/updated-at-offset.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/wrong-case-field.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/valid/category-without-subcategory.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/valid/full.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/valid/minimal.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/valid/padded-text.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/valid/unicode-name.json +11 -0
- package/src/telemetry-contract/ranking/listing.v1.schema.json +61 -0
- package/src/telemetry-contract/ranking/taxonomy.json +8 -0
- package/src/telemetry-contract/ranking-taxonomy.gen.ts +11 -0
- package/src/telemetry-contract/types.gen.ts +708 -1
- package/src/telemetry.ts +26 -2
- package/src/tests/apps-rbac.test.ts +189 -2
- package/src/tests/apps-script-types.test.ts +16 -3
- 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/check-pr-body.test.ts +23 -0
- package/src/tests/check-ranking-entry.test.ts +146 -0
- package/src/tests/db-extraction-contract.test.ts +1 -0
- package/src/tests/db-task-read-contract.test.ts +1 -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/org-identity.test.ts +44 -0
- package/src/tests/ranking-listing.test.ts +112 -0
- package/src/tests/ranking-route.test.ts +319 -0
- package/src/tests/ranking-usage.test.ts +50 -0
- package/src/tests/rbac-admission-e2e.test.ts +8 -0
- package/src/tests/rbac-admission.test.ts +38 -0
- package/src/tests/rbac-charact-http.test.ts +4 -3
- package/src/tests/rbac-engine.test.ts +2 -0
- package/src/tests/rbac-roles.test.ts +2 -0
- package/src/tests/realtime-room-surfaces.test.ts +3 -1
- package/src/tests/realtime-session-join-race.test.ts +142 -0
- package/src/tests/realtime-tickets.test.ts +24 -17
- package/src/tests/realtime-transport.test.ts +91 -0
- package/src/tests/script-connections.test.ts +2 -0
- package/src/tests/session-route-posture.test.ts +639 -0
- package/src/tests/session-sharing-routes.test.ts +282 -0
- package/src/tests/session-sharing.test.ts +526 -0
- package/src/tests/sessions-detail-batch.test.ts +7 -1
- package/src/tests/slack-tool-blocks.test.ts +101 -0
- package/src/tests/slack-update.test.ts +55 -0
- package/src/tests/slack-user-dm.test.ts +177 -0
- package/src/tests/used-by-route.test.ts +10 -0
- package/src/tools/slack-post.ts +18 -12
- package/src/tools/slack-read.ts +13 -1
- package/src/tools/slack-reply.ts +7 -10
- package/src/tools/slack-start-thread.ts +18 -11
- package/src/tools/slack-update.ts +2 -9
- package/src/types.ts +37 -0
- package/src/utils/org-identity.ts +33 -0
- package/templates/skills/apps/SKILL.md +17 -0
- package/templates/skills/apps/content.md +17 -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
- package/dist/handlers-t5pbabwf.js +0 -37
- package/dist/index-qyjbb7br.js +0 -39
- package/dist/{cli-n5mrsg0f.js → cli-60cgh1rv.js} +3 -3
- /package/dist/{cli-j0v75y13.js → cli-y83zk5sb.js} +0 -0
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
createTaskExtended,
|
|
10
10
|
createUser,
|
|
11
11
|
createWorkflow,
|
|
12
|
+
deleteTask,
|
|
13
|
+
deleteTaskAttachment,
|
|
12
14
|
getAssetKeyMappingByProvider,
|
|
13
15
|
getDb,
|
|
14
16
|
getDbClient,
|
|
@@ -166,6 +168,11 @@ describe("cross-entity asset namespace invariants", () => {
|
|
|
166
168
|
).sourceEntityType,
|
|
167
169
|
).toBe("task-attachment");
|
|
168
170
|
|
|
171
|
+
const lastUpdatedAt = "2026-01-01T00:00:00.000Z";
|
|
172
|
+
await getDbClient().run("UPDATE agent_tasks SET lastUpdatedAt = ? WHERE id = ?", [
|
|
173
|
+
lastUpdatedAt,
|
|
174
|
+
task.id,
|
|
175
|
+
]);
|
|
169
176
|
expect(
|
|
170
177
|
await moveAssetKey({
|
|
171
178
|
entityType: "task",
|
|
@@ -173,7 +180,8 @@ describe("cross-entity asset namespace invariants", () => {
|
|
|
173
180
|
key: "shared/archive/",
|
|
174
181
|
changedBy: userId,
|
|
175
182
|
}),
|
|
176
|
-
).
|
|
183
|
+
).toEqual({ movedTaskIds: [task.id] });
|
|
184
|
+
expect((await getTaskById(task.id))?.lastUpdatedAt).toBe(lastUpdatedAt);
|
|
177
185
|
expect((await getTaskById(task.id))?.key).toBe("shared/archive/");
|
|
178
186
|
const after = await getAssetKeyMappingByProvider({
|
|
179
187
|
providerId: "agent-fs",
|
|
@@ -251,7 +259,7 @@ describe("cross-entity asset namespace invariants", () => {
|
|
|
251
259
|
key: "shared/products/",
|
|
252
260
|
changedBy: userId,
|
|
253
261
|
}),
|
|
254
|
-
).
|
|
262
|
+
).toEqual({});
|
|
255
263
|
expect(
|
|
256
264
|
await moveAssetKey({
|
|
257
265
|
entityType: "script",
|
|
@@ -259,7 +267,7 @@ describe("cross-entity asset namespace invariants", () => {
|
|
|
259
267
|
key: "shared/automation/",
|
|
260
268
|
changedBy: userId,
|
|
261
269
|
}),
|
|
262
|
-
).
|
|
270
|
+
).toEqual({});
|
|
263
271
|
|
|
264
272
|
expect(
|
|
265
273
|
await getDbClient().get<{ key: string }>('SELECT "key" AS key FROM apps WHERE id = ?', [
|
|
@@ -330,6 +338,174 @@ describe("cross-entity asset namespace invariants", () => {
|
|
|
330
338
|
expect(auditAssetKeys(getDb()).warningCount).toBe(0);
|
|
331
339
|
});
|
|
332
340
|
|
|
341
|
+
test("shared provider files allow moves and follow only their source task", async () => {
|
|
342
|
+
const first = await createTaskExtended("first attachment owner", {
|
|
343
|
+
agentId,
|
|
344
|
+
key: "shared/first/",
|
|
345
|
+
});
|
|
346
|
+
const source = await createTaskExtended("source attachment owner", {
|
|
347
|
+
agentId,
|
|
348
|
+
key: "shared/source/",
|
|
349
|
+
});
|
|
350
|
+
const provider = {
|
|
351
|
+
providerId: "agent-fs",
|
|
352
|
+
providerOrgId: "shared-org",
|
|
353
|
+
providerDriveId: "shared-drive",
|
|
354
|
+
providerKey: "shared-file.md",
|
|
355
|
+
};
|
|
356
|
+
for (const task of [first, source]) {
|
|
357
|
+
await insertTaskAttachment({
|
|
358
|
+
taskId: task.id,
|
|
359
|
+
name: "shared-file.md",
|
|
360
|
+
kind: "agent-fs",
|
|
361
|
+
providerId: provider.providerId,
|
|
362
|
+
providerKey: provider.providerKey,
|
|
363
|
+
orgId: provider.providerOrgId,
|
|
364
|
+
driveId: provider.providerDriveId,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
const mapping = (await getAssetKeyMappingByProvider(provider))!;
|
|
368
|
+
expect(mapping.key).toBe(source.key);
|
|
369
|
+
const historyBefore = await getDbClient().query(
|
|
370
|
+
"SELECT id FROM asset_key_history WHERE entity_id = ? ORDER BY id",
|
|
371
|
+
[mapping.id],
|
|
372
|
+
);
|
|
373
|
+
expect(auditAssetKeys(getDb()).ok).toBe(true);
|
|
374
|
+
|
|
375
|
+
await moveAssetKey({ entityType: "task", id: first.id, key: "shared/first-moved/" });
|
|
376
|
+
expect(await getAssetKeyMappingByProvider(provider)).toEqual(mapping);
|
|
377
|
+
expect(auditAssetKeys(getDb()).ok).toBe(true);
|
|
378
|
+
expect(
|
|
379
|
+
await getDbClient().query(
|
|
380
|
+
"SELECT id FROM asset_key_history WHERE entity_id = ? ORDER BY id",
|
|
381
|
+
[mapping.id],
|
|
382
|
+
),
|
|
383
|
+
).toEqual(historyBefore);
|
|
384
|
+
|
|
385
|
+
await moveAssetKey({ entityType: "task", id: source.id, key: "shared/source-moved/" });
|
|
386
|
+
expect((await getAssetKeyMappingByProvider(provider))?.key).toBe("shared/source-moved/");
|
|
387
|
+
expect(auditAssetKeys(getDb()).ok).toBe(true);
|
|
388
|
+
expect(
|
|
389
|
+
await getDbClient().query("SELECT entity_type FROM asset_key_history WHERE entity_id = ?", [
|
|
390
|
+
mapping.id,
|
|
391
|
+
]),
|
|
392
|
+
).toHaveLength(historyBefore.length + 1);
|
|
393
|
+
|
|
394
|
+
await getDbClient().run(
|
|
395
|
+
`UPDATE asset_key_mappings SET "key" = 'shared/no-attaching-task/' WHERE id = ?`,
|
|
396
|
+
[mapping.id],
|
|
397
|
+
);
|
|
398
|
+
const drift = auditAssetKeys(getDb()).issues.filter(
|
|
399
|
+
(issue) => issue.code === "provider-mapping-drift",
|
|
400
|
+
);
|
|
401
|
+
expect(drift).toHaveLength(1);
|
|
402
|
+
expect(drift[0]?.entityId).toBe(mapping.id);
|
|
403
|
+
await expect(
|
|
404
|
+
moveAssetKey({ entityType: "task", id: first.id, key: "shared/blocked/" }),
|
|
405
|
+
).rejects.toThrow("blocked until");
|
|
406
|
+
await getDbClient().run(
|
|
407
|
+
`UPDATE asset_key_mappings SET "key" = ?, source_entity_type = 'task-attachment', source_entity_id = ? WHERE id = ?`,
|
|
408
|
+
["shared/source-moved/", mapping.sourceEntityId!, mapping.id],
|
|
409
|
+
);
|
|
410
|
+
expect(auditAssetKeys(getDb()).ok).toBe(true);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test("a task move takes only the descendants the caller passes", async () => {
|
|
414
|
+
const root = await createTaskExtended("partial root", { agentId, key: "shared/partial/" });
|
|
415
|
+
const checked = await createTaskExtended("checked child", { agentId, parentTaskId: root.id });
|
|
416
|
+
const unchecked = await createTaskExtended("unchecked child", {
|
|
417
|
+
agentId,
|
|
418
|
+
parentTaskId: root.id,
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
expect(
|
|
422
|
+
await moveAssetKey({
|
|
423
|
+
entityType: "task",
|
|
424
|
+
id: root.id,
|
|
425
|
+
key: "shared/partial-moved/",
|
|
426
|
+
descendantTaskIds: [checked.id],
|
|
427
|
+
}),
|
|
428
|
+
).toEqual({ movedTaskIds: [root.id, checked.id] });
|
|
429
|
+
expect((await getTaskById(checked.id))?.key).toBe("shared/partial-moved/");
|
|
430
|
+
expect((await getTaskById(unchecked.id))?.key).toBe("shared/partial/");
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test("deleting the source attachment of a shared file leaves moves open", async () => {
|
|
434
|
+
const [kept, deleted, removed] = await Promise.all(
|
|
435
|
+
["shared/detach-a/", "shared/detach-b/", "shared/detach-c/"].map((key) =>
|
|
436
|
+
createTaskExtended(`detach ${key}`, { agentId, key }),
|
|
437
|
+
),
|
|
438
|
+
);
|
|
439
|
+
const other = await createTaskExtended("unrelated move", { agentId, key: "shared/other/" });
|
|
440
|
+
const provider = {
|
|
441
|
+
providerId: "agent-fs",
|
|
442
|
+
providerOrgId: "shared-org",
|
|
443
|
+
providerDriveId: "shared-drive",
|
|
444
|
+
providerKey: "detach.md",
|
|
445
|
+
};
|
|
446
|
+
const attach = (taskId: string) =>
|
|
447
|
+
insertTaskAttachment({
|
|
448
|
+
taskId,
|
|
449
|
+
name: "detach.md",
|
|
450
|
+
kind: "agent-fs",
|
|
451
|
+
providerId: provider.providerId,
|
|
452
|
+
providerKey: provider.providerKey,
|
|
453
|
+
orgId: provider.providerOrgId,
|
|
454
|
+
driveId: provider.providerDriveId,
|
|
455
|
+
});
|
|
456
|
+
await attach(kept!.id);
|
|
457
|
+
const sourceAttachment = await attach(deleted!.id);
|
|
458
|
+
expect((await getAssetKeyMappingByProvider(provider))?.key).toBe("shared/detach-b/");
|
|
459
|
+
|
|
460
|
+
expect(await deleteTaskAttachment(sourceAttachment.id)).toBe(true);
|
|
461
|
+
expect(await getAssetKeyMappingByProvider(provider)).toMatchObject({
|
|
462
|
+
key: "shared/detach-b/",
|
|
463
|
+
sourceEntityType: "external",
|
|
464
|
+
});
|
|
465
|
+
expect(auditAssetKeys(getDb()).ok).toBe(true);
|
|
466
|
+
await moveAssetKey({ entityType: "task", id: other.id, key: "shared/other-moved/" });
|
|
467
|
+
|
|
468
|
+
await attach(removed!.id);
|
|
469
|
+
expect(await deleteTask(removed!.id)).toBe(true);
|
|
470
|
+
expect(await getAssetKeyMappingByProvider(provider)).toMatchObject({
|
|
471
|
+
key: "shared/detach-c/",
|
|
472
|
+
sourceEntityType: "external",
|
|
473
|
+
});
|
|
474
|
+
expect(auditAssetKeys(getDb()).ok).toBe(true);
|
|
475
|
+
|
|
476
|
+
await moveAssetKey({ entityType: "task", id: kept!.id, key: "shared/detach-moved/" });
|
|
477
|
+
expect((await getAssetKeyMappingByProvider(provider))?.key).toBe("shared/detach-moved/");
|
|
478
|
+
expect(auditAssetKeys(getDb()).ok).toBe(true);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test("an external mapping that a task still attaches follows that task's move", async () => {
|
|
482
|
+
const task = await createTaskExtended("external owner", { agentId, key: "shared/external/" });
|
|
483
|
+
const provider = {
|
|
484
|
+
providerId: "agent-fs",
|
|
485
|
+
providerOrgId: "shared-org",
|
|
486
|
+
providerDriveId: "shared-drive",
|
|
487
|
+
providerKey: "external.md",
|
|
488
|
+
};
|
|
489
|
+
await insertTaskAttachment({
|
|
490
|
+
taskId: task.id,
|
|
491
|
+
name: "external.md",
|
|
492
|
+
kind: "agent-fs",
|
|
493
|
+
providerId: provider.providerId,
|
|
494
|
+
providerKey: provider.providerKey,
|
|
495
|
+
orgId: provider.providerOrgId,
|
|
496
|
+
driveId: provider.providerDriveId,
|
|
497
|
+
});
|
|
498
|
+
const mapping = (await getAssetKeyMappingByProvider(provider))!;
|
|
499
|
+
await getDbClient().run(
|
|
500
|
+
"UPDATE asset_key_mappings SET source_entity_type = 'external', source_entity_id = NULL WHERE id = ?",
|
|
501
|
+
[mapping.id],
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
await moveAssetKey({ entityType: "task", id: task.id, key: "shared/external-moved/" });
|
|
505
|
+
expect((await getAssetKeyMappingByProvider(provider))?.key).toBe("shared/external-moved/");
|
|
506
|
+
expect(auditAssetKeys(getDb()).ok).toBe(true);
|
|
507
|
+
});
|
|
508
|
+
|
|
333
509
|
test("personal namespace users are audited and missing users are repairable warnings", async () => {
|
|
334
510
|
const task = await createTaskExtended("personal", {
|
|
335
511
|
agentId,
|
|
@@ -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();
|
|
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import {
|
|
3
3
|
checkPrBody,
|
|
4
4
|
isFixTitle,
|
|
5
|
+
isRankingEntryPr,
|
|
5
6
|
pickedChoices,
|
|
6
7
|
prBodyWarnings,
|
|
7
8
|
proseWordCount,
|
|
@@ -34,6 +35,28 @@ const TEMPLATE = `<!-- top comment -->
|
|
|
34
35
|
|
|
35
36
|
const URGENT = "## Urgency\n- [ ] asap\n- [x] nice to have\n";
|
|
36
37
|
|
|
38
|
+
describe("isRankingEntryPr", () => {
|
|
39
|
+
test("exempts only a one-file entry PR with the matching title", () => {
|
|
40
|
+
expect(
|
|
41
|
+
isRankingEntryPr("chore(ranking): add acme.dev", ["community/ranking/acme.dev.json"]),
|
|
42
|
+
).toBe(true);
|
|
43
|
+
expect(
|
|
44
|
+
isRankingEntryPr("chore(ranking): remove acme.dev", ["community/ranking/acme.dev.json"]),
|
|
45
|
+
).toBe(true);
|
|
46
|
+
expect(
|
|
47
|
+
isRankingEntryPr("chore(ranking): add acme.dev", ["community/ranking/acme.com.json"]),
|
|
48
|
+
).toBe(false);
|
|
49
|
+
expect(
|
|
50
|
+
isRankingEntryPr("chore(ranking): add acme.dev", [
|
|
51
|
+
"community/ranking/acme.dev.json",
|
|
52
|
+
"src/server.ts",
|
|
53
|
+
]),
|
|
54
|
+
).toBe(false);
|
|
55
|
+
expect(isRankingEntryPr("feat: add acme.dev", ["community/ranking/acme.dev.json"])).toBe(false);
|
|
56
|
+
expect(isRankingEntryPr("chore(ranking): add acme.dev", undefined)).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
37
60
|
describe("check-pr-body", () => {
|
|
38
61
|
test("template markers set when a section is required", () => {
|
|
39
62
|
expect(templateSections(TEMPLATE)).toEqual([
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
checkRankingEntries,
|
|
4
|
+
type Entry,
|
|
5
|
+
isEntryPath,
|
|
6
|
+
parseEntry,
|
|
7
|
+
type Verification,
|
|
8
|
+
} from "../../scripts/check-ranking-entry";
|
|
9
|
+
|
|
10
|
+
const CLAIM = "rk_3f9a1c0e7b2d4a6f8e1c3b5d7f9a2c4e";
|
|
11
|
+
const OTHER_CLAIM = "rk_00000000000000000000000000000001";
|
|
12
|
+
const file = (domain: string, claim = CLAIM) =>
|
|
13
|
+
JSON.stringify({ $schema: "./schema.json", domain, claim });
|
|
14
|
+
|
|
15
|
+
const VERIFIED: Verification = {
|
|
16
|
+
verified: true,
|
|
17
|
+
reasons: [],
|
|
18
|
+
telemetryActive7d: true,
|
|
19
|
+
websiteMatchesDomain: true,
|
|
20
|
+
listing: {
|
|
21
|
+
name: "Acme",
|
|
22
|
+
domain: "acme.dev",
|
|
23
|
+
category: "engineering",
|
|
24
|
+
subcategory: "qa-testing",
|
|
25
|
+
description: "QA | release automation",
|
|
26
|
+
sourceCodeUrl: null,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
describe("parseEntry", () => {
|
|
31
|
+
test("accepts the opt-in file", () => {
|
|
32
|
+
expect(parseEntry("acme.dev.json", file("acme.dev"))).toEqual({
|
|
33
|
+
domain: "acme.dev",
|
|
34
|
+
claim: CLAIM,
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test.each([
|
|
39
|
+
["acme.dev.json", "{", "valid JSON"],
|
|
40
|
+
["acme.dev.json", "[]", "JSON object"],
|
|
41
|
+
[
|
|
42
|
+
"acme.dev.json",
|
|
43
|
+
JSON.stringify({ domain: "acme.dev", claim: CLAIM, name: "Acme" }),
|
|
44
|
+
"unknown keys",
|
|
45
|
+
],
|
|
46
|
+
["www.acme.dev.json", file("www.acme.dev"), "bare lowercase hostname"],
|
|
47
|
+
["Acme.ba.json", file("Acme.ba"), "bare lowercase hostname"],
|
|
48
|
+
["other.ba.json", file("acme.dev"), "must be named acme.dev.json"],
|
|
49
|
+
["acme.dev.json", file("acme.dev", "rk_short"), "claim"],
|
|
50
|
+
])("rejects %s %s", (name, text, message) => {
|
|
51
|
+
expect(() => parseEntry(name, text)).toThrow(message);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("README and schema are not entries", () => {
|
|
55
|
+
expect(isEntryPath("community/ranking/acme.dev.json")).toBe(true);
|
|
56
|
+
expect(isEntryPath("community/ranking/schema.json")).toBe(false);
|
|
57
|
+
expect(isEntryPath("community/ranking/README.md")).toBe(false);
|
|
58
|
+
expect(isEntryPath("community/ranking/nested/acme.dev.json")).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("checkRankingEntries", () => {
|
|
63
|
+
const files: Record<string, string> = {
|
|
64
|
+
"community/ranking/acme.dev.json": file("acme.dev"),
|
|
65
|
+
"community/ranking/acme.com.json": file("acme.com", CLAIM),
|
|
66
|
+
};
|
|
67
|
+
const run = (
|
|
68
|
+
paths: string[],
|
|
69
|
+
verify: (entry: Entry) => Promise<Verification>,
|
|
70
|
+
all = ["acme.dev.json"],
|
|
71
|
+
) =>
|
|
72
|
+
checkRankingEntries({
|
|
73
|
+
changes: paths.map((path) => ({ status: "A", path })),
|
|
74
|
+
readEntry: (path) => files[path] ?? "",
|
|
75
|
+
allEntries: all.map((name) => [name, files[`community/ranking/${name}`] ?? ""]),
|
|
76
|
+
verify,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("a verified entry passes and the summary shows the listing", async () => {
|
|
80
|
+
const asked: Entry[] = [];
|
|
81
|
+
const result = await run(["community/ranking/acme.dev.json"], async (entry) => {
|
|
82
|
+
asked.push(entry);
|
|
83
|
+
return VERIFIED;
|
|
84
|
+
});
|
|
85
|
+
expect(result.problems).toEqual([]);
|
|
86
|
+
expect(asked).toEqual([{ domain: "acme.dev", claim: CLAIM }]);
|
|
87
|
+
const summary = result.summary.join("\n");
|
|
88
|
+
expect(summary).toContain("### acme.dev: verified");
|
|
89
|
+
expect(summary).toContain(
|
|
90
|
+
"| Acme | engineering | qa-testing | QA \\| release automation | — |",
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("an unverified entry fails with the proxy's reasons", async () => {
|
|
95
|
+
const result = await run(["community/ranking/acme.dev.json"], async () => ({
|
|
96
|
+
...VERIFIED,
|
|
97
|
+
verified: false,
|
|
98
|
+
reasons: ["telemetry_inactive_7d"],
|
|
99
|
+
}));
|
|
100
|
+
expect(result.problems).toHaveLength(1);
|
|
101
|
+
expect(result.problems[0]).toContain("telemetry_inactive_7d");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("a proxy error fails without throwing", async () => {
|
|
105
|
+
const result = await run(["community/ranking/acme.dev.json"], async () => {
|
|
106
|
+
throw new Error("the proxy answered 503");
|
|
107
|
+
});
|
|
108
|
+
expect(result.problems[0]).toContain("503");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("a claim shared with another entry fails before verify", async () => {
|
|
112
|
+
let called = false;
|
|
113
|
+
const result = await run(["community/ranking/acme.com.json"], async () => {
|
|
114
|
+
called = true;
|
|
115
|
+
return VERIFIED;
|
|
116
|
+
}, ["acme.dev.json", "acme.com.json"]);
|
|
117
|
+
expect(called).toBe(false);
|
|
118
|
+
expect(result.problems[0]).toContain("acme.dev.json");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("a removal needs no proof, and README edits are ignored", async () => {
|
|
122
|
+
const result = await checkRankingEntries({
|
|
123
|
+
changes: [
|
|
124
|
+
{ status: "D", path: "community/ranking/acme.dev.json" },
|
|
125
|
+
{ status: "M", path: "community/ranking/README.md" },
|
|
126
|
+
],
|
|
127
|
+
readEntry: () => "",
|
|
128
|
+
allEntries: [],
|
|
129
|
+
verify: async () => {
|
|
130
|
+
throw new Error("must not be called");
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
expect(result.problems).toEqual([]);
|
|
134
|
+
expect(result.summary).toEqual(["- Removed `acme.dev.json`."]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("the claim differs per entry in a valid two-entry PR", async () => {
|
|
138
|
+
files["community/ranking/acme.com.json"] = file("acme.com", OTHER_CLAIM);
|
|
139
|
+
const result = await run(
|
|
140
|
+
["community/ranking/acme.com.json", "community/ranking/acme.dev.json"],
|
|
141
|
+
async () => VERIFIED,
|
|
142
|
+
["acme.dev.json", "acme.com.json"],
|
|
143
|
+
);
|
|
144
|
+
expect(result.problems).toEqual([]);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -15,6 +15,7 @@ test("facade exposes repository bindings without internal exports", () => {
|
|
|
15
15
|
"rowToAgentTaskSummary",
|
|
16
16
|
"configureTaskReadDependencies",
|
|
17
17
|
"NEVER_STARTED_TASK_STATUSES",
|
|
18
|
+
"readableSessionCondition",
|
|
18
19
|
].includes(name)
|
|
19
20
|
) {
|
|
20
21
|
expect(name in db).toBe(false);
|
|
@@ -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(436);
|
|
107
|
+
expect(generatedKeys).toHaveLength(436);
|
|
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 () => {
|