@vellumai/assistant 0.11.1 → 0.11.2-staging.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (318) hide show
  1. package/Dockerfile +1 -3
  2. package/README.md +1 -1
  3. package/eslint.config.mjs +28 -8
  4. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  8. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  9. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  10. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  11. package/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  12. package/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  13. package/node_modules/@vellumai/slack-text/src/index.test.ts +48 -0
  14. package/node_modules/@vellumai/slack-text/src/index.ts +25 -6
  15. package/openapi.yaml +292 -5
  16. package/package.json +1 -1
  17. package/src/__tests__/agent-loop-resume-interrupted.test.ts +223 -0
  18. package/src/__tests__/byok-default-profile-ensure.test.ts +43 -0
  19. package/src/__tests__/cli-logger-boundary-guard.test.ts +87 -0
  20. package/src/__tests__/compaction-events.test.ts +139 -2
  21. package/src/__tests__/config-loader-backfill.test.ts +35 -3
  22. package/src/__tests__/config-schema.test.ts +1 -0
  23. package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +30 -0
  24. package/src/__tests__/conversation-agent-loop.test.ts +180 -7
  25. package/src/__tests__/conversation-error.test.ts +211 -4
  26. package/src/__tests__/conversation-options-turn-scoped-transport.test.ts +64 -0
  27. package/src/__tests__/conversation-queue.test.ts +126 -0
  28. package/src/__tests__/conversation-retry-route.test.ts +46 -7
  29. package/src/__tests__/conversation-slash.test.ts +4 -2
  30. package/src/__tests__/conversation-summarize-route.test.ts +7 -1
  31. package/src/__tests__/conversation-summarize-up-to.test.ts +71 -5
  32. package/src/__tests__/document-create-dedupe.test.ts +2 -1
  33. package/src/__tests__/document-find-replace.test.ts +2 -1
  34. package/src/__tests__/document-tool-security.test.ts +2 -1
  35. package/src/__tests__/document-update-default-surface.test.ts +2 -1
  36. package/src/__tests__/document-workspace-file.test.ts +467 -0
  37. package/src/__tests__/emit-signal-routing-intent.test.ts +290 -49
  38. package/src/__tests__/guardian-card-withdrawal.test.ts +91 -3
  39. package/src/__tests__/http-user-message-parity.test.ts +66 -0
  40. package/src/__tests__/list-messages-attachments.test.ts +87 -0
  41. package/src/__tests__/list-messages-provider-error.test.ts +143 -0
  42. package/src/__tests__/list-messages-system-card.test.ts +100 -0
  43. package/src/__tests__/llm-resolver.test.ts +19 -9
  44. package/src/__tests__/managed-profile-guard.test.ts +23 -0
  45. package/src/__tests__/notification-platform-adapter.test.ts +130 -2
  46. package/src/__tests__/notification-telegram-adapter.test.ts +6 -0
  47. package/src/__tests__/notification-vellum-adapter.test.ts +45 -0
  48. package/src/__tests__/plugin-api-model-profiles.test.ts +10 -1
  49. package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
  50. package/src/__tests__/provider-error-scenarios.test.ts +140 -0
  51. package/src/__tests__/provider-send-message-override-profile.test.ts +95 -0
  52. package/src/__tests__/run-conversation-turn-persistence.test.ts +66 -3
  53. package/src/__tests__/scripted-turn-metadata-persistence.test.ts +209 -0
  54. package/src/__tests__/skills.test.ts +27 -0
  55. package/src/__tests__/slack-channels-routes.test.ts +0 -2
  56. package/src/__tests__/slack-share-routes.test.ts +0 -3
  57. package/src/__tests__/slack-users-routes.test.ts +0 -2
  58. package/src/__tests__/subagent-tools.test.ts +11 -0
  59. package/src/__tests__/tool-preview-lifecycle.test.ts +58 -0
  60. package/src/__tests__/tool-result-spool.test.ts +5 -4
  61. package/src/__tests__/turn-boundary-resolution.test.ts +53 -0
  62. package/src/__tests__/turn-events-store.test.ts +26 -0
  63. package/src/__tests__/ui-visual-surface.test.ts +695 -0
  64. package/src/__tests__/unified-turn-context-visible-app.test.ts +99 -0
  65. package/src/__tests__/visible-app-context.test.ts +189 -0
  66. package/src/__tests__/workspace-git-service.test.ts +173 -33
  67. package/src/__tests__/workspace-migration-137-repair-retired-fireworks-minimax-model-id.test.ts +157 -0
  68. package/src/__tests__/workspace-migration-138-backfill-home-feed-titles.test.ts +373 -0
  69. package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +137 -0
  70. package/src/agent/loop.ts +36 -7
  71. package/src/api/events/context-window-usage.ts +31 -0
  72. package/src/api/events/notification-intent.ts +8 -0
  73. package/src/api/events/ui-surface-pending.ts +35 -0
  74. package/src/api/index.ts +14 -0
  75. package/src/api/responses/conversation-message.ts +25 -4
  76. package/src/api/surfaces.ts +90 -1
  77. package/src/approvals/guardian-card-withdrawal.ts +66 -31
  78. package/src/approvals/guardian-decision-primitive.ts +4 -0
  79. package/src/calls/__tests__/call-setup-router.test.ts +156 -26
  80. package/src/calls/__tests__/voice-session-bridge.test.ts +201 -12
  81. package/src/calls/call-setup-router.ts +80 -36
  82. package/src/calls/voice-session-bridge.ts +173 -26
  83. package/src/cli/commands/inference.help.ts +3 -3
  84. package/src/cli/commands/notifications.help.ts +3 -2
  85. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +42 -128
  86. package/src/cli/commands/platform/__tests__/credits.test.ts +9 -78
  87. package/src/cli/commands/platform/__tests__/helpers.ts +90 -0
  88. package/src/cli/commands/platform/__tests__/invoices.test.ts +238 -0
  89. package/src/cli/commands/platform/__tests__/plans.test.ts +9 -88
  90. package/src/cli/commands/platform/__tests__/status.test.ts +12 -87
  91. package/src/cli/commands/platform/__tests__/subscription.test.ts +9 -86
  92. package/src/cli/commands/platform/index.help.ts +92 -0
  93. package/src/cli/commands/platform/index.ts +7 -0
  94. package/src/cli/commands/platform/invoices.ts +132 -0
  95. package/src/cli/commands/usage.help.ts +1 -1
  96. package/src/cli/lib/list-installed-plugins.ts +2 -1
  97. package/src/config/__tests__/default-profile-catalog.test.ts +7 -10
  98. package/src/config/__tests__/deployment-context-defaults.test.ts +27 -5
  99. package/src/config/assistant-feature-flags.ts +7 -2
  100. package/src/config/bundled-skills/app-builder/SKILL.md +3 -2
  101. package/src/config/bundled-skills/subagent/SKILL.md +3 -1
  102. package/src/config/bundled-skills/subagent/TOOLS.json +3 -3
  103. package/src/config/bundled-skills/visualize/SKILL.md +163 -0
  104. package/src/config/call-site-defaults.ts +3 -2
  105. package/src/config/default-profile-catalog.ts +75 -70
  106. package/src/config/default-profile-names.ts +13 -28
  107. package/src/config/env-registry.ts +1 -0
  108. package/src/config/feature-flag-registry.json +16 -0
  109. package/src/config/llm-resolver.ts +21 -5
  110. package/src/config/loader.ts +18 -11
  111. package/src/config/schemas/llm.ts +1 -9
  112. package/src/config/schemas/memory-retrospective.ts +9 -0
  113. package/src/config/schemas/monitoring.ts +28 -2
  114. package/src/config/schemas/workspace-git.ts +15 -0
  115. package/src/config/seed-inference-profiles.ts +19 -5
  116. package/src/context/post-turn-tool-result-truncation.ts +2 -2
  117. package/src/conversations/__tests__/message-consolidation.test.ts +54 -0
  118. package/src/conversations/message-consolidation.ts +17 -15
  119. package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +182 -0
  120. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +181 -0
  121. package/src/daemon/conversation-agent-loop-handlers.ts +90 -7
  122. package/src/daemon/conversation-agent-loop.ts +107 -22
  123. package/src/daemon/conversation-error.ts +169 -48
  124. package/src/daemon/conversation-messaging.ts +87 -4
  125. package/src/daemon/conversation-process.ts +57 -45
  126. package/src/daemon/conversation-runtime-assembly.ts +57 -0
  127. package/src/daemon/conversation-store.ts +36 -1
  128. package/src/daemon/conversation-surfaces.ts +68 -6
  129. package/src/daemon/conversation-turn-finalize.ts +76 -18
  130. package/src/daemon/conversation.ts +122 -27
  131. package/src/daemon/lifecycle.ts +9 -0
  132. package/src/daemon/message-types/conversations.ts +8 -0
  133. package/src/daemon/message-types/surfaces.ts +3 -0
  134. package/src/documents/document-store.ts +247 -10
  135. package/src/home/__tests__/feed-types.test.ts +43 -8
  136. package/src/home/__tests__/feed-writer.test.ts +59 -0
  137. package/src/home/feed-types.ts +34 -7
  138. package/src/home/feed-writer.ts +15 -6
  139. package/src/live-voice/__tests__/activity-label.test.ts +95 -0
  140. package/src/live-voice/__tests__/live-activity-reporter.test.ts +86 -5
  141. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +310 -25
  142. package/src/live-voice/__tests__/live-voice-events.test.ts +4 -4
  143. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +8 -4
  144. package/src/live-voice/__tests__/live-voice-vad.test.ts +9 -1
  145. package/src/live-voice/activity-label.ts +169 -0
  146. package/src/live-voice/live-activity-reporter.ts +35 -6
  147. package/src/live-voice/live-voice-session.ts +282 -41
  148. package/src/live-voice/protocol.ts +35 -0
  149. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +84 -2
  150. package/src/messaging/providers/slack/adapter.ts +86 -19
  151. package/src/messaging/providers/slack/api.test.ts +85 -1
  152. package/src/messaging/providers/slack/api.ts +121 -288
  153. package/src/messaging/providers/slack/client.ts +20 -251
  154. package/src/messaging/providers/slack/send.test.ts +4 -9
  155. package/src/messaging/providers/slack/send.ts +1 -1
  156. package/src/messaging/providers/slack/types.ts +5 -0
  157. package/src/messaging/providers/slack/web-api-transport.test.ts +181 -0
  158. package/src/messaging/providers/slack/web-api-transport.ts +367 -0
  159. package/src/messaging/providers/slack/withdraw.ts +6 -14
  160. package/src/messaging/providers/telegram-bot/send.test.ts +31 -1
  161. package/src/messaging/providers/telegram-bot/send.ts +23 -2
  162. package/src/messaging/providers/telegram-bot/withdraw.test.ts +152 -0
  163. package/src/messaging/providers/telegram-bot/withdraw.ts +166 -0
  164. package/src/monitoring/__tests__/db-integrity-sample.test.ts +18 -4
  165. package/src/monitoring/__tests__/file-descriptors.test.ts +144 -0
  166. package/src/monitoring/file-descriptors.ts +262 -0
  167. package/src/monitoring/process-memory.ts +4 -11
  168. package/src/monitoring/resource-sampler.ts +5 -0
  169. package/src/monitoring/worker.ts +12 -0
  170. package/src/notifications/__tests__/assistant-reply-producer.test.ts +740 -0
  171. package/src/notifications/__tests__/broadcaster.test.ts +347 -7
  172. package/src/notifications/__tests__/copy-composer.test.ts +53 -3
  173. package/src/notifications/__tests__/decision-engine.test.ts +349 -29
  174. package/src/notifications/__tests__/deterministic-checks.test.ts +21 -0
  175. package/src/notifications/__tests__/edit-notification.test.ts +330 -0
  176. package/src/notifications/__tests__/guardian-delivery-recorder.test.ts +71 -0
  177. package/src/notifications/__tests__/home-feed-side-effect.test.ts +170 -12
  178. package/src/notifications/adapters/macos.ts +3 -0
  179. package/src/notifications/adapters/platform.ts +90 -14
  180. package/src/notifications/adapters/telegram.ts +6 -4
  181. package/src/notifications/assistant-reply-producer.ts +219 -0
  182. package/src/notifications/broadcaster.ts +523 -317
  183. package/src/notifications/copy-composer.ts +27 -6
  184. package/src/notifications/decision-engine.ts +154 -77
  185. package/src/notifications/deterministic-checks.ts +6 -7
  186. package/src/notifications/edit-notification.ts +8 -4
  187. package/src/notifications/emit-signal.ts +49 -16
  188. package/src/notifications/guardian-delivery-recorder.ts +8 -2
  189. package/src/notifications/home-feed-side-effect.ts +43 -6
  190. package/src/notifications/notification-utils.ts +39 -4
  191. package/src/notifications/signal.ts +10 -0
  192. package/src/notifications/types.ts +17 -0
  193. package/src/persistence/bookmark-crud.ts +18 -9
  194. package/src/persistence/conversation-attention-store.ts +26 -0
  195. package/src/persistence/conversation-crud.ts +79 -32
  196. package/src/persistence/conversation-queries.ts +30 -15
  197. package/src/persistence/conversation-title-service.ts +5 -170
  198. package/src/persistence/conversation-types.ts +192 -1
  199. package/src/persistence/db-init.ts +14 -1
  200. package/src/persistence/db-maintenance.ts +5 -4
  201. package/src/persistence/embeddings/__tests__/plugin-index-qdrant-init.test.ts +219 -0
  202. package/src/persistence/embeddings/__tests__/plugin-index.test.ts +22 -0
  203. package/src/persistence/embeddings/__tests__/worker-script-version.test.ts +76 -0
  204. package/src/persistence/embeddings/embedding-local.ts +2 -1
  205. package/src/persistence/embeddings/embedding-runtime-manager.ts +29 -5
  206. package/src/persistence/embeddings/plugin-index.ts +84 -12
  207. package/src/persistence/migrations/360-add-document-workspace-path.test.ts +110 -0
  208. package/src/persistence/migrations/360-add-document-workspace-path.ts +39 -0
  209. package/src/persistence/planner-statistics.ts +14 -0
  210. package/src/persistence/schema/documents.ts +26 -11
  211. package/src/persistence/steps.ts +2 -0
  212. package/src/platform/client.test.ts +172 -2
  213. package/src/platform/client.ts +178 -55
  214. package/src/plugin-api/conversation-turn.ts +13 -0
  215. package/src/plugin-api/model-profiles.test.ts +6 -2
  216. package/src/plugins/defaults/memory/__tests__/bookmark-crud.test.ts +34 -0
  217. package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +22 -0
  218. package/src/plugins/defaults/memory/__tests__/jobs-store-enqueue-gate.test.ts +11 -0
  219. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +199 -0
  220. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +98 -1
  221. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +105 -1
  222. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +24 -2
  223. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +524 -0
  224. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +1 -1
  225. package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +9 -4
  226. package/src/plugins/defaults/memory/host-utils.ts +5 -0
  227. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +105 -1
  228. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +64 -5
  229. package/src/plugins/defaults/memory/memory-retrospective-job.ts +36 -1
  230. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +12 -3
  231. package/src/plugins/defaults/memory/src/__tests__/memory-v2-simulate-route.test.ts +1 -0
  232. package/src/plugins/defaults/memory/substrate/__tests__/skill-store.test.ts +195 -1
  233. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +1 -1
  234. package/src/plugins/defaults/memory/substrate/skill-store.ts +167 -29
  235. package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +131 -0
  236. package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +1 -0
  237. package/src/plugins/defaults/memory/v2/activation-log-store.ts +4 -0
  238. package/src/plugins/defaults/memory/v2/injection.ts +48 -3
  239. package/src/plugins/defaults/memory/v2/rerank-local.ts +6 -2
  240. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -1
  241. package/src/plugins/defaults/platform-hosted/routes/reengage.ts +1 -1
  242. package/src/plugins/defaults/turn-context/injectors.ts +1 -0
  243. package/src/plugins/defaults/turn-context/unified-turn-context.ts +26 -0
  244. package/src/plugins/types.ts +17 -0
  245. package/src/prompts/templates/system-sections.ts +2 -2
  246. package/src/providers/__tests__/dispatch-connection-routing.test.ts +43 -2
  247. package/src/providers/__tests__/vellum-mismatch-routing.test.ts +8 -0
  248. package/src/providers/call-site-routing.ts +77 -14
  249. package/src/providers/connection-resolution.ts +42 -2
  250. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +127 -1
  251. package/src/providers/inference/adapter-factory.ts +70 -12
  252. package/src/providers/model-catalog.ts +0 -11
  253. package/src/providers/model-intents.ts +11 -0
  254. package/src/providers/registry.ts +8 -0
  255. package/src/providers/retry.ts +85 -17
  256. package/src/providers/types.ts +8 -1
  257. package/src/runtime/__tests__/agent-wake.test.ts +181 -0
  258. package/src/runtime/agent-wake.ts +132 -9
  259. package/src/runtime/channel-approval-types.ts +25 -0
  260. package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +2 -2
  261. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +11 -0
  262. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +121 -9
  263. package/src/runtime/routes/__tests__/platform-invoice-routes.test.ts +392 -0
  264. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +7 -0
  265. package/src/runtime/routes/__tests__/workspace-commit-routes.test.ts +63 -0
  266. package/src/runtime/routes/consolidation-routes.ts +1 -1
  267. package/src/runtime/routes/conversation-management-routes.ts +18 -6
  268. package/src/runtime/routes/conversation-query-routes.ts +14 -23
  269. package/src/runtime/routes/conversation-routes.ts +113 -8
  270. package/src/runtime/routes/credential-routes.ts +1 -5
  271. package/src/runtime/routes/documents-routes.ts +207 -12
  272. package/src/runtime/routes/identity-routes.ts +3 -93
  273. package/src/runtime/routes/inbound-stages/admission-policy.test.ts +31 -1
  274. package/src/runtime/routes/inbound-stages/admission-policy.ts +18 -0
  275. package/src/runtime/routes/inference-provider-connection-routes.ts +52 -3
  276. package/src/runtime/routes/notification-routes.ts +4 -1
  277. package/src/runtime/routes/platform-routes.ts +238 -3
  278. package/src/runtime/routes/playground/guard.ts +1 -2
  279. package/src/runtime/routes/slack-channel-routes.ts +2 -4
  280. package/src/runtime/routes/workspace-commit-routes.ts +49 -2
  281. package/src/runtime/routes/workspace-routes.ts +11 -12
  282. package/src/runtime/routes/workspace-utils.ts +49 -2
  283. package/src/runtime/services/conversation-serializer.ts +2 -4
  284. package/src/subagent/__tests__/consult-context-gating.test.ts +98 -0
  285. package/src/subagent/__tests__/consult-context-skills.test.ts +82 -0
  286. package/src/subagent/__tests__/consult-context.test.ts +84 -0
  287. package/src/subagent/__tests__/consult-prompt.test.ts +36 -0
  288. package/src/subagent/consult-context.ts +410 -0
  289. package/src/subagent/consult-prompt.ts +38 -5
  290. package/src/subagent/manager.ts +9 -4
  291. package/src/subagent/types.ts +8 -0
  292. package/src/telemetry/telemetry-event-sources.ts +7 -0
  293. package/src/telemetry/telemetry-wire-source.json +1 -1
  294. package/src/telemetry/telemetry-wire.generated.ts +1 -0
  295. package/src/telemetry/turn-events-store.ts +30 -1
  296. package/src/telemetry/types.ts +24 -0
  297. package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
  298. package/src/tools/browser/pinned-tabs.ts +3 -1
  299. package/src/tools/subagent/spawn.ts +23 -1
  300. package/src/tools/terminal/safe-env.ts +1 -0
  301. package/src/tools/ui-surface/definitions.ts +39 -1
  302. package/src/tools/ui-surface/surface-shape-docs.ts +38 -4
  303. package/src/tools/ui-surface/visual-validation.ts +787 -0
  304. package/src/tools/workflows/run-workflow.test.ts +1 -0
  305. package/src/util/__tests__/short-title.test.ts +229 -0
  306. package/src/util/__tests__/worker-compute.test.ts +65 -0
  307. package/src/util/cgroup-cpu.ts +93 -0
  308. package/src/util/errors.ts +27 -0
  309. package/src/util/process-tree.ts +19 -0
  310. package/src/util/short-title.ts +189 -0
  311. package/src/util/worker-compute.ts +83 -0
  312. package/src/workspace/byok-default-profile-ensure.ts +25 -15
  313. package/src/workspace/git-service.ts +79 -30
  314. package/src/workspace/migrations/137-repair-retired-fireworks-minimax-model-id.ts +131 -0
  315. package/src/workspace/migrations/138-backfill-home-feed-titles.ts +179 -0
  316. package/src/workspace/migrations/139-clear-renamed-cost-profile-label.ts +92 -0
  317. package/src/workspace/migrations/registry.ts +6 -0
  318. package/src/plugins/defaults/memory/substrate/constants.ts +0 -8
@@ -207,7 +207,7 @@ describe("POST inference/provider-connections (create)", () => {
207
207
  {
208
208
  body: {
209
209
  name: "managed-openai",
210
- provider: "openai",
210
+ provider: "vellum",
211
211
  auth: { type: "platform" },
212
212
  },
213
213
  },
@@ -215,6 +215,39 @@ describe("POST inference/provider-connections (create)", () => {
215
215
  expect(result.auth).toEqual({ type: "platform" });
216
216
  });
217
217
 
218
+ test("rejects platform auth on a real provider", async () => {
219
+ const err = await call(
220
+ findHandler("inference_provider_connections_create"),
221
+ {
222
+ body: {
223
+ name: "managed-openai",
224
+ provider: "openai",
225
+ auth: { type: "platform" },
226
+ },
227
+ },
228
+ ).catch((e: unknown) => e);
229
+
230
+ expect(err).toBeInstanceOf(BadRequestError);
231
+ expect((err as BadRequestError).message).toContain("platform");
232
+ expect((err as BadRequestError).message).toContain("vellum");
233
+ });
234
+
235
+ test("rejects key auth on the vellum provider", async () => {
236
+ const err = await call(
237
+ findHandler("inference_provider_connections_create"),
238
+ {
239
+ body: {
240
+ name: "keyed-vellum",
241
+ provider: "vellum",
242
+ auth: { type: "api_key", credential: "vault/vellum/key" },
243
+ },
244
+ },
245
+ ).catch((e: unknown) => e);
246
+
247
+ expect(err).toBeInstanceOf(BadRequestError);
248
+ expect((err as BadRequestError).message).toContain("vellum");
249
+ });
250
+
218
251
  test("creates connection with none auth (e.g. ollama)", async () => {
219
252
  const result = (await call(
220
253
  findHandler("inference_provider_connections_create"),
@@ -504,7 +537,7 @@ describe("POST inference/provider-connections (create)", () => {
504
537
  body: {
505
538
  name: "dup-name",
506
539
  provider: "openai",
507
- auth: { type: "platform" },
540
+ auth: { type: "api_key", credential: "vault/openai/key" },
508
541
  },
509
542
  }),
510
543
  ).rejects.toBeInstanceOf(ConflictError);
@@ -585,6 +618,85 @@ describe("PATCH inference/provider-connections/:name (update)", () => {
585
618
  ).rejects.toBeInstanceOf(NotFoundError);
586
619
  });
587
620
 
621
+ test("rejects switching a real provider's connection to platform auth", async () => {
622
+ seedConnection({
623
+ name: "byok-openai",
624
+ provider: "openai",
625
+ auth: { type: "api_key", credential: "vault/openai/key" },
626
+ });
627
+
628
+ const err = await call(
629
+ findHandler("inference_provider_connections_update"),
630
+ {
631
+ pathParams: { name: "byok-openai" },
632
+ body: { auth: { type: "platform" } },
633
+ },
634
+ ).catch((e: unknown) => e);
635
+
636
+ expect(err).toBeInstanceOf(BadRequestError);
637
+ expect((err as BadRequestError).message).toContain("platform");
638
+ });
639
+
640
+ // A persisted row whose provider and auth disagree stays editable: both the
641
+ // web editor and the CLI resend the stored auth on every edit, so the guard
642
+ // keys on an actual auth change rather than on the field being present.
643
+ test("allows editing a legacy mismatched row when the client resends the stored auth", async () => {
644
+ seedConnection({
645
+ name: "legacy-managed-openai",
646
+ provider: "openai",
647
+ auth: { type: "platform" },
648
+ });
649
+
650
+ const result = (await call(
651
+ findHandler("inference_provider_connections_update"),
652
+ {
653
+ pathParams: { name: "legacy-managed-openai" },
654
+ body: { auth: { type: "platform" }, label: "Legacy" },
655
+ },
656
+ )) as { label: string | null; auth: object };
657
+ expect(result.label).toBe("Legacy");
658
+ expect(result.auth).toEqual({ type: "platform" });
659
+ });
660
+
661
+ test("allows editing a legacy mismatched row when auth is omitted", async () => {
662
+ seedConnection({
663
+ name: "legacy-managed-gemini",
664
+ provider: "gemini",
665
+ auth: { type: "platform" },
666
+ });
667
+
668
+ const result = (await call(
669
+ findHandler("inference_provider_connections_update"),
670
+ {
671
+ pathParams: { name: "legacy-managed-gemini" },
672
+ body: { label: "Legacy" },
673
+ },
674
+ )) as { label: string | null };
675
+ expect(result.label).toBe("Legacy");
676
+ });
677
+
678
+ // The guard must not trap a legacy row in its mismatched state: an auth
679
+ // change that repairs the pairing is exactly what should be allowed.
680
+ test("allows repairing a legacy mismatched row with a valid auth change", async () => {
681
+ seedConnection({
682
+ name: "legacy-managed-anthropic",
683
+ provider: "anthropic",
684
+ auth: { type: "platform" },
685
+ });
686
+
687
+ const result = (await call(
688
+ findHandler("inference_provider_connections_update"),
689
+ {
690
+ pathParams: { name: "legacy-managed-anthropic" },
691
+ body: { auth: { type: "api_key", credential: "vault/anthropic/key" } },
692
+ },
693
+ )) as { auth: object };
694
+ expect(result.auth).toEqual({
695
+ type: "api_key",
696
+ credential: "vault/anthropic/key",
697
+ });
698
+ });
699
+
588
700
  test("throws 400 when auth schema is invalid", async () => {
589
701
  seedConnection({
590
702
  name: "bad-auth",
@@ -971,7 +1083,7 @@ describe("POST with label", () => {
971
1083
  body: {
972
1084
  name: "labeled-conn",
973
1085
  provider: "anthropic",
974
- auth: { type: "platform" },
1086
+ auth: { type: "api_key", credential: "vault/anthropic/key" },
975
1087
  label: "My Anthropic",
976
1088
  },
977
1089
  },
@@ -987,7 +1099,7 @@ describe("POST with label", () => {
987
1099
  body: {
988
1100
  name: "no-label-conn",
989
1101
  provider: "openai",
990
- auth: { type: "platform" },
1102
+ auth: { type: "api_key", credential: "vault/openai/key" },
991
1103
  },
992
1104
  },
993
1105
  )) as { label: string | null };
@@ -999,7 +1111,7 @@ describe("PATCH with label", () => {
999
1111
  test("updates label to a string", async () => {
1000
1112
  seedConnection({
1001
1113
  name: "set-label",
1002
- provider: "openai",
1114
+ provider: "vellum",
1003
1115
  auth: { type: "platform" },
1004
1116
  });
1005
1117
 
@@ -1016,7 +1128,7 @@ describe("PATCH with label", () => {
1016
1128
  test("clears label by setting it to null", async () => {
1017
1129
  seedConnection({
1018
1130
  name: "clear-label",
1019
- provider: "gemini",
1131
+ provider: "vellum",
1020
1132
  auth: { type: "platform" },
1021
1133
  });
1022
1134
  // First set a label.
@@ -1038,7 +1150,7 @@ describe("PATCH with label", () => {
1038
1150
  test("rejects label: empty string with 400", async () => {
1039
1151
  seedConnection({
1040
1152
  name: "reject-empty",
1041
- provider: "anthropic",
1153
+ provider: "vellum",
1042
1154
  auth: { type: "platform" },
1043
1155
  });
1044
1156
 
@@ -1168,7 +1280,7 @@ describe("Managed connection write protection", () => {
1168
1280
  test("allows PATCH with auth still set to platform (no-op auth change)", async () => {
1169
1281
  seedConnection({
1170
1282
  name: "vellum",
1171
- provider: "anthropic",
1283
+ provider: "vellum",
1172
1284
  auth: { type: "platform" },
1173
1285
  });
1174
1286
 
@@ -1190,7 +1302,7 @@ describe("Managed connection write protection", () => {
1190
1302
  test("allows relabeling a managed connection", async () => {
1191
1303
  seedConnection({
1192
1304
  name: "vellum",
1193
- provider: "openai",
1305
+ provider: "vellum",
1194
1306
  auth: { type: "platform" },
1195
1307
  });
1196
1308
 
@@ -0,0 +1,392 @@
1
+ /**
2
+ * Unit tests for the platform_invoices_list and platform_invoices_by_id_get route
3
+ * handlers: cursor forwarding, page walking, the page cap, and error paths.
4
+ */
5
+
6
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
7
+
8
+ import {
9
+ BadRequestError,
10
+ InternalError,
11
+ NotFoundError,
12
+ UnprocessableEntityError,
13
+ } from "../errors.js";
14
+
15
+ let platformBaseUrl = "https://platform.test";
16
+ let authHeader: string | null = "Api-Key test";
17
+
18
+ // Spread the real module: replacing it wholesale breaks unrelated importers
19
+ // pulled in by the module under test.
20
+ const actualRegistration =
21
+ await import("../../../inbound/platform-callback-registration.js");
22
+ mock.module("../../../inbound/platform-callback-registration.js", () => ({
23
+ ...actualRegistration,
24
+ resolvePlatformCallbackRegistrationContext: async () => ({
25
+ isPlatform: false,
26
+ platformBaseUrl,
27
+ assistantId: "assistant-123",
28
+ hasAssistantApiKey: !!authHeader,
29
+ authHeader,
30
+ enabled: !!(platformBaseUrl && authHeader),
31
+ }),
32
+ }));
33
+
34
+ const { INVOICE_WALK_DEADLINE_MS, MAX_INVOICE_PAGES, ROUTES } =
35
+ await import("../platform-routes.js");
36
+
37
+ const listHandler = ROUTES.find(
38
+ (r) => r.operationId === "platform_invoices_list",
39
+ )!.handler;
40
+ const getHandler = ROUTES.find(
41
+ (r) => r.operationId === "platform_invoices_by_id_get",
42
+ )!.handler;
43
+
44
+ const INVOICES_URL = "https://platform.test/v1/organizations/billing/invoices/";
45
+
46
+ function invoice(id: string) {
47
+ return {
48
+ id,
49
+ number: `INV-${id}`,
50
+ status: "paid",
51
+ currency: "usd",
52
+ amount_due: 1234,
53
+ amount_paid: 1234,
54
+ amount_remaining: 0,
55
+ created: 1750000000,
56
+ hosted_invoice_url: `https://stripe.test/${id}`,
57
+ invoice_pdf: null,
58
+ };
59
+ }
60
+
61
+ interface FetchCall {
62
+ url: string;
63
+ init?: RequestInit;
64
+ }
65
+
66
+ const realFetch = globalThis.fetch;
67
+ let fetchCalls: FetchCall[] = [];
68
+
69
+ /** Stub globalThis.fetch, recording calls and answering via `respond`. */
70
+ function stubFetch(
71
+ respond: (url: string, callIndex: number) => Response | Promise<Response>,
72
+ ): void {
73
+ globalThis.fetch = (async (
74
+ input: string | URL | Request,
75
+ init?: RequestInit,
76
+ ) => {
77
+ const url =
78
+ typeof input === "string"
79
+ ? input
80
+ : input instanceof URL
81
+ ? input.toString()
82
+ : input.url;
83
+ fetchCalls.push({ url, init });
84
+ return respond(url, fetchCalls.length - 1);
85
+ }) as typeof fetch;
86
+ }
87
+
88
+ /** Await a handler call that must reject, returning the thrown error. */
89
+ async function expectRejection(run: () => unknown): Promise<Error> {
90
+ try {
91
+ await run();
92
+ } catch (err) {
93
+ return err as Error;
94
+ }
95
+ throw new Error("expected rejection");
96
+ }
97
+
98
+ function jsonResponse(body: unknown, status = 200): Response {
99
+ return new Response(JSON.stringify(body), {
100
+ status,
101
+ headers: { "Content-Type": "application/json" },
102
+ });
103
+ }
104
+
105
+ beforeEach(() => {
106
+ platformBaseUrl = "https://platform.test";
107
+ authHeader = "Api-Key test";
108
+ fetchCalls = [];
109
+ });
110
+
111
+ afterEach(() => {
112
+ globalThis.fetch = realFetch;
113
+ });
114
+
115
+ describe("platform_invoices_list", () => {
116
+ test("returns the platform page and calls the bare invoices URL", async () => {
117
+ const page = { invoices: [invoice("in_1")], has_more: false };
118
+ stubFetch(() => jsonResponse(page));
119
+
120
+ expect(await listHandler({})).toEqual(page);
121
+ expect(fetchCalls).toHaveLength(1);
122
+ expect(fetchCalls[0]!.url).toBe(INVOICES_URL);
123
+ expect(
124
+ (fetchCalls[0]!.init?.headers as Record<string, string>).Authorization,
125
+ ).toBe("Api-Key test");
126
+ });
127
+
128
+ test("forwards starting_after as a cursor and passes has_more through", async () => {
129
+ const page = { invoices: [invoice("in_2")], has_more: true };
130
+ stubFetch(() => jsonResponse(page));
131
+
132
+ const result = await listHandler({
133
+ queryParams: { starting_after: "in_a" },
134
+ });
135
+
136
+ expect(fetchCalls[0]!.url).toBe(`${INVOICES_URL}?starting_after=in_a`);
137
+ expect((result as { has_more: boolean }).has_more).toBe(true);
138
+ });
139
+
140
+ test("rejects with UnprocessableEntityError when credentials are missing", async () => {
141
+ platformBaseUrl = "";
142
+ stubFetch(() => jsonResponse({ invoices: [], has_more: false }));
143
+
144
+ await expect(listHandler({})).rejects.toBeInstanceOf(
145
+ UnprocessableEntityError,
146
+ );
147
+ expect(fetchCalls).toHaveLength(0);
148
+ });
149
+
150
+ test("resolves an upstream 404 to an empty page", async () => {
151
+ stubFetch(() => new Response("Not found", { status: 404 }));
152
+
153
+ // The platform 404s the invoice list for an organization without invoice
154
+ // history; that is an empty page, not a server error.
155
+ expect(await listHandler({})).toEqual({ invoices: [], has_more: false });
156
+ expect(fetchCalls).toHaveLength(1);
157
+ });
158
+
159
+ test("rejects with InternalError naming the status on a non-OK response", async () => {
160
+ stubFetch(() => jsonResponse({ detail: "boom" }, 500));
161
+
162
+ const err = await expectRejection(() => listHandler({}));
163
+ expect(err).toBeInstanceOf(InternalError);
164
+ expect(err.message).toMatch(/HTTP 500/);
165
+ });
166
+
167
+ test("rejects with BadRequestError carrying the detail on an upstream 400", async () => {
168
+ stubFetch(
169
+ () => new Response("Invalid starting_after cursor", { status: 400 }),
170
+ );
171
+
172
+ const err = await expectRejection(() =>
173
+ listHandler({ queryParams: { starting_after: "in_bogus" } }),
174
+ );
175
+ expect(err).toBeInstanceOf(BadRequestError);
176
+ expect(err.message).toMatch(/Invalid starting_after cursor/);
177
+ expect(err.message).toMatch(
178
+ /use the id of the last invoice from the previous page/,
179
+ );
180
+ });
181
+
182
+ test("rejects with InternalError, not BadRequestError, on a 400 without a cursor", async () => {
183
+ stubFetch(() => new Response("malformed request", { status: 400 }));
184
+
185
+ const err = await expectRejection(() => listHandler({}));
186
+ // Without a caller-supplied starting_after there is nothing
187
+ // caller-correctable in a 400, so it stays an internal failure.
188
+ expect(err).not.toBeInstanceOf(BadRequestError);
189
+ expect(err).toBeInstanceOf(InternalError);
190
+ expect(err.message).toMatch(/HTTP 400/);
191
+ });
192
+ });
193
+
194
+ describe("platform_invoices_by_id_get", () => {
195
+ test("resolves an invoice found on the first page with one fetch", async () => {
196
+ const target = invoice("in_target");
197
+ stubFetch(() =>
198
+ jsonResponse({ invoices: [invoice("in_1"), target], has_more: false }),
199
+ );
200
+
201
+ expect(await getHandler({ pathParams: { id: "in_target" } })).toEqual(
202
+ target,
203
+ );
204
+ expect(fetchCalls).toHaveLength(1);
205
+ });
206
+
207
+ test("follows the cursor to a later page using the last invoice's id", async () => {
208
+ const target = invoice("in_target");
209
+ stubFetch((_url, callIndex) =>
210
+ callIndex === 0
211
+ ? jsonResponse({
212
+ invoices: [invoice("in_a"), invoice("in_b")],
213
+ has_more: true,
214
+ })
215
+ : jsonResponse({ invoices: [target], has_more: false }),
216
+ );
217
+
218
+ expect(await getHandler({ pathParams: { id: "in_target" } })).toEqual(
219
+ target,
220
+ );
221
+ expect(fetchCalls).toHaveLength(2);
222
+ expect(fetchCalls[1]!.url).toBe(`${INVOICES_URL}?starting_after=in_b`);
223
+ });
224
+
225
+ test("rejects with an actionable NotFoundError when the id is absent", async () => {
226
+ stubFetch(() =>
227
+ jsonResponse({ invoices: [invoice("in_other")], has_more: false }),
228
+ );
229
+
230
+ const err = await expectRejection(() =>
231
+ getHandler({ pathParams: { id: "in_missing" } }),
232
+ );
233
+ expect(err).toBeInstanceOf(NotFoundError);
234
+ expect(err.message).toMatch(
235
+ /Invoice "in_missing" not found.*assistant platform invoices list/,
236
+ );
237
+ // No paging beyond the final (has_more: false) page.
238
+ expect(fetchCalls).toHaveLength(1);
239
+ });
240
+
241
+ test("rejects with NotFoundError, not InternalError, on an upstream 404", async () => {
242
+ stubFetch(() => new Response("Not found", { status: 404 }));
243
+
244
+ // A 404 from the platform means no invoice history: the walk sees an
245
+ // empty first page and reports the invoice as not found.
246
+ const err = await expectRejection(() =>
247
+ getHandler({ pathParams: { id: "in_missing" } }),
248
+ );
249
+ expect(err).not.toBeInstanceOf(InternalError);
250
+ expect(err).toBeInstanceOf(NotFoundError);
251
+ expect(fetchCalls).toHaveLength(1);
252
+ });
253
+
254
+ test("stops after MAX_INVOICE_PAGES pages when every page has more", async () => {
255
+ stubFetch((_url, callIndex) =>
256
+ jsonResponse({
257
+ invoices: [invoice(`in_${callIndex}`)],
258
+ has_more: true,
259
+ }),
260
+ );
261
+
262
+ await expect(
263
+ getHandler({ pathParams: { id: "in_never" } }),
264
+ ).rejects.toBeInstanceOf(InternalError);
265
+ expect(fetchCalls).toHaveLength(MAX_INVOICE_PAGES);
266
+ });
267
+
268
+ test("stops the cursor walk when the request abort signal fires", async () => {
269
+ const controller = new AbortController();
270
+ stubFetch(() => {
271
+ // Abort after the first page has been served; the handler must not
272
+ // fetch a second page.
273
+ controller.abort();
274
+ return jsonResponse({ invoices: [invoice("in_a")], has_more: true });
275
+ });
276
+
277
+ const err = await expectRejection(() =>
278
+ getHandler({
279
+ pathParams: { id: "in_never" },
280
+ abortSignal: controller.signal,
281
+ }),
282
+ );
283
+ expect(err).toBeInstanceOf(InternalError);
284
+ expect(err.message).toMatch(/aborted/);
285
+ expect(fetchCalls).toHaveLength(1);
286
+ });
287
+
288
+ test("rejects with InternalError when the walk exceeds the aggregate deadline", async () => {
289
+ const realDateNow = Date.now;
290
+ let now = 0;
291
+ Date.now = () => now;
292
+ stubFetch(() => {
293
+ // Push the clock past the deadline once the first page has been
294
+ // served; the handler must not fetch a second page.
295
+ now = INVOICE_WALK_DEADLINE_MS + 1;
296
+ return jsonResponse({ invoices: [invoice("in_a")], has_more: true });
297
+ });
298
+
299
+ try {
300
+ const err = await expectRejection(() =>
301
+ getHandler({ pathParams: { id: "in_never" } }),
302
+ );
303
+ expect(err).toBeInstanceOf(InternalError);
304
+ expect(err.message).toMatch(/timed out after 30 seconds/);
305
+ } finally {
306
+ Date.now = realDateNow;
307
+ }
308
+ expect(fetchCalls).toHaveLength(1);
309
+ });
310
+
311
+ test("caps the per-page fetch timeout to the remaining walk deadline", async () => {
312
+ const realDateNow = Date.now;
313
+ let now = 0;
314
+ Date.now = () => now;
315
+ stubFetch((_url, callIndex) => {
316
+ if (callIndex === 0) {
317
+ // Leave only 50ms of aggregate budget for the next page.
318
+ now = INVOICE_WALK_DEADLINE_MS - 50;
319
+ return jsonResponse({ invoices: [invoice("in_a")], has_more: true });
320
+ }
321
+ // Hang until the fetch's combined signal aborts. With the timeout
322
+ // capped to the remaining 50ms this resolves almost immediately; an
323
+ // uncapped 10s timeout would blow the assertion on real elapsed time.
324
+ const signal = fetchCalls[callIndex]!.init?.signal as AbortSignal;
325
+ return new Promise<Response>((_resolve, reject) => {
326
+ signal.addEventListener("abort", () => {
327
+ now = INVOICE_WALK_DEADLINE_MS + 1;
328
+ reject(signal.reason as Error);
329
+ });
330
+ });
331
+ });
332
+
333
+ const startedRealMs = realDateNow();
334
+ try {
335
+ const err = await expectRejection(() =>
336
+ getHandler({ pathParams: { id: "in_never" } }),
337
+ );
338
+ expect(err).toBeInstanceOf(InternalError);
339
+ expect(err.message).toMatch(/timed out after 30 seconds/);
340
+ } finally {
341
+ Date.now = realDateNow;
342
+ }
343
+ expect(fetchCalls).toHaveLength(2);
344
+ // The second fetch was cancelled by the capped ~50ms timeout, not the
345
+ // flat 10s per-request timeout.
346
+ expect(realDateNow() - startedRealMs).toBeLessThan(5_000);
347
+ });
348
+
349
+ test("threads the request abort signal into the page fetch", async () => {
350
+ const controller = new AbortController();
351
+ stubFetch(() =>
352
+ jsonResponse({ invoices: [invoice("in_target")], has_more: false }),
353
+ );
354
+
355
+ await getHandler({
356
+ pathParams: { id: "in_target" },
357
+ abortSignal: controller.signal,
358
+ });
359
+
360
+ // The fetch signal must be derived from the caller's signal: aborting
361
+ // the request aborts the in-flight platform fetch.
362
+ const fetchSignal = fetchCalls[0]!.init?.signal as AbortSignal;
363
+ expect(fetchSignal.aborted).toBe(false);
364
+ controller.abort();
365
+ expect(fetchSignal.aborted).toBe(true);
366
+ });
367
+
368
+ test("rejects with InternalError, not BadRequestError, on a 400 mid-walk", async () => {
369
+ stubFetch((_url, callIndex) =>
370
+ callIndex === 0
371
+ ? jsonResponse({ invoices: [invoice("in_a")], has_more: true })
372
+ : new Response("Invalid starting_after cursor", { status: 400 }),
373
+ );
374
+
375
+ const err = await expectRejection(() =>
376
+ getHandler({ pathParams: { id: "in_never" } }),
377
+ );
378
+ // The walk's cursor is internally generated, so an upstream 400 is an
379
+ // internal failure rather than a caller input error.
380
+ expect(err).not.toBeInstanceOf(BadRequestError);
381
+ expect(err).toBeInstanceOf(InternalError);
382
+ expect(err.message).toMatch(/HTTP 400/);
383
+ expect(fetchCalls).toHaveLength(2);
384
+ });
385
+
386
+ test("rejects with BadRequestError when the id path param is missing", async () => {
387
+ stubFetch(() => jsonResponse({ invoices: [], has_more: false }));
388
+
389
+ await expect(getHandler({})).rejects.toBeInstanceOf(BadRequestError);
390
+ expect(fetchCalls).toHaveLength(0);
391
+ });
392
+ });
@@ -16,6 +16,13 @@ const originalFetch = globalThis.fetch;
16
16
 
17
17
  mock.module("../../../security/secure-keys.js", () => ({
18
18
  getSecureKeyAsync: async () => "xoxb-test",
19
+ // The route's import graph (api.ts -> auth.ts -> oauth/*) also reads the
20
+ // result-shaped accessor; provide it so the mock stays import-complete
21
+ // even if these static imports ever become dynamic.
22
+ getSecureKeyResultAsync: async () => ({
23
+ value: "xoxb-test",
24
+ unreachable: false,
25
+ }),
19
26
  }));
20
27
 
21
28
  mock.module("../../sync/sync-publisher.js", () => ({
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Request-body validation for the workspace git admin routes.
3
+ *
4
+ * Each handler validates its body before touching the workspace git
5
+ * service, so a malformed body is rejected with a `BadRequestError`
6
+ * (→ 400) without running any git operation. The handlers are async,
7
+ * hence the `.rejects` form.
8
+ */
9
+
10
+ import { describe, expect, test } from "bun:test";
11
+
12
+ import { BadRequestError } from "../errors.js";
13
+ import { ROUTES } from "../workspace-commit-routes.js";
14
+
15
+ const routeFor = (operationId: string) => {
16
+ const route = ROUTES.find((r) => r.operationId === operationId);
17
+ if (!route) {
18
+ throw new Error(`no route: ${operationId}`);
19
+ }
20
+ return route;
21
+ };
22
+
23
+ describe("workspace git route body validation", () => {
24
+ test("workspace_commit rejects a missing message", async () => {
25
+ await expect(
26
+ routeFor("workspace_commit").handler({
27
+ body: {} as Record<string, unknown>,
28
+ }),
29
+ ).rejects.toThrow(BadRequestError);
30
+ });
31
+
32
+ test("workspace_commit rejects an empty message", async () => {
33
+ await expect(
34
+ routeFor("workspace_commit").handler({
35
+ body: { message: "" } as Record<string, unknown>,
36
+ }),
37
+ ).rejects.toThrow(BadRequestError);
38
+ });
39
+
40
+ test("workspace_compact_history rejects a non-boolean force", async () => {
41
+ await expect(
42
+ routeFor("workspace_compact_history").handler({
43
+ body: { force: "yes" } as Record<string, unknown>,
44
+ }),
45
+ ).rejects.toThrow(BadRequestError);
46
+ });
47
+
48
+ test("workspace_compact_history rejects a non-integer retentionDays", async () => {
49
+ await expect(
50
+ routeFor("workspace_compact_history").handler({
51
+ body: { retentionDays: 1.5 } as Record<string, unknown>,
52
+ }),
53
+ ).rejects.toThrow(BadRequestError);
54
+ });
55
+
56
+ test("workspace_compact_history rejects a non-positive retentionDays", async () => {
57
+ await expect(
58
+ routeFor("workspace_compact_history").handler({
59
+ body: { retentionDays: 0 } as Record<string, unknown>,
60
+ }),
61
+ ).rejects.toThrow(BadRequestError);
62
+ });
63
+ });
@@ -22,6 +22,7 @@ import {
22
22
  getMessageRoleStatsByConversation,
23
23
  listConversationsBySource,
24
24
  } from "../../persistence/conversation-queries.js";
25
+ import { MEMORY_V2_CONSOLIDATION_SOURCE } from "../../persistence/conversation-types.js";
25
26
  import {
26
27
  enqueueMemoryJob,
27
28
  hasActiveJobOfType,
@@ -29,7 +30,6 @@ import {
29
30
  } from "../../persistence/jobs-store.js";
30
31
  import { getUsageCostForConversationWindow } from "../../persistence/llm-usage-store.js";
31
32
  import { GRAPH_MAINTENANCE_CHECKPOINTS } from "../../plugins/defaults/memory/jobs-worker.js";
32
- import { MEMORY_V2_CONSOLIDATION_SOURCE } from "../../plugins/defaults/memory/substrate/constants.js";
33
33
  import { resolveSubstrateTuning } from "../../plugins/defaults/memory/substrate/tuning.js";
34
34
  import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
35
35
  import { BadRequestError } from "./errors.js";