@app-vitals/agent-admin 0.0.0-stage → 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 +64 -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,801 @@
1
+ import { afterEach, describe, expect, it } from "bun:test";
2
+ import type { PrismaClient } from "../prisma/client/client.ts";
3
+ import {
4
+ buildProvisioner,
5
+ checkDbReady,
6
+ DEFAULT_SESSION_ALERT_INTERVAL_MS,
7
+ isAnthropicFederationEnabled,
8
+ parseAnthropicFederation,
9
+ resolvePublicRepo,
10
+ resolveSessionAlertIntervalMs,
11
+ resolveTrialExpiryWarningDays,
12
+ runMigrations,
13
+ } from "./admin-boot-config.ts";
14
+ import { AgentAnthropicIdentityService } from "./agent-anthropic-identity.ts";
15
+ import type {
16
+ AgentProvisioner,
17
+ KubernetesAgentProvisionerConfig,
18
+ } from "./agent-provisioner.ts";
19
+ import {
20
+ KubernetesAgentProvisioner,
21
+ NoopAgentProvisioner,
22
+ } from "./agent-provisioner.ts";
23
+ import type { AgentTokenService } from "./agent-tokens.ts";
24
+ import { HttpAnthropicIdentityClient } from "./anthropic-identity-client.ts";
25
+ import { AnthropicIdentityNameAllocator } from "./anthropic-identity-name.ts";
26
+
27
+ /** Stub token service — buildProvisioner only threads it through, never calls it. */
28
+ function stubAgentTokenService(): AgentTokenService {
29
+ return {} as unknown as AgentTokenService;
30
+ }
31
+
32
+ /** Reaches into the private `config` field so branch behavior is actually verifiable. */
33
+ function configOf(
34
+ provisioner: AgentProvisioner,
35
+ ): KubernetesAgentProvisionerConfig {
36
+ return (
37
+ provisioner as unknown as { config: KubernetesAgentProvisionerConfig }
38
+ ).config;
39
+ }
40
+
41
+ // resolvePublicRepo is the pure env rule wired into main.ts startServer():
42
+ // it sources SHIPWRIGHT_ADMIN_PUBLIC_REPO and feeds createAdminUIApp's
43
+ // publicRepo option, which gates the unauthenticated GET /public/tasks board.
44
+ // Tested without touching process.env.
45
+ describe("resolvePublicRepo", () => {
46
+ it("returns the repo slug when set", () => {
47
+ expect(
48
+ resolvePublicRepo({
49
+ SHIPWRIGHT_ADMIN_PUBLIC_REPO: "app-vitals/shipwright",
50
+ }),
51
+ ).toBe("app-vitals/shipwright");
52
+ });
53
+
54
+ it("returns undefined when unset (board stays in degraded mode)", () => {
55
+ expect(resolvePublicRepo({})).toBeUndefined();
56
+ });
57
+
58
+ it("treats an empty string as unset", () => {
59
+ expect(
60
+ resolvePublicRepo({ SHIPWRIGHT_ADMIN_PUBLIC_REPO: "" }),
61
+ ).toBeUndefined();
62
+ });
63
+
64
+ it("treats whitespace-only as unset rather than an empty repo filter", () => {
65
+ expect(
66
+ resolvePublicRepo({ SHIPWRIGHT_ADMIN_PUBLIC_REPO: " " }),
67
+ ).toBeUndefined();
68
+ });
69
+
70
+ it("trims surrounding whitespace from a valid value", () => {
71
+ expect(
72
+ resolvePublicRepo({
73
+ SHIPWRIGHT_ADMIN_PUBLIC_REPO: " app-vitals/shipwright ",
74
+ }),
75
+ ).toBe("app-vitals/shipwright");
76
+ });
77
+ });
78
+
79
+ // ─── buildProvisioner ───────────────────────────────────────────────────────
80
+
81
+ describe("buildProvisioner", () => {
82
+ it("returns a NoopAgentProvisioner when SHIPWRIGHT_K8S_PROVISIONING is unset", () => {
83
+ const provisioner = buildProvisioner({}, stubAgentTokenService());
84
+ expect(provisioner).toBeInstanceOf(NoopAgentProvisioner);
85
+ });
86
+
87
+ it("returns a NoopAgentProvisioner for any value other than 'enabled'", () => {
88
+ const provisioner = buildProvisioner(
89
+ { SHIPWRIGHT_K8S_PROVISIONING: "true" },
90
+ stubAgentTokenService(),
91
+ );
92
+ expect(provisioner).toBeInstanceOf(NoopAgentProvisioner);
93
+ });
94
+
95
+ it("returns a KubernetesAgentProvisioner when SHIPWRIGHT_K8S_PROVISIONING=enabled", () => {
96
+ const provisioner = buildProvisioner(
97
+ { SHIPWRIGHT_K8S_PROVISIONING: "enabled" },
98
+ stubAgentTokenService(),
99
+ );
100
+ expect(provisioner).toBeInstanceOf(KubernetesAgentProvisioner);
101
+ });
102
+
103
+ it("builds with defaults when only the flag is set", () => {
104
+ const provisioner = buildProvisioner(
105
+ { SHIPWRIGHT_K8S_PROVISIONING: "enabled" },
106
+ stubAgentTokenService(),
107
+ );
108
+ const config = configOf(provisioner);
109
+ expect(config.namespace).toBe("default");
110
+ expect(config.image).toBe("");
111
+ expect(config.imageTag).toBe("latest");
112
+ expect(config.apiUrl).toBe("");
113
+ expect(config.replicas).toBeUndefined();
114
+ expect(config.pvcStorageGi).toBeUndefined();
115
+ expect(config.voice).toBeUndefined();
116
+ expect(config.serviceIntegrations).toBeUndefined();
117
+ expect(config.chatService).toBeUndefined();
118
+ expect(config.pvcName).toBeUndefined();
119
+ });
120
+
121
+ it("parses numeric env (replicas, pvcStorageGi) when set and finite", () => {
122
+ const provisioner = buildProvisioner(
123
+ {
124
+ SHIPWRIGHT_K8S_PROVISIONING: "enabled",
125
+ SHIPWRIGHT_AGENT_REPLICAS: "3",
126
+ SHIPWRIGHT_AGENT_PVC_STORAGE_GI: "80",
127
+ },
128
+ stubAgentTokenService(),
129
+ );
130
+ const config = configOf(provisioner);
131
+ expect(config.replicas).toBe(3);
132
+ expect(config.pvcStorageGi).toBe(80);
133
+ });
134
+
135
+ it("passes claudePolicy through when the policy ConfigMap env is set", () => {
136
+ const provisioner = buildProvisioner(
137
+ {
138
+ SHIPWRIGHT_K8S_PROVISIONING: "enabled",
139
+ SHIPWRIGHT_K8S_AGENT_CLAUDE_POLICY_CONFIGMAP: "claude-policy",
140
+ SHIPWRIGHT_K8S_AGENT_CLAUDE_POLICY_HASH: "abc123",
141
+ },
142
+ stubAgentTokenService(),
143
+ );
144
+ expect(configOf(provisioner).claudePolicy).toEqual({
145
+ configMapName: "claude-policy",
146
+ hash: "abc123",
147
+ });
148
+ });
149
+
150
+ it("omits the hash when only the ConfigMap env is set", () => {
151
+ const provisioner = buildProvisioner(
152
+ {
153
+ SHIPWRIGHT_K8S_PROVISIONING: "enabled",
154
+ SHIPWRIGHT_K8S_AGENT_CLAUDE_POLICY_CONFIGMAP: "claude-policy",
155
+ },
156
+ stubAgentTokenService(),
157
+ );
158
+ expect(configOf(provisioner).claudePolicy).toEqual({
159
+ configMapName: "claude-policy",
160
+ });
161
+ });
162
+
163
+ it("omits claudePolicy when the ConfigMap env is unset or empty", () => {
164
+ for (const cm of [undefined, ""]) {
165
+ const provisioner = buildProvisioner(
166
+ {
167
+ SHIPWRIGHT_K8S_PROVISIONING: "enabled",
168
+ SHIPWRIGHT_K8S_AGENT_CLAUDE_POLICY_CONFIGMAP: cm,
169
+ SHIPWRIGHT_K8S_AGENT_CLAUDE_POLICY_HASH: "abc123",
170
+ },
171
+ stubAgentTokenService(),
172
+ );
173
+ expect(configOf(provisioner).claudePolicy).toBeUndefined();
174
+ }
175
+ });
176
+
177
+ it("ignores non-finite numeric env (NaN) rather than passing it through", () => {
178
+ const provisioner = buildProvisioner(
179
+ {
180
+ SHIPWRIGHT_K8S_PROVISIONING: "enabled",
181
+ SHIPWRIGHT_AGENT_REPLICAS: "not-a-number",
182
+ },
183
+ stubAgentTokenService(),
184
+ );
185
+ const config = configOf(provisioner);
186
+ expect(config.replicas).toBeUndefined();
187
+ });
188
+
189
+ it("builds a voice env block only from the voice vars that are set", () => {
190
+ const provisioner = buildProvisioner(
191
+ {
192
+ SHIPWRIGHT_K8S_PROVISIONING: "enabled",
193
+ WHISPER_SERVICE_URL: "http://whisper.svc:9000",
194
+ GROQ_API_KEY: "groq-key",
195
+ ELEVENLABS_API_KEY: "elevenlabs-key",
196
+ ELEVENLABS_VOICE_ID: "voice-1",
197
+ },
198
+ stubAgentTokenService(),
199
+ );
200
+ const config = configOf(provisioner);
201
+ expect(config.voice).toEqual({
202
+ whisperServiceUrl: "http://whisper.svc:9000",
203
+ groqApiKey: "groq-key",
204
+ elevenLabsApiKey: "elevenlabs-key",
205
+ voiceId: "voice-1",
206
+ });
207
+ });
208
+
209
+ it("omits the voice block entirely when no voice env vars are set", () => {
210
+ const provisioner = buildProvisioner(
211
+ { SHIPWRIGHT_K8S_PROVISIONING: "enabled" },
212
+ stubAgentTokenService(),
213
+ );
214
+ expect(configOf(provisioner).voice).toBeUndefined();
215
+ });
216
+
217
+ it("registers no integration for a bare platform (no extensions)", () => {
218
+ const provisioner = buildProvisioner(
219
+ {
220
+ SHIPWRIGHT_K8S_PROVISIONING: "enabled",
221
+ SHIPWRIGHT_TASK_STORE_URL: "http://task-store.svc:3002",
222
+ SHIPWRIGHT_TASK_STORE_ADMIN_TOKEN: "ts-admin-token",
223
+ },
224
+ stubAgentTokenService(),
225
+ );
226
+ expect(configOf(provisioner).serviceIntegrations).toBeUndefined();
227
+ });
228
+
229
+ it("wires a chat-service client only when both URL and admin token are set", () => {
230
+ const withChatService = buildProvisioner(
231
+ {
232
+ SHIPWRIGHT_K8S_PROVISIONING: "enabled",
233
+ SHIPWRIGHT_CHAT_SERVICE_URL: "http://chat.svc:3003",
234
+ SHIPWRIGHT_CHAT_SERVICE_ADMIN_TOKEN: "chat-admin-token",
235
+ },
236
+ stubAgentTokenService(),
237
+ );
238
+ const withConfig = configOf(withChatService);
239
+ expect(withConfig.chatService).toBeDefined();
240
+ expect(withConfig.chatServiceUrl).toBe("http://chat.svc:3003");
241
+
242
+ const withoutChatService = buildProvisioner(
243
+ {
244
+ SHIPWRIGHT_K8S_PROVISIONING: "enabled",
245
+ SHIPWRIGHT_CHAT_SERVICE_URL: "http://chat.svc:3003",
246
+ // admin token missing
247
+ },
248
+ stubAgentTokenService(),
249
+ );
250
+ expect(configOf(withoutChatService).chatService).toBeUndefined();
251
+ expect(configOf(withoutChatService).chatServiceUrl).toBeUndefined();
252
+ });
253
+
254
+ it("omits the resources block entirely when no SHIPWRIGHT_K8S_AGENT_* vars are set", () => {
255
+ const provisioner = buildProvisioner(
256
+ { SHIPWRIGHT_K8S_PROVISIONING: "enabled" },
257
+ stubAgentTokenService(),
258
+ );
259
+ expect(configOf(provisioner).resources).toBeUndefined();
260
+ });
261
+
262
+ it("parses SHIPWRIGHT_K8S_AGENT_CPU_REQUEST individually", () => {
263
+ const provisioner = buildProvisioner(
264
+ {
265
+ SHIPWRIGHT_K8S_PROVISIONING: "enabled",
266
+ SHIPWRIGHT_K8S_AGENT_CPU_REQUEST: "1000m",
267
+ },
268
+ stubAgentTokenService(),
269
+ );
270
+ expect(configOf(provisioner).resources).toEqual({ cpuRequest: "1000m" });
271
+ });
272
+
273
+ it("parses SHIPWRIGHT_K8S_AGENT_MEMORY_REQUEST individually", () => {
274
+ const provisioner = buildProvisioner(
275
+ {
276
+ SHIPWRIGHT_K8S_PROVISIONING: "enabled",
277
+ SHIPWRIGHT_K8S_AGENT_MEMORY_REQUEST: "4Gi",
278
+ },
279
+ stubAgentTokenService(),
280
+ );
281
+ expect(configOf(provisioner).resources).toEqual({
282
+ memoryRequest: "4Gi",
283
+ });
284
+ });
285
+
286
+ it("parses SHIPWRIGHT_K8S_AGENT_MEMORY_LIMIT individually", () => {
287
+ const provisioner = buildProvisioner(
288
+ {
289
+ SHIPWRIGHT_K8S_PROVISIONING: "enabled",
290
+ SHIPWRIGHT_K8S_AGENT_MEMORY_LIMIT: "16Gi",
291
+ },
292
+ stubAgentTokenService(),
293
+ );
294
+ expect(configOf(provisioner).resources).toEqual({
295
+ memoryLimit: "16Gi",
296
+ });
297
+ });
298
+
299
+ it("parses SHIPWRIGHT_K8S_AGENT_EPHEMERAL_STORAGE individually", () => {
300
+ const provisioner = buildProvisioner(
301
+ {
302
+ SHIPWRIGHT_K8S_PROVISIONING: "enabled",
303
+ SHIPWRIGHT_K8S_AGENT_EPHEMERAL_STORAGE: "8Gi",
304
+ },
305
+ stubAgentTokenService(),
306
+ );
307
+ expect(configOf(provisioner).resources).toEqual({
308
+ ephemeralStorage: "8Gi",
309
+ });
310
+ });
311
+
312
+ it("parses all four SHIPWRIGHT_K8S_AGENT_* vars together", () => {
313
+ const provisioner = buildProvisioner(
314
+ {
315
+ SHIPWRIGHT_K8S_PROVISIONING: "enabled",
316
+ SHIPWRIGHT_K8S_AGENT_CPU_REQUEST: "2000m",
317
+ SHIPWRIGHT_K8S_AGENT_MEMORY_REQUEST: "3Gi",
318
+ SHIPWRIGHT_K8S_AGENT_MEMORY_LIMIT: "10Gi",
319
+ SHIPWRIGHT_K8S_AGENT_EPHEMERAL_STORAGE: "6Gi",
320
+ },
321
+ stubAgentTokenService(),
322
+ );
323
+ expect(configOf(provisioner).resources).toEqual({
324
+ cpuRequest: "2000m",
325
+ memoryRequest: "3Gi",
326
+ memoryLimit: "10Gi",
327
+ ephemeralStorage: "6Gi",
328
+ });
329
+ });
330
+
331
+ it("sets a pvcName template function when SHIPWRIGHT_AGENT_PVC_NAME_TEMPLATE is set", () => {
332
+ const provisioner = buildProvisioner(
333
+ {
334
+ SHIPWRIGHT_K8S_PROVISIONING: "enabled",
335
+ SHIPWRIGHT_AGENT_PVC_NAME_TEMPLATE: "acme-agent-{name}-home",
336
+ },
337
+ stubAgentTokenService(),
338
+ );
339
+ const config = configOf(provisioner);
340
+ expect(config.pvcName).toBeDefined();
341
+ expect(config.pvcName?.("my-agent")).toBe("acme-agent-my-agent-home");
342
+ });
343
+
344
+ // ─── Anthropic workload identity federation ───────────────────────────────
345
+
346
+ const FEDERATION_ENV = {
347
+ SHIPWRIGHT_K8S_PROVISIONING: "enabled",
348
+ SHIPWRIGHT_K8S_AGENT_SERVICE_ACCOUNT: "shipwright-agent",
349
+ SHIPWRIGHT_K8S_AGENT_ANTHROPIC_FEDERATION_RULE_ID: "fdrl_1",
350
+ SHIPWRIGHT_K8S_AGENT_ANTHROPIC_FEDERATION_ORGANIZATION_ID: "org-1",
351
+ SHIPWRIGHT_K8S_AGENT_ANTHROPIC_FEDERATION_SERVICE_ACCOUNT_ID: "svac_1",
352
+ };
353
+
354
+ it("leaves federation disabled when the rule ID is unset", () => {
355
+ const { SHIPWRIGHT_K8S_AGENT_ANTHROPIC_FEDERATION_RULE_ID: _, ...rest } =
356
+ FEDERATION_ENV;
357
+ for (const env of [
358
+ rest,
359
+ { ...rest, SHIPWRIGHT_K8S_AGENT_ANTHROPIC_FEDERATION_RULE_ID: "" },
360
+ ]) {
361
+ const config = configOf(buildProvisioner(env, stubAgentTokenService()));
362
+ expect(config.anthropicFederation).toBeUndefined();
363
+ expect("anthropicFederation" in config).toBe(false);
364
+ }
365
+ });
366
+
367
+ it("parses the federation config with default audience and expiration", () => {
368
+ const provisioner = buildProvisioner(
369
+ FEDERATION_ENV,
370
+ stubAgentTokenService(),
371
+ );
372
+ expect(configOf(provisioner).anthropicFederation).toEqual({
373
+ serviceAccountName: "shipwright-agent",
374
+ ruleId: "fdrl_1",
375
+ organizationId: "org-1",
376
+ serviceAccountId: "svac_1",
377
+ audience: "https://api.anthropic.com",
378
+ expirationSeconds: 600,
379
+ });
380
+ });
381
+
382
+ it("parses the optional workspace, audience and expiration overrides", () => {
383
+ const provisioner = buildProvisioner(
384
+ {
385
+ ...FEDERATION_ENV,
386
+ SHIPWRIGHT_K8S_AGENT_ANTHROPIC_FEDERATION_WORKSPACE_ID: "wrkspc_1",
387
+ SHIPWRIGHT_K8S_AGENT_ANTHROPIC_FEDERATION_AUDIENCE:
388
+ "https://example.test",
389
+ SHIPWRIGHT_K8S_AGENT_ANTHROPIC_FEDERATION_TOKEN_EXPIRATION_SECONDS:
390
+ "3600",
391
+ },
392
+ stubAgentTokenService(),
393
+ );
394
+ expect(configOf(provisioner).anthropicFederation).toEqual({
395
+ serviceAccountName: "shipwright-agent",
396
+ ruleId: "fdrl_1",
397
+ organizationId: "org-1",
398
+ serviceAccountId: "svac_1",
399
+ workspaceId: "wrkspc_1",
400
+ audience: "https://example.test",
401
+ expirationSeconds: 3600,
402
+ });
403
+ });
404
+
405
+ for (const missing of [
406
+ "SHIPWRIGHT_K8S_AGENT_SERVICE_ACCOUNT",
407
+ "SHIPWRIGHT_K8S_AGENT_ANTHROPIC_FEDERATION_ORGANIZATION_ID",
408
+ "SHIPWRIGHT_K8S_AGENT_ANTHROPIC_FEDERATION_SERVICE_ACCOUNT_ID",
409
+ ] as const) {
410
+ it(`fails startup on a partial federation config (missing ${missing})`, () => {
411
+ const env = { ...FEDERATION_ENV, [missing]: "" };
412
+ expect(() => buildProvisioner(env, stubAgentTokenService())).toThrow(
413
+ missing,
414
+ );
415
+ });
416
+ }
417
+
418
+ it("names every missing variable in a single error", () => {
419
+ expect(() =>
420
+ buildProvisioner(
421
+ {
422
+ SHIPWRIGHT_K8S_PROVISIONING: "enabled",
423
+ SHIPWRIGHT_K8S_AGENT_ANTHROPIC_FEDERATION_RULE_ID: "fdrl_1",
424
+ },
425
+ stubAgentTokenService(),
426
+ ),
427
+ ).toThrow(
428
+ /SHIPWRIGHT_K8S_AGENT_SERVICE_ACCOUNT.*ORGANIZATION_ID.*SERVICE_ACCOUNT_ID/,
429
+ );
430
+ });
431
+
432
+ for (const bad of ["abc", "599", "0", "-600", "600.5"]) {
433
+ it(`rejects token expiration ${JSON.stringify(bad)}`, () => {
434
+ expect(() =>
435
+ buildProvisioner(
436
+ {
437
+ ...FEDERATION_ENV,
438
+ SHIPWRIGHT_K8S_AGENT_ANTHROPIC_FEDERATION_TOKEN_EXPIRATION_SECONDS:
439
+ bad,
440
+ },
441
+ stubAgentTokenService(),
442
+ ),
443
+ ).toThrow(
444
+ "SHIPWRIGHT_K8S_AGENT_ANTHROPIC_FEDERATION_TOKEN_EXPIRATION_SECONDS",
445
+ );
446
+ });
447
+ }
448
+
449
+ it("ignores federation env when Kubernetes provisioning is disabled", () => {
450
+ const { SHIPWRIGHT_K8S_PROVISIONING: _, ...rest } = FEDERATION_ENV;
451
+ expect(
452
+ buildProvisioner(
453
+ { ...rest, SHIPWRIGHT_K8S_AGENT_SERVICE_ACCOUNT: "" },
454
+ stubAgentTokenService(),
455
+ ),
456
+ ).toBeInstanceOf(NoopAgentProvisioner);
457
+ });
458
+ });
459
+
460
+ // ─── resolveSessionAlertIntervalMs ──────────────────────────────────────────
461
+
462
+ // The pure env rule behind the session-alert sweeper's setInterval cadence in
463
+ // startServer() (SESH-7.4). Tested without touching process.env.
464
+ describe("resolveSessionAlertIntervalMs", () => {
465
+ it("defaults to 60s when unset", () => {
466
+ expect(resolveSessionAlertIntervalMs({})).toBe(
467
+ DEFAULT_SESSION_ALERT_INTERVAL_MS,
468
+ );
469
+ expect(DEFAULT_SESSION_ALERT_INTERVAL_MS).toBe(60_000);
470
+ });
471
+
472
+ it("uses an explicit positive override", () => {
473
+ expect(
474
+ resolveSessionAlertIntervalMs({
475
+ SHIPWRIGHT_ADMIN_SESSION_ALERT_INTERVAL_MS: "5000",
476
+ }),
477
+ ).toBe(5000);
478
+ });
479
+
480
+ it("falls back to the default for blank, non-numeric, zero, or negative values", () => {
481
+ for (const raw of ["", " ", "soon", "0", "-1"]) {
482
+ expect(
483
+ resolveSessionAlertIntervalMs({
484
+ SHIPWRIGHT_ADMIN_SESSION_ALERT_INTERVAL_MS: raw,
485
+ }),
486
+ ).toBe(DEFAULT_SESSION_ALERT_INTERVAL_MS);
487
+ }
488
+ });
489
+ });
490
+
491
+ // ─── resolveTrialExpiryWarningDays ──────────────────────────────────────────
492
+
493
+ // The pure env rule behind the trial-expiry sweeper's warning window
494
+ // (ATE-2.1). Tested without touching process.env. Unlike
495
+ // resolveSessionAlertIntervalMs, returning `undefined` on an unset/invalid
496
+ // override is intentional: TrialExpiryWarningSweeper's own constructor
497
+ // applies DEFAULT_TRIAL_EXPIRY_WARNING_DAYS when its `warningDays` option is
498
+ // omitted, so this resolver stays a thin env-parsing layer rather than
499
+ // duplicating that default.
500
+ describe("resolveTrialExpiryWarningDays", () => {
501
+ it("returns undefined when unset, letting the sweeper apply its own default", () => {
502
+ expect(resolveTrialExpiryWarningDays({})).toBeUndefined();
503
+ });
504
+
505
+ it("uses an explicit positive override", () => {
506
+ expect(
507
+ resolveTrialExpiryWarningDays({
508
+ SHIPWRIGHT_ADMIN_TRIAL_EXPIRY_WARNING_DAYS: "7",
509
+ }),
510
+ ).toBe(7);
511
+ });
512
+
513
+ it("returns undefined for blank, non-numeric, zero, or negative values", () => {
514
+ for (const raw of ["", " ", "soon", "0", "-1"]) {
515
+ expect(
516
+ resolveTrialExpiryWarningDays({
517
+ SHIPWRIGHT_ADMIN_TRIAL_EXPIRY_WARNING_DAYS: raw,
518
+ }),
519
+ ).toBeUndefined();
520
+ }
521
+ });
522
+ });
523
+
524
+ // ─── runMigrations ──────────────────────────────────────────────────────────
525
+
526
+ // Only the early-return branch (DATABASE_URL_SHIPWRIGHT_ADMIN unset) is safe
527
+ // to unit-test directly — it returns before touching Prisma or Bun.spawn. The
528
+ // Bun.spawn("bunx prisma migrate deploy") path requires a real Postgres and is
529
+ // process-wiring / real-I/O, exercised in deployed environments instead.
530
+ describe("runMigrations", () => {
531
+ const originalUrl = process.env.DATABASE_URL_SHIPWRIGHT_ADMIN;
532
+
533
+ afterEach(() => {
534
+ if (originalUrl === undefined) {
535
+ delete process.env.DATABASE_URL_SHIPWRIGHT_ADMIN;
536
+ } else {
537
+ process.env.DATABASE_URL_SHIPWRIGHT_ADMIN = originalUrl;
538
+ }
539
+ });
540
+
541
+ it("returns early without spawning a process when DATABASE_URL_SHIPWRIGHT_ADMIN is unset", async () => {
542
+ delete process.env.DATABASE_URL_SHIPWRIGHT_ADMIN;
543
+ await expect(runMigrations()).resolves.toBeUndefined();
544
+ });
545
+ });
546
+
547
+ // ─── checkDbReady ────────────────────────────────────────────────────────────
548
+
549
+ // Backs GET /health/ready. Exercised here with a mocked $queryRaw (no real
550
+ // Postgres) — the real query is a plain `SELECT 1` ping.
551
+ describe("checkDbReady", () => {
552
+ it("returns true when the DB is reachable", async () => {
553
+ const prisma = { $queryRaw: async () => [{ "?column?": 1 }] };
554
+ await expect(checkDbReady(prisma)).resolves.toBe(true);
555
+ });
556
+
557
+ it("returns false when the DB is unreachable", async () => {
558
+ const prisma = {
559
+ $queryRaw: async () => {
560
+ throw new Error("Can't reach database server at 127.0.0.1:5432");
561
+ },
562
+ };
563
+ await expect(checkDbReady(prisma)).resolves.toBe(false);
564
+ });
565
+ });
566
+
567
+ // ─── isAnthropicFederationEnabled (WIF-2.1) ─────────────────────────────────
568
+
569
+ describe("isAnthropicFederationEnabled", () => {
570
+ const RULE = "SHIPWRIGHT_K8S_AGENT_ANTHROPIC_FEDERATION_RULE_ID";
571
+
572
+ it("false by default", () => {
573
+ expect(isAnthropicFederationEnabled({})).toBe(false);
574
+ });
575
+
576
+ it("true when provisioning is enabled and the rule ID is set", () => {
577
+ expect(
578
+ isAnthropicFederationEnabled({
579
+ SHIPWRIGHT_K8S_PROVISIONING: "enabled",
580
+ [RULE]: "rule-1",
581
+ }),
582
+ ).toBe(true);
583
+ });
584
+
585
+ it("false when the rule ID is set but provisioning is not enabled", () => {
586
+ expect(isAnthropicFederationEnabled({ [RULE]: "rule-1" })).toBe(false);
587
+ });
588
+
589
+ it("false when the rule ID is empty", () => {
590
+ expect(
591
+ isAnthropicFederationEnabled({
592
+ SHIPWRIGHT_K8S_PROVISIONING: "enabled",
593
+ [RULE]: "",
594
+ }),
595
+ ).toBe(false);
596
+ });
597
+ });
598
+
599
+ // ─── perAgent identity mode (PAI-2.1) ───────────────────────────────────────
600
+
601
+ describe("parseAnthropicFederation identity modes", () => {
602
+ const P = "SHIPWRIGHT_K8S_AGENT_ANTHROPIC_FEDERATION_";
603
+ const SHARED_ENV = {
604
+ SHIPWRIGHT_K8S_PROVISIONING: "enabled",
605
+ SHIPWRIGHT_K8S_AGENT_SERVICE_ACCOUNT: "shipwright-agent",
606
+ [`${P}RULE_ID`]: "fdrl_1",
607
+ [`${P}ORGANIZATION_ID`]: "org-1",
608
+ [`${P}SERVICE_ACCOUNT_ID`]: "svac_1",
609
+ };
610
+ const PER_AGENT_ENV = {
611
+ SHIPWRIGHT_K8S_PROVISIONING: "enabled",
612
+ [`${P}IDENTITY_MODE`]: "perAgent",
613
+ [`${P}ORGANIZATION_ID`]: "org-1",
614
+ [`${P}ISSUER_ID`]: "issuer-1",
615
+ };
616
+
617
+ it("returns undefined when nothing is configured", () => {
618
+ expect(parseAnthropicFederation({})).toBeUndefined();
619
+ });
620
+
621
+ it("returns the shared shape tagged mode=shared for the shared config", () => {
622
+ expect(parseAnthropicFederation(SHARED_ENV)).toEqual({
623
+ mode: "shared",
624
+ serviceAccountName: "shipwright-agent",
625
+ ruleId: "fdrl_1",
626
+ organizationId: "org-1",
627
+ serviceAccountId: "svac_1",
628
+ audience: "https://api.anthropic.com",
629
+ expirationSeconds: 600,
630
+ });
631
+ });
632
+
633
+ it("treats an explicit IDENTITY_MODE=shared like the default", () => {
634
+ expect(
635
+ parseAnthropicFederation({
636
+ ...SHARED_ENV,
637
+ [`${P}IDENTITY_MODE`]: "shared",
638
+ })?.mode,
639
+ ).toBe("shared");
640
+ expect(
641
+ parseAnthropicFederation({ [`${P}IDENTITY_MODE`]: "shared" }),
642
+ ).toBeUndefined();
643
+ });
644
+
645
+ it("parses the perAgent config without any shared rule ID", () => {
646
+ expect(parseAnthropicFederation(PER_AGENT_ENV)).toEqual({
647
+ mode: "perAgent",
648
+ organizationId: "org-1",
649
+ issuerId: "issuer-1",
650
+ audience: "https://api.anthropic.com",
651
+ expirationSeconds: 600,
652
+ });
653
+ });
654
+
655
+ it("parses the perAgent optional workspace, suffix, audience and expiration", () => {
656
+ expect(
657
+ parseAnthropicFederation({
658
+ ...PER_AGENT_ENV,
659
+ [`${P}WORKSPACE_ID`]: "wrkspc_1",
660
+ [`${P}NAME_SUFFIX`]: "-dev-1",
661
+ [`${P}AUDIENCE`]: "https://example.test",
662
+ [`${P}TOKEN_EXPIRATION_SECONDS`]: "3600",
663
+ }),
664
+ ).toEqual({
665
+ mode: "perAgent",
666
+ organizationId: "org-1",
667
+ workspaceId: "wrkspc_1",
668
+ issuerId: "issuer-1",
669
+ nameSuffix: "-dev-1",
670
+ audience: "https://example.test",
671
+ expirationSeconds: 3600,
672
+ });
673
+ });
674
+
675
+ for (const missing of ["ORGANIZATION_ID", "ISSUER_ID"]) {
676
+ it(`throws when perAgent mode is missing ${missing}`, () => {
677
+ expect(() =>
678
+ parseAnthropicFederation({ ...PER_AGENT_ENV, [`${P}${missing}`]: "" }),
679
+ ).toThrow(new RegExp(`partially configured.*${P}${missing}`));
680
+ });
681
+ }
682
+
683
+ it("throws on an unknown IDENTITY_MODE", () => {
684
+ expect(() =>
685
+ parseAnthropicFederation({
686
+ ...PER_AGENT_ENV,
687
+ [`${P}IDENTITY_MODE`]: "both",
688
+ }),
689
+ ).toThrow(/IDENTITY_MODE.*"both"/);
690
+ });
691
+
692
+ it("throws on a NAME_SUFFIX outside [a-z0-9-]", () => {
693
+ for (const bad of ["Dev", "a_b", "x.y", "a b"]) {
694
+ expect(() =>
695
+ parseAnthropicFederation({
696
+ ...PER_AGENT_ENV,
697
+ [`${P}NAME_SUFFIX`]: bad,
698
+ }),
699
+ ).toThrow(/NAME_SUFFIX/);
700
+ }
701
+ });
702
+
703
+ it("applies the token-expiration floor in perAgent mode too", () => {
704
+ expect(() =>
705
+ parseAnthropicFederation({
706
+ ...PER_AGENT_ENV,
707
+ [`${P}TOKEN_EXPIRATION_SECONDS`]: "599",
708
+ }),
709
+ ).toThrow(/TOKEN_EXPIRATION_SECONDS/);
710
+ });
711
+
712
+ it("derives the enabled flag from the mode: true for perAgent without a rule ID", () => {
713
+ expect(isAnthropicFederationEnabled(PER_AGENT_ENV)).toBe(true);
714
+ });
715
+
716
+ it("is disabled for perAgent mode when provisioning is not enabled", () => {
717
+ const { SHIPWRIGHT_K8S_PROVISIONING: _, ...rest } = PER_AGENT_ENV;
718
+ expect(isAnthropicFederationEnabled(rest)).toBe(false);
719
+ });
720
+
721
+ it("buildProvisioner passes the shared shape (no mode) in shared mode", () => {
722
+ const config = configOf(
723
+ buildProvisioner(SHARED_ENV, stubAgentTokenService()),
724
+ );
725
+ expect(config.anthropicFederation).not.toHaveProperty("mode");
726
+ expect(config.anthropicFederation?.ruleId).toBe("fdrl_1");
727
+ });
728
+
729
+ // The admin pod's own sign-in (chart-rendered in perAgent mode).
730
+ const ADMIN_SIGN_IN_ENV = {
731
+ ANTHROPIC_IDENTITY_TOKEN_FILE: "/var/run/secrets/anthropic.com/token",
732
+ ANTHROPIC_FEDERATION_RULE_ID: "fdrl_admin",
733
+ ANTHROPIC_ORGANIZATION_ID: "org-1",
734
+ ANTHROPIC_SERVICE_ACCOUNT_ID: "svac_admin",
735
+ };
736
+
737
+ /** Prisma stub — the identity services only keep the reference at build time. */
738
+ function stubPrisma(): PrismaClient {
739
+ return {} as unknown as PrismaClient;
740
+ }
741
+
742
+ it("buildProvisioner wires the per-agent identity deps in perAgent mode", () => {
743
+ const config = configOf(
744
+ buildProvisioner(
745
+ {
746
+ ...PER_AGENT_ENV,
747
+ ...ADMIN_SIGN_IN_ENV,
748
+ [`${P}WORKSPACE_ID`]: "wrkspc_1",
749
+ [`${P}AUDIENCE`]: "https://example.test",
750
+ [`${P}TOKEN_EXPIRATION_SECONDS`]: "900",
751
+ },
752
+ stubAgentTokenService(),
753
+ stubPrisma(),
754
+ ),
755
+ );
756
+ expect("anthropicFederation" in config).toBe(false);
757
+ const perAgent = config.perAgentIdentity;
758
+ expect(perAgent).toMatchObject({
759
+ organizationId: "org-1",
760
+ workspaceId: "wrkspc_1",
761
+ issuerId: "issuer-1",
762
+ audience: "https://example.test",
763
+ expirationSeconds: 900,
764
+ });
765
+ expect(perAgent?.client).toBeInstanceOf(HttpAnthropicIdentityClient);
766
+ expect(perAgent?.identities).toBeInstanceOf(AgentAnthropicIdentityService);
767
+ expect(perAgent?.names).toBeInstanceOf(AnthropicIdentityNameAllocator);
768
+ });
769
+
770
+ it("buildProvisioner leaves per-agent identity unset in shared mode", () => {
771
+ const config = configOf(
772
+ buildProvisioner(SHARED_ENV, stubAgentTokenService(), stubPrisma()),
773
+ );
774
+ expect("perAgentIdentity" in config).toBe(false);
775
+ });
776
+
777
+ for (const missing of [
778
+ "ANTHROPIC_IDENTITY_TOKEN_FILE",
779
+ "ANTHROPIC_FEDERATION_RULE_ID",
780
+ "ANTHROPIC_ORGANIZATION_ID",
781
+ ] as const) {
782
+ it(`buildProvisioner fails startup in perAgent mode without the admin's ${missing}`, () => {
783
+ expect(() =>
784
+ buildProvisioner(
785
+ { ...PER_AGENT_ENV, ...ADMIN_SIGN_IN_ENV, [missing]: "" },
786
+ stubAgentTokenService(),
787
+ stubPrisma(),
788
+ ),
789
+ ).toThrow(missing);
790
+ });
791
+ }
792
+
793
+ it("buildProvisioner fails in perAgent mode without a database client", () => {
794
+ expect(() =>
795
+ buildProvisioner(
796
+ { ...PER_AGENT_ENV, ...ADMIN_SIGN_IN_ENV },
797
+ stubAgentTokenService(),
798
+ ),
799
+ ).toThrow(/perAgent/);
800
+ });
801
+ });