@desplega.ai/agent-swarm 2.4.0 → 2.4.2

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.
Files changed (239) hide show
  1. package/README.md +1 -1
  2. package/dist/{acp-adapter-kedstxx6.js → acp-adapter-dv7hmx4r.js} +9 -9
  3. package/dist/{actions-vget6r1k.js → actions-51nc1gny.js} +21 -21
  4. package/dist/{amp-adapter-xeb5mktv.js → amp-adapter-abp5tdx8.js} +4 -4
  5. package/dist/{app-mqap8t8m.js → app-9h14qvr6.js} +3 -3
  6. package/dist/{assistant-h0gv97mv.js → assistant-wv1gz3mt.js} +22 -22
  7. package/dist/{boot-reembed-qr8a894z.js → boot-reembed-6yn53ms9.js} +1 -1
  8. package/dist/{boot-reembed-rj85jjhw.js → boot-reembed-7fcka4z5.js} +1 -1
  9. package/dist/{boot-scrub-sweep-eyfvn6v9.js → boot-scrub-sweep-pm1580ax.js} +4 -4
  10. package/dist/{boot-seeds-xs9a8dx1.js → boot-seeds-g5hfc1x8.js} +4 -4
  11. package/dist/{claude-adapter-bm23cye7.js → claude-adapter-gf3kxhf0.js} +9 -9
  12. package/dist/{claude-managed-adapter-1mwrsm9d.js → claude-managed-adapter-h2y889ge.js} +2 -2
  13. package/dist/{claude-managed-setup-gkfgks4j.js → claude-managed-setup-h00hsgtz.js} +2 -2
  14. package/dist/{claude-sdk-session-apb4ypz3.js → claude-sdk-session-s2k1qd8y.js} +13 -13
  15. package/dist/{cli-tj4j2ts4.js → cli-02qm7src.js} +8 -8
  16. package/dist/{cli-1cevz46c.js → cli-0qvtffe9.js} +6 -6
  17. package/dist/{cli-avmqhatn.js → cli-10hhg0km.js} +8 -8
  18. package/dist/{cli-7gh7020n.js → cli-1dmcq2eh.js} +1 -1
  19. package/dist/{cli-kz116kvw.js → cli-239yjk27.js} +7 -7
  20. package/dist/{cli-7ayprxp7.js → cli-2dfbv8qr.js} +8 -0
  21. package/dist/{cli-a49vpbf3.js → cli-5kqnemen.js} +1 -1
  22. package/dist/{cli-x719j3vs.js → cli-5rk36sjf.js} +442 -205
  23. package/dist/{cli-fb6t92v8.js → cli-6x9ma5w5.js} +1 -1
  24. package/dist/{cli-c8fwpgqb.js → cli-7225gwj7.js} +2 -2
  25. package/dist/{cli-5s5hx57y.js → cli-7awf1fp8.js} +1 -1
  26. package/dist/{cli-bn26xbcs.js → cli-8nyhf7za.js} +1 -1
  27. package/dist/{cli-ywyrffff.js → cli-8sxqsbbq.js} +1 -1
  28. package/dist/{cli-rsd7ga7v.js → cli-8zwe9mpk.js} +11 -11
  29. package/dist/{cli-y83zk5sb.js → cli-a0kcty51.js} +4 -4
  30. package/dist/{cli-8b9wvjps.js → cli-a4gvwttf.js} +4 -4
  31. package/dist/{cli-jhp7w1fy.js → cli-a5ebc95n.js} +2 -2
  32. package/dist/{cli-mvg2hf8p.js → cli-ahtpy9kf.js} +14 -7
  33. package/dist/{cli-x1p5dhq2.js → cli-bcj753sq.js} +3 -3
  34. package/dist/{cli-p22zdvnt.js → cli-bn0rewsy.js} +3 -3
  35. package/dist/{cli-p1m2b6gc.js → cli-d2mb5s8a.js} +7 -7
  36. package/dist/{cli-rg7ntypm.js → cli-d3qqdy4c.js} +456 -94
  37. package/dist/{cli-c5sbmw1v.js → cli-d82btycs.js} +94 -15
  38. package/dist/{cli-02vhhssk.js → cli-efaqv1c7.js} +5 -5
  39. package/dist/{cli-vjfpshn3.js → cli-ejs3m1sx.js} +1 -1
  40. package/dist/{cli-kk2r80n5.js → cli-faypxz7q.js} +4 -4
  41. package/dist/{cli-a1esmmee.js → cli-fpmsqq59.js} +7 -2
  42. package/dist/{cli-wrv09v7q.js → cli-ftyypbg5.js} +1 -1
  43. package/dist/{cli-zvze9rbm.js → cli-fyzjmseb.js} +3 -3
  44. package/dist/{cli-8rwvksbc.js → cli-g0hgtz4p.js} +5 -5
  45. package/dist/{cli-nrrg0fvy.js → cli-h38rjv87.js} +133 -12
  46. package/dist/{cli-tg9ksbn8.js → cli-h93c9vb3.js} +2 -2
  47. package/dist/{cli-eqgfs202.js → cli-hqhzg1qq.js} +2 -2
  48. package/dist/{cli-bhwa8rzc.js → cli-hyfw2j1e.js} +13 -12
  49. package/dist/{cli-3wjs22x3.js → cli-j4rqdf2n.js} +2 -2
  50. package/dist/{cli-6ftrrwm1.js → cli-n85dqg6d.js} +5 -5
  51. package/dist/{cli-8cxkw1f5.js → cli-nmqdc2t3.js} +2 -2
  52. package/dist/{cli-4fhyq711.js → cli-nn47d7e8.js} +13 -13
  53. package/dist/{cli-x3en6ct3.js → cli-nwyw3zhg.js} +1 -1
  54. package/dist/{cli-3avfh5az.js → cli-pemywhfq.js} +2 -2
  55. package/dist/{cli-xfe63zx3.js → cli-pkht5hap.js} +2 -2
  56. package/dist/{cli-sppc0cmw.js → cli-pnp2j5zg.js} +2 -2
  57. package/dist/{cli-wk8ccrz6.js → cli-pw7v69vp.js} +2 -2
  58. package/dist/{cli-tgvpmnwz.js → cli-r99kc9yq.js} +5 -5
  59. package/dist/{cli-bfc3k1x5.js → cli-rdyw3mct.js} +9 -9
  60. package/dist/{cli-b80a1100.js → cli-s6vr7r2d.js} +2 -2
  61. package/dist/{cli-zv4vy2b0.js → cli-s95dknxh.js} +1 -1
  62. package/dist/{cli-ttaz6c7m.js → cli-saw9a6f6.js} +6 -5
  63. package/dist/{cli-mz1ezzzh.js → cli-shvfedvm.js} +3 -3
  64. package/dist/{cli-rdr166ht.js → cli-smvcw9z5.js} +1 -1
  65. package/dist/{cli-2rwet3r7.js → cli-sv5gb0yb.js} +1 -1
  66. package/dist/{cli-5ff2sjyt.js → cli-t30yj95q.js} +2 -2
  67. package/dist/{cli-ptkpaqfy.js → cli-wt645v7q.js} +1 -1
  68. package/dist/{cli-f3nx1h8z.js → cli-wxbn3yfx.js} +2 -2
  69. package/dist/{cli-wyrr38xm.js → cli-x5mq1fyq.js} +1 -1
  70. package/dist/{cli-ccn0eybf.js → cli-xbzvet96.js} +6 -6
  71. package/dist/{cli-4mkvwgzr.js → cli-xf234ws8.js} +1 -1
  72. package/dist/{cli-nme7wery.js → cli-y2x52gs2.js} +46 -13
  73. package/dist/{cli-ffbrkh1r.js → cli-y8n5k515.js} +46 -38
  74. package/dist/{cli-ecshf504.js → cli-z4wd2ftv.js} +7 -7
  75. package/dist/{cli-dsgj3e7a.js → cli-zfg9enh9.js} +2 -2
  76. package/dist/{cli-yxqag76b.js → cli-zw88zjba.js} +1 -1
  77. package/dist/cli.js +20 -20
  78. package/dist/{codex-adapter-pp2gq8gr.js → codex-adapter-ttygw382.js} +6 -6
  79. package/dist/{codex-hook-qwzccd64.js → codex-hook-x7nejyrr.js} +7 -4
  80. package/dist/{codex-login-35p4sk85.js → codex-login-m819c41r.js} +1 -1
  81. package/dist/{codex-session-runner-0vzn09t8.js → codex-session-runner-60b58vv3.js} +7 -7
  82. package/dist/{commands-psqhkttb.js → commands-edmdwa4x.js} +2 -2
  83. package/dist/{controller-t20vz2k8.js → controller-m263fg43.js} +4 -4
  84. package/dist/{controllers-connect-gkxmqw9d.js → controllers-connect-5fym8sw1.js} +23 -23
  85. package/dist/{cursor-adapter-3v542qe5.js → cursor-adapter-cz6nm6mj.js} +3 -3
  86. package/dist/{db-nc1rd869.js → db-2y0mgstc.js} +3 -3
  87. package/dist/{db-2xbyenjr.js → db-nn0hcmx5.js} +6 -2
  88. package/dist/{devin-adapter-7egzmt0k.js → devin-adapter-zgqadvnm.js} +1 -1
  89. package/dist/{dispatcher-ewceb8ts.js → dispatcher-0a9wwjcx.js} +20 -20
  90. package/dist/{docker-njavd3fb.js → docker-fkgtr75t.js} +2 -2
  91. package/dist/{dsh-adapter-75xwh78t.js → dsh-adapter-3n3net33.js} +3 -3
  92. package/dist/{e2b-5r6a5jeb.js → e2b-7am6tmq6.js} +2 -2
  93. package/dist/{e2b-pw1nvtmh.js → e2b-thdcxtjd.js} +2 -2
  94. package/dist/{easter-egg-3d-xsc47jwf.js → easter-egg-3d-5cgynaja.js} +1 -1
  95. package/dist/{event-bus-101fenkh.js → event-bus-9vpk8gmq.js} +1 -1
  96. package/dist/{execute-gzmw1mnw.js → execute-t4e6m3rw.js} +1 -1
  97. package/dist/{external-channel-a5dsmxn4.js → external-channel-zhvt95n3.js} +2 -2
  98. package/dist/{grok-adapter-vj88xr23.js → grok-adapter-qnr1hp2y.js} +10 -10
  99. package/dist/handlers-dkcarkd0.js +39 -0
  100. package/dist/{heartbeat-t2m9r3wa.js → heartbeat-gz97r7ac.js} +23 -23
  101. package/dist/{hook-xa677f8r.js → hook-1mmdgjqs.js} +16 -9
  102. package/dist/{hook-8femx1bg.js → hook-x8rthca4.js} +6 -6
  103. package/dist/{http-xpmmq028.js → http-r8f85xr9.js} +274 -160
  104. package/dist/index-5jecgp3n.js +41 -0
  105. package/dist/index-aex0eba9.js +40 -0
  106. package/dist/{index-syztqdt6.js → index-g6tbb6ve.js} +1 -1
  107. package/dist/{index-cdzyfrsp.js → index-smxyk04s.js} +1 -1
  108. package/dist/{index-dg2tmj99.js → index-vzvfqzfj.js} +20 -20
  109. package/dist/{index-s7ckdajq.js → index-y7rbcrc1.js} +10 -10
  110. package/dist/{keepalive-3nr4st3r.js → keepalive-64c0f34c.js} +4 -4
  111. package/dist/lead-b1da0f97.js +78 -0
  112. package/dist/{maintenance-verwpe2c.js → maintenance-mnxpar12.js} +1 -1
  113. package/dist/{oauth-refresh-sweep-9q2t0y3q.js → oauth-refresh-sweep-36hs2v4h.js} +3 -3
  114. package/dist/{onboard-aytqedmq.js → onboard-xes8de61.js} +3 -3
  115. package/dist/{opencode-adapter-f4hryjny.js → opencode-adapter-3s7ax02e.js} +7 -7
  116. package/dist/{otel-impl-hckw4wt1.js → otel-impl-fzc57kcq.js} +2 -2
  117. package/dist/{pi-mono-adapter-hxby0ce1.js → pi-mono-adapter-ytp7gz8b.js} +22 -11
  118. package/dist/{pricing-refresh-5nvme3sm.js → pricing-refresh-1nek2w7b.js} +3 -3
  119. package/dist/{rbac-roles-vbpy21mg.js → rbac-roles-sqefb5hw.js} +2 -2
  120. package/dist/{rbac-roles-pd8c26m1.js → rbac-roles-ttyfaep2.js} +1 -1
  121. package/dist/{render-v2-7nqgvk2q.js → render-v2-vb527jwn.js} +3 -3
  122. package/dist/{resume-b6djcj3c.js → resume-jcdzg1bn.js} +10 -8
  123. package/dist/{runtime-txrxc9za.js → runtime-xe3aqy4x.js} +1 -1
  124. package/dist/{session-access-fc3f9y12.js → session-access-zsp4wsp0.js} +4 -4
  125. package/dist/{setup-n39szm6b.js → setup-1nb2zqd3.js} +1 -1
  126. package/dist/{virtual-modules-wyp2k6z3.js → virtual-modules-4r3zkc20.js} +2 -2
  127. package/dist/{work-objects-jem2fre0.js → work-objects-61w08fwr.js} +23 -23
  128. package/dist/worker-c1e68hpa.js +78 -0
  129. package/dist/{x-8ewt8gxn.js → x-y45mthwn.js} +2 -2
  130. package/openapi.json +491 -52
  131. package/package.json +3 -3
  132. package/src/apps/action-runner.ts +146 -0
  133. package/src/apps/catalog.generated.json +3 -2
  134. package/src/apps/definition.ts +152 -4
  135. package/src/apps/row-store.ts +23 -9
  136. package/src/apps/schema-migrate.ts +14 -6
  137. package/src/apps/script-types.ts +2 -1
  138. package/src/apps/sync.ts +20 -19
  139. package/src/be/db/tasks/read.ts +112 -0
  140. package/src/be/db.ts +16 -4
  141. package/src/be/fleet-benchmark-stats.ts +10 -5
  142. package/src/be/models-catalog.ts +4 -0
  143. package/src/be/onboarding.ts +6 -0
  144. package/src/be/scripts/typecheck.ts +13 -0
  145. package/src/be/swarm-config-guard.ts +25 -1
  146. package/src/be/telemetry-enable.ts +1 -1
  147. package/src/commands/provider-credentials.ts +12 -2
  148. package/src/commands/runner.ts +3 -0
  149. package/src/heartbeat/heartbeat.ts +2 -2
  150. package/src/hooks/codex-hook.ts +6 -2
  151. package/src/hooks/hook.ts +7 -0
  152. package/src/hooks/pr-body-guard.ts +38 -11
  153. package/src/hooks/pr-room-guard.ts +88 -0
  154. package/src/http/agents.ts +1 -0
  155. package/src/http/apps.ts +99 -183
  156. package/src/http/fleet-benchmark.ts +79 -30
  157. package/src/http/global-config.ts +7 -0
  158. package/src/http/ranking.ts +24 -40
  159. package/src/http/status.ts +49 -43
  160. package/src/http/tasks.ts +49 -1
  161. package/src/http/telemetry-ui.ts +1 -1
  162. package/src/http/used-by.ts +55 -19
  163. package/src/http/users.ts +2 -2
  164. package/src/providers/acp-targets.ts +1 -1
  165. package/src/providers/amp-adapter.ts +1 -1
  166. package/src/providers/pi-mono-adapter.ts +15 -0
  167. package/src/providers/reasoning-effort.ts +2 -1
  168. package/src/providers/types.ts +2 -0
  169. package/src/ranking/telemetry-ranking.ts +5 -33
  170. package/src/scripts-runtime/sdk-allowlist.ts +7 -0
  171. package/src/scripts-runtime/types/stdlib.d.ts +31 -0
  172. package/src/scripts-runtime/types/swarm-sdk.d.ts +31 -0
  173. package/src/server.ts +4 -0
  174. package/src/telemetry-context.ts +19 -1
  175. package/src/telemetry-contract/EVENTS.md +42 -8
  176. package/src/telemetry-contract/SOURCE.json +6 -5
  177. package/src/telemetry-contract/catalog.json +225 -5
  178. package/src/telemetry-contract/ranking/fixtures/valid/no-claim.json +10 -0
  179. package/src/telemetry-contract/ranking/listing.v1.schema.json +2 -2
  180. package/src/telemetry-contract/types.gen.ts +225 -27
  181. package/src/telemetry-identity.ts +47 -9
  182. package/src/telemetry-snapshot.ts +9 -1
  183. package/src/telemetry-ui-events.ts +9 -2
  184. package/src/telemetry.ts +29 -6
  185. package/src/tests/app-action-run-tool.test.ts +178 -0
  186. package/src/tests/app-row-tools.test.ts +385 -0
  187. package/src/tests/apps-json-column.test.ts +611 -0
  188. package/src/tests/apps-script-types.test.ts +2 -0
  189. package/src/tests/apps-sync-engine.test.ts +42 -0
  190. package/src/tests/check-pr-body.test.ts +0 -23
  191. package/src/tests/credential-check.test.ts +38 -0
  192. package/src/tests/db-task-read-contract.test.ts +4 -0
  193. package/src/tests/db-task-write-contract.test.ts +2 -0
  194. package/src/tests/docs-index-footprint.test.js +2 -2
  195. package/src/tests/fleet-benchmark.test.ts +122 -2
  196. package/src/tests/http-users.test.ts +62 -1
  197. package/src/tests/model-catalog-table.test.ts +63 -0
  198. package/src/tests/model-profiles-api.test.ts +19 -7
  199. package/src/tests/models-catalog.test.ts +50 -0
  200. package/src/tests/onboarding.test.ts +21 -0
  201. package/src/tests/org-website.test.ts +62 -0
  202. package/src/tests/pi-mono-adapter.test.ts +23 -0
  203. package/src/tests/pr-body-leaks.test.ts +4 -0
  204. package/src/tests/pr-room-guard.test.ts +242 -0
  205. package/src/tests/ranking-listing.test.ts +8 -23
  206. package/src/tests/ranking-route.test.ts +49 -47
  207. package/src/tests/reasoning-effort.test.ts +39 -3
  208. package/src/tests/runner-harness-telemetry-env.test.ts +62 -1
  209. package/src/tests/status.test.ts +158 -9
  210. package/src/tests/swarm-tool-result-gate.test.ts +2 -4
  211. package/src/tests/task-lineage.test.ts +180 -0
  212. package/src/tests/telemetry-context.test.ts +128 -2
  213. package/src/tests/telemetry-init.test.ts +62 -5
  214. package/src/tests/telemetry-ui-route.test.ts +123 -0
  215. package/src/tests/tool-annotations.test.ts +3 -2
  216. package/src/tests/used-by-route.test.ts +144 -10
  217. package/src/tests/user-token-rest-auth.test.ts +38 -0
  218. package/src/tests/workflow-agent-task-retry.test.ts +130 -4
  219. package/src/tests/workflow-wait-recovery.test.ts +116 -4
  220. package/src/tools/app-action-run.ts +63 -0
  221. package/src/tools/app-rows.ts +239 -0
  222. package/src/tools/model-catalog/index.ts +1 -1
  223. package/src/tools/tool-config.ts +8 -0
  224. package/src/tools/utils.ts +1 -1
  225. package/src/tools/workflows/retry-workflow-run.ts +1 -1
  226. package/src/types.ts +17 -0
  227. package/src/utils/company-profile.ts +24 -0
  228. package/src/utils/org-website.ts +58 -21
  229. package/src/workflows/recovery.ts +10 -22
  230. package/src/workflows/resume.ts +16 -0
  231. package/src/workflows/task-step-routing.ts +47 -7
  232. package/templates/skills/apps/SKILL.md +44 -4
  233. package/templates/skills/apps/content.md +44 -4
  234. package/dist/handlers-zbs90bny.js +0 -39
  235. package/dist/index-8pwc1jc8.js +0 -40
  236. package/dist/index-nxa6me0q.js +0 -41
  237. package/dist/lead-td8h2sfn.js +0 -78
  238. package/dist/worker-wr6xgfr2.js +0 -78
  239. package/src/tests/check-ranking-entry.test.ts +0 -146
@@ -1,11 +1,13 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
2
  import { unlink } from "node:fs/promises";
3
- import { closeDb, createUser, initDb } from "../be/db";
3
+ import { closeDb, createUser, getDbClient, initDb, updateUser, upsertSwarmConfig } from "../be/db";
4
4
  import { attachRole, detachRole } from "../be/rbac-roles";
5
+ import { loadGlobalConfigsIntoEnv } from "../http/core";
5
6
  import {
6
7
  _getOrgIdForTests,
7
8
  _resetTelemetryStateForTests,
8
9
  initTelemetry,
10
+ setTelemetryFunctionResolver,
9
11
  setTelemetryOrgDomain,
10
12
  track,
11
13
  } from "../telemetry";
@@ -19,12 +21,22 @@ import {
19
21
  validOrgDomain,
20
22
  validOrgId,
21
23
  } from "../telemetry-context";
22
- import { _resetTelemetryIdentityForTests, computeOrgDomain } from "../telemetry-identity";
24
+ import {
25
+ _resetTelemetryIdentityForTests,
26
+ cachedUserFunction,
27
+ computeOrgDomain,
28
+ primeUserRole,
29
+ } from "../telemetry-identity";
23
30
 
24
31
  // initTelemetry no-ops when ANONYMIZED_TELEMETRY=false, and the CI env may set it.
25
32
  process.env.ANONYMIZED_TELEMETRY = "true";
26
33
 
27
34
  const TEST_DB_PATH = "./test-telemetry-context.sqlite";
35
+ const PROFILE_KEYS = [
36
+ "SWARM_ORG_TEAM_SIZE",
37
+ "SWARM_ORG_USE_CASE",
38
+ "SWARM_ORG_ACQUISITION_SOURCE",
39
+ ] as const;
28
40
 
29
41
  async function removeTestDb(): Promise<void> {
30
42
  for (const suffix of ["", "-wal", "-shm"]) {
@@ -116,6 +128,30 @@ describe("telemetry context helpers", () => {
116
128
  expect(mapTriggerSurface(null)).toBe("other");
117
129
  });
118
130
 
131
+ test("buildContext omits invalid profile values and a function without an actor", () => {
132
+ const base = {
133
+ orgId: "org_0123456789abcdef",
134
+ installationId: "install_test",
135
+ isCloud: false,
136
+ isE2b: false,
137
+ swarmVersion: "1.2.3",
138
+ };
139
+ const context = buildContext({
140
+ ...base,
141
+ teamSize: "invalid",
142
+ useCase: "invalid",
143
+ acquisitionSource: "invalid",
144
+ userFunction: "founder",
145
+ });
146
+ expect(context).not.toHaveProperty("team_size");
147
+ expect(context).not.toHaveProperty("use_case");
148
+ expect(context).not.toHaveProperty("user_function");
149
+ expect(context.acquisition_source).toBeNull();
150
+ expect(
151
+ buildContext({ ...base, actor: { userId: "u-1" }, userFunction: "administrator" }),
152
+ ).not.toHaveProperty("user_function");
153
+ });
154
+
119
155
  test("normalizeProperties keeps an event alive when one optional value is unsendable", () => {
120
156
  const out = normalizeProperties("task.completed", {
121
157
  taskId: "t-1",
@@ -248,6 +284,19 @@ describe("telemetry envelope", () => {
248
284
  }
249
285
  });
250
286
 
287
+ test("track resolves the actor's job function without changing the RBAC role", async () => {
288
+ const config = memoryConfig({ telemetry_installation_id: "install_function" });
289
+ await initTelemetry("api-server", config.get, config.set, { generateIfMissing: true });
290
+ setTelemetryFunctionResolver(() => "founder");
291
+ track({
292
+ event: "workflow.deleted",
293
+ properties: { workflowId: "wf-1" },
294
+ actor: { userId: "u-1", role: "member" },
295
+ });
296
+ await tick();
297
+ expect(sent[0]?.context).toMatchObject({ user_function: "founder", user_role: "member" });
298
+ });
299
+
251
300
  test("an org with no ID yet sends nothing instead of a half-identified event", async () => {
252
301
  // A worker on an old server: installation ID known, no org ID stored.
253
302
  const config = memoryConfig({ telemetry_installation_id: "install_noorg" });
@@ -270,18 +319,95 @@ describe("telemetry envelope", () => {
270
319
  });
271
320
 
272
321
  describe("org email domain", () => {
322
+ const savedProfile = Object.fromEntries(PROFILE_KEYS.map((key) => [key, process.env[key]]));
323
+
273
324
  beforeEach(async () => {
274
325
  closeDb();
275
326
  await removeTestDb();
276
327
  initDb(TEST_DB_PATH);
277
328
  _resetTelemetryIdentityForTests();
329
+ for (const key of PROFILE_KEYS) delete process.env[key];
278
330
  });
279
331
 
280
332
  afterEach(async () => {
333
+ for (const key of PROFILE_KEYS) {
334
+ if (savedProfile[key] === undefined) delete process.env[key];
335
+ else process.env[key] = savedProfile[key];
336
+ }
281
337
  closeDb();
282
338
  await removeTestDb();
283
339
  });
284
340
 
341
+ test("stored company profile reaches context after reload and clears without restart", async () => {
342
+ const values = ["11-50", "features", "hacker_news"];
343
+ for (const [index, key] of PROFILE_KEYS.entries()) {
344
+ await upsertSwarmConfig({ scope: "global", key, value: values[index] as string });
345
+ }
346
+ await loadGlobalConfigsIntoEnv(true);
347
+ const originalFetch = globalThis.fetch;
348
+ const sent: Array<{ context: Record<string, unknown> }> = [];
349
+ globalThis.fetch = (async (_url: string, init?: { body?: string }) => {
350
+ if (init?.body) sent.push(JSON.parse(init.body));
351
+ return new Response(null, { status: 204 });
352
+ }) as typeof fetch;
353
+ _resetTelemetryStateForTests();
354
+ try {
355
+ const config = memoryConfig({
356
+ telemetry_installation_id: "install_profile",
357
+ telemetry_org_id: "org_0123456789abcdef",
358
+ });
359
+ await initTelemetry("api-server", config.get, config.set);
360
+ track({ event: "server.started", properties: { port: 3013 } });
361
+ expect(sent).toHaveLength(1);
362
+ expect(sent[0]?.context).toMatchObject({
363
+ team_size: "11-50",
364
+ use_case: "features",
365
+ acquisition_source: "hacker_news",
366
+ });
367
+ for (const key of PROFILE_KEYS) {
368
+ await upsertSwarmConfig({ scope: "global", key, value: "" });
369
+ }
370
+ await loadGlobalConfigsIntoEnv(true);
371
+ track({ event: "server.started", properties: { port: 3013 } });
372
+ expect(sent).toHaveLength(2);
373
+ expect(sent[1]?.context).not.toHaveProperty("team_size");
374
+ expect(sent[1]?.context).not.toHaveProperty("use_case");
375
+ expect(sent[1]?.context.acquisition_source).toBeNull();
376
+ } finally {
377
+ globalThis.fetch = originalFetch;
378
+ _resetTelemetryStateForTests();
379
+ }
380
+ });
381
+
382
+ test("job function uses the role cache and invalidates after a committed user update", async () => {
383
+ const user = await createUser({
384
+ name: "Founder",
385
+ role: "admin",
386
+ metadata: { jobFunction: "founder" },
387
+ });
388
+ await primeUserRole(user.id);
389
+ expect(cachedUserFunction(user.id)).toBe("founder");
390
+ await getDbClient().transaction(async () => {
391
+ await updateUser(user.id, { metadata: { jobFunction: "engineer" } });
392
+ expect(cachedUserFunction(user.id)).toBe("founder");
393
+ });
394
+ await primeUserRole(user.id);
395
+ expect(cachedUserFunction(user.id)).toBe("engineer");
396
+ await updateUser(user.id, { metadata: { jobFunction: "a free-text legacy role" } });
397
+ await primeUserRole(user.id);
398
+ expect(cachedUserFunction(user.id)).toBeUndefined();
399
+ const context = buildContext({
400
+ orgId: "org_0123456789abcdef",
401
+ installationId: "install_profile",
402
+ isCloud: false,
403
+ isE2b: false,
404
+ swarmVersion: "1.2.3",
405
+ actor: { userId: user.id },
406
+ userFunction: cachedUserFunction(user.id),
407
+ });
408
+ expect(context).not.toHaveProperty("user_function");
409
+ });
410
+
285
411
  test("is undefined with no users", async () => {
286
412
  expect(await computeOrgDomain()).toBeUndefined();
287
413
  });
@@ -1,4 +1,8 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { pathToFileURL } from "node:url";
2
6
  import pkg from "../../package.json";
3
7
  import {
4
8
  _getInstallationIdForTests,
@@ -14,6 +18,7 @@ import {
14
18
  _resolveIntegrationProvider,
15
19
  _resolveIntegrationType,
16
20
  emitIntegrationConnected,
21
+ getTelemetryEnvironment,
17
22
  initTelemetry,
18
23
  telemetry,
19
24
  track,
@@ -686,8 +691,11 @@ describe("initTelemetry", () => {
686
691
  const originalFetch = globalThis.fetch;
687
692
  const originalNodeEnv = process.env.NODE_ENV;
688
693
  let captured: Record<string, unknown> | null = null;
694
+ let packageRoot: URL;
689
695
 
690
696
  beforeEach(() => {
697
+ packageRoot = pathToFileURL(`${mkdtempSync(join(tmpdir(), "telemetry-env-"))}/`);
698
+ process.env.NODE_ENV = "production";
691
699
  captured = null;
692
700
  globalThis.fetch = (async (_url: string, init?: { body?: string }) => {
693
701
  captured = init?.body ? JSON.parse(init.body) : null;
@@ -697,14 +705,59 @@ describe("initTelemetry", () => {
697
705
  });
698
706
 
699
707
  afterEach(() => {
708
+ rmSync(packageRoot, { recursive: true, force: true });
700
709
  globalThis.fetch = originalFetch;
701
710
  delete process.env.DESPLEGA_TELEMETRY_ENV;
702
711
  if (originalNodeEnv === undefined) delete process.env.NODE_ENV;
703
712
  else process.env.NODE_ENV = originalNodeEnv;
704
713
  });
705
714
 
715
+ test("track detects the real checkout through the default package root with an unprimed cache", async () => {
716
+ await initTelemetry(
717
+ "api-server",
718
+ async (key) => (key === "telemetry_installation_id" ? "install_checkout_env" : undefined),
719
+ async () => {},
720
+ { generateIfMissing: true },
721
+ );
722
+
723
+ track({ event: "test.event", properties: {} });
724
+ await new Promise((r) => setTimeout(r, 0));
725
+
726
+ const metadata = (captured as { metadata: Record<string, unknown> }).metadata;
727
+ expect(metadata.environment).toBe("development");
728
+ });
729
+
730
+ test("a .git directory marks a checkout as development", () => {
731
+ mkdirSync(new URL(".git", packageRoot));
732
+ expect(getTelemetryEnvironment(packageRoot)).toBe("development");
733
+ rmSync(new URL(".git", packageRoot), { recursive: true });
734
+ expect(getTelemetryEnvironment(packageRoot)).toBe("development");
735
+ });
736
+
737
+ test("a .git file marks a worktree as development", async () => {
738
+ await Bun.write(new URL(".git", packageRoot), "gitdir: /some/worktree");
739
+ expect(getTelemetryEnvironment(packageRoot)).toBe("development");
740
+ });
741
+
742
+ test("caches the checkout check across events", () => {
743
+ expect(getTelemetryEnvironment(packageRoot)).toBe("production");
744
+ mkdirSync(new URL(".git", packageRoot));
745
+ expect(getTelemetryEnvironment(packageRoot)).toBe("production");
746
+ process.env.DESPLEGA_TELEMETRY_ENV = "staging";
747
+ expect(getTelemetryEnvironment(packageRoot)).toBe("staging");
748
+ });
749
+
750
+ test("an unusable package root falls back to production without throwing", () => {
751
+ expect(getTelemetryEnvironment(new URL("data:text/plain,unavailable"))).toBe("production");
752
+ });
753
+
754
+ test("compiled binary paths default to production without throwing", () => {
755
+ expect(getTelemetryEnvironment(new URL("file:///$bunfs/root/"))).toBe("production");
756
+ });
757
+
706
758
  test("defaults to production even when NODE_ENV is development", async () => {
707
759
  process.env.NODE_ENV = "development";
760
+ expect(getTelemetryEnvironment(packageRoot)).toBe("production");
708
761
  await initTelemetry(
709
762
  "api-server",
710
763
  async (key) => (key === "telemetry_installation_id" ? "install_default_env" : undefined),
@@ -719,9 +772,11 @@ describe("initTelemetry", () => {
719
772
  expect(metadata.environment).toBe("production");
720
773
  });
721
774
 
722
- test("uses DESPLEGA_TELEMETRY_ENV when set", async () => {
723
- process.env.NODE_ENV = "production";
724
- process.env.DESPLEGA_TELEMETRY_ENV = "development";
775
+ test("uses DESPLEGA_TELEMETRY_ENV over a git checkout and NODE_ENV=test", async () => {
776
+ mkdirSync(new URL(".git", packageRoot));
777
+ process.env.NODE_ENV = "test";
778
+ process.env.DESPLEGA_TELEMETRY_ENV = "staging";
779
+ expect(getTelemetryEnvironment(packageRoot)).toBe("staging");
725
780
  await initTelemetry(
726
781
  "api-server",
727
782
  async (key) => (key === "telemetry_installation_id" ? "install_explicit_env" : undefined),
@@ -733,11 +788,13 @@ describe("initTelemetry", () => {
733
788
  await new Promise((r) => setTimeout(r, 0));
734
789
 
735
790
  const metadata = (captured as { metadata: Record<string, unknown> }).metadata;
736
- expect(metadata.environment).toBe("development");
791
+ expect(metadata.environment).toBe("staging");
737
792
  });
738
793
 
739
- test("preserves NODE_ENV=test when telemetry env is unset", async () => {
794
+ test("preserves NODE_ENV=test over a git checkout when telemetry env is unset", async () => {
795
+ mkdirSync(new URL(".git", packageRoot));
740
796
  process.env.NODE_ENV = "test";
797
+ expect(getTelemetryEnvironment(packageRoot)).toBe("test");
741
798
  await initTelemetry(
742
799
  "api-server",
743
800
  async (key) => (key === "telemetry_installation_id" ? "install_test_env" : undefined),
@@ -5,6 +5,7 @@ import { isPublicRoute } from "../http/route-def";
5
5
  import { handleTelemetryUi } from "../http/telemetry-ui";
6
6
  import { getPathSegments, parseQueryParams } from "../http/utils";
7
7
  import { _resetTelemetryStateForTests, initTelemetry } from "../telemetry";
8
+ import catalog from "../telemetry-contract/catalog.json";
8
9
  import { UI_EVENT_NAMES, validateUiEvent } from "../telemetry-ui-events";
9
10
  import type { HttpRequestAuth } from "../utils/request-auth-context";
10
11
  import { setRequestAuth } from "../utils/request-auth-context";
@@ -118,6 +119,128 @@ describe("POST /api/telemetry/ui-event", () => {
118
119
  expect(sent).toHaveLength(0);
119
120
  });
120
121
 
122
+ test.each([
123
+ { event: "identity.lookup", properties: { matched: true } },
124
+ ...["identity_modal", "setup"].map((surface) => ({
125
+ event: "identity.user_created",
126
+ properties: { surface, email_kind: "work", has_function: true, has_company: true },
127
+ })),
128
+ {
129
+ event: "fleet_benchmark.unlocked",
130
+ properties: { fields: ["website", "team_size", "use_case"] },
131
+ },
132
+ ...["compose", "docker", "kubernetes"].map((target) => ({
133
+ event: "setup.worker_command_copied",
134
+ properties: { target, harness: "codex" },
135
+ })),
136
+ ...[
137
+ "prompt",
138
+ "setup",
139
+ "settings",
140
+ "org_name_popup",
141
+ "feedback",
142
+ "fleet_benchmark",
143
+ "telemetry_banner",
144
+ "identity_modal",
145
+ ].map((surface) => ({
146
+ event: "org.profile_updated",
147
+ properties: {
148
+ fields: ["name", "website", "team_size", "use_case", "acquisition_source"],
149
+ surface,
150
+ },
151
+ })),
152
+ {
153
+ event: "popup.submitted",
154
+ properties: {
155
+ popup: "feedback",
156
+ has_message: false,
157
+ has_contact: false,
158
+ answered: 3,
159
+ perks: ["used_by", "slack_connect"],
160
+ },
161
+ },
162
+ ...["notification.viewed", "notification.dismissed", "notification.submitted"].flatMap(
163
+ (event) =>
164
+ ["task_failed", "first_workflow", "default"].map((moment) => ({
165
+ event,
166
+ properties: {
167
+ key: "slack_connect_invite",
168
+ moment,
169
+ ...(event === "notification.submitted" ? { user_function: "engineer" } : {}),
170
+ },
171
+ })),
172
+ ),
173
+ ])("accepts the company-profile catalog event $event with $properties", async ({
174
+ event,
175
+ properties,
176
+ }) => {
177
+ const result = await post({ event, properties });
178
+ await flush();
179
+ expect(result.statusCode).toBe(204);
180
+ expect(sent).toHaveLength(1);
181
+ expect(sent[0]).toMatchObject({ event, source: "ui", properties });
182
+ });
183
+
184
+ test.each([
185
+ {
186
+ event: "identity.user_created",
187
+ properties: {
188
+ surface: "invalid",
189
+ email_kind: "work",
190
+ has_function: true,
191
+ has_company: false,
192
+ },
193
+ },
194
+ { event: "setup.worker_command_copied", properties: { target: "invalid", harness: "codex" } },
195
+ { event: "org.profile_updated", properties: { fields: ["team_size"], surface: "invalid" } },
196
+ { event: "org.profile_updated", properties: { fields: ["invalid"], surface: "settings" } },
197
+ { event: "fleet_benchmark.unlocked", properties: { fields: ["invalid"] } },
198
+ {
199
+ event: "popup.submitted",
200
+ properties: { popup: "feedback", has_message: false, has_contact: false, perks: ["invalid"] },
201
+ },
202
+ {
203
+ event: "notification.submitted",
204
+ properties: { key: "slack_connect_invite", moment: "invalid" },
205
+ },
206
+ {
207
+ event: "notification.submitted",
208
+ properties: { key: "slack_connect_invite", user_function: "invalid" },
209
+ },
210
+ ])("rejects a bad enum in $event with $properties", async ({ event, properties }) => {
211
+ const result = await post({ event, properties });
212
+ expect(result.statusCode).toBe(400);
213
+ expect(sent).toHaveLength(0);
214
+ });
215
+
216
+ test("array item references enforce the referenced enum", async () => {
217
+ const entry = catalog.events.find((entry) => entry.event === "fleet_benchmark.unlocked")!;
218
+ const fields = entry.properties.fields!;
219
+ const original = fields.items;
220
+ // Exercise a referenced item definition without changing the vendored catalog.
221
+ Object.assign(fields, { items: { ref: "use_case" } });
222
+ try {
223
+ expect(
224
+ (
225
+ await post({
226
+ event: "fleet_benchmark.unlocked",
227
+ properties: { fields: ["features"] },
228
+ })
229
+ ).statusCode,
230
+ ).toBe(204);
231
+ sent = [];
232
+ const result = await post({
233
+ event: "fleet_benchmark.unlocked",
234
+ properties: { fields: ["invalid"] },
235
+ });
236
+ expect(result.statusCode).toBe(400);
237
+ expect(result.body).toContain("items must be one of");
238
+ expect(sent).toHaveLength(0);
239
+ } finally {
240
+ fields.items = original;
241
+ }
242
+ });
243
+
121
244
  test("an unknown event name gets 400 and sends nothing", async () => {
122
245
  const result = await post({ event: "task.created", properties: {} });
123
246
  await flush();
@@ -376,9 +376,10 @@ describe("Tool Annotations & Classification", () => {
376
376
  // (this suite forces ALL_CAPABILITIES, including default-disabled ones).
377
377
  // Includes 11 skill tools, 7 MCP server tools, reusable script tools, and the
378
378
  // native Kapso/WhatsApp tools (register/unregister number + send/reply message),
379
- // and the six Chat SDK tools (chat-* + register/unregister-chat-channel).
379
+ // the six Chat SDK tools (chat-* + register/unregister-chat-channel),
380
+ // and the seven app row tools plus app-action-run.
380
381
  expect(count).toBeGreaterThanOrEqual(45);
381
- expect(count).toBeLessThanOrEqual(163);
382
+ expect(count).toBeLessThanOrEqual(169);
382
383
  });
383
384
 
384
385
  test("core tools are fewer than deferred tools", () => {