@app-vitals/agent-admin 0.0.1 → 0.1.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.
Files changed (325) hide show
  1. package/Dockerfile +43 -0
  2. package/README.md +72 -2
  3. package/e2e/agents-page.e2e.ts +338 -0
  4. package/e2e/chat-degraded.e2e.ts +153 -0
  5. package/e2e/chat-page.e2e.ts +169 -0
  6. package/e2e/chat-progress.e2e.ts +206 -0
  7. package/e2e/chat-thread-page.e2e.ts +444 -0
  8. package/e2e/login-page.e2e.ts +148 -0
  9. package/e2e/test-server.ts +427 -0
  10. package/openapi.json +6098 -0
  11. package/openapi.platform.json +5553 -0
  12. package/package.json +62 -4
  13. package/playwright.config.ts +25 -0
  14. package/prisma/client/browser.ts +134 -0
  15. package/prisma/client/client.ts +158 -0
  16. package/prisma/client/commonInputTypes.ts +578 -0
  17. package/prisma/client/enums.ts +15 -0
  18. package/prisma/client/internal/class.ts +424 -0
  19. package/prisma/client/internal/prismaNamespace.ts +2898 -0
  20. package/prisma/client/internal/prismaNamespaceBrowser.ts +476 -0
  21. package/prisma/client/models/Account.ts +1774 -0
  22. package/prisma/client/models/AccountInvite.ts +1386 -0
  23. package/prisma/client/models/AccountMember.ts +1351 -0
  24. package/prisma/client/models/Agent.ts +4554 -0
  25. package/prisma/client/models/AgentAsk.ts +1373 -0
  26. package/prisma/client/models/AgentChatTokenUsageDailyByModel.ts +1638 -0
  27. package/prisma/client/models/AgentCronJob.ts +2416 -0
  28. package/prisma/client/models/AgentCronRun.ts +2858 -0
  29. package/prisma/client/models/AgentCronRunModelBreakdown.ts +1532 -0
  30. package/prisma/client/models/AgentCronRunSkillUsage.ts +1642 -0
  31. package/prisma/client/models/AgentEnv.ts +1421 -0
  32. package/prisma/client/models/AgentGitHubInstallationsSnapshot.ts +1289 -0
  33. package/prisma/client/models/AgentMember.ts +1316 -0
  34. package/prisma/client/models/AgentPhaseMethodology.ts +1355 -0
  35. package/prisma/client/models/AgentPlugin.ts +1421 -0
  36. package/prisma/client/models/AgentToken.ts +1380 -0
  37. package/prisma/client/models/AgentTool.ts +1351 -0
  38. package/prisma/client/models/AgentWorkQueueSnapshot.ts +1297 -0
  39. package/prisma/client/models/ChatThreadWatch.ts +1183 -0
  40. package/prisma/client/models/PushSubscription.ts +1233 -0
  41. package/prisma/client/models/SessionAlertState.ts +1187 -0
  42. package/prisma/client/models/SessionFollow.ts +1187 -0
  43. package/prisma/client/models/UserNotificationPrefs.ts +1221 -0
  44. package/prisma/client/models.ts +34 -0
  45. package/prisma/migrations/20260608000000_init/migration.sql +112 -0
  46. package/prisma/migrations/20260616000000_agent_members/migration.sql +18 -0
  47. package/prisma/migrations/20260624000000_add_agent_self_hosted/migration.sql +2 -0
  48. package/prisma/migrations/20260624120000_add_agent_repos/migration.sql +3 -0
  49. package/prisma/migrations/20260625000000_add_agent_cron_run/migration.sql +26 -0
  50. package/prisma/migrations/20260626000000_add_cron_run_token_fields/migration.sql +10 -0
  51. package/prisma/migrations/20260627000000_add_agent_chat_token_usage_daily/migration.sql +18 -0
  52. package/prisma/migrations/20260627010000_add_agent_chat_token_usage_fk/migration.sql +2 -0
  53. package/prisma/migrations/20260701000000_add_chat_threads_messages/migration.sql +37 -0
  54. package/prisma/migrations/20260701010000_drop_chat_threads_messages/migration.sql +17 -0
  55. package/prisma/migrations/20260701020000_add_cron_run_model_breakdown/migration.sql +23 -0
  56. package/prisma/migrations/20260701020000_chat_token_by_model/migration.sql +25 -0
  57. package/prisma/migrations/20260701030000_add_secret_flag_to_agent_env/migration.sql +2 -0
  58. package/prisma/migrations/20260701030000_drop_cron_run_legacy_columns/migration.sql +5 -0
  59. package/prisma/migrations/20260706000000_consolidate_cron_run_token_accounting/migration.sql +54 -0
  60. package/prisma/migrations/20260709000000_add_phase_to_agent_cron_run/migration.sql +8 -0
  61. package/prisma/migrations/20260717000000_add_item_tracking_to_agent_cron_run/migration.sql +10 -0
  62. package/prisma/migrations/20260717010000_add_agent_work_queue_snapshot/migration.sql +20 -0
  63. package/prisma/migrations/20260718000000_add_cron_phase_columns/migration.sql +25 -0
  64. package/prisma/migrations/20260718100000_drop_agent_cron_run_phase_column/migration.sql +8 -0
  65. package/prisma/migrations/20260724000000_add_agent_type_name/migration.sql +3 -0
  66. package/prisma/migrations/20260725232433_add_agent_author_allowlist/migration.sql +2 -0
  67. package/prisma/migrations/20260727120000_add_not_null_to_agent_author_allowlist/migration.sql +3 -0
  68. package/prisma/migrations/20260731000000_add_session_id_to_agent_cron_run/migration.sql +2 -0
  69. package/prisma/migrations/20260821000000_add_agent_restrict_slack_to_members/migration.sql +3 -0
  70. package/prisma/migrations/20260901000000_add_push_subscription_and_chat_thread_watch/migration.sql +40 -0
  71. package/prisma/migrations/20260903000000_add_agent_patch_author_allowlist/migration.sql +2 -0
  72. package/prisma/migrations/20260903120000_add_agent_review_author_allowlist/migration.sql +18 -0
  73. package/prisma/migrations/20260903150000_drop_agent_author_allowlist/migration.sql +14 -0
  74. package/prisma/migrations/20260905000000_add_started_at_index_to_agent_cron_run/migration.sql +11 -0
  75. package/prisma/migrations/20260906000000_add_session_follow_notification_prefs_alert_state/migration.sql +47 -0
  76. package/prisma/migrations/20260911000000_backfill_auto_follow_since/migration.sql +16 -0
  77. package/prisma/migrations/20260923115814_add_last_heartbeat_at_to_agent_cron_run/migration.sql +2 -0
  78. package/prisma/migrations/20260924120000_add_trial_expiry_fields_to_agent/migration.sql +6 -0
  79. package/prisma/migrations/20260929000000_add_agent_phase_methodology/migration.sql +21 -0
  80. package/prisma/migrations/20260929120000_add_agent_policy_fields/migration.sql +15 -0
  81. package/prisma/migrations/20261005000000_add_agent_github_installations_snapshot/migration.sql +21 -0
  82. package/prisma/migrations/20261008000000_add_cron_run_telemetry/migration.sql +50 -0
  83. package/prisma/migrations/20261008010000_add_account_models/migration.sql +48 -0
  84. package/prisma/migrations/20261008020000_add_cron_lockdown_disabled_at/migration.sql +3 -0
  85. package/prisma/migrations/20261009000000_chat_token_usage_bigint/migration.sql +7 -0
  86. package/prisma/migrations/20261010000000_add_cron_run_command_variant/migration.sql +2 -0
  87. package/prisma/migrations/20261019000000_add_agent_anthropic_identity/migration.sql +9 -0
  88. package/prisma/migrations/20261025000000_add_agent_ask/migration.sql +23 -0
  89. package/prisma/migrations/migration_lock.toml +3 -0
  90. package/prisma/schema/coding.prisma +85 -0
  91. package/prisma/schema/schema.prisma +506 -0
  92. package/prisma.config.ts +34 -0
  93. package/pwa-assets/icons/apple-touch-icon.png +0 -0
  94. package/pwa-assets/icons/favicon-32.png +0 -0
  95. package/pwa-assets/icons/icon-192.png +0 -0
  96. package/pwa-assets/icons/icon-512.png +0 -0
  97. package/pwa-assets/icons/icon-maskable-192.png +0 -0
  98. package/pwa-assets/icons/icon-maskable-512.png +0 -0
  99. package/src/account-agent-create.ts +42 -0
  100. package/src/account-agent-create.unit.test.ts +28 -0
  101. package/src/account-created-notifier.ts +71 -0
  102. package/src/account-created-notifier.unit.test.ts +70 -0
  103. package/src/account-email.ts +9 -0
  104. package/src/account-email.unit.test.ts +21 -0
  105. package/src/account-invites.ts +70 -0
  106. package/src/account-lifecycle.integration.test.ts +329 -0
  107. package/src/account-lifecycle.ts +206 -0
  108. package/src/account-lifecycle.unit.test.ts +108 -0
  109. package/src/account-members.ts +128 -0
  110. package/src/account-onboarding.integration.test.ts +101 -0
  111. package/src/account-onboarding.ts +95 -0
  112. package/src/account-trial-expiry-sweeper.ts +85 -0
  113. package/src/account-trial-warning-sweeper.integration.test.ts +228 -0
  114. package/src/account-trial-warning-sweeper.ts +216 -0
  115. package/src/accounts-api.integration.test.ts +196 -0
  116. package/src/accounts-api.smoke.test.ts +292 -0
  117. package/src/accounts-api.ts +289 -0
  118. package/src/accounts-api.unit.test.ts +93 -0
  119. package/src/accounts.integration.test.ts +178 -0
  120. package/src/accounts.ts +208 -0
  121. package/src/admin-boot-config.ts +574 -0
  122. package/src/admin-boot-config.unit.test.ts +28 -0
  123. package/src/admin-boot.boot-order.unit.test.ts +91 -0
  124. package/src/admin-boot.ts +690 -0
  125. package/src/admin-dev-login.smoke.test.ts +317 -0
  126. package/src/admin-extension.ts +432 -0
  127. package/src/admin-extension.unit.test.ts +532 -0
  128. package/src/admin-local-agent.smoke.test.ts +1643 -0
  129. package/src/admin-spec.smoke.test.ts +96 -0
  130. package/src/admin-spec.ts +186 -0
  131. package/src/admin-ui-account-nav.ts +20 -0
  132. package/src/admin-ui-account-pages.ts +131 -0
  133. package/src/admin-ui-account-pages.unit.test.ts +112 -0
  134. package/src/admin-ui-account.smoke.test.ts +286 -0
  135. package/src/admin-ui-account.ts +185 -0
  136. package/src/admin-ui-accounts-pages.ts +202 -0
  137. package/src/admin-ui-accounts-pages.unit.test.ts +138 -0
  138. package/src/admin-ui-accounts.smoke.test.ts +434 -0
  139. package/src/admin-ui-accounts.ts +266 -0
  140. package/src/admin-ui-layout.ts +60 -0
  141. package/src/admin-ui-layout.unit.test.ts +140 -0
  142. package/src/admin-ui-pages.ts +3489 -0
  143. package/src/admin-ui-pages.unit.test.ts +4355 -0
  144. package/src/admin-ui-push-test.smoke.test.ts +125 -0
  145. package/src/admin-ui-push-test.ts +102 -0
  146. package/src/admin-ui-styles.ts +656 -0
  147. package/src/admin-ui-styles.unit.test.ts +103 -0
  148. package/src/admin-ui.fixtures.ts +457 -0
  149. package/src/admin-ui.smoke.test.ts +7227 -0
  150. package/src/admin-ui.ts +3824 -0
  151. package/src/agent-account-assignment.ts +49 -0
  152. package/src/agent-account-assignment.unit.test.ts +99 -0
  153. package/src/agent-anthropic-identity.integration.test.ts +99 -0
  154. package/src/agent-anthropic-identity.ts +78 -0
  155. package/src/agent-anthropic-identity.unit.test.ts +77 -0
  156. package/src/agent-ask-config.ts +59 -0
  157. package/src/agent-ask-config.unit.test.ts +70 -0
  158. package/src/agent-ask.integration.test.ts +175 -0
  159. package/src/agent-ask.smoke.test.ts +457 -0
  160. package/src/agent-ask.ts +378 -0
  161. package/src/agent-chat-tokens.integration.test.ts +420 -0
  162. package/src/agent-chat-tokens.smoke.test.ts +551 -0
  163. package/src/agent-chat-tokens.ts +259 -0
  164. package/src/agent-cron-jobs.integration.test.ts +582 -0
  165. package/src/agent-cron-jobs.ts +615 -0
  166. package/src/agent-cron-jobs.unit.test.ts +420 -0
  167. package/src/agent-cron-run-stats.integration.test.ts +1551 -0
  168. package/src/agent-cron-run-stats.smoke.test.ts +953 -0
  169. package/src/agent-cron-run-stats.ts +752 -0
  170. package/src/agent-cron-runs.integration.test.ts +1491 -0
  171. package/src/agent-cron-runs.ts +489 -0
  172. package/src/agent-deletion-checklist.ts +128 -0
  173. package/src/agent-deletion-checklist.unit.test.ts +201 -0
  174. package/src/agent-deletion-per-agent.integration.test.ts +323 -0
  175. package/src/agent-deletion.integration.test.ts +323 -0
  176. package/src/agent-deletion.ts +329 -0
  177. package/src/agent-deletion.unit.test.ts +412 -0
  178. package/src/agent-envs.integration.test.ts +323 -0
  179. package/src/agent-envs.ts +244 -0
  180. package/src/agent-github-installations.integration.test.ts +115 -0
  181. package/src/agent-github-installations.ts +52 -0
  182. package/src/agent-manifest.ts +685 -0
  183. package/src/agent-manifest.unit.test.ts +870 -0
  184. package/src/agent-members.ts +69 -0
  185. package/src/agent-members.unit.test.ts +250 -0
  186. package/src/agent-patch-author-allowlist.integration.test.ts +218 -0
  187. package/src/agent-plugins.integration.test.ts +157 -0
  188. package/src/agent-plugins.ts +95 -0
  189. package/src/agent-plugins.unit.test.ts +43 -0
  190. package/src/agent-provisioner-per-agent.integration.test.ts +949 -0
  191. package/src/agent-provisioner.integration.test.ts +1245 -0
  192. package/src/agent-provisioner.ts +1650 -0
  193. package/src/agent-provisioner.unit.test.ts +1852 -0
  194. package/src/agent-tokens.integration.test.ts +150 -0
  195. package/src/agent-tokens.ts +112 -0
  196. package/src/agent-tools.integration.test.ts +129 -0
  197. package/src/agent-tools.ts +111 -0
  198. package/src/agents-api.integration.test.ts +705 -0
  199. package/src/agents-api.sentry.smoke.test.ts +478 -0
  200. package/src/agents-api.smoke.test.ts +5986 -0
  201. package/src/agents-api.ts +2826 -0
  202. package/src/agents.integration.test.ts +512 -0
  203. package/src/agents.ts +1097 -0
  204. package/src/agents.unit.test.ts +1736 -0
  205. package/src/anthropic-credential-guard.ts +44 -0
  206. package/src/anthropic-credential-guard.unit.test.ts +88 -0
  207. package/src/anthropic-federated-token-provider.integration.test.ts +248 -0
  208. package/src/anthropic-federated-token-provider.ts +249 -0
  209. package/src/anthropic-identity-client.fixtures.ts +237 -0
  210. package/src/anthropic-identity-client.integration.test.ts +342 -0
  211. package/src/anthropic-identity-client.ts +473 -0
  212. package/src/anthropic-identity-fixture-spec.integration.test.ts +169 -0
  213. package/src/anthropic-identity-name.integration.test.ts +83 -0
  214. package/src/anthropic-identity-name.ts +163 -0
  215. package/src/anthropic-identity-name.unit.test.ts +345 -0
  216. package/src/anthropic-openapi-contract.fixtures.ts +192 -0
  217. package/src/api-auth.ts +383 -0
  218. package/src/api-auth.unit.test.ts +837 -0
  219. package/src/api.smoke.test.ts +1225 -0
  220. package/src/api.ts +376 -0
  221. package/src/attachment-validation.ts +124 -0
  222. package/src/attachment-validation.unit.test.ts +218 -0
  223. package/src/caller-scope.ts +131 -0
  224. package/src/caller-scope.unit.test.ts +116 -0
  225. package/src/chat-credential-config.ts +22 -0
  226. package/src/chat-credentials-logging.unit.test.ts +92 -0
  227. package/src/chat-markers.ts +114 -0
  228. package/src/chat-markers.unit.test.ts +116 -0
  229. package/src/chat-service-provisioning-client.integration.test.ts +412 -0
  230. package/src/chat-service-provisioning-client.ts +221 -0
  231. package/src/chat.smoke.test.ts +1875 -0
  232. package/src/clock.ts +47 -0
  233. package/src/coding-task-store-provisioning-client.integration.test.ts +372 -0
  234. package/src/coding-task-store-provisioning-client.ts +156 -0
  235. package/src/crypto.ts +83 -0
  236. package/src/dev-auth-guard.ts +29 -0
  237. package/src/dev-auth-guard.unit.test.ts +51 -0
  238. package/src/errors.ts +77 -0
  239. package/src/errors.unit.test.ts +52 -0
  240. package/src/fixtures/anthropic-identity-recorded-fixtures.json +850 -0
  241. package/src/fixtures/anthropic-openapi/SDK_VERSION +1 -0
  242. package/src/fixtures/anthropic-openapi/mock-spec.json.gz +0 -0
  243. package/src/fixtures/chat-service-provisioning-cassette.json +156 -0
  244. package/src/fixtures/github-app-manifest-cassette.json +44 -0
  245. package/src/fixtures/google-auth-cassette.json +36 -0
  246. package/src/fixtures/http-chat-client-cassette.json +226 -0
  247. package/src/fixtures/k8s-cassette.json +470 -0
  248. package/src/fixtures/okta-auth-cassette.json +36 -0
  249. package/src/fixtures/slack-provision-cassette.json +12 -0
  250. package/src/fixtures/slack-provisioning-http-cassette.json +98 -0
  251. package/src/fixtures/task-store-provisioning-cassette.json +85 -0
  252. package/src/github-app-provisioning-client.integration.test.ts +149 -0
  253. package/src/github-app-provisioning-client.ts +161 -0
  254. package/src/github-app-provisioning-client.unit.test.ts +114 -0
  255. package/src/github-provisioning-service.ts +569 -0
  256. package/src/github-provisioning-service.unit.test.ts +123 -0
  257. package/src/google-auth-client.integration.test.ts +195 -0
  258. package/src/google-auth-client.ts +121 -0
  259. package/src/http-chat-client.integration.test.ts +554 -0
  260. package/src/http-chat-client.ts +533 -0
  261. package/src/http-chat-client.unit.test.ts +119 -0
  262. package/src/import-boundary.unit.test.ts +240 -0
  263. package/src/kubernetes-client.integration.test.ts +697 -0
  264. package/src/kubernetes-client.ts +1027 -0
  265. package/src/kubernetes-client.unit.test.ts +726 -0
  266. package/src/main.smoke.test.ts +91 -0
  267. package/src/main.ts +21 -0
  268. package/src/main.unit.test.ts +801 -0
  269. package/src/okta-auth-client.integration.test.ts +263 -0
  270. package/src/okta-auth-client.ts +152 -0
  271. package/src/openapi-schemas.ts +1310 -0
  272. package/src/openapi-schemas.unit.test.ts +348 -0
  273. package/src/per-agent-identity.fixtures.ts +305 -0
  274. package/src/platform-doc-route.ts +40 -0
  275. package/src/prisma-client.integration.test.ts +57 -0
  276. package/src/prisma-client.ts +187 -0
  277. package/src/prisma-client.unit.test.ts +178 -0
  278. package/src/prisma-tx.ts +23 -0
  279. package/src/push-content.ts +177 -0
  280. package/src/push-content.unit.test.ts +213 -0
  281. package/src/push-crypto.ts +135 -0
  282. package/src/push-crypto.unit.test.ts +66 -0
  283. package/src/push-sender.ts +169 -0
  284. package/src/push-sender.unit.test.ts +107 -0
  285. package/src/push-service.ts +332 -0
  286. package/src/push-service.unit.test.ts +679 -0
  287. package/src/push-toggle.ts +135 -0
  288. package/src/push-toggle.unit.test.ts +78 -0
  289. package/src/pwa-https-gating.smoke.test.ts +229 -0
  290. package/src/pwa-icons.unit.test.ts +41 -0
  291. package/src/pwa-routes.smoke.test.ts +276 -0
  292. package/src/pwa.ts +380 -0
  293. package/src/pwa.unit.test.ts +321 -0
  294. package/src/schema.unit.test.ts +122 -0
  295. package/src/self-serve-config.ts +52 -0
  296. package/src/self-serve-config.unit.test.ts +79 -0
  297. package/src/server.smoke.test.ts +273 -0
  298. package/src/server.ts +50 -0
  299. package/src/service-integration.ts +67 -0
  300. package/src/service-integration.unit.test.ts +129 -0
  301. package/src/slack-provisioning-client.integration.test.ts +308 -0
  302. package/src/slack-provisioning-client.ts +411 -0
  303. package/src/slack-provisioning-client.unit.test.ts +80 -0
  304. package/src/slack-provisioning-service.integration.test.ts +637 -0
  305. package/src/slack-provisioning-service.ts +451 -0
  306. package/src/slack-provisioning.integration.test.ts +395 -0
  307. package/src/specialty-manifest-loader.fixture.integration.test.ts +63 -0
  308. package/src/specialty-manifest-loader.ts +255 -0
  309. package/src/specialty-manifest-loader.unit.test.ts +301 -0
  310. package/src/specialty-registry.schema.unit.test.ts +35 -0
  311. package/src/specialty-registry.ts +236 -0
  312. package/src/specialty-registry.unit.test.ts +272 -0
  313. package/src/startup-policy-reconcile.ts +77 -0
  314. package/src/startup-policy-reconcile.unit.test.ts +251 -0
  315. package/src/theme-tokens.json +12 -0
  316. package/src/token-crypto.ts +60 -0
  317. package/src/token-crypto.unit.test.ts +182 -0
  318. package/src/trial-expiry-sweeper.integration.test.ts +318 -0
  319. package/src/trial-expiry-sweeper.ts +132 -0
  320. package/src/trial-expiry-sweeper.unit.test.ts +391 -0
  321. package/src/trial-expiry-warning-sweeper.integration.test.ts +468 -0
  322. package/src/trial-expiry-warning-sweeper.ts +447 -0
  323. package/src/trial-expiry-warning-sweeper.unit.test.ts +148 -0
  324. package/src/voice-contract.unit.test.ts +102 -0
  325. package/tsconfig.json +9 -0
@@ -0,0 +1,478 @@
1
+ /**
2
+ * admin/src/agents-api.sentry.smoke.test.ts
3
+ *
4
+ * Smoke tests for the onError hook's Sentry wiring (SEN-1.3) and caller
5
+ * logging (AOB-3.4).
6
+ *
7
+ * Asserts:
8
+ * - an unhandled (non-ApiError) error triggers sentryClient.captureException
9
+ * when a fake sentryClient is injected via AdminDeps
10
+ * - 4xx ApiError instances (expected, typed client errors mapped to real
11
+ * HTTP status codes) do NOT trigger captureException
12
+ * - 5xx ApiError instances (server faults, e.g. BadGatewayError) DO trigger
13
+ * captureException
14
+ * - with no sentryClient dep (undefined — Sentry not initialized), onError
15
+ * does not throw and behaves exactly as before
16
+ * - the unhandled-error console.error log line includes the resolved
17
+ * caller's label (via callerLabel()), for both a DB agent token and an
18
+ * admin env key
19
+ */
20
+
21
+ import { describe, expect, it, spyOn } from "bun:test";
22
+ import { callerLabel } from "@app-vitals/agent-lib/request-context";
23
+ import type { ErrorCapturingClient } from "@app-vitals/agent-lib/sentry";
24
+ import type { AgentEnvService } from "./agent-envs.ts";
25
+ import type { AgentProvisioner, ProvisionResult } from "./agent-provisioner.ts";
26
+ import type { AdminDeps } from "./agents-api.ts";
27
+ import { createAdminApp } from "./agents-api.ts";
28
+ import { BadGatewayError, NotFoundError } from "./errors.ts";
29
+
30
+ /** Sentry double that records scope tags/contexts set inside withScope. */
31
+ function fakeScopedSentryClient() {
32
+ const events: Array<{
33
+ error: unknown;
34
+ tags: Record<string, string>;
35
+ contexts: Record<string, unknown>;
36
+ }> = [];
37
+ let current: (typeof events)[number] | undefined;
38
+ const client: ErrorCapturingClient = {
39
+ captureException: (error) => {
40
+ events.push({
41
+ error,
42
+ tags: { ...(current?.tags ?? {}) },
43
+ contexts: { ...(current?.contexts ?? {}) },
44
+ });
45
+ },
46
+ withScope: async (cb) => {
47
+ current = { error: undefined, tags: {}, contexts: {} };
48
+ const scope = {
49
+ setTag: (k: string, v: string) => {
50
+ (current as NonNullable<typeof current>).tags[k] = v;
51
+ },
52
+ setContext: (k: string, v: unknown) => {
53
+ (current as NonNullable<typeof current>).contexts[k] = v;
54
+ },
55
+ };
56
+ try {
57
+ return await cb(scope);
58
+ } finally {
59
+ current = undefined;
60
+ }
61
+ },
62
+ };
63
+ return { client, events };
64
+ }
65
+
66
+ function p2028Service(): AdminDeps["agentEnvService"] {
67
+ return new Proxy(
68
+ {},
69
+ {
70
+ get() {
71
+ return async () => {
72
+ throw Object.assign(new Error("Unable to start a transaction"), {
73
+ code: "P2028",
74
+ });
75
+ };
76
+ },
77
+ },
78
+ ) as AgentEnvService;
79
+ }
80
+
81
+ const AGENT_ID = "agent-test-123";
82
+ const VALID_BEARER_TOKEN = "valid-bearer-token-value";
83
+ const SESSION_SECRET = "test-admin-session-secret-32-bytes!";
84
+
85
+ /** No-op AgentProvisioner double — provisioning is not exercised by these tests. */
86
+ class NoopProvisioner implements AgentProvisioner {
87
+ readonly canProvision = false;
88
+
89
+ async provision(): Promise<ProvisionResult> {
90
+ return {
91
+ resourceName: AGENT_ID,
92
+ secretName: `${AGENT_ID}-token`,
93
+ deploymentName: AGENT_ID,
94
+ };
95
+ }
96
+ async deprovision(): Promise<void> {}
97
+ async reconcile() {
98
+ return {
99
+ recreated: [],
100
+ migrated: [],
101
+ noOwner: [],
102
+ dryRun: false,
103
+ updated: [],
104
+ orphans: [],
105
+ failed: [],
106
+ };
107
+ }
108
+ }
109
+
110
+ function fakeErrorCapturingClient(): ErrorCapturingClient & {
111
+ capturedErrors: unknown[];
112
+ } {
113
+ const capturedErrors: unknown[] = [];
114
+ return {
115
+ captureException: (err: unknown) => {
116
+ capturedErrors.push(err);
117
+ },
118
+ capturedErrors,
119
+ };
120
+ }
121
+
122
+ /** An AgentEnvService whose getByAgentId throws an unhandled, non-ApiError error. */
123
+ function throwingAgentEnvService(): AdminDeps["agentEnvService"] {
124
+ return new Proxy(
125
+ {},
126
+ {
127
+ get() {
128
+ return async () => {
129
+ throw new Error("boom: unexpected failure");
130
+ };
131
+ },
132
+ },
133
+ ) as AgentEnvService;
134
+ }
135
+
136
+ /** An AgentEnvService whose getByAgentId throws a typed ApiError (NotFoundError). */
137
+ function apiErrorAgentEnvService(): AdminDeps["agentEnvService"] {
138
+ return new Proxy(
139
+ {},
140
+ {
141
+ get() {
142
+ return async () => {
143
+ throw new NotFoundError("agent not found");
144
+ };
145
+ },
146
+ },
147
+ ) as AgentEnvService;
148
+ }
149
+
150
+ /** An AgentEnvService whose getByAgentId throws a 5xx ApiError (BadGatewayError). */
151
+ function serverFaultAgentEnvService(): AdminDeps["agentEnvService"] {
152
+ return new Proxy(
153
+ {},
154
+ {
155
+ get() {
156
+ return async () => {
157
+ throw new BadGatewayError("upstream Kubernetes API failed");
158
+ };
159
+ },
160
+ },
161
+ ) as AgentEnvService;
162
+ }
163
+
164
+ function makeBaseDeps(
165
+ agentEnvService: AdminDeps["agentEnvService"],
166
+ ): Omit<AdminDeps, "sentryClient"> {
167
+ return {
168
+ agentService: {
169
+ create: async () => {
170
+ throw new Error("not implemented");
171
+ },
172
+ delete: async () => {},
173
+ list: async () => [],
174
+ getSummary: async () => null,
175
+ getDetail: async () => null,
176
+ exists: async () => false,
177
+ updateSelfHosted: async () => {
178
+ throw new Error("not implemented");
179
+ },
180
+ updateFields: async () => {
181
+ throw new Error("not implemented");
182
+ },
183
+ runTransaction: async (fn) => fn(undefined as never),
184
+ },
185
+ agentEnvService,
186
+ agentCronJobService: {
187
+ list: async () => [],
188
+ listWithRunSummary: async () => [],
189
+ create: async () => ({}) as never,
190
+ update: async () => ({}) as never,
191
+ delete: async () => {},
192
+ get: async () => ({}) as never,
193
+ setEnabled: async () => ({}) as never,
194
+ updatePreCheck: async () => ({}) as never,
195
+ reconcileSystemCrons: async () => ({
196
+ created: 0,
197
+ updated: 0,
198
+ deleted: 0,
199
+ }),
200
+ },
201
+ agentCronRunService: {
202
+ create: async () => ({}) as never,
203
+ list: async () => ({ items: [], total: 0, limit: 20, offset: 0 }),
204
+ patch: async () => ({}) as never,
205
+ },
206
+ agentCronRunStatsService: {
207
+ outcomes: async () => ({ series: [] }),
208
+ query: async () => ({
209
+ totals: {
210
+ input: 0,
211
+ output: 0,
212
+ cacheRead: 0,
213
+ cacheCreation: 0,
214
+ total: 0,
215
+ },
216
+ byAgent: [],
217
+ byCron: [],
218
+ byModel: [],
219
+ daily: [],
220
+ byCronModel: [],
221
+ byPhase: [],
222
+ }),
223
+ },
224
+ agentToolService: {
225
+ list: async () => [],
226
+ add: async () => ({}) as never,
227
+ toggle: async () => ({}) as never,
228
+ remove: async () => {},
229
+ },
230
+ agentTokenService: {
231
+ create: async () => ({}) as never,
232
+ listForAgent: async () => [],
233
+ revoke: async () => ({}) as never,
234
+ validate: async (raw: string) =>
235
+ raw === VALID_BEARER_TOKEN ? { agentId: AGENT_ID } : null,
236
+ },
237
+ agentPluginService: {
238
+ list: async () => [],
239
+ add: async () => ({}) as never,
240
+ remove: async () => {},
241
+ removeByName: async () => {},
242
+ },
243
+ agentMemberService: {
244
+ exists: async () => false,
245
+ listByEmail: async () => [],
246
+ add: async () => ({}) as never,
247
+ listByAgentId: async () => [],
248
+ },
249
+ specialtyRegistry: {
250
+ getManifest: () => {
251
+ throw new Error("not implemented");
252
+ },
253
+ tryGetManifest: () => undefined,
254
+ listTypes: () => [],
255
+ },
256
+ agentChatTokenService: {
257
+ upsertDailyByModel: async () => ({}) as never,
258
+ queryStats: async () => ({
259
+ totals: {
260
+ input: 0,
261
+ output: 0,
262
+ cacheRead: 0,
263
+ cacheCreation: 0,
264
+ total: 0,
265
+ },
266
+ byAgent: [],
267
+ byModel: [],
268
+ daily: [],
269
+ }),
270
+ },
271
+ agentGitHubInstallationsService: {
272
+ push: async () => {
273
+ throw new Error("not implemented");
274
+ },
275
+ get: async () => null,
276
+ },
277
+ prisma: {
278
+ agent: {} as never,
279
+ agentEnv: { findMany: async () => [] },
280
+ } as unknown as AdminDeps["prisma"],
281
+ provisioner: new NoopProvisioner(),
282
+ serviceIntegrations: [
283
+ {
284
+ name: "task-store",
285
+ listTokensForAgent: async () => [],
286
+ revokeToken: async () => {},
287
+ },
288
+ ],
289
+ chatService: {
290
+ listTokensForAgent: async () => [],
291
+ revokeToken: async () => {},
292
+ deleteThreadsForAgent: async () => ({ deleted: 0 }),
293
+ },
294
+ slack: {
295
+ deleteApp: async () => {},
296
+ },
297
+ decrypt: (value: string) => value,
298
+ sessionSecret: SESSION_SECRET,
299
+ };
300
+ }
301
+
302
+ describe("onError — Sentry capture wiring", () => {
303
+ it("calls sentryClient.captureException for an unhandled (non-ApiError) error", async () => {
304
+ const sentryClient = fakeErrorCapturingClient();
305
+ const app = createAdminApp({
306
+ ...makeBaseDeps(throwingAgentEnvService()),
307
+ sentryClient,
308
+ });
309
+
310
+ const res = await app.request(`/agents/${AGENT_ID}/envs`, {
311
+ headers: { Authorization: `Bearer ${VALID_BEARER_TOKEN}` },
312
+ });
313
+
314
+ expect(res.status).toBe(500);
315
+ expect(sentryClient.capturedErrors.length).toBe(1);
316
+ expect((sentryClient.capturedErrors[0] as Error).message).toBe(
317
+ "boom: unexpected failure",
318
+ );
319
+ });
320
+
321
+ it("does NOT call sentryClient.captureException for a 4xx ApiError", async () => {
322
+ const sentryClient = fakeErrorCapturingClient();
323
+ const app = createAdminApp({
324
+ ...makeBaseDeps(apiErrorAgentEnvService()),
325
+ sentryClient,
326
+ });
327
+
328
+ const res = await app.request(`/agents/${AGENT_ID}/envs`, {
329
+ headers: { Authorization: `Bearer ${VALID_BEARER_TOKEN}` },
330
+ });
331
+
332
+ expect(res.status).toBe(404);
333
+ expect(sentryClient.capturedErrors.length).toBe(0);
334
+ });
335
+
336
+ it("calls sentryClient.captureException for a 5xx ApiError (server fault)", async () => {
337
+ const sentryClient = fakeErrorCapturingClient();
338
+ const app = createAdminApp({
339
+ ...makeBaseDeps(serverFaultAgentEnvService()),
340
+ sentryClient,
341
+ });
342
+
343
+ const res = await app.request(`/agents/${AGENT_ID}/envs`, {
344
+ headers: { Authorization: `Bearer ${VALID_BEARER_TOKEN}` },
345
+ });
346
+
347
+ expect(res.status).toBe(502);
348
+ expect(sentryClient.capturedErrors.length).toBe(1);
349
+ expect((sentryClient.capturedErrors[0] as Error).message).toBe(
350
+ "upstream Kubernetes API failed",
351
+ );
352
+ });
353
+
354
+ it("does not throw when sentryClient is undefined (Sentry not initialized)", async () => {
355
+ const app = createAdminApp(makeBaseDeps(throwingAgentEnvService()));
356
+
357
+ const res = await app.request(`/agents/${AGENT_ID}/envs`, {
358
+ headers: { Authorization: `Bearer ${VALID_BEARER_TOKEN}` },
359
+ });
360
+
361
+ expect(res.status).toBe(500);
362
+ });
363
+ });
364
+
365
+ describe("onError — pool timeout context (PLO-1.3)", () => {
366
+ const pool = {
367
+ totalCount: 10,
368
+ idleCount: 0,
369
+ waitingCount: 4,
370
+ options: { max: 10 },
371
+ };
372
+
373
+ it("attaches pool stats context and route/server_name tags for a P2028 error", async () => {
374
+ const { client, events } = fakeScopedSentryClient();
375
+ const app = createAdminApp({
376
+ ...makeBaseDeps(p2028Service()),
377
+ sentryClient: client,
378
+ pool,
379
+ });
380
+
381
+ const res = await app.request(`/agents/${AGENT_ID}/envs`, {
382
+ headers: { Authorization: `Bearer ${VALID_BEARER_TOKEN}` },
383
+ });
384
+
385
+ expect(res.status).toBe(500);
386
+ expect(events.length).toBe(1);
387
+ expect(events[0]?.tags.server_name).toBe("admin");
388
+ expect(events[0]?.tags.route).toBe("GET /agents/:id/envs");
389
+ expect(events[0]?.contexts.pg_pool).toMatchObject({
390
+ total: 10,
391
+ idle: 0,
392
+ waiting: 4,
393
+ max: 10,
394
+ route: "GET /agents/:id/envs",
395
+ });
396
+ });
397
+
398
+ it("leaves non-pool errors unchanged (no scope, no pool context)", async () => {
399
+ const { client, events } = fakeScopedSentryClient();
400
+ const app = createAdminApp({
401
+ ...makeBaseDeps(throwingAgentEnvService()),
402
+ sentryClient: client,
403
+ pool,
404
+ });
405
+
406
+ const res = await app.request(`/agents/${AGENT_ID}/envs`, {
407
+ headers: { Authorization: `Bearer ${VALID_BEARER_TOKEN}` },
408
+ });
409
+
410
+ expect(res.status).toBe(500);
411
+ expect(events.length).toBe(1);
412
+ expect(events[0]?.tags).toEqual({});
413
+ expect(events[0]?.contexts).toEqual({});
414
+ });
415
+
416
+ it("still captures a pool timeout when no pool or withScope is available", async () => {
417
+ const sentryClient = fakeErrorCapturingClient();
418
+ const app = createAdminApp({
419
+ ...makeBaseDeps(p2028Service()),
420
+ sentryClient,
421
+ });
422
+ const res = await app.request(`/agents/${AGENT_ID}/envs`, {
423
+ headers: { Authorization: `Bearer ${VALID_BEARER_TOKEN}` },
424
+ });
425
+ expect(res.status).toBe(500);
426
+ expect(sentryClient.capturedErrors.length).toBe(1);
427
+ });
428
+ });
429
+
430
+ describe("onError — caller label logging (AOB-3.4)", () => {
431
+ it("includes the DB agent token's caller label in the unhandled-error console.error line", async () => {
432
+ const consoleErrorSpy = spyOn(console, "error").mockImplementation(
433
+ () => {},
434
+ );
435
+ try {
436
+ const app = createAdminApp(makeBaseDeps(throwingAgentEnvService()));
437
+
438
+ const res = await app.request(`/agents/${AGENT_ID}/envs`, {
439
+ headers: { Authorization: `Bearer ${VALID_BEARER_TOKEN}` },
440
+ });
441
+
442
+ expect(res.status).toBe(500);
443
+ expect(consoleErrorSpy).toHaveBeenCalled();
444
+ const loggedArgs = consoleErrorSpy.mock.calls.flat().join(" ");
445
+ expect(loggedArgs).toContain(
446
+ callerLabel({ name: AGENT_ID, scope: AGENT_ID }),
447
+ );
448
+ } finally {
449
+ consoleErrorSpy.mockRestore();
450
+ }
451
+ });
452
+
453
+ it("includes the admin env key's caller label in the unhandled-error console.error line", async () => {
454
+ const consoleErrorSpy = spyOn(console, "error").mockImplementation(
455
+ () => {},
456
+ );
457
+ try {
458
+ const adminApiKeys = new Map([
459
+ ["admin-env-key-token", { name: "admin", scope: "*" }],
460
+ ]);
461
+ const app = createAdminApp({
462
+ ...makeBaseDeps(throwingAgentEnvService()),
463
+ adminApiKeys,
464
+ });
465
+
466
+ const res = await app.request(`/agents/${AGENT_ID}/envs`, {
467
+ headers: { Authorization: "Bearer admin-env-key-token" },
468
+ });
469
+
470
+ expect(res.status).toBe(500);
471
+ expect(consoleErrorSpy).toHaveBeenCalled();
472
+ const loggedArgs = consoleErrorSpy.mock.calls.flat().join(" ");
473
+ expect(loggedArgs).toContain(callerLabel({ name: "admin", scope: "*" }));
474
+ } finally {
475
+ consoleErrorSpy.mockRestore();
476
+ }
477
+ });
478
+ });