@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
@@ -0,0 +1,157 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
11
+
12
+ import { repairRetiredFireworksMinimaxModelIdMigration } from "../workspace/migrations/137-repair-retired-fireworks-minimax-model-id.js";
13
+ import { WORKSPACE_MIGRATIONS } from "../workspace/migrations/registry.js";
14
+
15
+ const RETIRED = "accounts/fireworks/models/minimax-m2p5";
16
+ const REPLACEMENT = "accounts/fireworks/models/minimax-m2p7";
17
+
18
+ let workspaceDir: string;
19
+
20
+ function freshWorkspace(): void {
21
+ workspaceDir = join(
22
+ tmpdir(),
23
+ `vellum-migration-137-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
24
+ );
25
+ mkdirSync(workspaceDir, { recursive: true });
26
+ }
27
+
28
+ function writeConfig(data: Record<string, unknown>): void {
29
+ writeFileSync(
30
+ join(workspaceDir, "config.json"),
31
+ JSON.stringify(data, null, 2) + "\n",
32
+ );
33
+ }
34
+
35
+ function readConfig(): Record<string, unknown> {
36
+ return JSON.parse(readFileSync(join(workspaceDir, "config.json"), "utf-8"));
37
+ }
38
+
39
+ beforeEach(() => {
40
+ freshWorkspace();
41
+ });
42
+
43
+ afterEach(() => {
44
+ if (existsSync(workspaceDir)) {
45
+ rmSync(workspaceDir, { recursive: true, force: true });
46
+ }
47
+ });
48
+
49
+ describe("137-repair-retired-fireworks-minimax-model-id migration", () => {
50
+ test("has correct migration id and is registered", () => {
51
+ expect(repairRetiredFireworksMinimaxModelIdMigration.id).toBe(
52
+ "137-repair-retired-fireworks-minimax-model-id",
53
+ );
54
+ expect(
55
+ WORKSPACE_MIGRATIONS.some(
56
+ (m) => m.id === "137-repair-retired-fireworks-minimax-model-id",
57
+ ),
58
+ ).toBe(true);
59
+ });
60
+
61
+ test("repairs the retired ID in default, call sites, and profiles", () => {
62
+ writeConfig({
63
+ llm: {
64
+ default: { provider: "fireworks", model: RETIRED },
65
+ callSites: {
66
+ recall: { model: RETIRED, maxTokens: 4096 },
67
+ heartbeat: { model: `${RETIRED}-tuned` },
68
+ malformed: RETIRED,
69
+ },
70
+ profiles: {
71
+ "cost-optimized": { provider: "vellum", model: RETIRED },
72
+ legacy: { model: RETIRED },
73
+ },
74
+ },
75
+ });
76
+
77
+ repairRetiredFireworksMinimaxModelIdMigration.run(workspaceDir);
78
+
79
+ const llm = readConfig().llm as Record<string, any>;
80
+ expect(llm.default.model).toBe(REPLACEMENT);
81
+ expect(llm.callSites.recall.model).toBe(REPLACEMENT);
82
+ expect(llm.callSites.recall.maxTokens).toBe(4096);
83
+ // Non-exact matches and malformed leaves are untouched.
84
+ expect(llm.callSites.heartbeat.model).toBe(`${RETIRED}-tuned`);
85
+ expect(llm.callSites.malformed).toBe(RETIRED);
86
+ // Managed profiles stamped provider "vellum" carry Fireworks model IDs.
87
+ expect(llm.profiles["cost-optimized"].model).toBe(REPLACEMENT);
88
+ expect(llm.profiles.legacy.model).toBe(REPLACEMENT);
89
+ });
90
+
91
+ test("leaves fragments with an explicit other provider untouched", () => {
92
+ writeConfig({
93
+ llm: {
94
+ default: { provider: "openai-compatible", model: RETIRED },
95
+ profiles: {
96
+ byo: { provider: "openai-compatible", model: RETIRED },
97
+ },
98
+ },
99
+ });
100
+
101
+ repairRetiredFireworksMinimaxModelIdMigration.run(workspaceDir);
102
+
103
+ const llm = readConfig().llm as Record<string, any>;
104
+ expect(llm.default.model).toBe(RETIRED);
105
+ expect(llm.profiles.byo.model).toBe(RETIRED);
106
+ });
107
+
108
+ test("is idempotent and a no-op without the retired ID", () => {
109
+ writeConfig({
110
+ llm: {
111
+ default: { provider: "fireworks", model: RETIRED },
112
+ profiles: { fine: { provider: "fireworks", model: REPLACEMENT } },
113
+ },
114
+ });
115
+
116
+ repairRetiredFireworksMinimaxModelIdMigration.run(workspaceDir);
117
+ const first = readConfig();
118
+ repairRetiredFireworksMinimaxModelIdMigration.run(workspaceDir);
119
+ expect(readConfig()).toEqual(first);
120
+
121
+ const llm = first.llm as Record<string, any>;
122
+ expect(llm.default.model).toBe(REPLACEMENT);
123
+ expect(llm.profiles.fine.model).toBe(REPLACEMENT);
124
+ });
125
+
126
+ test("handles missing config, missing llm block, and invalid JSON", () => {
127
+ // No config.json at all.
128
+ expect(() =>
129
+ repairRetiredFireworksMinimaxModelIdMigration.run(workspaceDir),
130
+ ).not.toThrow();
131
+
132
+ writeConfig({ theme: "dark" });
133
+ repairRetiredFireworksMinimaxModelIdMigration.run(workspaceDir);
134
+ expect(readConfig()).toEqual({ theme: "dark" });
135
+
136
+ writeFileSync(join(workspaceDir, "config.json"), "{not json");
137
+ expect(() =>
138
+ repairRetiredFireworksMinimaxModelIdMigration.run(workspaceDir),
139
+ ).not.toThrow();
140
+ });
141
+
142
+ test("the replacement model is not the current Fireworks intent template", async () => {
143
+ // Repairing onto the balanced-intent model would make a hand-edited
144
+ // custom-* profile read as unedited to ensureByokDefaultProfiles.
145
+ const { resolveModelIntent } =
146
+ await import("../providers/model-intents.js");
147
+ const fireworksIntentModels = (
148
+ [
149
+ "balanced",
150
+ "latency-optimized",
151
+ "quality-optimized",
152
+ "vision-optimized",
153
+ ] as const
154
+ ).map((intent) => resolveModelIntent("fireworks", intent));
155
+ expect(fireworksIntentModels).not.toContain(REPLACEMENT);
156
+ });
157
+ });
@@ -0,0 +1,373 @@
1
+ /**
2
+ * Tests for workspace migration `138-backfill-home-feed-titles`.
3
+ *
4
+ * The migration gives every item in `<workspace>/data/home-feed.json` a
5
+ * non-empty `title`, derived from the item's own `summary` when the
6
+ * field is missing or blank.
7
+ *
8
+ * Cases covered:
9
+ * 1. Missing file, malformed JSON, and a non-object root are no-ops.
10
+ * 2. Title-less items gain a first-sentence title from the summary.
11
+ * 3. Items that already have a title are untouched.
12
+ * 4. A long summary truncates to 60 characters with an ellipsis, the
13
+ * same cap the notification pipeline's derivation applies.
14
+ * 5. Markdown-rich and multi-line summaries yield a single line of
15
+ * plain text.
16
+ * 6. `version`, `updatedAt`, and unrelated item fields are preserved.
17
+ * 7. A second run writes nothing.
18
+ */
19
+
20
+ import {
21
+ existsSync,
22
+ mkdirSync,
23
+ mkdtempSync,
24
+ readFileSync,
25
+ rmSync,
26
+ statSync,
27
+ writeFileSync,
28
+ } from "node:fs";
29
+ import { tmpdir } from "node:os";
30
+ import { join } from "node:path";
31
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
32
+
33
+ import { backfillHomeFeedTitlesMigration } from "../workspace/migrations/138-backfill-home-feed-titles.js";
34
+ import { WORKSPACE_MIGRATIONS } from "../workspace/migrations/registry.js";
35
+ import { getLastWorkspaceMigrationId } from "../workspace/migrations/runner.js";
36
+
37
+ let workspaceDir: string;
38
+ let feedPath: string;
39
+
40
+ beforeEach(() => {
41
+ workspaceDir = mkdtempSync(join(tmpdir(), "vellum-migration-138-test-"));
42
+ feedPath = join(workspaceDir, "data", "home-feed.json");
43
+ });
44
+
45
+ afterEach(() => {
46
+ if (existsSync(workspaceDir)) {
47
+ rmSync(workspaceDir, { recursive: true, force: true });
48
+ }
49
+ });
50
+
51
+ function writeFeedFile(contents: unknown): void {
52
+ mkdirSync(join(workspaceDir, "data"), { recursive: true });
53
+ writeFileSync(feedPath, JSON.stringify(contents, null, 2), "utf-8");
54
+ }
55
+
56
+ function readFeedFile(): {
57
+ version: number;
58
+ items: Array<Record<string, unknown>>;
59
+ updatedAt: string;
60
+ } {
61
+ return JSON.parse(readFileSync(feedPath, "utf-8"));
62
+ }
63
+
64
+ function makeItem(
65
+ overrides: Record<string, unknown> & { id: string },
66
+ ): Record<string, unknown> {
67
+ return {
68
+ type: "notification",
69
+ priority: 50,
70
+ summary: "Something happened.",
71
+ timestamp: "2026-08-01T12:00:00.000Z",
72
+ status: "new",
73
+ createdAt: "2026-08-01T12:00:00.000Z",
74
+ ...overrides,
75
+ };
76
+ }
77
+
78
+ function writeFeed(items: Array<Record<string, unknown>>): void {
79
+ writeFeedFile({
80
+ version: 2,
81
+ updatedAt: "2026-08-01T12:00:00.000Z",
82
+ items,
83
+ });
84
+ }
85
+
86
+ describe("workspace migration 138-backfill-home-feed-titles", () => {
87
+ test("has the expected id and description", () => {
88
+ expect(backfillHomeFeedTitlesMigration.id).toBe(
89
+ "138-backfill-home-feed-titles",
90
+ );
91
+ expect(backfillHomeFeedTitlesMigration.description).toContain("title");
92
+ });
93
+
94
+ // getLastWorkspaceMigrationId() reports the final array entry as the
95
+ // registry ceiling to the identity and rollback routes, so the
96
+ // highest-numbered migration must sit last in the registry.
97
+ test("the registry ceiling stays at the highest-numbered migration", () => {
98
+ const numericId = (id: string) => Number.parseInt(id, 10);
99
+ const highest = Math.max(
100
+ ...WORKSPACE_MIGRATIONS.map((m) => numericId(m.id)).filter(
101
+ Number.isFinite,
102
+ ),
103
+ );
104
+ const last = getLastWorkspaceMigrationId(WORKSPACE_MIGRATIONS);
105
+ expect(last).not.toBeNull();
106
+ expect(numericId(last!)).toBe(highest);
107
+ });
108
+
109
+ test("missing file is a no-op (no error, no file created)", () => {
110
+ expect(existsSync(feedPath)).toBe(false);
111
+
112
+ expect(() =>
113
+ backfillHomeFeedTitlesMigration.run(workspaceDir),
114
+ ).not.toThrow();
115
+
116
+ expect(existsSync(feedPath)).toBe(false);
117
+ });
118
+
119
+ test("derives a title from the summary's first sentence", () => {
120
+ writeFeed([
121
+ makeItem({
122
+ id: "a-1",
123
+ summary: "Alice replied to your thread. She needs an answer.",
124
+ }),
125
+ makeItem({ id: "a-2", summary: "Deploy finished!" }),
126
+ makeItem({ id: "a-3", summary: "Ready to review? The draft is up." }),
127
+ ]);
128
+
129
+ backfillHomeFeedTitlesMigration.run(workspaceDir);
130
+
131
+ const titles = readFeedFile().items.map((item) => item.title);
132
+ expect(titles).toEqual([
133
+ "Alice replied to your thread.",
134
+ "Deploy finished!",
135
+ "Ready to review?",
136
+ ]);
137
+ });
138
+
139
+ test("backfills items whose title is missing, empty, blank, or not a string", () => {
140
+ writeFeed([
141
+ makeItem({ id: "missing", summary: "First item summary." }),
142
+ makeItem({ id: "empty", title: "", summary: "Second item summary." }),
143
+ makeItem({ id: "blank", title: " ", summary: "Third item summary." }),
144
+ makeItem({ id: "wrong-type", title: null, summary: "Fourth summary." }),
145
+ ]);
146
+
147
+ backfillHomeFeedTitlesMigration.run(workspaceDir);
148
+
149
+ const titles = readFeedFile().items.map((item) => item.title);
150
+ expect(titles).toEqual([
151
+ "First item summary.",
152
+ "Second item summary.",
153
+ "Third item summary.",
154
+ "Fourth summary.",
155
+ ]);
156
+ });
157
+
158
+ test("leaves items that already have a title untouched", () => {
159
+ writeFeed([
160
+ makeItem({
161
+ id: "titled",
162
+ title: "Existing headline",
163
+ summary: "A completely different summary sentence.",
164
+ }),
165
+ makeItem({ id: "untitled", summary: "Needs a headline." }),
166
+ ]);
167
+
168
+ backfillHomeFeedTitlesMigration.run(workspaceDir);
169
+
170
+ const items = readFeedFile().items;
171
+ expect(items[0]!.title).toBe("Existing headline");
172
+ expect(items[1]!.title).toBe("Needs a headline.");
173
+ });
174
+
175
+ test("truncates a long summary to 60 characters with an ellipsis", () => {
176
+ const summary =
177
+ "Bob shared a very long document that keeps going well past the limit";
178
+ writeFeed([makeItem({ id: "long", summary })]);
179
+
180
+ backfillHomeFeedTitlesMigration.run(workspaceDir);
181
+
182
+ const title = readFeedFile().items[0]!.title as string;
183
+ expect(title).toBe(
184
+ "Bob shared a very long document that keeps going well past t…",
185
+ );
186
+ // The ellipsis sits past the cap, so the retained text is exactly 60.
187
+ expect(title.slice(0, -1).length).toBe(60);
188
+ });
189
+
190
+ test("truncates a long first sentence rather than the whole summary", () => {
191
+ writeFeed([
192
+ makeItem({
193
+ id: "long-sentence",
194
+ summary:
195
+ "This opening sentence runs well beyond the sixty character cap on derived titles. Then another.",
196
+ }),
197
+ ]);
198
+
199
+ backfillHomeFeedTitlesMigration.run(workspaceDir);
200
+
201
+ const title = readFeedFile().items[0]!.title as string;
202
+ expect(title).toBe(
203
+ "This opening sentence runs well beyond the sixty character c…",
204
+ );
205
+ });
206
+
207
+ test("flattens a markdown-rich multi-line summary into one plain line", () => {
208
+ writeFeed([
209
+ makeItem({
210
+ id: "markdown",
211
+ summary:
212
+ "## Nightly run\n\n- **Deploy** finished\n- Tests passed\n\nSee the `report` for details.",
213
+ }),
214
+ ]);
215
+
216
+ backfillHomeFeedTitlesMigration.run(workspaceDir);
217
+
218
+ expect(readFeedFile().items[0]!.title).toBe(
219
+ "Nightly run Deploy finished Tests passed See the report for…",
220
+ );
221
+ });
222
+
223
+ test("unwraps inline markdown before taking the first sentence", () => {
224
+ writeFeed([
225
+ makeItem({
226
+ id: "inline",
227
+ summary:
228
+ "**Alice** shared the [design doc](https://example.com/doc). Review it soon.",
229
+ }),
230
+ ]);
231
+
232
+ backfillHomeFeedTitlesMigration.run(workspaceDir);
233
+
234
+ expect(readFeedFile().items[0]!.title).toBe("Alice shared the design doc.");
235
+ });
236
+
237
+ test("collapses a newline-only summary run into single spaces", () => {
238
+ writeFeed([
239
+ makeItem({ id: "newlines", summary: "Backup complete\n\nNo errors" }),
240
+ ]);
241
+
242
+ backfillHomeFeedTitlesMigration.run(workspaceDir);
243
+
244
+ expect(readFeedFile().items[0]!.title).toBe("Backup complete No errors");
245
+ });
246
+
247
+ test("skips an item whose summary is markdown structure only", () => {
248
+ writeFeed([makeItem({ id: "structure-only", summary: "##\n\n- \n\n> " })]);
249
+
250
+ backfillHomeFeedTitlesMigration.run(workspaceDir);
251
+
252
+ expect(readFeedFile().items[0]!.title).toBeUndefined();
253
+ });
254
+
255
+ test("preserves version, updatedAt, and unrelated item fields", () => {
256
+ writeFeedFile({
257
+ version: 2,
258
+ updatedAt: "2026-08-01T12:00:00.000Z",
259
+ items: [
260
+ makeItem({
261
+ id: "keep-fields",
262
+ summary: "Payment failed.",
263
+ urgency: "high",
264
+ conversationId: "conv-abc",
265
+ actions: [{ label: "Retry", kind: "prompt" }],
266
+ detailPanel: { kind: "text", body: "Details" },
267
+ }),
268
+ ],
269
+ });
270
+
271
+ backfillHomeFeedTitlesMigration.run(workspaceDir);
272
+
273
+ const out = readFeedFile();
274
+ expect(out.version).toBe(2);
275
+ expect(out.updatedAt).toBe("2026-08-01T12:00:00.000Z");
276
+
277
+ const item = out.items[0]!;
278
+ expect(item.title).toBe("Payment failed.");
279
+ expect(item.id).toBe("keep-fields");
280
+ expect(item.type).toBe("notification");
281
+ expect(item.priority).toBe(50);
282
+ expect(item.status).toBe("new");
283
+ expect(item.urgency).toBe("high");
284
+ expect(item.conversationId).toBe("conv-abc");
285
+ expect(item.actions).toEqual([{ label: "Retry", kind: "prompt" }]);
286
+ expect(item.detailPanel).toEqual({ kind: "text", body: "Details" });
287
+ });
288
+
289
+ test("idempotent: a second run writes nothing", () => {
290
+ writeFeed([
291
+ makeItem({ id: "a-1", summary: "Alice replied to your thread." }),
292
+ makeItem({ id: "a-2", title: "Already titled", summary: "Body text." }),
293
+ ]);
294
+
295
+ backfillHomeFeedTitlesMigration.run(workspaceDir);
296
+ const afterFirst = readFileSync(feedPath, "utf-8");
297
+ const afterFirstStat = statSync(feedPath);
298
+
299
+ backfillHomeFeedTitlesMigration.run(workspaceDir);
300
+
301
+ expect(readFileSync(feedPath, "utf-8")).toBe(afterFirst);
302
+ expect(statSync(feedPath).mtimeMs).toBe(afterFirstStat.mtimeMs);
303
+ });
304
+
305
+ test("a fully titled feed is left byte-identical", () => {
306
+ writeFeed([makeItem({ id: "a-1", title: "Titled", summary: "Body." })]);
307
+ const before = readFileSync(feedPath, "utf-8");
308
+ const beforeStat = statSync(feedPath);
309
+
310
+ backfillHomeFeedTitlesMigration.run(workspaceDir);
311
+
312
+ expect(readFileSync(feedPath, "utf-8")).toBe(before);
313
+ expect(statSync(feedPath).mtimeMs).toBe(beforeStat.mtimeMs);
314
+ });
315
+
316
+ test("malformed JSON does not throw and leaves the file alone", () => {
317
+ mkdirSync(join(workspaceDir, "data"), { recursive: true });
318
+ writeFileSync(feedPath, "{not valid json", "utf-8");
319
+ const before = readFileSync(feedPath, "utf-8");
320
+
321
+ expect(() =>
322
+ backfillHomeFeedTitlesMigration.run(workspaceDir),
323
+ ).not.toThrow();
324
+
325
+ expect(readFileSync(feedPath, "utf-8")).toBe(before);
326
+ });
327
+
328
+ test("non-object root is a no-op", () => {
329
+ writeFeedFile([1, 2, 3]);
330
+ const before = readFileSync(feedPath, "utf-8");
331
+
332
+ expect(() =>
333
+ backfillHomeFeedTitlesMigration.run(workspaceDir),
334
+ ).not.toThrow();
335
+
336
+ expect(readFileSync(feedPath, "utf-8")).toBe(before);
337
+ });
338
+
339
+ test("items that cannot yield a title are skipped without throwing", () => {
340
+ writeFeedFile({
341
+ version: 2,
342
+ updatedAt: "2026-08-01T12:00:00.000Z",
343
+ items: [
344
+ "not an object",
345
+ makeItem({ id: "no-summary", summary: undefined }),
346
+ makeItem({ id: "blank-summary", summary: " " }),
347
+ makeItem({ id: "ok", summary: "Has a summary." }),
348
+ ],
349
+ });
350
+
351
+ expect(() =>
352
+ backfillHomeFeedTitlesMigration.run(workspaceDir),
353
+ ).not.toThrow();
354
+
355
+ const items: unknown[] = readFeedFile().items;
356
+ expect(items[0]).toBe("not an object");
357
+ expect((items[1] as Record<string, unknown>).title).toBeUndefined();
358
+ expect((items[2] as Record<string, unknown>).title).toBeUndefined();
359
+ expect((items[3] as Record<string, unknown>).title).toBe("Has a summary.");
360
+ });
361
+
362
+ test("down() is a no-op (forward-only migration)", () => {
363
+ writeFeed([makeItem({ id: "a-1", summary: "Alice replied." })]);
364
+ backfillHomeFeedTitlesMigration.run(workspaceDir);
365
+ const after = readFileSync(feedPath, "utf-8");
366
+
367
+ expect(() =>
368
+ backfillHomeFeedTitlesMigration.down(workspaceDir),
369
+ ).not.toThrow();
370
+
371
+ expect(readFileSync(feedPath, "utf-8")).toBe(after);
372
+ });
373
+ });
@@ -0,0 +1,137 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
11
+
12
+ import { clearRenamedCostProfileLabelMigration } from "../workspace/migrations/139-clear-renamed-cost-profile-label.js";
13
+ import { WORKSPACE_MIGRATIONS } from "../workspace/migrations/registry.js";
14
+
15
+ let workspaceDir: string;
16
+
17
+ function freshWorkspace(): void {
18
+ workspaceDir = join(
19
+ tmpdir(),
20
+ `vellum-migration-139-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
21
+ );
22
+ mkdirSync(workspaceDir, { recursive: true });
23
+ }
24
+
25
+ function writeConfig(data: Record<string, unknown>): void {
26
+ writeFileSync(
27
+ join(workspaceDir, "config.json"),
28
+ JSON.stringify(data, null, 2) + "\n",
29
+ );
30
+ }
31
+
32
+ function costProfile(): Record<string, unknown> {
33
+ const config = JSON.parse(
34
+ readFileSync(join(workspaceDir, "config.json"), "utf-8"),
35
+ );
36
+ return config.llm.profiles["cost-optimized"];
37
+ }
38
+
39
+ function run(): void {
40
+ clearRenamedCostProfileLabelMigration.run(workspaceDir);
41
+ }
42
+
43
+ beforeEach(() => {
44
+ freshWorkspace();
45
+ });
46
+
47
+ afterEach(() => {
48
+ if (existsSync(workspaceDir)) {
49
+ rmSync(workspaceDir, { recursive: true, force: true });
50
+ }
51
+ });
52
+
53
+ describe("139-clear-renamed-cost-profile-label migration", () => {
54
+ test("has correct migration id and is registered last", () => {
55
+ expect(clearRenamedCostProfileLabelMigration.id).toBe(
56
+ "139-clear-renamed-cost-profile-label",
57
+ );
58
+ // getLastWorkspaceMigrationId() reports the final entry as the registry
59
+ // ceiling, so the highest id must stay last.
60
+ expect(WORKSPACE_MIGRATIONS[WORKSPACE_MIGRATIONS.length - 1]?.id).toBe(
61
+ "139-clear-renamed-cost-profile-label",
62
+ );
63
+ });
64
+
65
+ test("clears the seeded label so the code catalog label applies", () => {
66
+ writeConfig({
67
+ llm: {
68
+ profiles: {
69
+ "cost-optimized": { source: "managed", label: "Speed" },
70
+ },
71
+ },
72
+ });
73
+
74
+ run();
75
+
76
+ expect(costProfile()).toEqual({ source: "managed" });
77
+ });
78
+
79
+ test("leaves a user rename, an explicit null, and hatch stubs alone", () => {
80
+ for (const label of ["My Fast One", null, "Speed (Managed)"]) {
81
+ freshWorkspace();
82
+ writeConfig({
83
+ llm: { profiles: { "cost-optimized": { source: "managed", label } } },
84
+ });
85
+
86
+ run();
87
+
88
+ expect(costProfile().label).toBe(label as string);
89
+ }
90
+ });
91
+
92
+ test("leaves a source-less legacy entry alone", () => {
93
+ // A source-less entry under a default name shadows the catalog outright,
94
+ // so its label is user state.
95
+ writeConfig({
96
+ llm: { profiles: { "cost-optimized": { label: "Speed" } } },
97
+ });
98
+
99
+ run();
100
+
101
+ expect(costProfile().label).toBe("Speed");
102
+ });
103
+
104
+ test("leaves a user-owned profile shadowing the default name alone", () => {
105
+ writeConfig({
106
+ llm: {
107
+ profiles: {
108
+ "cost-optimized": {
109
+ source: "user",
110
+ label: "Speed",
111
+ provider: "anthropic",
112
+ model: "claude-haiku-4-5-20251001",
113
+ },
114
+ },
115
+ },
116
+ });
117
+
118
+ run();
119
+
120
+ expect(costProfile().label).toBe("Speed");
121
+ });
122
+
123
+ test("is idempotent and no-ops on absent or malformed config", () => {
124
+ writeConfig({
125
+ llm: { profiles: { "cost-optimized": { source: "managed" } } },
126
+ });
127
+ run();
128
+ run();
129
+ expect(costProfile()).toEqual({ source: "managed" });
130
+
131
+ freshWorkspace();
132
+ expect(() => run()).not.toThrow();
133
+
134
+ writeFileSync(join(workspaceDir, "config.json"), "{not json");
135
+ expect(() => run()).not.toThrow();
136
+ });
137
+ });