@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
@@ -1,73 +1,25 @@
1
- import { beforeEach, describe, expect, mock, test } from "bun:test";
1
+ import { beforeEach, describe, expect, test } from "bun:test";
2
2
 
3
- import { Command } from "commander";
3
+ import {
4
+ runPlatform,
5
+ runPlatformCaught,
6
+ setupPlatformIpcMock,
7
+ } from "./helpers.js";
4
8
 
5
- // ---------------------------------------------------------------------------
6
- // Mock state
7
- // ---------------------------------------------------------------------------
8
-
9
- let mockCalls: Array<[string, Record<string, unknown>]> = [];
10
- let mockResponse: unknown = { ok: true, result: { routes: [] } };
11
-
12
- // ---------------------------------------------------------------------------
13
- // Mocks
14
- // ---------------------------------------------------------------------------
15
-
16
- mock.module("../../../../ipc/cli-client.js", () => ({
17
- cliIpcCall: async (method: string, params: Record<string, unknown>) => {
18
- mockCalls.push([method, params]);
19
- return mockResponse;
20
- },
21
- exitFromIpcResult: (_r: unknown, _cmd: unknown) => {
22
- throw new Error("exitFromIpcResult called");
23
- },
24
- }));
25
-
26
- const { registerPlatformCommand } = await import("../index.js");
27
-
28
- function buildProgram(): Command {
29
- const program = new Command();
30
- program.exitOverride();
31
- registerPlatformCommand(program);
32
- return program;
33
- }
34
-
35
- // ---------------------------------------------------------------------------
36
- // Tests
37
- // ---------------------------------------------------------------------------
9
+ const ipc = setupPlatformIpcMock();
38
10
 
39
11
  describe("assistant platform callback-routes list", () => {
40
12
  beforeEach(() => {
41
- mockCalls = [];
42
- mockResponse = { ok: true, result: { routes: [] } };
43
- process.exitCode = 0;
13
+ ipc.calls = [];
14
+ ipc.response = { ok: true, result: { routes: [] } };
44
15
  });
45
16
 
46
17
  test("returns empty list when no routes registered", async () => {
47
- const program = buildProgram();
48
- const stdoutChunks: string[] = [];
49
- const origWrite = process.stdout.write.bind(process.stdout);
50
- process.stdout.write = ((chunk: unknown) => {
51
- stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk));
52
- return true;
53
- }) as typeof process.stdout.write;
54
-
55
- try {
56
- await program.parseAsync([
57
- "node",
58
- "assistant",
59
- "platform",
60
- "callback-routes",
61
- "list",
62
- "--json",
63
- ]);
64
- } finally {
65
- process.stdout.write = origWrite;
66
- }
67
-
68
- expect(mockCalls[0][0]).toBe("platform_callback_routes_list");
69
-
70
- const parsed = JSON.parse(stdoutChunks.join(""));
18
+ const out = await runPlatform(["callback-routes", "list", "--json"]);
19
+
20
+ expect(ipc.calls[0][0]).toBe("platform_callback_routes_list");
21
+
22
+ const parsed = JSON.parse(out.join(""));
71
23
  expect(parsed.ok).toBe(true);
72
24
  expect(parsed.routes).toEqual([]);
73
25
  });
@@ -91,30 +43,11 @@ describe("assistant platform callback-routes list", () => {
91
43
  "https://dev-platform.vellum.ai/v1/gateway/callbacks/019d6d4f-6dbd-779f-91d3-cb273b9429a5/webhooks/telegram/",
92
44
  },
93
45
  ];
94
- mockResponse = { ok: true, result: { routes } };
95
-
96
- const program = buildProgram();
97
- const stdoutChunks: string[] = [];
98
- const origWrite = process.stdout.write.bind(process.stdout);
99
- process.stdout.write = ((chunk: unknown) => {
100
- stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk));
101
- return true;
102
- }) as typeof process.stdout.write;
103
-
104
- try {
105
- await program.parseAsync([
106
- "node",
107
- "assistant",
108
- "platform",
109
- "callback-routes",
110
- "list",
111
- "--json",
112
- ]);
113
- } finally {
114
- process.stdout.write = origWrite;
115
- }
116
-
117
- const parsed = JSON.parse(stdoutChunks.join(""));
46
+ ipc.response = { ok: true, result: { routes } };
47
+
48
+ const out = await runPlatform(["callback-routes", "list", "--json"]);
49
+
50
+ const parsed = JSON.parse(out.join(""));
118
51
  expect(parsed.ok).toBe(true);
119
52
  expect(parsed.routes).toHaveLength(2);
120
53
  expect(parsed.routes[0].type).toBe("email");
@@ -122,27 +55,23 @@ describe("assistant platform callback-routes list", () => {
122
55
  });
123
56
 
124
57
  test("fails when platform credentials are missing", async () => {
125
- mockResponse = {
58
+ ipc.response = {
126
59
  ok: false,
127
60
  error: "Platform credentials not available",
128
61
  statusCode: 422,
129
62
  };
130
63
 
131
- const program = buildProgram();
132
- await expect(
133
- program.parseAsync([
134
- "node",
135
- "assistant",
136
- "platform",
137
- "callback-routes",
138
- "list",
139
- "--json",
140
- ]),
141
- ).rejects.toThrow("exitFromIpcResult called");
64
+ const { thrown } = await runPlatformCaught([
65
+ "callback-routes",
66
+ "list",
67
+ "--json",
68
+ ]);
69
+
70
+ expect((thrown as Error).message).toBe("exitFromIpcResult called");
142
71
  });
143
72
 
144
73
  test("callback-routes register calls platform_callback_routes_register", async () => {
145
- mockResponse = {
74
+ ipc.response = {
146
75
  ok: true,
147
76
  result: {
148
77
  callbackUrl:
@@ -152,40 +81,25 @@ describe("assistant platform callback-routes list", () => {
152
81
  },
153
82
  };
154
83
 
155
- const program = buildProgram();
156
- const stdoutChunks: string[] = [];
157
- const origWrite = process.stdout.write.bind(process.stdout);
158
- process.stdout.write = ((chunk: unknown) => {
159
- stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk));
160
- return true;
161
- }) as typeof process.stdout.write;
162
-
163
- try {
164
- await program.parseAsync([
165
- "node",
166
- "assistant",
167
- "platform",
168
- "callback-routes",
169
- "register",
170
- "--path",
171
- "webhooks/telegram",
172
- "--type",
173
- "telegram",
174
- "--json",
175
- ]);
176
- } finally {
177
- process.stdout.write = origWrite;
178
- }
179
-
180
- expect(mockCalls[0][0]).toBe("platform_callback_routes_register");
181
- expect((mockCalls[0][1].body as Record<string, unknown>).path).toBe(
84
+ const out = await runPlatform([
85
+ "callback-routes",
86
+ "register",
87
+ "--path",
88
+ "webhooks/telegram",
89
+ "--type",
90
+ "telegram",
91
+ "--json",
92
+ ]);
93
+
94
+ expect(ipc.calls[0][0]).toBe("platform_callback_routes_register");
95
+ expect((ipc.calls[0][1].body as Record<string, unknown>).path).toBe(
182
96
  "webhooks/telegram",
183
97
  );
184
- expect((mockCalls[0][1].body as Record<string, unknown>).type).toBe(
98
+ expect((ipc.calls[0][1].body as Record<string, unknown>).type).toBe(
185
99
  "telegram",
186
100
  );
187
101
 
188
- const parsed = JSON.parse(stdoutChunks.join(""));
102
+ const parsed = JSON.parse(out.join(""));
189
103
  expect(parsed.ok).toBe(true);
190
104
  expect(parsed.callbackPath).toBe("webhooks/telegram");
191
105
  });
@@ -1,69 +1,13 @@
1
- import { beforeEach, describe, expect, mock, test } from "bun:test";
1
+ import { beforeEach, describe, expect, test } from "bun:test";
2
2
 
3
- import { Command } from "commander";
3
+ import { runPlatform, setupPlatformIpcMock } from "./helpers.js";
4
4
 
5
- // ---------------------------------------------------------------------------
6
- // Mock state
7
- // ---------------------------------------------------------------------------
8
-
9
- let mockCalls: Array<[string, Record<string, unknown>]> = [];
10
- let mockResponse: unknown = {
11
- ok: true,
12
- result: {
13
- remaining: 42.17,
14
- settled: 50,
15
- pending: 7.83,
16
- unit: "USD",
17
- stale: false,
18
- as_of: "2026-07-06T00:00:00.000Z",
19
- },
20
- };
21
-
22
- // ---------------------------------------------------------------------------
23
- // Mocks
24
- // ---------------------------------------------------------------------------
25
-
26
- mock.module("../../../../ipc/cli-client.js", () => ({
27
- cliIpcCall: async (method: string, params: Record<string, unknown>) => {
28
- mockCalls.push([method, params]);
29
- return mockResponse;
30
- },
31
- exitFromIpcResult: (_r: unknown, _cmd: unknown) => {
32
- throw new Error("exitFromIpcResult called");
33
- },
34
- }));
35
-
36
- const { registerPlatformCommand } = await import("../index.js");
37
-
38
- function buildProgram(): Command {
39
- const program = new Command();
40
- program.exitOverride();
41
- registerPlatformCommand(program);
42
- return program;
43
- }
44
-
45
- function captureStdout(fn: () => Promise<void>): Promise<string[]> {
46
- const chunks: string[] = [];
47
- const origWrite = process.stdout.write.bind(process.stdout);
48
- process.stdout.write = ((chunk: unknown) => {
49
- chunks.push(typeof chunk === "string" ? chunk : String(chunk));
50
- return true;
51
- }) as typeof process.stdout.write;
52
- return fn()
53
- .then(() => chunks)
54
- .finally(() => {
55
- process.stdout.write = origWrite;
56
- });
57
- }
58
-
59
- // ---------------------------------------------------------------------------
60
- // Tests
61
- // ---------------------------------------------------------------------------
5
+ const ipc = setupPlatformIpcMock();
62
6
 
63
7
  describe("assistant platform credits", () => {
64
8
  beforeEach(() => {
65
- mockCalls = [];
66
- mockResponse = {
9
+ ipc.calls = [];
10
+ ipc.response = {
67
11
  ok: true,
68
12
  result: {
69
13
  remaining: 42.17,
@@ -74,22 +18,12 @@ describe("assistant platform credits", () => {
74
18
  as_of: "2026-07-06T00:00:00.000Z",
75
19
  },
76
20
  };
77
- process.exitCode = 0;
78
21
  });
79
22
 
80
23
  test("calls platform_credits and emits balance JSON with --json", async () => {
81
- const out = await captureStdout(async () => {
82
- const program = buildProgram();
83
- await program.parseAsync([
84
- "node",
85
- "assistant",
86
- "platform",
87
- "credits",
88
- "--json",
89
- ]);
90
- });
24
+ const out = await runPlatform(["credits", "--json"]);
91
25
 
92
- expect(mockCalls[0][0]).toBe("platform_credits");
26
+ expect(ipc.calls[0][0]).toBe("platform_credits");
93
27
 
94
28
  const parsed = JSON.parse(out.join(""));
95
29
  expect(parsed.remaining).toBe(42.17);
@@ -100,12 +34,9 @@ describe("assistant platform credits", () => {
100
34
  });
101
35
 
102
36
  test("plain text mode does not emit JSON to stdout", async () => {
103
- const out = await captureStdout(async () => {
104
- const program = buildProgram();
105
- await program.parseAsync(["node", "assistant", "platform", "credits"]);
106
- });
37
+ const out = await runPlatform(["credits"]);
107
38
 
108
- // Plain-text mode logs via log.info verify writeOutput (JSON) was NOT called
39
+ // Plain-text mode logs via log.info; verify writeOutput (JSON) was NOT called
109
40
  expect(() => JSON.parse(out.join("").trim())).toThrow();
110
41
  });
111
42
  });
@@ -0,0 +1,90 @@
1
+ import { mock } from "bun:test";
2
+
3
+ import { Command } from "commander";
4
+
5
+ /** Mutable per-file mock state returned by {@link setupPlatformIpcMock}. */
6
+ export interface PlatformIpcMockState {
7
+ /** Every (method, params) pair passed to the mocked cliIpcCall. */
8
+ calls: Array<[string, Record<string, unknown>]>;
9
+ /** The value the mocked cliIpcCall resolves with. */
10
+ response: unknown;
11
+ }
12
+
13
+ /**
14
+ * Install the shared IPC client mock for a platform command test file.
15
+ *
16
+ * bun's mock.module is global per test file, so each file must call this
17
+ * once at module top level (before importing ../index.js) and drive the
18
+ * mock through the returned state object. The mocked exitFromIpcResult
19
+ * throws so tests can assert the error path without exiting the process.
20
+ */
21
+ export function setupPlatformIpcMock(): PlatformIpcMockState {
22
+ const state: PlatformIpcMockState = { calls: [], response: undefined };
23
+ mock.module("../../../../ipc/cli-client.js", () => ({
24
+ cliIpcCall: async (method: string, params: Record<string, unknown>) => {
25
+ state.calls.push([method, params]);
26
+ return state.response;
27
+ },
28
+ exitFromIpcResult: (_r: unknown, _cmd: unknown) => {
29
+ throw new Error("exitFromIpcResult called");
30
+ },
31
+ }));
32
+ return state;
33
+ }
34
+
35
+ /**
36
+ * Build a fresh commander program with the platform command registered.
37
+ * Imported lazily so the file-level mock installed by
38
+ * {@link setupPlatformIpcMock} is in place first.
39
+ */
40
+ export async function buildPlatformProgram(): Promise<Command> {
41
+ const { registerPlatformCommand } = await import("../index.js");
42
+ const program = new Command();
43
+ program.exitOverride();
44
+ registerPlatformCommand(program);
45
+ return program;
46
+ }
47
+
48
+ /** Capture everything written to stdout while fn runs. */
49
+ export function captureStdout(fn: () => Promise<void>): Promise<string[]> {
50
+ const chunks: string[] = [];
51
+ const origWrite = process.stdout.write.bind(process.stdout);
52
+ process.stdout.write = ((chunk: unknown) => {
53
+ chunks.push(typeof chunk === "string" ? chunk : String(chunk));
54
+ return true;
55
+ }) as typeof process.stdout.write;
56
+ return fn()
57
+ .then(() => chunks)
58
+ .finally(() => {
59
+ process.stdout.write = origWrite;
60
+ });
61
+ }
62
+
63
+ /** Run 'assistant platform <args>' against a fresh program, capturing stdout. */
64
+ export async function runPlatform(args: string[]): Promise<string[]> {
65
+ const { out, thrown } = await runPlatformCaught(args);
66
+ if (thrown !== undefined) {
67
+ throw thrown;
68
+ }
69
+ return out;
70
+ }
71
+
72
+ /**
73
+ * Like {@link runPlatform}, but capture whatever the parse throws instead of
74
+ * propagating it (the mocked exitFromIpcResult error, Commander's exitOverride
75
+ * on --help), so tests can assert on both the output and the thrown value.
76
+ */
77
+ export async function runPlatformCaught(
78
+ args: string[],
79
+ ): Promise<{ out: string[]; thrown: unknown }> {
80
+ let thrown: unknown;
81
+ const out = await captureStdout(async () => {
82
+ const program = await buildPlatformProgram();
83
+ try {
84
+ await program.parseAsync(["node", "assistant", "platform", ...args]);
85
+ } catch (err) {
86
+ thrown = err;
87
+ }
88
+ });
89
+ return { out, thrown };
90
+ }
@@ -0,0 +1,238 @@
1
+ import { beforeEach, describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ runPlatform,
5
+ runPlatformCaught,
6
+ setupPlatformIpcMock,
7
+ } from "./helpers.js";
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Fixtures
11
+ // ---------------------------------------------------------------------------
12
+
13
+ const invoiceA = {
14
+ id: "in_a",
15
+ number: "INV-0001",
16
+ status: "paid",
17
+ currency: "usd",
18
+ amount_due: 1234,
19
+ amount_paid: 1234,
20
+ amount_remaining: 0,
21
+ created: 1750000000,
22
+ hosted_invoice_url: "https://pay.example.com/in_a",
23
+ invoice_pdf: "https://pay.example.com/in_a.pdf",
24
+ };
25
+
26
+ const invoiceB = {
27
+ id: "in_b",
28
+ number: null,
29
+ status: "open",
30
+ currency: "usd",
31
+ amount_due: 5600,
32
+ amount_paid: 0,
33
+ amount_remaining: 5600,
34
+ created: 1747000000,
35
+ hosted_invoice_url: null,
36
+ invoice_pdf: null,
37
+ };
38
+
39
+ const ipc = setupPlatformIpcMock();
40
+
41
+ function runInvoices(args: string[]): Promise<string[]> {
42
+ return runPlatform(["invoices", ...args]);
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Tests
47
+ // ---------------------------------------------------------------------------
48
+
49
+ describe("assistant platform invoices list", () => {
50
+ beforeEach(() => {
51
+ ipc.calls = [];
52
+ ipc.response = {
53
+ ok: true,
54
+ result: { invoices: [invoiceA, invoiceB], has_more: false },
55
+ };
56
+ });
57
+
58
+ test("calls platform_invoices_list and emits the page as JSON with --json", async () => {
59
+ const out = await runInvoices(["list", "--json"]);
60
+
61
+ expect(ipc.calls[0][0]).toBe("platform_invoices_list");
62
+ expect(ipc.calls[0][1]).toEqual({});
63
+
64
+ const parsed = JSON.parse(out.join(""));
65
+ expect(parsed.invoices).toHaveLength(2);
66
+ expect(parsed.invoices[0].id).toBe("in_a");
67
+ expect(parsed.has_more).toBe(false);
68
+ });
69
+
70
+ test("forwards --starting-after as the starting_after query param", async () => {
71
+ await runInvoices(["list", "--starting-after", "in_a", "--json"]);
72
+
73
+ expect(ipc.calls[0][0]).toBe("platform_invoices_list");
74
+ expect(ipc.calls[0][1]).toEqual({
75
+ queryParams: { starting_after: "in_a" },
76
+ });
77
+ });
78
+
79
+ test("plain text mode renders number, status, and amount lines", async () => {
80
+ const out = await runInvoices(["list"]);
81
+ const text = out.join("");
82
+
83
+ expect(text).toContain("INV-0001");
84
+ expect(text).toContain("Status: paid");
85
+ expect(text).toContain("Amount: USD 12.34");
86
+ expect(text).toContain("https://pay.example.com/in_a");
87
+ // Invoice with a null number falls back to its id.
88
+ expect(text).toContain("in_b");
89
+ expect(text).toContain("Amount: USD 56.00");
90
+ // Not JSON output.
91
+ expect(() => JSON.parse(text.trim())).toThrow();
92
+ });
93
+
94
+ test("plain text mode reports an empty page", async () => {
95
+ ipc.response = { ok: true, result: { invoices: [], has_more: false } };
96
+
97
+ const out = await runInvoices(["list"]);
98
+
99
+ expect(out.join("").trim()).toBe("No invoices found.");
100
+ });
101
+
102
+ test("plain text mode ends with a paging hint when has_more is true", async () => {
103
+ ipc.response = {
104
+ ok: true,
105
+ result: { invoices: [invoiceA, invoiceB], has_more: true },
106
+ };
107
+
108
+ const out = await runInvoices(["list"]);
109
+
110
+ expect(out.join("").trim()).toEndWith(
111
+ "More invoices available. Run 'assistant platform invoices list --starting-after in_b' for the next page.",
112
+ );
113
+ });
114
+
115
+ test("plain text mode omits the paging hint when has_more is false", async () => {
116
+ const out = await runInvoices(["list"]);
117
+
118
+ expect(out.join("")).not.toContain("More invoices available");
119
+ });
120
+ });
121
+
122
+ describe("assistant platform invoices get", () => {
123
+ beforeEach(() => {
124
+ ipc.calls = [];
125
+ ipc.response = { ok: true, result: invoiceA };
126
+ });
127
+
128
+ test("calls platform_invoices_by_id_get with the id path param and emits JSON with --json", async () => {
129
+ const out = await runInvoices(["get", "in_123", "--json"]);
130
+
131
+ expect(ipc.calls[0][0]).toBe("platform_invoices_by_id_get");
132
+ expect(ipc.calls[0][1]).toEqual({ pathParams: { id: "in_123" } });
133
+
134
+ const parsed = JSON.parse(out.join(""));
135
+ expect(parsed.id).toBe("in_a");
136
+ expect(parsed.amount_due).toBe(1234);
137
+ });
138
+
139
+ test("plain text mode renders the invoice block including the PDF link", async () => {
140
+ const out = await runInvoices(["get", "in_a"]);
141
+ const text = out.join("");
142
+
143
+ expect(text).toContain("INV-0001");
144
+ expect(text).toContain("Status: paid");
145
+ expect(text).toContain("Amount: USD 12.34");
146
+ expect(text).toContain("PDF: https://pay.example.com/in_a.pdf");
147
+ });
148
+
149
+ test("plain text mode scales zero-decimal currencies without dividing by 100", async () => {
150
+ ipc.response = {
151
+ ok: true,
152
+ result: { ...invoiceA, currency: "jpy", amount_due: 1200 },
153
+ };
154
+
155
+ const out = await runInvoices(["get", "in_a"]);
156
+
157
+ expect(out.join("")).toContain("Amount: JPY 1,200");
158
+ });
159
+
160
+ test("plain text mode uses three decimals for three-decimal currencies", async () => {
161
+ ipc.response = {
162
+ ok: true,
163
+ result: { ...invoiceA, currency: "bhd", amount_due: 12345 },
164
+ };
165
+
166
+ const out = await runInvoices(["get", "in_a"]);
167
+
168
+ expect(out.join("")).toContain("Amount: BHD 12.345");
169
+ });
170
+
171
+ test("plain text mode keeps Stripe's two-decimal scaling for ISK despite ISO zero-decimal metadata", async () => {
172
+ ipc.response = {
173
+ ok: true,
174
+ result: { ...invoiceA, currency: "isk", amount_due: 500 },
175
+ };
176
+
177
+ const out = await runInvoices(["get", "in_a"]);
178
+
179
+ expect(out.join("")).toContain("Amount: ISK 5.00");
180
+ });
181
+
182
+ test("plain text mode falls back to raw minor units for invalid currency codes", async () => {
183
+ ipc.response = {
184
+ ok: true,
185
+ result: { ...invoiceA, currency: "zz", amount_due: 1234 },
186
+ };
187
+
188
+ const out = await runInvoices(["get", "in_a"]);
189
+
190
+ expect(out.join("")).toContain("Amount: 1234 ZZ (minor units)");
191
+ });
192
+ });
193
+
194
+ describe("assistant platform invoices error handling", () => {
195
+ beforeEach(() => {
196
+ ipc.calls = [];
197
+ ipc.response = {
198
+ ok: false,
199
+ error: "Platform credentials not available",
200
+ statusCode: 422,
201
+ };
202
+ });
203
+
204
+ test("list exits via exitFromIpcResult on IPC failure without writing output", async () => {
205
+ const { out, thrown } = await runPlatformCaught(["invoices", "list"]);
206
+
207
+ expect((thrown as Error).message).toBe("exitFromIpcResult called");
208
+ expect(out.join("")).toBe("");
209
+ });
210
+
211
+ test("get exits via exitFromIpcResult on IPC failure without writing output", async () => {
212
+ const { out, thrown } = await runPlatformCaught([
213
+ "invoices",
214
+ "get",
215
+ "in_missing",
216
+ ]);
217
+
218
+ expect((thrown as Error).message).toBe("exitFromIpcResult called");
219
+ expect(out.join("")).toBe("");
220
+ });
221
+ });
222
+
223
+ describe("assistant platform invoices --help", () => {
224
+ test("group help renders both subcommands", async () => {
225
+ const { out } = await runPlatformCaught(["invoices", "--help"]);
226
+ const text = out.join("");
227
+
228
+ expect(text).toContain("list");
229
+ expect(text).toContain("get");
230
+ expect(text).toContain("Stripe invoice history");
231
+ });
232
+
233
+ test("list help renders the --starting-after option", async () => {
234
+ const { out } = await runPlatformCaught(["invoices", "list", "--help"]);
235
+
236
+ expect(out.join("")).toContain("--starting-after <invoice-id>");
237
+ });
238
+ });