@desplega.ai/agent-swarm 1.161.0 → 1.162.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 +3 -2
- package/dist/{acp-adapter-xx489vvb.js → acp-adapter-74m73ae9.js} +1 -1
- package/dist/{actions-ykjkn0m9.js → actions-4sr14gnt.js} +34 -31
- package/dist/{anthropic-messages-5qx80j9k.js → anthropic-messages-8xxrzky9.js} +22 -26
- package/dist/{app-0hdqa4k1.js → app-pt68fbwp.js} +13 -11
- package/dist/{artifact-3v5t9jtg.js → artifact-fey7dvj9.js} +3 -2
- package/dist/{assistant-am2g5g8a.js → assistant-cv3jytks.js} +37 -34
- package/dist/{azure-openai-responses-fszbkp0e.js → azure-openai-responses-f5cqsyj8.js} +15 -10
- package/dist/{boot-reembed-xy9t7avg.js → boot-reembed-4ht4gqth.js} +13 -11
- package/dist/{boot-reembed-gt5gjkt4.js → boot-reembed-nxpcvpj9.js} +12 -10
- package/dist/{boot-scrub-logs-9x0985y5.js → boot-scrub-logs-k93kyw8k.js} +9 -7
- package/dist/{boot-seeds-trv0d4vg.js → boot-seeds-1skcrs0e.js} +13 -10
- package/dist/{claude-adapter-jb6e1e54.js → claude-adapter-r5g0tzge.js} +23 -20
- package/dist/{claude-managed-setup-d1q9hv2b.js → claude-managed-setup-pkyrgshb.js} +3 -2
- package/dist/{claude-sdk-session-zga700s2.js → claude-sdk-session-jxyxnrwd.js} +5565 -5512
- package/dist/{cli-we0q7t0e.js → cli-2zy95vhm.js} +137 -64
- package/dist/{cli-35pj2yva.js → cli-4cwxn7gp.js} +38 -11
- package/dist/{cli-ad4qxm6j.js → cli-4dcpaefp.js} +2 -2
- package/dist/{cli-82c1pacn.js → cli-4x0csdjq.js} +71 -37
- package/dist/{cli-sqm3r5ps.js → cli-5bcd7sfy.js} +14 -18
- package/dist/{cli-d56tgd45.js → cli-5k6k8ta3.js} +2 -2
- package/dist/{cli-1er0rhz8.js → cli-5xhpb3kv.js} +15 -15
- package/dist/{cli-yrg7c73k.js → cli-7af8yhav.js} +10 -183
- package/dist/{cli-r6fsxc76.js → cli-7tre70zp.js} +513 -961
- package/dist/{cli-bh7jed3k.js → cli-8ev9xqnx.js} +34 -18
- package/dist/cli-bsky8g7w.js +449 -0
- package/dist/{cli-hn04t9db.js → cli-cebmgwsc.js} +14 -10
- package/dist/{cli-cfkrztza.js → cli-cvatsam7.js} +2 -2
- package/dist/{cli-2658f6at.js → cli-cvk6yzw5.js} +6 -8
- package/dist/{cli-ptr8h7kp.js → cli-eg0b3p0k.js} +5 -218
- package/dist/{cli-zb6xbk61.js → cli-g739zqzt.js} +4 -4
- package/dist/{cli-dv6cdjs2.js → cli-gd4favx9.js} +28 -7
- package/dist/{cli-5p3x4zna.js → cli-gpqpmns7.js} +2 -2
- package/dist/{cli-5nbmpy8w.js → cli-hb5c2x5y.js} +1 -1
- package/dist/{cli-jpf7e1zq.js → cli-hmsw1gpy.js} +3 -3
- package/dist/{cli-1cz0c18q.js → cli-jf7wmv1r.js} +2521 -733
- package/dist/{cli-qwsnmjdd.js → cli-jxs2ygaw.js} +168 -61
- package/dist/cli-kdkwd3yr.js +615 -0
- package/dist/{cli-hgmnf38m.js → cli-kh04eqwe.js} +1 -1
- package/dist/{cli-0383bp7f.js → cli-kp42yek0.js} +425 -90
- package/dist/{cli-ybgv4kw2.js → cli-mbssg1xv.js} +9 -2
- package/dist/{cli-jsp14b6c.js → cli-mzfth0bp.js} +38 -1
- package/dist/{cli-8g6mc14k.js → cli-nq7yqw7b.js} +16 -5
- package/dist/{cli-ee8bc46j.js → cli-nt8hecg9.js} +33 -32
- package/dist/{cli-9s68w752.js → cli-p0r8sxj5.js} +1 -1
- package/dist/{cli-58e8yr0d.js → cli-pkd6s2wy.js} +4 -2
- package/dist/{cli-17xpf7cm.js → cli-q24re23n.js} +6 -4
- package/dist/cli-qjjvzwzd.js +222 -0
- package/dist/cli-qrgt5bqf.js +189 -0
- package/dist/{cli-et8m9xsv.js → cli-rfjw9jw2.js} +22 -10
- package/dist/{cli-798vkbks.js → cli-rj0gqwwp.js} +2 -2
- package/dist/{cli-121ph74b.js → cli-rnyzn44p.js} +1 -1
- package/dist/{cli-vnnz5ncg.js → cli-s3hwpt9v.js} +95 -16
- package/dist/{cli-7119ja3w.js → cli-sptrj5f5.js} +2 -2
- package/dist/{cli-a8c82p2t.js → cli-sqvfa090.js} +13 -6
- package/dist/{cli-phfg8vy8.js → cli-swfmp1fx.js} +26 -26
- package/dist/{cli-nsdxtcbg.js → cli-tcen9p5w.js} +8 -5
- package/dist/{cli-9m03pzga.js → cli-vc9bp061.js} +5 -5
- package/dist/{cli-rtx9dygp.js → cli-vxe6qa4h.js} +1 -1
- package/dist/{cli-ecfxwnyb.js → cli-vy3ed88x.js} +1 -1
- package/dist/{cli-0c7ata9h.js → cli-vyqs9ya8.js} +6 -6
- package/dist/cli-wbvnn6tk.js +861 -0
- package/dist/{cli-kgd3p0bm.js → cli-wjgs5wxe.js} +6 -6
- package/dist/cli-wxkkd0e0.js +33 -0
- package/dist/{cli-akfj32vj.js → cli-xqpk3rp7.js} +5 -4
- package/dist/cli-zp38xvtm.js +9487 -0
- package/dist/{cli-3zc55ctx.js → cli-zvhd3f64.js} +2 -2
- package/dist/{cli-ze1d3wzv.js → cli-zxqx25nz.js} +5 -5
- package/dist/cli.js +32 -25
- package/dist/{cloudflare-workers-ai-system-one-k6fbxw9d.js → cloudflare-workers-ai-system-one-k8ddqjkq.js} +8 -6
- package/dist/{codex-adapter-04v97vep.js → codex-adapter-esvmnhn7.js} +13 -13
- package/dist/{codex-hook-20me6byw.js → codex-hook-m73jmqgx.js} +19 -3
- package/dist/{codex-session-runner-ax69mqq3.js → codex-session-runner-ban9excr.js} +13 -13
- package/dist/{commands-18gy08n1.js → commands-75413p2n.js} +9 -7
- package/dist/{db-d2pvjpyt.js → db-7f5pecwg.js} +11 -7
- package/dist/{dispatcher-8wbwyzjg.js → dispatcher-fh9at8nz.js} +33 -30
- package/dist/{dsh-adapter-ysqk9snv.js → dsh-adapter-wpze65d4.js} +3 -3
- package/dist/{e2b-esmt1r16.js → e2b-gjgdjp91.js} +10 -9
- package/dist/{pi-logo-animation-e9a0t68z.js → easter-egg-3d-3w5c2n4c.js} +122 -68
- package/dist/{execute-cmagrbrz.js → execute-f50wg9nh.js} +29 -6
- package/dist/{google-generative-ai-k6n5nbpp.js → google-generative-ai-jxxpnegy.js} +2 -2
- package/dist/{google-vertex-44xxtcvp.js → google-vertex-x7q1angm.js} +5 -5
- package/dist/{handlers-6b69m5kp.js → handlers-w13787bb.js} +36 -33
- package/dist/{hook-srnkt0hb.js → hook-46g1jrvx.js} +28 -15
- package/dist/{hook-k3mydf2n.js → hook-4tpts2ry.js} +20 -17
- package/dist/{http-hrf80ca1.js → http-19pzbfrt.js} +964 -176
- package/dist/{index-9rxdvj6w.js → index-8fd4dm4h.js} +34 -31
- package/dist/{index-1asbaj62.js → index-dqmp15bq.js} +35 -32
- package/dist/{index-45evq4ee.js → index-qb4q1y3e.js} +33 -30
- package/dist/{index-k4nj18qs.js → index-s75wc4wd.js} +12 -10
- package/dist/{index-xmevk2b9.js → index-scptvdxg.js} +27 -25
- package/dist/{keepalive-sfdz6ksp.js → keepalive-xa5ts9cd.js} +11 -9
- package/dist/{lead-ej91e9hw.js → lead-qp4xkvjq.js} +45 -41
- package/dist/{maintenance-80dnd7ke.js → maintenance-rm5885hm.js} +13 -11
- package/dist/{mistral-conversations-pc3ntnxt.js → mistral-conversations-rw7fbt22.js} +4 -4
- package/dist/{node-a5e00qhp.js → node-8zkdq42h.js} +834 -833
- package/dist/{oauth-refresh-sweep-m1etk2mx.js → oauth-refresh-sweep-1csdfebf.js} +10 -8
- package/dist/{onboard-0q8sdfqc.js → onboard-r6b78cs4.js} +10 -10
- package/dist/{openai-codex-responses-n7n683p2.js → openai-codex-responses-efvrgrq3.js} +4 -4
- package/dist/{openai-completions-jpaj5g2b.js → openai-completions-bbbrrz4z.js} +12 -8
- package/dist/{openai-responses-9a3nme9j.js → openai-responses-9jjyrcm1.js} +15 -10
- package/dist/{opencode-adapter-4wcgmvgf.js → opencode-adapter-kb3g6pkf.js} +2 -2
- package/dist/{otel-impl-hnqm9mxq.js → otel-impl-0y9knpky.js} +5 -5
- package/dist/{pi-mono-adapter-d6sqm9wr.js → pi-mono-adapter-z6s1bme2.js} +100 -19
- package/dist/{pricing-refresh-y60z64pm.js → pricing-refresh-mth26tgp.js} +11 -9
- package/dist/{rbac-roles-8pa0754b.js → rbac-roles-kwntb23q.js} +13 -9
- package/dist/rbac-roles-vpk4ebm8.js +45 -0
- package/dist/{registry-v5wytg0s.js → registry-km6t1jd2.js} +1 -1
- package/dist/{render-v2-418s1vj7.js → render-v2-hcm7h00y.js} +13 -11
- package/dist/{runtime-3pknc168.js → runtime-vfjsfsc5.js} +4 -4
- package/dist/{setup-yghdkj4x.js → setup-hbyzd65h.js} +2 -2
- package/dist/{virtual-modules-jcceevt5.js → virtual-modules-9bv7ry91.js} +9 -8
- package/dist/{work-objects-j1sz20v9.js → work-objects-m09sxnhe.js} +36 -33
- package/dist/{worker-d4gcvkjt.js → worker-0h35rvk4.js} +45 -41
- package/openapi.json +491 -12
- package/package.json +13 -6
- package/src/be/api-drain.ts +184 -0
- package/src/be/db/tasks/read.ts +31 -6
- package/src/be/db/tasks/write.ts +5 -4
- package/src/be/db.ts +42 -18
- package/src/be/memory/link-resolver.ts +29 -3
- package/src/be/migrations/192_favorites_agent_fs_path.sql +36 -0
- package/src/be/onboarding.ts +7 -1
- package/src/be/seed-prompt-templates.ts +1 -0
- package/src/be/swarm-config-guard.ts +35 -16
- package/src/be/task-citations.ts +11 -5
- package/src/be/users.ts +3 -0
- package/src/cli.tsx +3 -2
- package/src/comb/agent-fs.ts +39 -0
- package/src/comb/config.ts +37 -0
- package/src/comb/markers.ts +37 -0
- package/src/comb/review-batch.ts +486 -0
- package/src/comb/templates.ts +88 -0
- package/src/commands/context-preamble.ts +4 -1
- package/src/commands/hook.ts +2 -1
- package/src/commands/runner.ts +262 -68
- package/src/commands/templates.ts +10 -0
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/fs/agent-fs-provider.ts +106 -2
- package/src/heartbeat/heartbeat.ts +4 -0
- package/src/hooks/codex-hook.ts +25 -1
- package/src/hooks/hook.ts +13 -0
- package/src/hooks/pr-body-guard.ts +983 -0
- package/src/http/all-routes.ts +1 -0
- package/src/http/assets.ts +5 -17
- package/src/http/comb.ts +131 -0
- package/src/http/core.ts +2 -1
- package/src/http/fs.ts +9 -17
- package/src/http/index.ts +23 -9
- package/src/http/onboarding.ts +21 -2
- package/src/http/poll.ts +30 -3
- package/src/http/realtime.ts +62 -0
- package/src/http/request-principal.ts +22 -0
- package/src/http/status.ts +34 -2
- package/src/kv-reserved-namespaces.ts +10 -5
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +123 -0
- package/src/rbac/legacy-policy.ts +1 -0
- package/src/rbac/permissions.ts +4 -0
- package/src/realtime/auth.ts +63 -1
- package/src/realtime/tickets.ts +52 -0
- package/src/realtime/transport.ts +51 -22
- package/src/scripts-runtime/types/swarm-extension.d.ts +3 -1
- package/src/telemetry-context.ts +175 -0
- package/src/telemetry-contract/EVENTS.md +81 -0
- package/src/telemetry-contract/SOURCE.json +9 -0
- package/src/telemetry-contract/catalog.json +1335 -0
- package/src/telemetry-contract/index.ts +60 -0
- package/src/telemetry-contract/types.gen.ts +1971 -0
- package/src/telemetry-identity.ts +129 -0
- package/src/telemetry-snapshot.ts +211 -0
- package/src/telemetry-trigger.ts +125 -0
- package/src/telemetry-types.typetest.ts +40 -0
- package/src/telemetry.ts +271 -67
- package/src/tests/api-drain-e2e.test.ts +249 -0
- package/src/tests/api-drain-worker-e2e.test.ts +216 -0
- package/src/tests/api-drain.test.ts +351 -0
- package/src/tests/comb-config-guard.test.ts +38 -0
- package/src/tests/comb-links.test.ts +162 -0
- package/src/tests/comb-markers.test.ts +68 -0
- package/src/tests/comb-review-batch.test.ts +770 -0
- package/src/tests/comb-status.test.ts +152 -0
- package/src/tests/db-task-read-contract.test.ts +2 -0
- package/src/tests/db-task-write-contract.test.ts +1 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/favorites-agent-fs-path.test.ts +226 -0
- package/src/tests/memory-link-resolver.test.ts +34 -0
- package/src/tests/onboarding.test.ts +108 -0
- package/src/tests/pi-mono-adapter.test.ts +265 -2
- package/src/tests/pr-body-leaks.test.ts +789 -0
- package/src/tests/preload.ts +8 -0
- package/src/tests/rbac-engine.test.ts +19 -0
- package/src/tests/realtime-room-auth.test.ts +104 -1
- package/src/tests/realtime-tickets.test.ts +74 -0
- package/src/tests/realtime-transport.test.ts +131 -4
- package/src/tests/runner-api-drain.test.ts +301 -0
- package/src/tests/runner-fallback-output.test.ts +61 -0
- package/src/tests/runner-fenced-json-output.test.ts +178 -0
- package/src/tests/script-connections.test.ts +2 -0
- package/src/tests/send-task-requested-by.test.ts +133 -15
- package/src/tests/task-lifecycle-telemetry.test.ts +8 -3
- package/src/tests/telemetry-context.test.ts +314 -0
- package/src/tests/telemetry-init.test.ts +51 -19
- package/src/tests/telemetry-snapshot.test.ts +229 -0
- package/src/tests/telemetry-trigger.test.ts +200 -0
- package/src/tests/template-registry-helpers.ts +1 -0
- package/src/tests/workflow-agent-task-retry.test.ts +363 -0
- package/src/tests/workflow-definition-telemetry.test.ts +4 -4
- package/src/tools/poll-task.ts +20 -1
- package/src/tools/send-task.ts +9 -1
- package/src/tools/task-action.ts +11 -0
- package/src/types.ts +2 -1
- package/src/utils/api-drain.ts +24 -0
- package/src/utils/constants.ts +57 -16
- package/src/utils/pr-body-leaks.ts +172 -0
- package/src/utils/task-attachment-links.ts +14 -8
- package/src/workflows/recovery.ts +26 -1
- package/src/workflows/resume.ts +31 -4
- package/src/workflows/task-step-routing.ts +75 -2
- package/tsconfig.json +3 -0
- package/dist/cli-tjf4cn8z.js +0 -9119
- package/dist/cli-tsk3nz8a.js +0 -582
- package/dist/{cli-t6v2ah3r.js → cli-twrh04ap.js} +3 -3
|
@@ -78,6 +78,7 @@ describe("initTelemetry", () => {
|
|
|
78
78
|
expect(writes).toEqual([
|
|
79
79
|
{ key: "telemetry_installation_id", value: id as string },
|
|
80
80
|
{ key: "telemetry_installed_at", value: installedAt as string },
|
|
81
|
+
{ key: "telemetry_org_id", value: expect.stringMatching(/^org_[0-9a-f]{16}$/) },
|
|
81
82
|
]);
|
|
82
83
|
});
|
|
83
84
|
|
|
@@ -96,7 +97,10 @@ describe("initTelemetry", () => {
|
|
|
96
97
|
const id = _getInstallationIdForTests();
|
|
97
98
|
expect(id).not.toBeNull();
|
|
98
99
|
expect(id).toMatch(/^ephemeral_[0-9a-f]{16}$/);
|
|
99
|
-
|
|
100
|
+
// Only the org ID is written (the install ID could not be).
|
|
101
|
+
expect(writes).toEqual([
|
|
102
|
+
{ key: "telemetry_org_id", value: expect.stringMatching(/^org_[0-9a-f]{16}$/) },
|
|
103
|
+
]);
|
|
100
104
|
// Config access is failing — nowhere durable to persist an anchor, so
|
|
101
105
|
// leave it null rather than faking a fresh mint.
|
|
102
106
|
expect(_getInstalledAtForTests()).toBeNull();
|
|
@@ -121,7 +125,10 @@ describe("initTelemetry", () => {
|
|
|
121
125
|
// installed_at failure must not unwind and discard the installationId
|
|
122
126
|
// write that already succeeded.
|
|
123
127
|
expect(id).toMatch(/^install_[0-9a-f]{16}$/);
|
|
124
|
-
expect(writes).toEqual([
|
|
128
|
+
expect(writes).toEqual([
|
|
129
|
+
{ key: "telemetry_installation_id", value: id as string },
|
|
130
|
+
{ key: "telemetry_org_id", value: expect.stringMatching(/^org_[0-9a-f]{16}$/) },
|
|
131
|
+
]);
|
|
125
132
|
// No fabricated/unpersisted anchor emitted this session.
|
|
126
133
|
expect(_getInstalledAtForTests()).toBeNull();
|
|
127
134
|
});
|
|
@@ -141,7 +148,10 @@ describe("initTelemetry", () => {
|
|
|
141
148
|
// now() as a stand-in install date — that back-fills a wrong date
|
|
142
149
|
// instead of an honestly-absent one. No write, no fabricated anchor.
|
|
143
150
|
expect(_getInstallationIdForTests()).toBe(existing);
|
|
144
|
-
|
|
151
|
+
// No install-anchor write; the org ID is the only thing minted.
|
|
152
|
+
expect(writes).toEqual([
|
|
153
|
+
{ key: "telemetry_org_id", value: expect.stringMatching(/^org_[0-9a-f]{16}$/) },
|
|
154
|
+
]);
|
|
145
155
|
expect(_getInstalledAtForTests()).toBeNull();
|
|
146
156
|
});
|
|
147
157
|
|
|
@@ -163,7 +173,9 @@ describe("initTelemetry", () => {
|
|
|
163
173
|
);
|
|
164
174
|
expect(_getInstallationIdForTests()).toBe(existing);
|
|
165
175
|
expect(_getInstalledAtForTests()).toBe(existingInstalledAt);
|
|
166
|
-
expect(writes).toEqual([
|
|
176
|
+
expect(writes).toEqual([
|
|
177
|
+
{ key: "telemetry_org_id", value: expect.stringMatching(/^org_[0-9a-f]{16}$/) },
|
|
178
|
+
]);
|
|
167
179
|
});
|
|
168
180
|
});
|
|
169
181
|
|
|
@@ -244,6 +256,7 @@ describe("initTelemetry", () => {
|
|
|
244
256
|
return undefined;
|
|
245
257
|
},
|
|
246
258
|
async () => {},
|
|
259
|
+
{ generateIfMissing: true },
|
|
247
260
|
);
|
|
248
261
|
|
|
249
262
|
telemetry.onboarding("step_completed", {
|
|
@@ -370,7 +383,9 @@ describe("initTelemetry", () => {
|
|
|
370
383
|
{ generateIfMissing: true },
|
|
371
384
|
);
|
|
372
385
|
expect(_getInstallationIdForTests()).toBe(existing);
|
|
373
|
-
expect(writesB).toEqual([
|
|
386
|
+
expect(writesB).toEqual([
|
|
387
|
+
{ key: "telemetry_org_id", value: expect.stringMatching(/^org_[0-9a-f]{16}$/) },
|
|
388
|
+
]);
|
|
374
389
|
});
|
|
375
390
|
|
|
376
391
|
describe("_resolveCloudMode (URL → is_cloud)", () => {
|
|
@@ -470,8 +485,9 @@ describe("initTelemetry", () => {
|
|
|
470
485
|
process.env.E2B_SANDBOX_ID = "sbx_test123";
|
|
471
486
|
await initTelemetry(
|
|
472
487
|
"api-server",
|
|
473
|
-
async () => "install_e2b_test",
|
|
488
|
+
async (key) => (key === "telemetry_installation_id" ? "install_e2b_test" : undefined),
|
|
474
489
|
async () => {},
|
|
490
|
+
{ generateIfMissing: true },
|
|
475
491
|
);
|
|
476
492
|
|
|
477
493
|
track({ event: "server.started", properties: { port: 3013 } });
|
|
@@ -486,8 +502,9 @@ describe("initTelemetry", () => {
|
|
|
486
502
|
delete process.env.E2B_SANDBOX_ID;
|
|
487
503
|
await initTelemetry(
|
|
488
504
|
"api-server",
|
|
489
|
-
async () => "install_no_e2b",
|
|
505
|
+
async (key) => (key === "telemetry_installation_id" ? "install_no_e2b" : undefined),
|
|
490
506
|
async () => {},
|
|
507
|
+
{ generateIfMissing: true },
|
|
491
508
|
);
|
|
492
509
|
|
|
493
510
|
track({ event: "test.event", properties: {} });
|
|
@@ -501,8 +518,9 @@ describe("initTelemetry", () => {
|
|
|
501
518
|
process.env.E2B_SANDBOX_ID = "sbx_override_test";
|
|
502
519
|
await initTelemetry(
|
|
503
520
|
"api-server",
|
|
504
|
-
async () => "install_e2b_override",
|
|
521
|
+
async (key) => (key === "telemetry_installation_id" ? "install_e2b_override" : undefined),
|
|
505
522
|
async () => {},
|
|
523
|
+
{ generateIfMissing: true },
|
|
506
524
|
);
|
|
507
525
|
|
|
508
526
|
track({ event: "test.event", properties: { is_e2b: false } });
|
|
@@ -534,8 +552,9 @@ describe("initTelemetry", () => {
|
|
|
534
552
|
process.env.MCP_BASE_URL = "https://agent-swarm-mcp.desplega.sh";
|
|
535
553
|
await initTelemetry(
|
|
536
554
|
"worker",
|
|
537
|
-
async () => "install_cloud_test",
|
|
555
|
+
async (key) => (key === "telemetry_installation_id" ? "install_cloud_test" : undefined),
|
|
538
556
|
async () => {},
|
|
557
|
+
{ generateIfMissing: true },
|
|
539
558
|
);
|
|
540
559
|
|
|
541
560
|
track({ event: "server.started", properties: { port: 3013 } });
|
|
@@ -553,8 +572,9 @@ describe("initTelemetry", () => {
|
|
|
553
572
|
process.env.MCP_BASE_URL = "http://localhost:3013";
|
|
554
573
|
await initTelemetry(
|
|
555
574
|
"worker",
|
|
556
|
-
async () => "install_self_test",
|
|
575
|
+
async (key) => (key === "telemetry_installation_id" ? "install_self_test" : undefined),
|
|
557
576
|
async () => {},
|
|
577
|
+
{ generateIfMissing: true },
|
|
558
578
|
);
|
|
559
579
|
|
|
560
580
|
track({ event: "test.event", properties: {} });
|
|
@@ -569,8 +589,9 @@ describe("initTelemetry", () => {
|
|
|
569
589
|
delete process.env.MCP_BASE_URL;
|
|
570
590
|
await initTelemetry(
|
|
571
591
|
"api-server",
|
|
572
|
-
async () => "install_no_url",
|
|
592
|
+
async (key) => (key === "telemetry_installation_id" ? "install_no_url" : undefined),
|
|
573
593
|
async () => {},
|
|
594
|
+
{ generateIfMissing: true },
|
|
574
595
|
);
|
|
575
596
|
|
|
576
597
|
track({ event: "test.event", properties: {} });
|
|
@@ -590,8 +611,10 @@ describe("initTelemetry", () => {
|
|
|
590
611
|
process.env.SWARM_CLOUD = "true";
|
|
591
612
|
await initTelemetry(
|
|
592
613
|
"api-server",
|
|
593
|
-
async () =>
|
|
614
|
+
async (key) =>
|
|
615
|
+
key === "telemetry_installation_id" ? "install_hosted_cloud_test" : undefined,
|
|
594
616
|
async () => {},
|
|
617
|
+
{ generateIfMissing: true },
|
|
595
618
|
);
|
|
596
619
|
|
|
597
620
|
track({ event: "server.started", properties: { port: 3013 } });
|
|
@@ -611,8 +634,9 @@ describe("initTelemetry", () => {
|
|
|
611
634
|
process.env.MCP_BASE_URL = "https://agent-swarm-mcp.desplega.sh";
|
|
612
635
|
await initTelemetry(
|
|
613
636
|
"worker",
|
|
614
|
-
async () => "install_override_test",
|
|
637
|
+
async (key) => (key === "telemetry_installation_id" ? "install_override_test" : undefined),
|
|
615
638
|
async () => {},
|
|
639
|
+
{ generateIfMissing: true },
|
|
616
640
|
);
|
|
617
641
|
|
|
618
642
|
track({
|
|
@@ -645,8 +669,9 @@ describe("initTelemetry", () => {
|
|
|
645
669
|
test("includes the package version on every event and ignores caller overrides", async () => {
|
|
646
670
|
await initTelemetry(
|
|
647
671
|
"worker",
|
|
648
|
-
async () => "install_version_test",
|
|
672
|
+
async (key) => (key === "telemetry_installation_id" ? "install_version_test" : undefined),
|
|
649
673
|
async () => {},
|
|
674
|
+
{ generateIfMissing: true },
|
|
650
675
|
);
|
|
651
676
|
|
|
652
677
|
track({ event: "test.event", properties: { swarmVersion: "spoofed" } });
|
|
@@ -682,8 +707,9 @@ describe("initTelemetry", () => {
|
|
|
682
707
|
process.env.NODE_ENV = "development";
|
|
683
708
|
await initTelemetry(
|
|
684
709
|
"api-server",
|
|
685
|
-
async () => "install_default_env",
|
|
710
|
+
async (key) => (key === "telemetry_installation_id" ? "install_default_env" : undefined),
|
|
686
711
|
async () => {},
|
|
712
|
+
{ generateIfMissing: true },
|
|
687
713
|
);
|
|
688
714
|
|
|
689
715
|
track({ event: "test.event", properties: {} });
|
|
@@ -698,8 +724,9 @@ describe("initTelemetry", () => {
|
|
|
698
724
|
process.env.DESPLEGA_TELEMETRY_ENV = "development";
|
|
699
725
|
await initTelemetry(
|
|
700
726
|
"api-server",
|
|
701
|
-
async () => "install_explicit_env",
|
|
727
|
+
async (key) => (key === "telemetry_installation_id" ? "install_explicit_env" : undefined),
|
|
702
728
|
async () => {},
|
|
729
|
+
{ generateIfMissing: true },
|
|
703
730
|
);
|
|
704
731
|
|
|
705
732
|
track({ event: "test.event", properties: {} });
|
|
@@ -713,8 +740,9 @@ describe("initTelemetry", () => {
|
|
|
713
740
|
process.env.NODE_ENV = "test";
|
|
714
741
|
await initTelemetry(
|
|
715
742
|
"api-server",
|
|
716
|
-
async () => "install_test_env",
|
|
743
|
+
async (key) => (key === "telemetry_installation_id" ? "install_test_env" : undefined),
|
|
717
744
|
async () => {},
|
|
745
|
+
{ generateIfMissing: true },
|
|
718
746
|
);
|
|
719
747
|
|
|
720
748
|
track({ event: "test.event", properties: {} });
|
|
@@ -915,8 +943,10 @@ describe("initTelemetry", () => {
|
|
|
915
943
|
test("integration.connected emits only allowlisted provider values", async () => {
|
|
916
944
|
await initTelemetry(
|
|
917
945
|
"api-server",
|
|
918
|
-
async () =>
|
|
946
|
+
async (key) =>
|
|
947
|
+
key === "telemetry_installation_id" ? "install_integration_test" : undefined,
|
|
919
948
|
async () => {},
|
|
949
|
+
{ generateIfMissing: true },
|
|
920
950
|
);
|
|
921
951
|
|
|
922
952
|
emitIntegrationConnected("code_repo", "https://github.com/desplega/agent-swarm", true);
|
|
@@ -935,8 +965,10 @@ describe("initTelemetry", () => {
|
|
|
935
965
|
test("integration.connected omits a free-form PII-looking provider", async () => {
|
|
936
966
|
await initTelemetry(
|
|
937
967
|
"api-server",
|
|
938
|
-
async () =>
|
|
968
|
+
async (key) =>
|
|
969
|
+
key === "telemetry_installation_id" ? "install_integration_test" : undefined,
|
|
939
970
|
async () => {},
|
|
971
|
+
{ generateIfMissing: true },
|
|
940
972
|
);
|
|
941
973
|
|
|
942
974
|
emitIntegrationConnected("other", "someone@example.com", false);
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import {
|
|
4
|
+
closeDb,
|
|
5
|
+
createAgent,
|
|
6
|
+
createTaskExtended,
|
|
7
|
+
createUser,
|
|
8
|
+
getDbClient,
|
|
9
|
+
initDb,
|
|
10
|
+
} from "../be/db";
|
|
11
|
+
import { attachRole, detachRole } from "../be/rbac-roles";
|
|
12
|
+
import { linkIdentity, mintToken, revokeToken } from "../be/users";
|
|
13
|
+
import { _resetTelemetryStateForTests, initTelemetry } from "../telemetry";
|
|
14
|
+
import { collectOrgSnapshot, emitOrgSnapshotIfDue, LAST_SNAPSHOT_KEY } from "../telemetry-snapshot";
|
|
15
|
+
|
|
16
|
+
// initTelemetry no-ops when ANONYMIZED_TELEMETRY=false, and the CI env may set it.
|
|
17
|
+
process.env.ANONYMIZED_TELEMETRY = "true";
|
|
18
|
+
|
|
19
|
+
const TEST_DB_PATH = "./test-telemetry-snapshot.sqlite";
|
|
20
|
+
const ACTOR = { kind: "system", id: "telemetry-snapshot-test" } as const;
|
|
21
|
+
const DAY_MS = 24 * 60 * 60_000;
|
|
22
|
+
|
|
23
|
+
async function removeTestDb(): Promise<void> {
|
|
24
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
25
|
+
try {
|
|
26
|
+
await unlink(TEST_DB_PATH + suffix);
|
|
27
|
+
} catch {
|
|
28
|
+
// File does not exist.
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function memoryConfig(initial: Record<string, string> = {}) {
|
|
34
|
+
const store = new Map(Object.entries(initial));
|
|
35
|
+
return {
|
|
36
|
+
store,
|
|
37
|
+
get: async (key: string) => store.get(key),
|
|
38
|
+
set: async (key: string, value: string) => {
|
|
39
|
+
store.set(key, value);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Fixture: 3 active users, 1 suspended, 1 invited, 2 agents. */
|
|
45
|
+
async function seedFixture() {
|
|
46
|
+
const admin = await createUser({ name: "Admin Person", email: "admin.person@corp.example" });
|
|
47
|
+
const tokenUser = await createUser({ name: "Token Person" });
|
|
48
|
+
const revokedUser = await createUser({ name: "Revoked Person" });
|
|
49
|
+
const suspended = await createUser({
|
|
50
|
+
name: "Suspended Person",
|
|
51
|
+
email: "suspended@corp.example",
|
|
52
|
+
status: "suspended",
|
|
53
|
+
});
|
|
54
|
+
const invited = await createUser({ name: "Invited Person", status: "invited" });
|
|
55
|
+
for (const user of [tokenUser, revokedUser, suspended, invited]) {
|
|
56
|
+
await detachRole(user.id, "admin");
|
|
57
|
+
await attachRole(user.id, "requester");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await mintToken(tokenUser.id, "live", ACTOR);
|
|
61
|
+
const revoked = await mintToken(revokedUser.id, "revoked", ACTOR);
|
|
62
|
+
const revokedRow = await getDbClient().get<{ id: string }>(
|
|
63
|
+
"SELECT id FROM user_tokens WHERE userId = ?",
|
|
64
|
+
[revokedUser.id],
|
|
65
|
+
);
|
|
66
|
+
await revokeToken(revokedRow?.id ?? revoked.tokenId, ACTOR);
|
|
67
|
+
|
|
68
|
+
await linkIdentity(admin.id, "slack", "U-ADMIN", ACTOR);
|
|
69
|
+
await linkIdentity(admin.id, "github", "admin-gh", ACTOR);
|
|
70
|
+
await linkIdentity(tokenUser.id, "composio", "c-1", ACTOR);
|
|
71
|
+
await linkIdentity(tokenUser.id, "kapso", "k-1", ACTOR);
|
|
72
|
+
await linkIdentity(revokedUser.id, "jira", "j-1", ACTOR);
|
|
73
|
+
await linkIdentity(revokedUser.id, "linear", "l-1", ACTOR);
|
|
74
|
+
await linkIdentity(revokedUser.id, "gitlab", "g-1", ACTOR);
|
|
75
|
+
// A suspended user's links and an invited user's links do not count.
|
|
76
|
+
await linkIdentity(suspended.id, "slack", "U-SUSPENDED", ACTOR);
|
|
77
|
+
|
|
78
|
+
await createAgent({
|
|
79
|
+
id: "aaaa0000-0000-4000-8000-0000000000a1",
|
|
80
|
+
name: "A1",
|
|
81
|
+
isLead: true,
|
|
82
|
+
status: "idle",
|
|
83
|
+
});
|
|
84
|
+
await createAgent({
|
|
85
|
+
id: "bbbb0000-0000-4000-8000-0000000000b1",
|
|
86
|
+
name: "B1",
|
|
87
|
+
isLead: false,
|
|
88
|
+
status: "idle",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Two recent tasks by one user, one recent by another, one old by a third.
|
|
92
|
+
const recentA = await createTaskExtended("recent a", { requestedByUserId: admin.id });
|
|
93
|
+
await createTaskExtended("recent a again", { requestedByUserId: admin.id });
|
|
94
|
+
await createTaskExtended("recent b", { requestedByUserId: revokedUser.id });
|
|
95
|
+
const old = await createTaskExtended("old", { requestedByUserId: tokenUser.id });
|
|
96
|
+
await getDbClient().run("UPDATE agent_tasks SET createdAt = ? WHERE id = ?", [
|
|
97
|
+
new Date(Date.now() - 10 * DAY_MS).toISOString(),
|
|
98
|
+
old.id,
|
|
99
|
+
]);
|
|
100
|
+
expect(recentA.id).toBeTruthy();
|
|
101
|
+
return { admin, tokenUser, revokedUser, suspended, invited };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
describe("org snapshot", () => {
|
|
105
|
+
const originalFetch = globalThis.fetch;
|
|
106
|
+
let sent: Array<{ event: string; properties: Record<string, unknown> }>;
|
|
107
|
+
|
|
108
|
+
beforeEach(async () => {
|
|
109
|
+
closeDb();
|
|
110
|
+
await removeTestDb();
|
|
111
|
+
initDb(TEST_DB_PATH);
|
|
112
|
+
_resetTelemetryStateForTests();
|
|
113
|
+
process.env.ANONYMIZED_TELEMETRY = "true";
|
|
114
|
+
sent = [];
|
|
115
|
+
globalThis.fetch = (async (_url: string, init?: { body?: string }) => {
|
|
116
|
+
if (init?.body) sent.push(JSON.parse(init.body));
|
|
117
|
+
return new Response(null, { status: 204 });
|
|
118
|
+
}) as typeof fetch;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
afterEach(async () => {
|
|
122
|
+
globalThis.fetch = originalFetch;
|
|
123
|
+
process.env.ANONYMIZED_TELEMETRY = "true";
|
|
124
|
+
_resetTelemetryStateForTests();
|
|
125
|
+
closeDb();
|
|
126
|
+
await removeTestDb();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
async function boot(config = memoryConfig({ telemetry_installation_id: "install_snapshot" })) {
|
|
130
|
+
await initTelemetry("api-server", config.get, config.set, { generateIfMissing: true });
|
|
131
|
+
return config;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
135
|
+
|
|
136
|
+
test("counts match the fixture rows", async () => {
|
|
137
|
+
await seedFixture();
|
|
138
|
+
expect(await collectOrgSnapshot()).toEqual({
|
|
139
|
+
users_total: 3,
|
|
140
|
+
users_suspended: 1,
|
|
141
|
+
users_admin: 1,
|
|
142
|
+
users_with_login: 2, // the admin's email and the live token; the revoked one has neither
|
|
143
|
+
users_linked_slack: 1,
|
|
144
|
+
users_linked_github: 1,
|
|
145
|
+
users_linked_gitlab: 1,
|
|
146
|
+
users_linked_linear: 1,
|
|
147
|
+
users_linked_jira: 1,
|
|
148
|
+
users_linked_other: 1, // composio + kapso on one user count once
|
|
149
|
+
users_active_7d: 2,
|
|
150
|
+
agents_total: 2,
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("an empty install snapshots all zeros", async () => {
|
|
155
|
+
const snapshot = await collectOrgSnapshot();
|
|
156
|
+
expect(Object.values(snapshot).every((n) => n === 0)).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("the payload has no @, no email local part, no name and no raw user ID", async () => {
|
|
160
|
+
const { admin, tokenUser } = await seedFixture();
|
|
161
|
+
const config = await boot();
|
|
162
|
+
expect(await emitOrgSnapshotIfDue({ getConfig: config.get, setConfig: config.set })).toBe(true);
|
|
163
|
+
await tick();
|
|
164
|
+
|
|
165
|
+
// Seeding fires task events after boot; only the snapshot is under test.
|
|
166
|
+
const snapshots = sent.filter((e) => e.event === "org.snapshot");
|
|
167
|
+
expect(snapshots).toHaveLength(1);
|
|
168
|
+
expect(snapshots[0]?.properties).toMatchObject({ users_total: 3, agents_total: 2 });
|
|
169
|
+
const text = JSON.stringify(snapshots[0]);
|
|
170
|
+
expect(text).not.toContain("@");
|
|
171
|
+
for (const secret of [admin.id, tokenUser.id, "admin.person", "Admin Person", "corp.example"]) {
|
|
172
|
+
expect(text).not.toContain(secret);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("one send per 24 hours, across a simulated restart", async () => {
|
|
177
|
+
await seedFixture();
|
|
178
|
+
const config = await boot();
|
|
179
|
+
const t0 = Date.parse("2026-10-01T00:00:00.000Z");
|
|
180
|
+
const deps = (now: number) => ({
|
|
181
|
+
getConfig: config.get,
|
|
182
|
+
setConfig: config.set,
|
|
183
|
+
now: () => now,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(await emitOrgSnapshotIfDue(deps(t0))).toBe(true);
|
|
187
|
+
expect(config.store.get(LAST_SNAPSHOT_KEY)).toBe(new Date(t0).toISOString());
|
|
188
|
+
expect(await emitOrgSnapshotIfDue(deps(t0 + 60_000))).toBe(false);
|
|
189
|
+
|
|
190
|
+
// Restart: module state is gone, the stored timestamp is what remains.
|
|
191
|
+
_resetTelemetryStateForTests();
|
|
192
|
+
await boot(config);
|
|
193
|
+
expect(await emitOrgSnapshotIfDue(deps(t0 + DAY_MS - 60_000))).toBe(false);
|
|
194
|
+
expect(await emitOrgSnapshotIfDue(deps(t0 + DAY_MS + 60_000))).toBe(true);
|
|
195
|
+
await tick();
|
|
196
|
+
expect(sent.filter((e) => e.event === "org.snapshot")).toHaveLength(2);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("two overlapping ticks send once", async () => {
|
|
200
|
+
await seedFixture();
|
|
201
|
+
const config = await boot();
|
|
202
|
+
const deps = { getConfig: config.get, setConfig: config.set };
|
|
203
|
+
const [a, b] = await Promise.all([emitOrgSnapshotIfDue(deps), emitOrgSnapshotIfDue(deps)]);
|
|
204
|
+
await tick();
|
|
205
|
+
expect([a, b]).toEqual([true, true]); // both callers share the one in-flight send
|
|
206
|
+
expect(sent.filter((e) => e.event === "org.snapshot")).toHaveLength(1);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("nothing is sent or recorded when opted out", async () => {
|
|
210
|
+
await seedFixture();
|
|
211
|
+
process.env.ANONYMIZED_TELEMETRY = "false";
|
|
212
|
+
const config = await boot(memoryConfig());
|
|
213
|
+
expect(await emitOrgSnapshotIfDue({ getConfig: config.get, setConfig: config.set })).toBe(
|
|
214
|
+
false,
|
|
215
|
+
);
|
|
216
|
+
await tick();
|
|
217
|
+
expect(sent).toEqual([]);
|
|
218
|
+
expect(config.store.has(LAST_SNAPSHOT_KEY)).toBe(false);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("nothing is sent or recorded before telemetry has an identity, so the next tick retries", async () => {
|
|
222
|
+
await seedFixture();
|
|
223
|
+
const config = memoryConfig(); // never booted: no install ID, no org ID
|
|
224
|
+
expect(await emitOrgSnapshotIfDue({ getConfig: config.get, setConfig: config.set })).toBe(
|
|
225
|
+
false,
|
|
226
|
+
);
|
|
227
|
+
expect(config.store.has(LAST_SNAPSHOT_KEY)).toBe(false);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import {
|
|
4
|
+
closeDb,
|
|
5
|
+
completeTask,
|
|
6
|
+
createAgent,
|
|
7
|
+
createTaskExtended,
|
|
8
|
+
failTask,
|
|
9
|
+
getDbClient,
|
|
10
|
+
initDb,
|
|
11
|
+
updateAgentProfile,
|
|
12
|
+
} from "../be/db";
|
|
13
|
+
import { checkHeartbeatChecklist, createBootTriageTask } from "../heartbeat/heartbeat";
|
|
14
|
+
// Side-effect import: the checklist and boot-triage prompts come from the heartbeat templates.
|
|
15
|
+
import "../heartbeat/templates";
|
|
16
|
+
import { telemetry } from "../telemetry";
|
|
17
|
+
import { _resetTriggerSurfaceCacheForTests, resolveTriggerSurface } from "../telemetry-trigger";
|
|
18
|
+
|
|
19
|
+
// initTelemetry is not called here, but emitTaskTelemetry checks the opt-out
|
|
20
|
+
// env first, and the CI env may set it.
|
|
21
|
+
process.env.ANONYMIZED_TELEMETRY = "true";
|
|
22
|
+
|
|
23
|
+
const TEST_DB_PATH = "./test-telemetry-trigger.sqlite";
|
|
24
|
+
const WORKER_ID = "bbbb0000-0000-4000-8000-000000000003";
|
|
25
|
+
const LEAD_ID = "aaaa0000-0000-4000-8000-000000000003";
|
|
26
|
+
|
|
27
|
+
async function removeTestDb(): Promise<void> {
|
|
28
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
29
|
+
try {
|
|
30
|
+
await unlink(TEST_DB_PATH + suffix);
|
|
31
|
+
} catch {
|
|
32
|
+
// File does not exist.
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function flush(): Promise<void> {
|
|
38
|
+
for (let i = 0; i < 8; i++) await new Promise((resolve) => setTimeout(resolve, 0));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type Call = { event: string; props: Record<string, unknown> };
|
|
42
|
+
|
|
43
|
+
describe("trigger_surface on task telemetry", () => {
|
|
44
|
+
let spy: ReturnType<typeof spyOn>;
|
|
45
|
+
let calls: Call[];
|
|
46
|
+
|
|
47
|
+
const forTask = (taskId: string, event: string) =>
|
|
48
|
+
calls.find((c) => c.props.taskId === taskId && c.event === event);
|
|
49
|
+
|
|
50
|
+
beforeEach(async () => {
|
|
51
|
+
closeDb();
|
|
52
|
+
await removeTestDb();
|
|
53
|
+
initDb(TEST_DB_PATH);
|
|
54
|
+
_resetTriggerSurfaceCacheForTests();
|
|
55
|
+
await createAgent({ id: WORKER_ID, name: "Trigger Worker", isLead: false, status: "idle" });
|
|
56
|
+
await createAgent({ id: LEAD_ID, name: "Trigger Lead", isLead: true, status: "idle" });
|
|
57
|
+
calls = [];
|
|
58
|
+
spy = spyOn(telemetry, "taskEvent").mockImplementation((event, props) => {
|
|
59
|
+
calls.push({ event, props: props as Record<string, unknown> });
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
afterEach(async () => {
|
|
64
|
+
spy.mockRestore();
|
|
65
|
+
closeDb();
|
|
66
|
+
await removeTestDb();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("a slack root with an mcp child and a system follow-up reports slack on every created and completed event", async () => {
|
|
70
|
+
const root = await createTaskExtended("slack ask", { agentId: LEAD_ID, source: "slack" });
|
|
71
|
+
const child = await createTaskExtended("delegated", {
|
|
72
|
+
agentId: WORKER_ID,
|
|
73
|
+
source: "mcp",
|
|
74
|
+
parentTaskId: root.id,
|
|
75
|
+
});
|
|
76
|
+
const followUp = await createTaskExtended("follow-up", {
|
|
77
|
+
agentId: LEAD_ID,
|
|
78
|
+
source: "system",
|
|
79
|
+
parentTaskId: child.id,
|
|
80
|
+
});
|
|
81
|
+
await flush();
|
|
82
|
+
await completeTask(root.id, "done");
|
|
83
|
+
await completeTask(child.id, "done");
|
|
84
|
+
await completeTask(followUp.id, "done");
|
|
85
|
+
await flush();
|
|
86
|
+
|
|
87
|
+
const tasks = [
|
|
88
|
+
{ id: root.id, taskSource: "slack" },
|
|
89
|
+
{ id: child.id, taskSource: "mcp" },
|
|
90
|
+
{ id: followUp.id, taskSource: "system" },
|
|
91
|
+
];
|
|
92
|
+
for (const { id, taskSource } of tasks) {
|
|
93
|
+
for (const event of ["created", "completed"]) {
|
|
94
|
+
const call = forTask(id, event);
|
|
95
|
+
expect(call?.props.trigger_surface).toBe("slack");
|
|
96
|
+
expect(call?.props.task_source).toBe(taskSource);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("a failed child reports its root's surface", async () => {
|
|
102
|
+
const root = await createTaskExtended("linear ask", { agentId: LEAD_ID, source: "linear" });
|
|
103
|
+
const child = await createTaskExtended("delegated", {
|
|
104
|
+
agentId: WORKER_ID,
|
|
105
|
+
source: "mcp",
|
|
106
|
+
parentTaskId: root.id,
|
|
107
|
+
});
|
|
108
|
+
await flush();
|
|
109
|
+
await failTask(child.id, "nope");
|
|
110
|
+
await flush();
|
|
111
|
+
|
|
112
|
+
const failed = forTask(child.id, "failed");
|
|
113
|
+
expect(failed?.props.trigger_surface).toBe("linear");
|
|
114
|
+
expect(failed?.props.task_source).toBe("mcp");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("no task event carries a source property", async () => {
|
|
118
|
+
const root = await createTaskExtended("api ask", { agentId: LEAD_ID, source: "api" });
|
|
119
|
+
await flush();
|
|
120
|
+
await completeTask(root.id, "done");
|
|
121
|
+
await flush();
|
|
122
|
+
expect(calls.length).toBeGreaterThan(0);
|
|
123
|
+
for (const call of calls) expect("source" in call.props).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// The two heartbeat creators default to source "mcp" when they omit `source`,
|
|
127
|
+
// so each one is run for real and judged on the row it persists and the
|
|
128
|
+
// task.created event it emits.
|
|
129
|
+
const heartbeatCreators: Array<{ taskType: string; create: () => Promise<void> }> = [
|
|
130
|
+
{ taskType: "boot-triage", create: createBootTriageTask },
|
|
131
|
+
{
|
|
132
|
+
taskType: "heartbeat-checklist",
|
|
133
|
+
create: async () => {
|
|
134
|
+
await updateAgentProfile(LEAD_ID, { heartbeatMd: "- Check for stuck tasks\n" });
|
|
135
|
+
await checkHeartbeatChecklist();
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
for (const { taskType, create } of heartbeatCreators) {
|
|
141
|
+
test(`a ${taskType} task reports system, not mcp`, async () => {
|
|
142
|
+
await create();
|
|
143
|
+
await flush();
|
|
144
|
+
|
|
145
|
+
const row = await getDbClient().get<{ id: string; source: string }>(
|
|
146
|
+
"SELECT id, source FROM agent_tasks WHERE taskType = ?",
|
|
147
|
+
[taskType],
|
|
148
|
+
);
|
|
149
|
+
// The row says system too, so the dashboard UI stops calling it mcp.
|
|
150
|
+
expect(row?.source).toBe("system");
|
|
151
|
+
|
|
152
|
+
const created = forTask(row?.id as string, "created");
|
|
153
|
+
expect(created?.props.trigger_surface).toBe("system");
|
|
154
|
+
expect(created?.props.task_source).toBe("system");
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
test("an orphan (parent row deleted) uses the deepest row that still exists", async () => {
|
|
159
|
+
const root = await createTaskExtended("root", { agentId: LEAD_ID, source: "github" });
|
|
160
|
+
const child = await createTaskExtended("child", {
|
|
161
|
+
agentId: WORKER_ID,
|
|
162
|
+
source: "mcp",
|
|
163
|
+
parentTaskId: root.id,
|
|
164
|
+
});
|
|
165
|
+
_resetTriggerSurfaceCacheForTests();
|
|
166
|
+
await getDbClient().run("PRAGMA foreign_keys = OFF");
|
|
167
|
+
await getDbClient().run("DELETE FROM agent_tasks WHERE id = ?", [root.id]);
|
|
168
|
+
expect(await resolveTriggerSurface(child.id)).toBe("mcp");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("a task that no longer exists falls back to the caller's source, uncached", async () => {
|
|
172
|
+
expect(await resolveTriggerSurface("missing-task", "schedule")).toBe("schedule");
|
|
173
|
+
expect(await resolveTriggerSurface("missing-task", "weird")).toBe("other");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("the root surface is memoized, because a task's root is fixed at creation", async () => {
|
|
177
|
+
const root = await createTaskExtended("root", { agentId: LEAD_ID, source: "jira" });
|
|
178
|
+
expect(await resolveTriggerSurface(root.id)).toBe("jira");
|
|
179
|
+
await getDbClient().run("PRAGMA foreign_keys = OFF");
|
|
180
|
+
await getDbClient().run("DELETE FROM agent_tasks WHERE id = ?", [root.id]);
|
|
181
|
+
expect(await resolveTriggerSurface(root.id)).toBe("jira");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("a chain deeper than the cap stops at the cap instead of looping", async () => {
|
|
185
|
+
// 60 tasks in a line, inserted as raw rows to avoid 60 full createTask calls.
|
|
186
|
+
const ids = Array.from({ length: 60 }, (_, i) => `chain-${String(i).padStart(2, "0")}`);
|
|
187
|
+
await getDbClient().run("PRAGMA foreign_keys = OFF");
|
|
188
|
+
for (const [i, id] of ids.entries()) {
|
|
189
|
+
await getDbClient().run(
|
|
190
|
+
`INSERT INTO agent_tasks (id, task, status, source, parentTaskId, createdAt, lastUpdatedAt)
|
|
191
|
+
VALUES (?, 'x', 'pending', ?, ?, datetime('now'), datetime('now'))`,
|
|
192
|
+
[id, i === 0 ? "slack" : "mcp", i === 0 ? null : ids[i - 1]],
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
// Depth 50 above the tail is chain-09, an mcp task: the walk stopped at the cap.
|
|
196
|
+
expect(await resolveTriggerSurface("chain-59")).toBe("mcp");
|
|
197
|
+
// A chain inside the cap reaches the slack root.
|
|
198
|
+
expect(await resolveTriggerSurface("chain-40")).toBe("slack");
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
export async function restoreAllTemplateDefinitions(): Promise<void> {
|
|
18
18
|
const ts = Date.now();
|
|
19
19
|
await import(`../agentmail/templates?t=${ts}`);
|
|
20
|
+
await import(`../comb/templates?t=${ts}`);
|
|
20
21
|
await import(`../commands/templates?t=${ts}`);
|
|
21
22
|
await import(`../github/templates?t=${ts}`);
|
|
22
23
|
await import(`../gitlab/templates?t=${ts}`);
|