@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,554 @@
1
+ /**
2
+ * admin/src/http-chat-client.integration.test.ts
3
+ * Integration tests for HttpChatClient against recorded chat-service
4
+ * thread/message 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
+ * chat-service-provisioning-client.integration.test.ts.
10
+ */
11
+
12
+ import { describe, expect, it } from "bun:test";
13
+ import { readFileSync } from "node:fs";
14
+ import { HttpChatClient, NoopChatClient } from "./http-chat-client.ts";
15
+
16
+ // ─── Cassette ───────────────────────────────────────────────────────────────
17
+
18
+ interface CassetteEntry {
19
+ status: number;
20
+ body: unknown;
21
+ }
22
+
23
+ const CASSETTE_PATH = new URL(
24
+ "./fixtures/http-chat-client-cassette.json",
25
+ import.meta.url,
26
+ ).pathname;
27
+
28
+ const cassette: Record<string, CassetteEntry> = JSON.parse(
29
+ readFileSync(CASSETTE_PATH, "utf-8"),
30
+ );
31
+
32
+ interface RecordedRequest {
33
+ url: string;
34
+ method: string;
35
+ headers: Headers;
36
+ body?: string;
37
+ }
38
+
39
+ /**
40
+ * Build an injected fetchFn that returns the cassette entry for `key` for
41
+ * every call. Records the last request so tests can assert URL/method/
42
+ * headers/body.
43
+ */
44
+ function cassetteFetch(key: string): {
45
+ fetchFn: typeof fetch;
46
+ lastRequest: () => RecordedRequest;
47
+ } {
48
+ let last: RecordedRequest | undefined;
49
+ const entry = cassette[key];
50
+ if (!entry) throw new Error(`cassette key not found: ${key}`);
51
+
52
+ const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
53
+ const url = typeof input === "string" ? input : input.toString();
54
+ last = {
55
+ url,
56
+ method: init?.method ?? "GET",
57
+ headers: new Headers(init?.headers),
58
+ body: typeof init?.body === "string" ? init.body : undefined,
59
+ };
60
+ return new Response(JSON.stringify(entry.body), {
61
+ status: entry.status,
62
+ statusText: `status-${entry.status}`,
63
+ headers: { "Content-Type": "application/json" },
64
+ });
65
+ }) as typeof fetch;
66
+
67
+ return {
68
+ fetchFn,
69
+ lastRequest: () => {
70
+ if (!last) throw new Error("fetchFn was not called");
71
+ return last;
72
+ },
73
+ };
74
+ }
75
+
76
+ function makeClient(key: string): {
77
+ client: HttpChatClient;
78
+ lastRequest: () => RecordedRequest;
79
+ } {
80
+ const { fetchFn, lastRequest } = cassetteFetch(key);
81
+ const client = new HttpChatClient(
82
+ "https://chat.example.com",
83
+ "admin-token-xyz",
84
+ {
85
+ fetchFn,
86
+ },
87
+ );
88
+ return { client, lastRequest };
89
+ }
90
+
91
+ // ─── listThreads ────────────────────────────────────────────────────────────
92
+
93
+ describe("HttpChatClient — listThreads", () => {
94
+ it("GETs /threads with Bearer auth and returns the parsed result", async () => {
95
+ const { client, lastRequest } = makeClient("listThreads_success");
96
+ const result = await client.listThreads();
97
+
98
+ const req = lastRequest();
99
+ expect(req.method).toBe("GET");
100
+ expect(req.url).toBe("https://chat.example.com/threads");
101
+ expect(req.headers.get("authorization")).toBe("Bearer admin-token-xyz");
102
+ expect(req.headers.get("content-type")).toBe("application/json");
103
+ expect(result.threads).toHaveLength(2);
104
+ expect(result.total).toBe(2);
105
+ });
106
+
107
+ it("appends agentId, limit, and offset as query params only when provided", async () => {
108
+ const { client, lastRequest } = makeClient("listThreads_success");
109
+ await client.listThreads("agent-123", { limit: 10, offset: 5 });
110
+
111
+ const req = lastRequest();
112
+ const url = new URL(req.url);
113
+ expect(url.pathname).toBe("/threads");
114
+ expect(url.searchParams.get("agentId")).toBe("agent-123");
115
+ expect(url.searchParams.get("limit")).toBe("10");
116
+ expect(url.searchParams.get("offset")).toBe("5");
117
+ });
118
+
119
+ it("omits query params entirely when none are provided", async () => {
120
+ const { client, lastRequest } = makeClient("listThreads_success");
121
+ await client.listThreads();
122
+
123
+ const req = lastRequest();
124
+ expect(req.url).toBe("https://chat.example.com/threads");
125
+ });
126
+
127
+ it("returns an empty result set", async () => {
128
+ const { client } = makeClient("listThreads_empty");
129
+ const result = await client.listThreads();
130
+ expect(result).toEqual({ threads: [], total: 0, limit: 50, offset: 0 });
131
+ });
132
+
133
+ it("throws on a non-ok response with the exact error message format", async () => {
134
+ const { client } = makeClient("listThreads_500");
135
+ await expect(client.listThreads()).rejects.toThrow(
136
+ "chat-service GET /threads failed: 500 status-500",
137
+ );
138
+ });
139
+ });
140
+
141
+ // ─── getThread ──────────────────────────────────────────────────────────────
142
+
143
+ describe("HttpChatClient — getThread", () => {
144
+ it("GETs /threads/:id with Bearer auth", async () => {
145
+ const { client, lastRequest } = makeClient("getThread_success");
146
+ const result = await client.getThread("thread_1");
147
+
148
+ const req = lastRequest();
149
+ expect(req.method).toBe("GET");
150
+ expect(req.url).toBe("https://chat.example.com/threads/thread_1");
151
+ expect(req.headers.get("authorization")).toBe("Bearer admin-token-xyz");
152
+ expect(result.id).toBe("thread_1");
153
+ expect(result.title).toBe("First thread");
154
+ });
155
+
156
+ it("throws on a non-ok response with the exact error message format", async () => {
157
+ const { client } = makeClient("getThread_404");
158
+ await expect(client.getThread("thread_1")).rejects.toThrow(
159
+ "chat-service GET /threads/thread_1 failed: 404 status-404",
160
+ );
161
+ });
162
+ });
163
+
164
+ // ─── createThread ───────────────────────────────────────────────────────────
165
+
166
+ describe("HttpChatClient — createThread", () => {
167
+ it("POSTs to /threads with agentId, title, and memberId", async () => {
168
+ const { client, lastRequest } = makeClient("createThread_success");
169
+ const result = await client.createThread("agent-123", {
170
+ title: "New thread",
171
+ memberId: "member-9",
172
+ });
173
+
174
+ const req = lastRequest();
175
+ expect(req.method).toBe("POST");
176
+ expect(req.url).toBe("https://chat.example.com/threads");
177
+ expect(req.headers.get("authorization")).toBe("Bearer admin-token-xyz");
178
+ expect(JSON.parse(req.body ?? "{}")).toEqual({
179
+ agentId: "agent-123",
180
+ title: "New thread",
181
+ memberId: "member-9",
182
+ });
183
+ expect(result.id).toBe("thread_new");
184
+ });
185
+
186
+ it("omits title and memberId from the body when not provided", async () => {
187
+ const { client, lastRequest } = makeClient("createThread_minimal_success");
188
+ await client.createThread("agent-123");
189
+
190
+ const req = lastRequest();
191
+ expect(JSON.parse(req.body ?? "{}")).toEqual({ agentId: "agent-123" });
192
+ });
193
+
194
+ it("throws on a non-ok response with the exact error message format", async () => {
195
+ const { client } = makeClient("createThread_500");
196
+ await expect(client.createThread("agent-123")).rejects.toThrow(
197
+ "chat-service POST /threads failed: 500 status-500",
198
+ );
199
+ });
200
+ });
201
+
202
+ // ─── updateThread ───────────────────────────────────────────────────────────
203
+
204
+ describe("HttpChatClient — updateThread", () => {
205
+ it("PATCHes /threads/:id with the update payload", async () => {
206
+ const { client, lastRequest } = makeClient("updateThread_success");
207
+ const result = await client.updateThread("thread_1", {
208
+ title: "Updated title",
209
+ });
210
+
211
+ const req = lastRequest();
212
+ expect(req.method).toBe("PATCH");
213
+ expect(req.url).toBe("https://chat.example.com/threads/thread_1");
214
+ expect(req.headers.get("authorization")).toBe("Bearer admin-token-xyz");
215
+ expect(JSON.parse(req.body ?? "{}")).toEqual({ title: "Updated title" });
216
+ expect(result.title).toBe("Updated title");
217
+ });
218
+
219
+ it("throws on a non-ok response with the exact error message format", async () => {
220
+ const { client } = makeClient("updateThread_500");
221
+ await expect(
222
+ client.updateThread("thread_1", { title: "x" }),
223
+ ).rejects.toThrow(
224
+ "chat-service PATCH /threads/thread_1 failed: 500 status-500",
225
+ );
226
+ });
227
+ });
228
+
229
+ // ─── deleteThread ───────────────────────────────────────────────────────────
230
+
231
+ describe("HttpChatClient — deleteThread", () => {
232
+ it("DELETEs /threads/:id with Bearer auth", async () => {
233
+ const { client, lastRequest } = makeClient("deleteThread_success");
234
+ await client.deleteThread("thread_1");
235
+
236
+ const req = lastRequest();
237
+ expect(req.method).toBe("DELETE");
238
+ expect(req.url).toBe("https://chat.example.com/threads/thread_1");
239
+ expect(req.headers.get("authorization")).toBe("Bearer admin-token-xyz");
240
+ });
241
+
242
+ it("throws on a non-ok response with the exact error message format", async () => {
243
+ const { client } = makeClient("deleteThread_500");
244
+ await expect(client.deleteThread("thread_1")).rejects.toThrow(
245
+ "chat-service DELETE /threads/thread_1 failed: 500 status-500",
246
+ );
247
+ });
248
+ });
249
+
250
+ // ─── listMessages ───────────────────────────────────────────────────────────
251
+
252
+ describe("HttpChatClient — listMessages", () => {
253
+ it("GETs /threads/:threadId/messages with Bearer auth", async () => {
254
+ const { client, lastRequest } = makeClient("listMessages_success");
255
+ const result = await client.listMessages("thread_1");
256
+
257
+ const req = lastRequest();
258
+ expect(req.method).toBe("GET");
259
+ expect(req.url).toBe("https://chat.example.com/threads/thread_1/messages");
260
+ expect(req.headers.get("authorization")).toBe("Bearer admin-token-xyz");
261
+ expect(result.messages).toHaveLength(2);
262
+ expect(result.messages[1]?.tokens).toEqual({
263
+ input_tokens: 12,
264
+ output_tokens: 8,
265
+ cache_read_input_tokens: 0,
266
+ cache_creation_input_tokens: 0,
267
+ });
268
+ });
269
+
270
+ it("appends limit and offset as query params only when provided", async () => {
271
+ const { client, lastRequest } = makeClient("listMessages_success");
272
+ await client.listMessages("thread_1", { limit: 20, offset: 40 });
273
+
274
+ const req = lastRequest();
275
+ const url = new URL(req.url);
276
+ expect(url.pathname).toBe("/threads/thread_1/messages");
277
+ expect(url.searchParams.get("limit")).toBe("20");
278
+ expect(url.searchParams.get("offset")).toBe("40");
279
+ });
280
+
281
+ it("omits query params entirely when none are provided", async () => {
282
+ const { client, lastRequest } = makeClient("listMessages_success");
283
+ await client.listMessages("thread_1");
284
+
285
+ const req = lastRequest();
286
+ expect(req.url).toBe("https://chat.example.com/threads/thread_1/messages");
287
+ });
288
+
289
+ it("returns an empty result set", async () => {
290
+ const { client } = makeClient("listMessages_empty");
291
+ const result = await client.listMessages("thread_1");
292
+ expect(result).toEqual({ messages: [], total: 0, limit: 50, offset: 0 });
293
+ });
294
+
295
+ it("throws on a non-ok response with the exact error message format", async () => {
296
+ const { client } = makeClient("listMessages_500");
297
+ await expect(client.listMessages("thread_1")).rejects.toThrow(
298
+ "chat-service GET /threads/thread_1/messages failed: 500 status-500",
299
+ );
300
+ });
301
+ });
302
+
303
+ // ─── createMessage ──────────────────────────────────────────────────────────
304
+
305
+ describe("HttpChatClient — createMessage", () => {
306
+ it("POSTs to /threads/:threadId/messages with role and body", async () => {
307
+ const { client, lastRequest } = makeClient("createMessage_success");
308
+ const result = await client.createMessage(
309
+ "thread_1",
310
+ "user",
311
+ "a message without attachment",
312
+ );
313
+
314
+ const req = lastRequest();
315
+ expect(req.method).toBe("POST");
316
+ expect(req.url).toBe("https://chat.example.com/threads/thread_1/messages");
317
+ expect(req.headers.get("authorization")).toBe("Bearer admin-token-xyz");
318
+ expect(JSON.parse(req.body ?? "{}")).toEqual({
319
+ role: "user",
320
+ body: "a message without attachment",
321
+ });
322
+ expect(result.id).toBe("msg_new");
323
+ });
324
+
325
+ it("base64-encodes an attachment's bytes and includes filename/size", async () => {
326
+ const { client, lastRequest } = makeClient(
327
+ "createMessage_with_attachment_success",
328
+ );
329
+ const bytes = new TextEncoder().encode("hello notes");
330
+ const result = await client.createMessage(
331
+ "thread_1",
332
+ "user",
333
+ "a message with an attachment",
334
+ { filename: "notes.txt", size: bytes.byteLength, bytes },
335
+ );
336
+
337
+ const req = lastRequest();
338
+ const payload = JSON.parse(req.body ?? "{}");
339
+ expect(payload.role).toBe("user");
340
+ expect(payload.body).toBe("a message with an attachment");
341
+ expect(payload.attachmentFilename).toBe("notes.txt");
342
+ expect(payload.attachmentSize).toBe(bytes.byteLength);
343
+ expect(payload.attachmentBytes).toBe(Buffer.from(bytes).toString("base64"));
344
+ expect(
345
+ Buffer.from(payload.attachmentBytes, "base64").toString("utf-8"),
346
+ ).toBe("hello notes");
347
+ expect(result.attachmentFilename).toBe("notes.txt");
348
+ expect(result.attachmentSize).toBe(11);
349
+ });
350
+
351
+ it("throws on a non-ok response with the exact error message format", async () => {
352
+ const { client } = makeClient("createMessage_500");
353
+ await expect(
354
+ client.createMessage("thread_1", "user", "hi"),
355
+ ).rejects.toThrow(
356
+ "chat-service POST /threads/thread_1/messages failed: 500 status-500",
357
+ );
358
+ });
359
+ });
360
+
361
+ // ─── getThreadStats ─────────────────────────────────────────────────────────
362
+
363
+ describe("HttpChatClient — getThreadStats", () => {
364
+ it("GETs /threads/:threadId/stats with Bearer auth", async () => {
365
+ const { client, lastRequest } = makeClient("getThreadStats_success");
366
+ const result = await client.getThreadStats("thread_1");
367
+
368
+ const req = lastRequest();
369
+ expect(req.method).toBe("GET");
370
+ expect(req.url).toBe("https://chat.example.com/threads/thread_1/stats");
371
+ expect(req.headers.get("authorization")).toBe("Bearer admin-token-xyz");
372
+ expect(result).toEqual({
373
+ messageCount: 2,
374
+ totalInputTokens: 12,
375
+ totalOutputTokens: 8,
376
+ totalCostUsd: 0.0012,
377
+ });
378
+ });
379
+
380
+ it("throws on a non-ok response with the exact error message format", async () => {
381
+ const { client } = makeClient("getThreadStats_500");
382
+ await expect(client.getThreadStats("thread_1")).rejects.toThrow(
383
+ "chat-service GET /threads/thread_1/stats failed: 500 status-500",
384
+ );
385
+ });
386
+ });
387
+
388
+ // ─── getAttachment ──────────────────────────────────────────────────────────
389
+ // VM-3.1: proxies the chat service's GET /:id/attachment so the browser (which
390
+ // never holds the chat service's admin bearer token) can play back a message's
391
+ // audio attachment. Uses a raw (non-JSON) fetchFn double since the response
392
+ // body is binary bytes, not JSON — the shared cassetteFetch() helper above
393
+ // only knows how to serve JSON bodies.
394
+
395
+ describe("HttpChatClient — getAttachment", () => {
396
+ it("GETs /threads/:threadId/messages/:id/attachment with Bearer auth and parses filename from Content-Disposition", async () => {
397
+ let lastRequest: { url: string; headers: Headers } | undefined;
398
+ const bytes = new TextEncoder().encode("fake audio bytes");
399
+ const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
400
+ lastRequest = {
401
+ url: typeof input === "string" ? input : input.toString(),
402
+ headers: new Headers(init?.headers),
403
+ };
404
+ return new Response(bytes, {
405
+ status: 200,
406
+ headers: {
407
+ "Content-Type": "application/octet-stream",
408
+ "Content-Disposition": 'attachment; filename="recording-123.webm"',
409
+ },
410
+ });
411
+ }) as typeof fetch;
412
+ const client = new HttpChatClient(
413
+ "https://chat.example.com",
414
+ "admin-token-xyz",
415
+ { fetchFn },
416
+ );
417
+
418
+ const result = await client.getAttachment("thread_1", "msg_1");
419
+
420
+ expect(lastRequest?.url).toBe(
421
+ "https://chat.example.com/threads/thread_1/messages/msg_1/attachment",
422
+ );
423
+ expect(lastRequest?.headers.get("authorization")).toBe(
424
+ "Bearer admin-token-xyz",
425
+ );
426
+ expect(result?.filename).toBe("recording-123.webm");
427
+ expect(
428
+ Buffer.from(result?.bytes ?? new Uint8Array()).toString("utf-8"),
429
+ ).toBe("fake audio bytes");
430
+ });
431
+
432
+ it("returns null on a 404 (no attachment, or already cleared by ephemeral retention)", async () => {
433
+ const fetchFn = (async (_input: RequestInfo | URL, _init?: RequestInit) =>
434
+ new Response(JSON.stringify({ error: "no attachment" }), {
435
+ status: 404,
436
+ headers: { "Content-Type": "application/json" },
437
+ })) as typeof fetch;
438
+ const client = new HttpChatClient(
439
+ "https://chat.example.com",
440
+ "admin-token-xyz",
441
+ { fetchFn },
442
+ );
443
+
444
+ const result = await client.getAttachment("thread_1", "msg_1");
445
+ expect(result).toBeNull();
446
+ });
447
+
448
+ it("falls back to a generic filename when Content-Disposition is missing", async () => {
449
+ const fetchFn = (async (_input: RequestInfo | URL, _init?: RequestInit) =>
450
+ new Response(new Uint8Array([1, 2, 3]), {
451
+ status: 200,
452
+ headers: { "Content-Type": "application/octet-stream" },
453
+ })) as typeof fetch;
454
+ const client = new HttpChatClient(
455
+ "https://chat.example.com",
456
+ "admin-token-xyz",
457
+ { fetchFn },
458
+ );
459
+
460
+ const result = await client.getAttachment("thread_1", "msg_1");
461
+ expect(result?.filename).toBe("attachment");
462
+ });
463
+
464
+ it("throws on a non-ok, non-404 response with the exact error message format", async () => {
465
+ const fetchFn = (async (_input: RequestInfo | URL, _init?: RequestInit) =>
466
+ new Response("", {
467
+ status: 500,
468
+ statusText: "status-500",
469
+ })) as typeof fetch;
470
+ const client = new HttpChatClient(
471
+ "https://chat.example.com",
472
+ "admin-token-xyz",
473
+ { fetchFn },
474
+ );
475
+
476
+ await expect(client.getAttachment("thread_1", "msg_1")).rejects.toThrow(
477
+ "chat-service GET /threads/thread_1/messages/msg_1/attachment failed: 500 status-500",
478
+ );
479
+ });
480
+ });
481
+
482
+ // ─── NoopChatClient ─────────────────────────────────────────────────────────
483
+
484
+ describe("NoopChatClient", () => {
485
+ it("listThreads returns an empty result set", async () => {
486
+ const client = new NoopChatClient();
487
+ const result = await client.listThreads();
488
+ expect(result).toEqual({ threads: [], total: 0, limit: 50, offset: 0 });
489
+ });
490
+
491
+ it("getThread returns an empty-shaped thread", async () => {
492
+ const client = new NoopChatClient();
493
+ const result = await client.getThread("thread_1");
494
+ expect(result.id).toBe("");
495
+ expect(result.title).toBeNull();
496
+ expect(result.memberId).toBeNull();
497
+ });
498
+
499
+ it("createThread echoes the given agentId", async () => {
500
+ const client = new NoopChatClient();
501
+ const result = await client.createThread("agent-123");
502
+ expect(result.agentId).toBe("agent-123");
503
+ expect(result.id).toBe("");
504
+ });
505
+
506
+ it("updateThread echoes the given id and title", async () => {
507
+ const client = new NoopChatClient();
508
+ const result = await client.updateThread("thread_1", { title: "t" });
509
+ expect(result.id).toBe("thread_1");
510
+ expect(result.title).toBe("t");
511
+ });
512
+
513
+ it("deleteThread resolves without error", async () => {
514
+ const client = new NoopChatClient();
515
+ await expect(client.deleteThread("thread_1")).resolves.toBeUndefined();
516
+ });
517
+
518
+ it("listMessages returns an empty result set", async () => {
519
+ const client = new NoopChatClient();
520
+ const result = await client.listMessages("thread_1");
521
+ expect(result).toEqual({ messages: [], total: 0, limit: 50, offset: 0 });
522
+ });
523
+
524
+ it("createMessage echoes threadId/role/body and attachment metadata", async () => {
525
+ const client = new NoopChatClient();
526
+ const result = await client.createMessage("thread_1", "user", "hi", {
527
+ filename: "a.txt",
528
+ size: 3,
529
+ bytes: new Uint8Array([1, 2, 3]),
530
+ });
531
+ expect(result.threadId).toBe("thread_1");
532
+ expect(result.role).toBe("user");
533
+ expect(result.body).toBe("hi");
534
+ expect(result.attachmentFilename).toBe("a.txt");
535
+ expect(result.attachmentSize).toBe(3);
536
+ });
537
+
538
+ it("getThreadStats returns zeroed stats", async () => {
539
+ const client = new NoopChatClient();
540
+ const result = await client.getThreadStats("thread_1");
541
+ expect(result).toEqual({
542
+ messageCount: 0,
543
+ totalInputTokens: 0,
544
+ totalOutputTokens: 0,
545
+ totalCostUsd: 0,
546
+ });
547
+ });
548
+
549
+ it("getAttachment returns null (no chat service to proxy)", async () => {
550
+ const client = new NoopChatClient();
551
+ const result = await client.getAttachment("thread_1", "msg_1");
552
+ expect(result).toBeNull();
553
+ });
554
+ });