@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,116 @@
1
+ /**
2
+ * admin/src/chat-markers.unit.test.ts
3
+ * Unit tests for parseChatMarkers() — marker parsing for chat display.
4
+ */
5
+
6
+ import { describe, expect, it } from "bun:test";
7
+ import { parseChatMarkers } from "./chat-markers.ts";
8
+
9
+ describe("parseChatMarkers", () => {
10
+ it("returns cleaned text without [silent] marker", () => {
11
+ const result = parseChatMarkers("Here is a response [silent]");
12
+ expect(result.cleaned).not.toContain("[silent]");
13
+ expect(result.cleaned).toContain("Here is a response");
14
+ });
15
+
16
+ it("extracts upload path from [upload:/path/to/file.pdf] marker", () => {
17
+ const result = parseChatMarkers("Check the file [upload:/tmp/report.pdf]");
18
+ expect(result.uploads).toContain("/tmp/report.pdf");
19
+ expect(result.cleaned).not.toContain("[upload:");
20
+ });
21
+
22
+ it("extracts plan URL from [plan:https://example.com/plan] marker", () => {
23
+ const result = parseChatMarkers("See the plan [plan:https://example.com/plan]");
24
+ expect(result.planUrls).toContain("https://example.com/plan");
25
+ expect(result.cleaned).not.toContain("[plan:");
26
+ });
27
+
28
+ it("strips [speak:text] marker from cleaned text", () => {
29
+ const result = parseChatMarkers("Done [speak:all work complete]");
30
+ expect(result.cleaned).not.toContain("[speak:");
31
+ expect(result.cleaned).not.toContain("all work complete");
32
+ });
33
+
34
+ it("strips [react:emoji] marker from cleaned text", () => {
35
+ const result = parseChatMarkers("Great work [react:thumbsup]");
36
+ expect(result.cleaned).not.toContain("[react:");
37
+ expect(result.cleaned).not.toContain("thumbsup");
38
+ });
39
+
40
+ it("handles multiple markers in one message", () => {
41
+ const text =
42
+ "Here is a response with multiple markers [upload:/tmp/file1.pdf] and [upload:/tmp/file2.pdf] plus [plan:https://example.com/plan] [react:eyes] [silent]";
43
+ const result = parseChatMarkers(text);
44
+ expect(result.uploads).toEqual(["/tmp/file1.pdf", "/tmp/file2.pdf"]);
45
+ expect(result.planUrls).toEqual(["https://example.com/plan"]);
46
+ expect(result.cleaned).not.toContain("[upload:");
47
+ expect(result.cleaned).not.toContain("[plan:");
48
+ expect(result.cleaned).not.toContain("[react:");
49
+ expect(result.cleaned).not.toContain("[silent]");
50
+ });
51
+
52
+ it("handles text with no markers", () => {
53
+ const text = "Just a normal message with no markers at all";
54
+ const result = parseChatMarkers(text);
55
+ expect(result.cleaned).toEqual(text);
56
+ expect(result.uploads).toEqual([]);
57
+ expect(result.planUrls).toEqual([]);
58
+ });
59
+
60
+ it("preserves cleaned text with markdown", () => {
61
+ const text = "This is **bold** and `code` [upload:/tmp/file.pdf]";
62
+ const result = parseChatMarkers(text);
63
+ expect(result.cleaned).toContain("**bold**");
64
+ expect(result.cleaned).toContain("`code`");
65
+ expect(result.cleaned).not.toContain("[upload:");
66
+ });
67
+
68
+ it("trims whitespace after marker removal", () => {
69
+ const text = "Message [upload:/tmp/file.pdf] ";
70
+ const result = parseChatMarkers(text);
71
+ expect(result.cleaned).not.toMatch(/\s+$/);
72
+ });
73
+
74
+ it("handles malformed markers gracefully", () => {
75
+ const text = "Message with [upload:] empty marker";
76
+ const result = parseChatMarkers(text);
77
+ // Malformed markers should be left in the cleaned text
78
+ expect(result.uploads.length).toBe(0);
79
+ });
80
+
81
+ it("handles empty input", () => {
82
+ const result = parseChatMarkers("");
83
+ expect(result.cleaned).toBe("");
84
+ expect(result.uploads).toEqual([]);
85
+ expect(result.planUrls).toEqual([]);
86
+ });
87
+
88
+ it("handles [silent] at end of line", () => {
89
+ const text = "Final message [silent]";
90
+ const result = parseChatMarkers(text);
91
+ expect(result.cleaned).toBe("Final message");
92
+ expect(result.cleaned).not.toContain("[silent]");
93
+ });
94
+
95
+ it("handles [silent] with trailing whitespace", () => {
96
+ const text = "Final message [silent] ";
97
+ const result = parseChatMarkers(text);
98
+ expect(result.cleaned).toBe("Final message");
99
+ });
100
+
101
+ it("returns arrays in order of occurrence", () => {
102
+ const text =
103
+ "[upload:/a.pdf] text [upload:/b.pdf] [plan:http://x] [plan:http://y]";
104
+ const result = parseChatMarkers(text);
105
+ expect(result.uploads).toEqual(["/a.pdf", "/b.pdf"]);
106
+ expect(result.planUrls).toEqual(["http://x", "http://y"]);
107
+ });
108
+
109
+ it("does not extract non-http plan URLs (defense-in-depth against javascript: injection)", () => {
110
+ const text = "See [plan:javascript:alert(1)] and [plan:https://safe.example.com]";
111
+ const result = parseChatMarkers(text);
112
+ // javascript: URL should not be extracted — left in text as-is
113
+ expect(result.planUrls).toEqual(["https://safe.example.com"]);
114
+ expect(result.cleaned).toContain("[plan:javascript:alert(1)]");
115
+ });
116
+ });
@@ -0,0 +1,412 @@
1
+ /**
2
+ * admin/src/chat-service-provisioning-client.integration.test.ts
3
+ * Integration tests for HttpChatServiceProvisioningClient against recorded
4
+ * chat-service token/thread API fixtures.
5
+ *
6
+ * Drives the client through an INJECTED fetchFn that replays canned Responses
7
+ * from a cassette keyed by scenario — no live API server, no global.fetch
8
+ * override, no mock.module(). Mirrors the pattern in
9
+ * task-store-provisioning-client.integration.test.ts.
10
+ */
11
+
12
+ import { describe, expect, it } from "bun:test";
13
+ import { readFileSync } from "node:fs";
14
+ import {
15
+ HttpChatServiceProvisioningClient,
16
+ NoopChatServiceProvisioningClient,
17
+ } from "./chat-service-provisioning-client.ts";
18
+
19
+ // ─── Cassette ───────────────────────────────────────────────────────────────
20
+
21
+ interface CassetteEntry {
22
+ status: number;
23
+ body: unknown;
24
+ }
25
+
26
+ const CASSETTE_PATH = new URL(
27
+ "./fixtures/chat-service-provisioning-cassette.json",
28
+ import.meta.url,
29
+ ).pathname;
30
+
31
+ const cassette: Record<string, CassetteEntry> = JSON.parse(
32
+ readFileSync(CASSETTE_PATH, "utf-8"),
33
+ );
34
+
35
+ interface RecordedRequest {
36
+ url: string;
37
+ method: string;
38
+ headers: Headers;
39
+ body?: string;
40
+ }
41
+
42
+ /**
43
+ * Build an injected fetchFn that returns the cassette entry for `key` for
44
+ * every call. Records the last request so tests can assert URL/method/
45
+ * headers/body.
46
+ */
47
+ function cassetteFetch(key: string): {
48
+ fetchFn: typeof fetch;
49
+ lastRequest: () => RecordedRequest;
50
+ } {
51
+ let last: RecordedRequest | undefined;
52
+ const entry = cassette[key];
53
+ if (!entry) throw new Error(`cassette key not found: ${key}`);
54
+
55
+ const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
56
+ const url = typeof input === "string" ? input : input.toString();
57
+ last = {
58
+ url,
59
+ method: init?.method ?? "GET",
60
+ headers: new Headers(init?.headers),
61
+ body: typeof init?.body === "string" ? init.body : undefined,
62
+ };
63
+ return new Response(JSON.stringify(entry.body), {
64
+ status: entry.status,
65
+ statusText: `status-${entry.status}`,
66
+ headers: { "Content-Type": "application/json" },
67
+ });
68
+ }) as typeof fetch;
69
+
70
+ return {
71
+ fetchFn,
72
+ lastRequest: () => {
73
+ if (!last) throw new Error("fetchFn was not called");
74
+ return last;
75
+ },
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Build an injected fetchFn for multi-request flows (e.g. list-then-delete).
81
+ * `routes` is an ordered list of `{ match, key }` pairs; the first entry whose
82
+ * `match(url, method)` returns true is used to resolve the cassette entry for
83
+ * that call. Records every request made, in order.
84
+ */
85
+ function multiCassetteFetch(
86
+ routes: { match: (url: string, method: string) => boolean; key: string }[],
87
+ ): {
88
+ fetchFn: typeof fetch;
89
+ requests: () => RecordedRequest[];
90
+ } {
91
+ const requests: RecordedRequest[] = [];
92
+
93
+ const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
94
+ const url = typeof input === "string" ? input : input.toString();
95
+ const method = init?.method ?? "GET";
96
+ requests.push({
97
+ url,
98
+ method,
99
+ headers: new Headers(init?.headers),
100
+ body: typeof init?.body === "string" ? init.body : undefined,
101
+ });
102
+
103
+ const route = routes.find((r) => r.match(url, method));
104
+ if (!route) {
105
+ throw new Error(`multiCassetteFetch: no route matched ${method} ${url}`);
106
+ }
107
+ const entry = cassette[route.key];
108
+ if (!entry) throw new Error(`cassette key not found: ${route.key}`);
109
+ return new Response(JSON.stringify(entry.body), {
110
+ status: entry.status,
111
+ statusText: `status-${entry.status}`,
112
+ headers: { "Content-Type": "application/json" },
113
+ });
114
+ }) as typeof fetch;
115
+
116
+ return { fetchFn, requests: () => requests };
117
+ }
118
+
119
+ function makeClient(key: string): {
120
+ client: HttpChatServiceProvisioningClient;
121
+ lastRequest: () => RecordedRequest;
122
+ } {
123
+ const { fetchFn, lastRequest } = cassetteFetch(key);
124
+ const client = new HttpChatServiceProvisioningClient(
125
+ "https://chat.example.com",
126
+ "admin-token-xyz",
127
+ { fetchFn },
128
+ );
129
+ return { client, lastRequest };
130
+ }
131
+
132
+ // ─── mintToken ───────────────────────────────────────────────────────────────
133
+
134
+ describe("HttpChatServiceProvisioningClient — mintToken", () => {
135
+ it("POSTs to /tokens with Bearer auth and returns id/rawToken", async () => {
136
+ const { client, lastRequest } = makeClient("mintToken_success");
137
+ const result = await client.mintToken("agent-abc label");
138
+
139
+ const req = lastRequest();
140
+ expect(req.method).toBe("POST");
141
+ expect(req.url).toBe("https://chat.example.com/tokens");
142
+ expect(req.headers.get("authorization")).toBe("Bearer admin-token-xyz");
143
+ expect(req.headers.get("content-type")).toBe("application/json");
144
+ expect(JSON.parse(req.body ?? "{}")).toEqual({ label: "agent-abc label" });
145
+ expect(result).toEqual({
146
+ id: "tok_abc123",
147
+ rawToken: "cst_raw_test_token_value",
148
+ });
149
+ });
150
+
151
+ it("includes agentId in the body when provided", async () => {
152
+ const { client, lastRequest } = makeClient(
153
+ "mintToken_success_with_agentId",
154
+ );
155
+ const result = await client.mintToken("agent-scoped label", "agent-123");
156
+
157
+ const req = lastRequest();
158
+ expect(JSON.parse(req.body ?? "{}")).toEqual({
159
+ label: "agent-scoped label",
160
+ agentId: "agent-123",
161
+ });
162
+ expect(result.id).toBe("tok_def456");
163
+ });
164
+
165
+ it("throws on a non-ok response", async () => {
166
+ const { client } = makeClient("mintToken_500");
167
+ await expect(client.mintToken("label")).rejects.toThrow(
168
+ /chat-service POST \/tokens failed: 500/,
169
+ );
170
+ });
171
+ });
172
+
173
+ // ─── revokeToken ─────────────────────────────────────────────────────────────
174
+
175
+ describe("HttpChatServiceProvisioningClient — revokeToken", () => {
176
+ it("DELETEs /tokens/:id with Bearer auth", async () => {
177
+ const { client, lastRequest } = makeClient("revokeToken_success");
178
+ await client.revokeToken("tok_abc123");
179
+
180
+ const req = lastRequest();
181
+ expect(req.method).toBe("DELETE");
182
+ expect(req.url).toBe("https://chat.example.com/tokens/tok_abc123");
183
+ expect(req.headers.get("authorization")).toBe("Bearer admin-token-xyz");
184
+ });
185
+
186
+ it("treats 404 as success (already revoked / not found is not an error)", async () => {
187
+ const { client } = makeClient("revokeToken_404");
188
+ await expect(client.revokeToken("tok_missing")).resolves.toBeUndefined();
189
+ });
190
+
191
+ it("throws on a non-ok, non-404 response", async () => {
192
+ const { client } = makeClient("revokeToken_500");
193
+ await expect(client.revokeToken("tok_abc123")).rejects.toThrow(
194
+ /chat-service DELETE \/tokens\/tok_abc123 failed: 500/,
195
+ );
196
+ });
197
+ });
198
+
199
+ // ─── listTokensForAgent ───────────────────────────────────────────────────────
200
+
201
+ describe("HttpChatServiceProvisioningClient — listTokensForAgent", () => {
202
+ it("GETs /tokens and filters client-side by agentId", async () => {
203
+ const { client, lastRequest } = makeClient("listTokens_mixed_agents");
204
+ const result = await client.listTokensForAgent("agent-123");
205
+
206
+ const req = lastRequest();
207
+ expect(req.method).toBe("GET");
208
+ expect(req.url).toBe("https://chat.example.com/tokens");
209
+ expect(req.headers.get("authorization")).toBe("Bearer admin-token-xyz");
210
+ expect(result.map((t) => t.id)).toEqual(["tok_1", "tok_3"]);
211
+ });
212
+
213
+ it("returns an empty array when no tokens exist", async () => {
214
+ const { client } = makeClient("listTokens_empty");
215
+ const result = await client.listTokensForAgent("agent-123");
216
+ expect(result).toEqual([]);
217
+ });
218
+
219
+ it("returns an empty array when no tokens match the agentId", async () => {
220
+ const { client } = makeClient("listTokens_mixed_agents");
221
+ const result = await client.listTokensForAgent("agent-does-not-exist");
222
+ expect(result).toEqual([]);
223
+ });
224
+
225
+ it("throws on a non-ok response", async () => {
226
+ const { client } = makeClient("listTokens_500");
227
+ await expect(client.listTokensForAgent("agent-123")).rejects.toThrow(
228
+ /chat-service GET \/tokens failed: 500/,
229
+ );
230
+ });
231
+ });
232
+
233
+ // ─── deleteThreadsForAgent ────────────────────────────────────────────────────
234
+
235
+ describe("HttpChatServiceProvisioningClient — deleteThreadsForAgent", () => {
236
+ it("deletes every thread scoped to agentId, following pagination", async () => {
237
+ const { fetchFn, requests } = multiCassetteFetch([
238
+ {
239
+ match: (url, method) =>
240
+ method === "GET" &&
241
+ url === "https://chat.example.com/threads?agentId=agent-123&offset=0",
242
+ key: "listThreads_page_agent123_page1",
243
+ },
244
+ {
245
+ match: (url, method) =>
246
+ method === "GET" &&
247
+ url === "https://chat.example.com/threads?agentId=agent-123&offset=2",
248
+ key: "listThreads_page_agent123_page2",
249
+ },
250
+ {
251
+ match: (url, method) =>
252
+ method === "DELETE" &&
253
+ url === "https://chat.example.com/threads/thread_1",
254
+ key: "deleteThread_success",
255
+ },
256
+ {
257
+ match: (url, method) =>
258
+ method === "DELETE" &&
259
+ url === "https://chat.example.com/threads/thread_2",
260
+ key: "deleteThread_success",
261
+ },
262
+ {
263
+ match: (url, method) =>
264
+ method === "DELETE" &&
265
+ url === "https://chat.example.com/threads/thread_3",
266
+ key: "deleteThread_success",
267
+ },
268
+ ]);
269
+ const client = new HttpChatServiceProvisioningClient(
270
+ "https://chat.example.com",
271
+ "admin-token-xyz",
272
+ { fetchFn },
273
+ );
274
+
275
+ const result = await client.deleteThreadsForAgent("agent-123");
276
+
277
+ expect(result).toEqual({ deleted: 3 });
278
+ const deleteRequests = requests().filter((r) => r.method === "DELETE");
279
+ expect(deleteRequests.map((r) => r.url).sort()).toEqual([
280
+ "https://chat.example.com/threads/thread_1",
281
+ "https://chat.example.com/threads/thread_2",
282
+ "https://chat.example.com/threads/thread_3",
283
+ ]);
284
+ for (const req of deleteRequests) {
285
+ expect(req.headers.get("authorization")).toBe("Bearer admin-token-xyz");
286
+ }
287
+ });
288
+
289
+ it("returns {deleted: 0} and makes no DELETE calls when there are no threads", async () => {
290
+ const { fetchFn, requests } = multiCassetteFetch([
291
+ {
292
+ match: (url, method) => method === "GET" && url.includes("/threads"),
293
+ key: "listThreads_empty",
294
+ },
295
+ ]);
296
+ const client = new HttpChatServiceProvisioningClient(
297
+ "https://chat.example.com",
298
+ "admin-token-xyz",
299
+ { fetchFn },
300
+ );
301
+
302
+ const result = await client.deleteThreadsForAgent("agent-none");
303
+
304
+ expect(result).toEqual({ deleted: 0 });
305
+ expect(requests().some((r) => r.method === "DELETE")).toBe(false);
306
+ });
307
+
308
+ it("tolerates a thread already deleted mid-loop (404) without throwing", async () => {
309
+ const { fetchFn } = multiCassetteFetch([
310
+ {
311
+ match: (url, method) => method === "GET" && url.includes("/threads"),
312
+ key: "listThreads_agent456_single_page",
313
+ },
314
+ {
315
+ match: (url, method) =>
316
+ method === "DELETE" &&
317
+ url === "https://chat.example.com/threads/thread_10",
318
+ key: "deleteThread_404",
319
+ },
320
+ {
321
+ match: (url, method) =>
322
+ method === "DELETE" &&
323
+ url === "https://chat.example.com/threads/thread_11",
324
+ key: "deleteThread_success",
325
+ },
326
+ ]);
327
+ const client = new HttpChatServiceProvisioningClient(
328
+ "https://chat.example.com",
329
+ "admin-token-xyz",
330
+ { fetchFn },
331
+ );
332
+
333
+ const result = await client.deleteThreadsForAgent("agent-456");
334
+
335
+ // A tolerated 404 still counts as an attempted (completed) delete — this
336
+ // matters for retries where a previous run already deleted some threads.
337
+ expect(result).toEqual({ deleted: 2 });
338
+ });
339
+
340
+ it("throws on a non-ok, non-404 thread delete response", async () => {
341
+ const { fetchFn } = multiCassetteFetch([
342
+ {
343
+ match: (url, method) => method === "GET" && url.includes("/threads"),
344
+ key: "listThreads_agent456_single_page",
345
+ },
346
+ {
347
+ match: (_url, method) => method === "DELETE",
348
+ key: "deleteThread_500",
349
+ },
350
+ ]);
351
+ const client = new HttpChatServiceProvisioningClient(
352
+ "https://chat.example.com",
353
+ "admin-token-xyz",
354
+ { fetchFn },
355
+ );
356
+
357
+ await expect(client.deleteThreadsForAgent("agent-456")).rejects.toThrow(
358
+ /chat-service DELETE \/threads\/thread_10 failed: 500/,
359
+ );
360
+ });
361
+
362
+ it("throws on a non-ok thread list response", async () => {
363
+ const { fetchFn } = multiCassetteFetch([
364
+ {
365
+ match: (url, method) => method === "GET" && url.includes("/threads"),
366
+ key: "listThreads_500",
367
+ },
368
+ ]);
369
+ const client = new HttpChatServiceProvisioningClient(
370
+ "https://chat.example.com",
371
+ "admin-token-xyz",
372
+ { fetchFn },
373
+ );
374
+
375
+ await expect(client.deleteThreadsForAgent("agent-123")).rejects.toThrow(
376
+ /chat-service GET \/threads failed: 500/,
377
+ );
378
+ });
379
+ });
380
+
381
+ // ─── NoopChatServiceProvisioningClient ────────────────────────────────────────
382
+
383
+ describe("NoopChatServiceProvisioningClient", () => {
384
+ it("mintToken returns empty id/rawToken", async () => {
385
+ const client = new NoopChatServiceProvisioningClient();
386
+ const result = await client.mintToken("label");
387
+ expect(result).toEqual({ id: "", rawToken: "" });
388
+ });
389
+
390
+ it("mintToken accepts an agentId without erroring", async () => {
391
+ const client = new NoopChatServiceProvisioningClient();
392
+ const result = await client.mintToken("label", "agent-1");
393
+ expect(result).toEqual({ id: "", rawToken: "" });
394
+ });
395
+
396
+ it("revokeToken resolves without error", async () => {
397
+ const client = new NoopChatServiceProvisioningClient();
398
+ await expect(client.revokeToken("any-id")).resolves.toBeUndefined();
399
+ });
400
+
401
+ it("listTokensForAgent returns an empty array", async () => {
402
+ const client = new NoopChatServiceProvisioningClient();
403
+ const result = await client.listTokensForAgent("agent-1");
404
+ expect(result).toEqual([]);
405
+ });
406
+
407
+ it("deleteThreadsForAgent returns {deleted: 0}", async () => {
408
+ const client = new NoopChatServiceProvisioningClient();
409
+ const result = await client.deleteThreadsForAgent("agent-1");
410
+ expect(result).toEqual({ deleted: 0 });
411
+ });
412
+ });