@vellumai/assistant 0.11.8 → 0.11.9-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 (326) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/Dockerfile +8 -48
  3. package/docker-entrypoint.sh +5 -1
  4. package/docker-kata-apt-env.sh +3 -0
  5. package/docker-kata-apt-shims.sh +127 -0
  6. package/docker-kata-apt-wrapper.sh +45 -0
  7. package/docker-kata-chroot-exec.sh +35 -0
  8. package/docker-kata-pip.sh +8 -2
  9. package/docs/architecture/memory.md +8 -4
  10. package/docs/guardian-request-flow.md +18 -14
  11. package/docs/trusted-contact-access.md +1 -7
  12. package/node_modules/@vellumai/avatar-catalog/src/catalog.ts +7 -1
  13. package/node_modules/@vellumai/avatar-catalog/src/index.ts +1 -1
  14. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +4 -1
  15. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  17. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
  18. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  19. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +4 -1
  20. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
  21. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  22. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
  23. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  24. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +0 -18
  25. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +110 -5
  26. package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +5 -33
  27. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +3 -0
  28. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +100 -9
  29. package/node_modules/@vellumai/gateway-client/src/index.ts +1 -2
  30. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +9 -0
  31. package/node_modules/@vellumai/service-contracts/package.json +4 -1
  32. package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
  33. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  34. package/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
  35. package/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  36. package/openapi.yaml +193 -4
  37. package/package.json +2 -2
  38. package/src/__tests__/access-request-card-view.test.ts +6 -5
  39. package/src/__tests__/access-request-seed-content-blocks.test.ts +5 -2
  40. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +43 -82
  41. package/src/__tests__/always-loaded-tools-guard.test.ts +8 -2
  42. package/src/__tests__/attachment-stored-path-annotation.test.ts +80 -0
  43. package/src/__tests__/attachments-store.test.ts +27 -0
  44. package/src/__tests__/attachments.test.ts +43 -0
  45. package/src/__tests__/canned-reply-release.test.ts +48 -5
  46. package/src/__tests__/channel-delivery-store.test.ts +129 -18
  47. package/src/__tests__/channel-reply-delivery.test.ts +553 -93
  48. package/src/__tests__/channel-retry-sweep.test.ts +199 -0
  49. package/src/__tests__/client-os-metadata-persistence.test.ts +32 -3
  50. package/src/__tests__/conversation-error.test.ts +15 -0
  51. package/src/__tests__/conversation-load-history-repair.test.ts +118 -0
  52. package/src/__tests__/conversation-pairing.test.ts +141 -1
  53. package/src/__tests__/conversation-routes-disk-view.test.ts +28 -1
  54. package/src/__tests__/conversation-routes-slash-commands.test.ts +125 -0
  55. package/src/__tests__/conversation-runtime-assembly.test.ts +79 -0
  56. package/src/__tests__/conversation-store-ephemeral.test.ts +323 -10
  57. package/src/__tests__/conversation-sync-tags.test.ts +2 -37
  58. package/src/__tests__/credential-health-service.test.ts +98 -10
  59. package/src/__tests__/delete-propagation.test.ts +469 -0
  60. package/src/__tests__/dm-persistence.test.ts +16 -0
  61. package/src/__tests__/docker-kata-apt-shims.test.ts +135 -0
  62. package/src/__tests__/forbidden-legacy-symbols.test.ts +12 -0
  63. package/src/__tests__/gemini-provider.test.ts +46 -0
  64. package/src/__tests__/guardian-card-withdrawal.test.ts +0 -22
  65. package/src/__tests__/guardian-gateway-sim.ts +0 -23
  66. package/src/__tests__/guardian-question-mode.test.ts +108 -0
  67. package/src/__tests__/guardian-reply-router-answer-mode.test.ts +0 -2
  68. package/src/__tests__/guardian-routing-invariants.test.ts +26 -12
  69. package/src/__tests__/host-proxy-interface.test.ts +10 -0
  70. package/src/__tests__/inbound-slack-persistence.test.ts +16 -0
  71. package/src/__tests__/injector-v3-suppression.test.ts +43 -9
  72. package/src/__tests__/list-messages-page-latest.test.ts +127 -0
  73. package/src/__tests__/list-messages-system-card.test.ts +103 -0
  74. package/src/__tests__/media-resolve-image-validation.test.ts +77 -0
  75. package/src/__tests__/notification-decision-fallback.test.ts +199 -77
  76. package/src/__tests__/notification-decision-strategy.test.ts +198 -213
  77. package/src/__tests__/notification-discord-adapter.test.ts +25 -0
  78. package/src/__tests__/notification-slack-adapter.test.ts +133 -0
  79. package/src/__tests__/notification-telegram-adapter.test.ts +36 -0
  80. package/src/__tests__/openai-provider.test.ts +5 -5
  81. package/src/__tests__/outbound-slack-persistence.test.ts +85 -72
  82. package/src/__tests__/persist-user-message-set-processing-failure.test.ts +83 -65
  83. package/src/__tests__/platform-client-verify-credential.test.ts +100 -0
  84. package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -1
  85. package/src/__tests__/pricing.test.ts +11 -0
  86. package/src/__tests__/process-message-display-content.test.ts +20 -9
  87. package/src/__tests__/processing-acquire-fenced-guard.test.ts +56 -0
  88. package/src/__tests__/provider-meta-persistence.test.ts +67 -0
  89. package/src/__tests__/reaction-persistence.test.ts +22 -198
  90. package/src/__tests__/run-conversation-turn-persistence.test.ts +5 -2
  91. package/src/__tests__/scripted-turn-metadata-persistence.test.ts +16 -0
  92. package/src/__tests__/skill-load-tool.test.ts +27 -0
  93. package/src/__tests__/skills.test.ts +34 -1
  94. package/src/__tests__/strip-memory-injections.test.ts +3 -4
  95. package/src/__tests__/terminal-tools.test.ts +9 -0
  96. package/src/__tests__/thread-backfill.test.ts +5 -3
  97. package/src/__tests__/unified-turn-context-location.test.ts +76 -0
  98. package/src/__tests__/voice-session-bridge.test.ts +18 -0
  99. package/src/__tests__/watch-retro-report-payload.test.ts +185 -0
  100. package/src/__tests__/watch-retro-tool-availability.test.ts +82 -0
  101. package/src/__tests__/workspace-migration-151-repair-renamed-fireworks-deepseek-pro-model-id.test.ts +235 -0
  102. package/src/__tests__/workspace-migration-152-repair-retired-fireworks-minimax-m2p7-model-id.test.ts +233 -0
  103. package/src/agent/attachments.ts +13 -2
  104. package/src/agent/loop.ts +3 -19
  105. package/src/api/README.md +9 -5
  106. package/src/api/index.ts +9 -8
  107. package/src/api/package.json +1 -0
  108. package/src/api/responses/conversation-message.ts +8 -0
  109. package/src/api/responses/home.ts +7 -18
  110. package/src/api/surfaces.ts +114 -7
  111. package/src/approvals/AGENTS.md +1 -1
  112. package/src/channels/__tests__/gateway-guardian-requests.test.ts +1 -23
  113. package/src/channels/__tests__/types.test.ts +22 -1
  114. package/src/channels/gateway-guardian-requests.ts +0 -22
  115. package/src/channels/types.ts +8 -6
  116. package/src/cli/__tests__/catalog-search-help.test.ts +18 -0
  117. package/src/cli/commands/channels/__tests__/channels.test.ts +26 -0
  118. package/src/cli/commands/channels/index.help.ts +19 -6
  119. package/src/cli/commands/channels/index.ts +6 -2
  120. package/src/cli/commands/db/__tests__/status.test.ts +22 -0
  121. package/src/cli/commands/db/index.help.ts +1 -1
  122. package/src/cli/commands/db/status.ts +172 -1
  123. package/src/cli/commands/platform/__tests__/connect.test.ts +29 -0
  124. package/src/cli/commands/platform/connect.ts +14 -5
  125. package/src/cli/commands/plugins.help.ts +6 -5
  126. package/src/cli/lib/__tests__/install-from-github.test.ts +0 -8
  127. package/src/cli/lib/__tests__/install-from-platform.test.ts +72 -0
  128. package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +33 -0
  129. package/src/cli/lib/bundled-marketplace.json +14 -0
  130. package/src/cli/lib/install-from-github.ts +9 -5
  131. package/src/cli/lib/install-from-platform.ts +12 -1
  132. package/src/config/__tests__/assistant-initiated-threads-gate.test.ts +61 -0
  133. package/src/config/assistant-initiated-threads-gate.ts +50 -0
  134. package/src/config/bundled-skills/acp/SKILL.md +10 -3
  135. package/src/config/bundled-skills/schedule/SKILL.md +25 -11
  136. package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +3 -1
  137. package/src/config/call-site-defaults.ts +4 -1
  138. package/src/config/feature-flag-registry.json +21 -4
  139. package/src/config/schemas/memory-v3.ts +4 -3
  140. package/src/context/strip-injections.ts +17 -56
  141. package/src/conversations/__tests__/message-consolidation.test.ts +39 -0
  142. package/src/conversations/message-consolidation.ts +4 -3
  143. package/src/credential-health/credential-health-service.ts +130 -0
  144. package/src/daemon/__tests__/conversation-tool-setup.test.ts +31 -0
  145. package/src/daemon/conversation-agent-loop-handlers.ts +55 -59
  146. package/src/daemon/conversation-error.ts +2 -0
  147. package/src/daemon/conversation-messaging.ts +195 -47
  148. package/src/daemon/conversation-process.ts +20 -5
  149. package/src/daemon/conversation-runtime-assembly.ts +48 -39
  150. package/src/daemon/conversation-store.ts +159 -11
  151. package/src/daemon/conversation-tool-setup.ts +9 -2
  152. package/src/daemon/conversation.ts +290 -19
  153. package/src/daemon/dictation-text-processing.ts +2 -7
  154. package/src/daemon/handlers/config-channels.ts +33 -3
  155. package/src/daemon/handlers/config-model.test.ts +1 -0
  156. package/src/daemon/handlers/shared.ts +9 -0
  157. package/src/daemon/port-oversized-content.test.ts +117 -0
  158. package/src/daemon/port-oversized-content.ts +110 -0
  159. package/src/daemon/process-message.ts +87 -16
  160. package/src/daemon/reaction-record.test.ts +23 -11
  161. package/src/daemon/reaction-record.ts +6 -9
  162. package/src/documents/document-store.ts +1 -5
  163. package/src/home/conversation-starter-validation.ts +1 -5
  164. package/src/live-voice/__tests__/live-voice-photo.test.ts +910 -50
  165. package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +31 -25
  166. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +70 -1
  167. package/src/live-voice/live-voice-photo.ts +544 -61
  168. package/src/live-voice/live-voice-session.ts +6 -2
  169. package/src/live-voice/protocol.ts +20 -1
  170. package/src/messaging/provider-message-metadata.ts +40 -1
  171. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +10 -2
  172. package/src/messaging/providers/channel-transport.ts +28 -0
  173. package/src/messaging/providers/discord/send.ts +3 -2
  174. package/src/messaging/providers/slack/api.ts +11 -1
  175. package/src/messaging/providers/slack/message-metadata.test.ts +133 -0
  176. package/src/messaging/providers/slack/message-metadata.ts +141 -1
  177. package/src/messaging/providers/slack/send.test.ts +58 -0
  178. package/src/messaging/providers/slack/send.ts +4 -4
  179. package/src/messaging/providers/slack/transport.ts +28 -2
  180. package/src/messaging/providers/telegram-bot/send.test.ts +159 -0
  181. package/src/messaging/providers/telegram-bot/send.ts +93 -0
  182. package/src/messaging/providers/telegram-bot/transport.ts +71 -0
  183. package/src/messaging/reaction-envelopes.test.ts +73 -0
  184. package/src/messaging/reaction-envelopes.ts +33 -5
  185. package/src/messaging/read-provider-metadata.ts +4 -3
  186. package/src/monitoring/recovery/__tests__/stranded-delivery-events.test.ts +208 -0
  187. package/src/monitoring/recovery/db.ts +35 -0
  188. package/src/monitoring/recovery/orphaned-channel-events.ts +3 -22
  189. package/src/monitoring/recovery/run-recovery.ts +6 -2
  190. package/src/monitoring/recovery/stale-processing.ts +3 -20
  191. package/src/monitoring/recovery/stranded-delivery-events.ts +68 -0
  192. package/src/notifications/AGENTS.md +2 -2
  193. package/src/notifications/README.md +2 -2
  194. package/src/notifications/__tests__/assistant-reply-producer.test.ts +11 -9
  195. package/src/notifications/__tests__/broadcaster.test.ts +35 -4
  196. package/src/notifications/__tests__/notification-utils.test.ts +31 -0
  197. package/src/notifications/access-request-copy.ts +126 -114
  198. package/src/notifications/adapters/discord.ts +9 -5
  199. package/src/notifications/adapters/shared.ts +17 -4
  200. package/src/notifications/adapters/slack.ts +14 -4
  201. package/src/notifications/adapters/telegram.ts +8 -4
  202. package/src/notifications/approval-card-data.ts +5 -2
  203. package/src/notifications/assistant-reply-producer.ts +7 -7
  204. package/src/notifications/broadcaster.ts +57 -25
  205. package/src/notifications/conversation-pairing.ts +68 -2
  206. package/src/notifications/copy-composer.ts +13 -32
  207. package/src/notifications/decision-engine.ts +56 -237
  208. package/src/notifications/guardian-delivery-recorder.ts +5 -9
  209. package/src/notifications/guardian-feed-projection.ts +0 -15
  210. package/src/notifications/guardian-question-mode.ts +147 -6
  211. package/src/notifications/notification-utils.ts +117 -1
  212. package/src/permissions/confirmation-guardian-request.ts +2 -2
  213. package/src/permissions/prompter.ts +1 -1
  214. package/src/persistence/conversation-crud.ts +93 -14
  215. package/src/persistence/conversation-queries.ts +144 -17
  216. package/src/persistence/conversation-types.ts +39 -5
  217. package/src/persistence/delivery-crud.ts +224 -28
  218. package/src/persistence/delivery-status.ts +21 -2
  219. package/src/persistence/migrations/374-channel-inbound-message-id-index.ts +26 -0
  220. package/src/persistence/migrations/375-create-channel-outbound-posts.ts +52 -0
  221. package/src/persistence/migrations/__tests__/375-create-channel-outbound-posts.test.ts +84 -0
  222. package/src/persistence/schema/conversations.ts +71 -2
  223. package/src/persistence/schema-contract.test.ts +78 -0
  224. package/src/persistence/schema-contract.ts +96 -0
  225. package/src/persistence/steps.ts +4 -0
  226. package/src/platform/client.ts +55 -0
  227. package/src/plugins/__tests__/mcp-servers.test.ts +46 -0
  228. package/src/plugins/defaults/injector-order.ts +2 -1
  229. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +2 -2
  230. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +13 -15
  231. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +10 -0
  232. package/src/plugins/defaults/memory/injectors.ts +1 -1
  233. package/src/plugins/defaults/memory/memory-marker.ts +17 -7
  234. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +1 -5
  235. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +17 -0
  236. package/src/plugins/defaults/memory/substrate/skill-content.ts +7 -0
  237. package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +54 -41
  238. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +3 -3
  239. package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +22 -0
  240. package/src/plugins/defaults/memory/v3/injector.ts +14 -15
  241. package/src/plugins/defaults/memory/v3/prune.test.ts +20 -2
  242. package/src/plugins/defaults/memory/v3/render-injection.ts +10 -1
  243. package/src/plugins/defaults/memory/v3/types.ts +15 -4
  244. package/src/plugins/defaults/turn-context/injectors.ts +3 -0
  245. package/src/plugins/defaults/turn-context/unified-turn-context.ts +24 -0
  246. package/src/plugins/mcp-servers.ts +14 -2
  247. package/src/providers/__tests__/dispatch-connection-routing.test.ts +50 -0
  248. package/src/providers/__tests__/registry-native-web-search.test.ts +49 -2
  249. package/src/providers/anthropic/client.ts +21 -24
  250. package/src/providers/call-site-routing.ts +5 -4
  251. package/src/providers/connection-resolution.ts +29 -1
  252. package/src/providers/content-block-size.test.ts +82 -0
  253. package/src/providers/content-block-size.ts +99 -0
  254. package/src/providers/file-block-text.test.ts +64 -0
  255. package/src/providers/file-block-text.ts +28 -0
  256. package/src/providers/gemini/client.ts +15 -9
  257. package/src/providers/inference/auth.ts +6 -6
  258. package/src/providers/media-resolve.ts +14 -0
  259. package/src/providers/model-catalog.ts +47 -34
  260. package/src/providers/openai/chat-completions-provider.ts +9 -25
  261. package/src/providers/openai/responses-provider.ts +11 -18
  262. package/src/providers/registry.ts +10 -7
  263. package/src/providers/routing-identity.ts +2 -1
  264. package/src/providers/types.ts +1 -4
  265. package/src/providers/vellum-model-routing.ts +3 -2
  266. package/src/runtime/AGENTS.md +40 -15
  267. package/src/runtime/approval-message-composer.ts +1 -6
  268. package/src/runtime/assistant-event-hub.ts +2 -39
  269. package/src/runtime/channel-approval-types.ts +1 -5
  270. package/src/runtime/channel-reply-delivery.ts +182 -115
  271. package/src/runtime/{slack-reply-session.test.ts → channel-reply-session.test.ts} +294 -156
  272. package/src/runtime/{slack-reply-session.ts → channel-reply-session.ts} +107 -86
  273. package/src/runtime/channel-retry-sweep.ts +102 -1
  274. package/src/runtime/finalize-event-delivery.ts +4 -4
  275. package/src/runtime/guardian-action-message-composer.ts +1 -4
  276. package/src/runtime/guardian-reply-router.ts +4 -75
  277. package/src/runtime/http-router.ts +1 -5
  278. package/src/runtime/question-request-guardian-bridge.ts +2 -3
  279. package/src/runtime/routes/__tests__/conversation-list-assistant-section.test.ts +359 -0
  280. package/src/runtime/routes/__tests__/dictation-command-mode.test.ts +120 -0
  281. package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +487 -0
  282. package/src/runtime/routes/acp-routes.ts +1 -1
  283. package/src/runtime/routes/canned-reply-release.ts +19 -9
  284. package/src/runtime/routes/channel-route-shared.ts +0 -39
  285. package/src/runtime/routes/channel-verification-routes.ts +4 -1
  286. package/src/runtime/routes/conversation-list-routes.ts +44 -3
  287. package/src/runtime/routes/conversation-management-routes.ts +2 -1
  288. package/src/runtime/routes/conversation-routes.ts +138 -59
  289. package/src/runtime/routes/diagnostics-routes.ts +111 -60
  290. package/src/runtime/routes/guardian-action-routes.ts +10 -14
  291. package/src/runtime/routes/guardian-approval-interception.ts +0 -5
  292. package/src/runtime/routes/inbound-message-handler.ts +170 -48
  293. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +4 -3
  294. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +169 -8
  295. package/src/runtime/routes/inbound-stages/background-dispatch.ts +82 -14
  296. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +23 -48
  297. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +454 -94
  298. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +309 -102
  299. package/src/runtime/routes/index.ts +2 -0
  300. package/src/runtime/routes/platform-routes.ts +99 -10
  301. package/src/runtime/routes/sight-frame-routes.ts +136 -0
  302. package/src/runtime/sync/resource-sync-events.ts +14 -37
  303. package/src/runtime/sync/sync-publisher.test.ts +5 -4
  304. package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
  305. package/src/tools/client-os.ts +11 -1
  306. package/src/tools/host-filesystem/edit.ts +2 -2
  307. package/src/tools/host-filesystem/read.ts +2 -2
  308. package/src/tools/host-filesystem/transfer.ts +2 -2
  309. package/src/tools/host-filesystem/write.ts +2 -2
  310. package/src/tools/host-terminal/host-shell.ts +2 -2
  311. package/src/tools/skills/load.ts +1 -1
  312. package/src/tools/terminal/safe-env.ts +4 -0
  313. package/src/tools/tool-input-schemas.ts +2 -0
  314. package/src/tools/tool-manifest.ts +2 -0
  315. package/src/tools/ui-surface/definitions.ts +4 -3
  316. package/src/tools/watch/watch-retro-report.ts +205 -0
  317. package/src/watch/__tests__/watch-retro.test.ts +268 -40
  318. package/src/watch/watch-retro.ts +190 -77
  319. package/src/workspace/migrations/151-repair-renamed-fireworks-deepseek-pro-model-id.ts +195 -0
  320. package/src/workspace/migrations/152-repair-retired-fireworks-minimax-m2p7-model-id.ts +198 -0
  321. package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +0 -10
  322. package/src/workspace/migrations/registry.ts +4 -0
  323. package/docker-kata-pip-chroot.sh +0 -22
  324. package/src/__tests__/slack-reaction-approvals.test.ts +0 -97
  325. package/src/__tests__/slack-reaction-guardian-approval.test.ts +0 -307
  326. package/src/api/events/conversation-list-invalidated.ts +0 -38
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Unit tests for the `chat_id:` and `thread_id:` lines in the
3
+ * unified-turn-context block.
4
+ *
5
+ * A channel turn already carries its chat and thread ids on the trust
6
+ * context; these lines make them visible to the model so a request like
7
+ * "what did you send me this morning" can start from the chat it is in
8
+ * instead of discovering it. The lines are channel-only: an app (vellum)
9
+ * turn has no external chat, and a value that would only be noise is not
10
+ * rendered. Values are passed through the same inline sanitizer as every
11
+ * other line, so a channel-supplied id cannot break out of the block.
12
+ */
13
+
14
+ import { describe, expect, test } from "bun:test";
15
+
16
+ import { buildUnifiedTurnContextBlock } from "../plugins/defaults/turn-context/unified-turn-context.js";
17
+
18
+ const TS = "2026-09-02T16:00:00.000Z";
19
+
20
+ describe("unified-turn-context chat_id and thread_id", () => {
21
+ test("renders both lines for a threaded Slack turn", () => {
22
+ const block = buildUnifiedTurnContextBlock({
23
+ timestamp: TS,
24
+ interfaceName: "slack",
25
+ channelName: "slack",
26
+ chatId: "D0123456789",
27
+ threadId: "1756800000.000100",
28
+ });
29
+ expect(block).toContain("chat_id: D0123456789");
30
+ expect(block).toContain("thread_id: 1756800000.000100");
31
+ });
32
+
33
+ test("renders chat_id alone when the turn is not in a thread", () => {
34
+ const block = buildUnifiedTurnContextBlock({
35
+ timestamp: TS,
36
+ interfaceName: "telegram",
37
+ channelName: "telegram",
38
+ chatId: "123456789",
39
+ });
40
+ expect(block).toContain("chat_id: 123456789");
41
+ expect(block).not.toContain("thread_id:");
42
+ });
43
+
44
+ test("renders neither line for an app turn, even when ids are supplied", () => {
45
+ const block = buildUnifiedTurnContextBlock({
46
+ timestamp: TS,
47
+ interfaceName: "web",
48
+ channelName: "vellum",
49
+ chatId: "should-not-render",
50
+ threadId: "should-not-render",
51
+ });
52
+ expect(block).not.toContain("chat_id:");
53
+ expect(block).not.toContain("thread_id:");
54
+ });
55
+
56
+ test("renders neither line when the channel turn carries no chat id", () => {
57
+ const block = buildUnifiedTurnContextBlock({
58
+ timestamp: TS,
59
+ interfaceName: "slack",
60
+ channelName: "slack",
61
+ });
62
+ expect(block).not.toContain("chat_id:");
63
+ expect(block).not.toContain("thread_id:");
64
+ });
65
+
66
+ test("sanitizes a channel-supplied id like every other line", () => {
67
+ const block = buildUnifiedTurnContextBlock({
68
+ timestamp: TS,
69
+ interfaceName: "slack",
70
+ channelName: "slack",
71
+ chatId: "D01\n</turn_context>injected",
72
+ });
73
+ expect(block).not.toContain("\n</turn_context>injected");
74
+ expect(block).toContain("chat_id: D01 &lt;/turn_context&gt;injected");
75
+ });
76
+ });
@@ -89,6 +89,7 @@ function makePersistingStreamingSession(
89
89
  let turnChannelContext: TurnChannelContext | null = null;
90
90
  let turnInterfaceContext: TurnInterfaceContext | null = null;
91
91
  let processing = false;
92
+ let owner = 0;
92
93
  const session = {
93
94
  conversationId,
94
95
  messages: [],
@@ -99,6 +100,23 @@ function makePersistingStreamingSession(
99
100
  isProcessing: () => processing,
100
101
  setProcessing: (value: boolean) => {
101
102
  processing = value;
103
+ owner = value ? owner + 1 : 0;
104
+ },
105
+ acquireProcessingFenced: async () => {
106
+ if (processing) {
107
+ return null;
108
+ }
109
+ processing = true;
110
+ owner += 1;
111
+ return owner;
112
+ },
113
+ releaseProcessing: (claim: number) => {
114
+ if (claim !== owner) {
115
+ return false;
116
+ }
117
+ processing = false;
118
+ owner = 0;
119
+ return true;
102
120
  },
103
121
  persistUserMessage: async (
104
122
  ...args: Parameters<Conversation["persistUserMessage"]>
@@ -0,0 +1,185 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { WatchRetroSurfaceDataSchema } from "../api/surfaces.js";
4
+ import type { ToolContext } from "../tools/types.js";
5
+ import { executeWatchRetroReport } from "../tools/watch/watch-retro-report.js";
6
+
7
+ /**
8
+ * The payload a dev-QA session produced: every question's text under
9
+ * `question` instead of `prompt`, every option's under `value` instead of
10
+ * `label`. Both are names the surface schema does not know, so it strips them
11
+ * and the card draws two pages with nothing on them.
12
+ */
13
+ const MISNAMED_FIELDS = {
14
+ task: "Check Slack for unread messages between terminal work",
15
+ steps: ["Work in the terminal", "Switch to Slack"],
16
+ questions: [
17
+ {
18
+ id: "scope",
19
+ kind: "pick",
20
+ question: "Where does this routine end?",
21
+ options: [
22
+ { note: "This matches what the recording showed.", value: "Just read" },
23
+ { value: "Also draft or send replies" },
24
+ ],
25
+ },
26
+ ],
27
+ } as const;
28
+
29
+ /** The same report with the names the card actually reads. */
30
+ const WELL_FORMED = {
31
+ task: "Check Slack for unread messages between terminal work",
32
+ steps: ["Work in the terminal", "Switch to Slack"],
33
+ questions: [
34
+ {
35
+ id: "scope",
36
+ kind: "pick",
37
+ prompt: "Where does this routine end?",
38
+ options: [
39
+ {
40
+ id: "scope-read",
41
+ label: "Just read",
42
+ note: "This matches what the recording showed.",
43
+ },
44
+ { id: "scope-reply", label: "Also draft or send replies" },
45
+ ],
46
+ },
47
+ ],
48
+ } as const;
49
+
50
+ const context = {} as ToolContext;
51
+
52
+ async function report(input: unknown): Promise<{
53
+ content: string;
54
+ isError: boolean | undefined;
55
+ }> {
56
+ const result = await executeWatchRetroReport(
57
+ input as Record<string, unknown>,
58
+ context,
59
+ );
60
+ return { content: result.content, isError: result.isError };
61
+ }
62
+
63
+ describe("watch_retro_report validates the question shape", () => {
64
+ test("a well-formed report is recorded", async () => {
65
+ const result = await report(WELL_FORMED);
66
+ expect(result.isError).toBe(false);
67
+ expect(result.content).toContain('"recorded":true');
68
+ });
69
+
70
+ // The failure this whole file exists for. The surface schema is tolerant by
71
+ // design and will not complain, so the tool is the only layer that can tell
72
+ // the model, while it still has a turn to spend, that the card it just
73
+ // described has no text on it.
74
+ test("a question whose text is under the wrong key is refused by field name", async () => {
75
+ const result = await report(MISNAMED_FIELDS);
76
+ expect(result.isError).toBe(true);
77
+ expect(result.content).toContain("questions.0.prompt");
78
+ });
79
+
80
+ test("an option whose label is under the wrong key is refused by field name", async () => {
81
+ const result = await report({
82
+ ...WELL_FORMED,
83
+ questions: [
84
+ {
85
+ ...WELL_FORMED.questions[0],
86
+ options: [
87
+ { id: "a", value: "Just read" },
88
+ { id: "b", value: "Also reply" },
89
+ ],
90
+ },
91
+ ],
92
+ });
93
+ expect(result.isError).toBe(true);
94
+ // Named as the missing field, not as the option count. An array whose
95
+ // entries failed to parse looks empty, and "send more options" is the one
96
+ // correction that cannot help here.
97
+ expect(result.content).toContain("questions.0.options.0.label");
98
+ expect(result.content).not.toContain("two to four options");
99
+ });
100
+
101
+ test("a pick with one option is refused, since the renderer drops it", async () => {
102
+ const result = await report({
103
+ ...WELL_FORMED,
104
+ questions: [
105
+ {
106
+ ...WELL_FORMED.questions[0],
107
+ options: [WELL_FORMED.questions[0].options[0]],
108
+ },
109
+ ],
110
+ });
111
+ expect(result.isError).toBe(true);
112
+ expect(result.content).toContain("two to four options");
113
+ });
114
+
115
+ test("a fill needs no options", async () => {
116
+ const result = await report({
117
+ ...WELL_FORMED,
118
+ questions: [
119
+ {
120
+ id: "start-phrase",
121
+ kind: "fill",
122
+ prompt: "What would you say to kick this off?",
123
+ suggestion: "check slack for unread",
124
+ },
125
+ ],
126
+ });
127
+ expect(result.isError).toBe(false);
128
+ });
129
+
130
+ // Two questions under one id is not a parse failure: the renderer keeps the
131
+ // first and drops the rest, because the id is the key an answer comes back
132
+ // under. The user loses a page and nothing says why.
133
+ test("two questions sharing an id are refused", async () => {
134
+ const result = await report({
135
+ ...WELL_FORMED,
136
+ questions: [WELL_FORMED.questions[0], { ...WELL_FORMED.questions[0] }],
137
+ });
138
+ expect(result.isError).toBe(true);
139
+ expect(result.content).toContain('id "scope"');
140
+ });
141
+
142
+ test("questions stay optional, since a settled recording asks nothing", async () => {
143
+ const result = await report({ task: "Rename a column", steps: ["Rename"] });
144
+ expect(result.isError).toBe(false);
145
+ });
146
+ });
147
+
148
+ describe("WatchRetroSurfaceDataSchema on a missing required string", () => {
149
+ // `z.coerce.string()` stringifies whatever it is handed, so an absent field
150
+ // parsed as the literal "undefined" and rendered as that word on the card.
151
+ // A blank is what the renderer's own filters are written against.
152
+ test("an absent prompt and label are blank, not the word undefined", () => {
153
+ const parsed = WatchRetroSurfaceDataSchema.parse(MISNAMED_FIELDS);
154
+ const question = parsed.questions?.[0];
155
+
156
+ expect(question?.prompt).toBe("");
157
+ expect(question?.options?.[0]?.label).toBe("");
158
+ expect(question?.options?.[0]?.id).toBe("");
159
+ });
160
+
161
+ test("an absent task is blank, so the card falls back rather than titling itself undefined", () => {
162
+ expect(WatchRetroSurfaceDataSchema.parse({ steps: [] }).task).toBe("");
163
+ });
164
+
165
+ // The point of the blank: it is what `isAnswerable` in the renderer filters
166
+ // an unusable question on. A card built from the misnamed payload shows its
167
+ // record and no question pages, instead of pages the user cannot read.
168
+ test("the fields the payload did name still survive", () => {
169
+ const parsed = WatchRetroSurfaceDataSchema.parse(MISNAMED_FIELDS);
170
+
171
+ expect(parsed.task).toBe(
172
+ "Check Slack for unread messages between terminal work",
173
+ );
174
+ expect(parsed.questions?.[0]?.options?.[0]?.note).toBe(
175
+ "This matches what the recording showed.",
176
+ );
177
+ });
178
+
179
+ test("a well-formed payload is unchanged by the coercion", () => {
180
+ const parsed = WatchRetroSurfaceDataSchema.parse(WELL_FORMED);
181
+
182
+ expect(parsed.questions?.[0]?.prompt).toBe("Where does this routine end?");
183
+ expect(parsed.questions?.[0]?.options?.[0]?.label).toBe("Just read");
184
+ });
185
+ });
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The gate that decides whether a watch retrospective can report at all.
3
+ *
4
+ * A retrospective runs as a `clientless` wake, which pins the turn
5
+ * non-interactive. `conversation-tool-setup` gates the whole `ui_surface`
6
+ * family on a client being present, so in that turn `ui_show` is not denied,
7
+ * it is absent: a retrospective told to call it can only tell the user it
8
+ * cannot. Nothing about the card's schema, its renderer, or the prompt's
9
+ * wording reveals that, which is why it is asserted here against the real
10
+ * registry rather than assumed anywhere else.
11
+ */
12
+
13
+ import { afterAll, describe, expect, test } from "bun:test";
14
+
15
+ import type { Conversation } from "../daemon/conversation.js";
16
+ import { isToolActiveForContext } from "../daemon/conversation-tool-setup.js";
17
+ import {
18
+ __resetRegistryForTesting,
19
+ getAllToolDefinitions,
20
+ initializeTools,
21
+ } from "../tools/registry.js";
22
+
23
+ afterAll(() => {
24
+ __resetRegistryForTesting();
25
+ });
26
+
27
+ /** What a clientless wake looks like to the tool gate. */
28
+ function clientlessContext(): Conversation {
29
+ return {
30
+ skillProjectionState: new Map(),
31
+ skillProjectionCache: {},
32
+ toolsDisabledDepth: 0,
33
+ hasNoClient: true,
34
+ channelCapabilities: undefined,
35
+ } as unknown as Conversation;
36
+ }
37
+
38
+ /** The same conversation with a client attached, for contrast. */
39
+ function clientfulContext(): Conversation {
40
+ return {
41
+ skillProjectionState: new Map(),
42
+ skillProjectionCache: {},
43
+ toolsDisabledDepth: 0,
44
+ hasNoClient: false,
45
+ channelCapabilities: undefined,
46
+ } as unknown as Conversation;
47
+ }
48
+
49
+ describe("watch retrospective tool availability", () => {
50
+ test("the retrospective can report without a client", async () => {
51
+ await initializeTools();
52
+ expect(
53
+ isToolActiveForContext("watch_retro_report", clientlessContext()),
54
+ ).toBe(true);
55
+ });
56
+
57
+ test("ui_show is absent from a clientless turn", async () => {
58
+ await initializeTools();
59
+ const ctx = clientlessContext();
60
+
61
+ // The reason the report does not go through `ui_show`. If this ever flips
62
+ // to true, the extra tool above can be reconsidered; while it is false,
63
+ // routing the retrospective's card through `ui_show` produces a turn that
64
+ // cannot report at all.
65
+ expect(isToolActiveForContext("ui_show", ctx)).toBe(false);
66
+ expect(isToolActiveForContext("ui_update", ctx)).toBe(false);
67
+ expect(isToolActiveForContext("ui_dismiss", ctx)).toBe(false);
68
+
69
+ // And the gate really is about the client, not about the tools being
70
+ // unregistered: with one attached, the same names are active.
71
+ const withClient = clientfulContext();
72
+ expect(isToolActiveForContext("ui_show", withClient)).toBe(true);
73
+ });
74
+
75
+ test("the report tool is registered, not skill-projected", async () => {
76
+ await initializeTools();
77
+ const names = getAllToolDefinitions().map((def) => def.name);
78
+ // A skill-projected tool would need its skill loaded first, and the
79
+ // retrospective's `skill_load` is itself denied in a clientless wake.
80
+ expect(names).toContain("watch_retro_report");
81
+ });
82
+ });
@@ -0,0 +1,235 @@
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 { Database } from "bun:sqlite";
11
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
12
+
13
+ import { repairRenamedFireworksDeepseekProModelIdMigration } from "../workspace/migrations/151-repair-renamed-fireworks-deepseek-pro-model-id.js";
14
+ import { WORKSPACE_MIGRATIONS } from "../workspace/migrations/registry.js";
15
+ import { assertNotLiveDb } from "./assert-not-live-db.js";
16
+
17
+ const STALE = "accounts/fireworks/models/deepseek-v4-pro";
18
+ const REPLACEMENT = "accounts/fireworks/models/deepseek-v4-pro-0813";
19
+
20
+ let workspaceDir: string;
21
+
22
+ function freshWorkspace(): void {
23
+ workspaceDir = join(
24
+ tmpdir(),
25
+ `vellum-migration-151-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
26
+ );
27
+ mkdirSync(workspaceDir, { recursive: true });
28
+ }
29
+
30
+ function writeConfig(data: Record<string, unknown>): void {
31
+ writeFileSync(
32
+ join(workspaceDir, "config.json"),
33
+ JSON.stringify(data, null, 2) + "\n",
34
+ );
35
+ }
36
+
37
+ function readConfig(): Record<string, unknown> {
38
+ return JSON.parse(readFileSync(join(workspaceDir, "config.json"), "utf-8"));
39
+ }
40
+
41
+ function seedRows(rows: Array<{ name: string; provider: string }>): void {
42
+ mkdirSync(join(workspaceDir, "data", "db"), { recursive: true });
43
+ const db = new Database(join(workspaceDir, "data", "db", "assistant.db"));
44
+ db.run(`CREATE TABLE IF NOT EXISTS provider_connections (
45
+ name TEXT PRIMARY KEY,
46
+ provider TEXT NOT NULL,
47
+ auth TEXT NOT NULL,
48
+ created_at INTEGER NOT NULL,
49
+ updated_at INTEGER NOT NULL
50
+ )`);
51
+ for (const row of rows) {
52
+ db.query(
53
+ `INSERT INTO provider_connections (name, provider, auth, created_at, updated_at) VALUES (?, ?, '{"type":"api_key"}', 1, 1)`,
54
+ ).run(row.name, row.provider);
55
+ }
56
+ db.close();
57
+ }
58
+
59
+ beforeEach(() => {
60
+ freshWorkspace();
61
+ });
62
+
63
+ afterEach(() => {
64
+ if (existsSync(workspaceDir)) {
65
+ assertNotLiveDb(workspaceDir);
66
+ rmSync(workspaceDir, { recursive: true, force: true });
67
+ }
68
+ });
69
+
70
+ describe("151-repair-renamed-fireworks-deepseek-pro-model-id migration", () => {
71
+ test("has correct migration id and is registered", () => {
72
+ expect(repairRenamedFireworksDeepseekProModelIdMigration.id).toBe(
73
+ "151-repair-renamed-fireworks-deepseek-pro-model-id",
74
+ );
75
+ expect(WORKSPACE_MIGRATIONS.map((m) => m.id)).toContain(
76
+ "151-repair-renamed-fireworks-deepseek-pro-model-id",
77
+ );
78
+ });
79
+
80
+ test("repairs the stale ID in default, call sites, and profiles", () => {
81
+ writeConfig({
82
+ llm: {
83
+ default: { provider: "fireworks", model: STALE },
84
+ callSites: {
85
+ recall: { model: STALE, maxTokens: 4096 },
86
+ heartbeat: { model: `${STALE}-tuned` },
87
+ malformed: STALE,
88
+ },
89
+ profiles: {
90
+ "cost-optimized": { provider: "vellum", model: STALE },
91
+ legacy: { model: STALE },
92
+ },
93
+ },
94
+ });
95
+
96
+ repairRenamedFireworksDeepseekProModelIdMigration.run(workspaceDir);
97
+
98
+ const llm = readConfig().llm as Record<string, any>;
99
+ expect(llm.default.model).toBe(REPLACEMENT);
100
+ expect(llm.callSites.recall.model).toBe(REPLACEMENT);
101
+ expect(llm.callSites.recall.maxTokens).toBe(4096);
102
+ // Non-exact matches and malformed leaves are untouched. The dated
103
+ // replacement itself extends the stale ID, so the exact match must not
104
+ // fire on already-repaired leaves.
105
+ expect(llm.callSites.heartbeat.model).toBe(`${STALE}-tuned`);
106
+ expect(llm.callSites.malformed).toBe(STALE);
107
+ // Managed profiles stamped provider "vellum" carry Fireworks model IDs.
108
+ expect(llm.profiles["cost-optimized"].model).toBe(REPLACEMENT);
109
+ expect(llm.profiles.legacy.model).toBe(REPLACEMENT);
110
+ });
111
+
112
+ test("repairs entry-bound fragments whose row kind is fireworks or vellum", () => {
113
+ seedRows([
114
+ { name: "my-fireworks", provider: "fireworks" },
115
+ { name: "managed-alt", provider: "vellum" },
116
+ ]);
117
+ writeConfig({
118
+ llm: {
119
+ profiles: {
120
+ bound: { provider: "my-fireworks", model: STALE },
121
+ managed: { provider: "managed-alt", model: STALE },
122
+ },
123
+ },
124
+ });
125
+
126
+ repairRenamedFireworksDeepseekProModelIdMigration.run(workspaceDir);
127
+
128
+ const llm = readConfig().llm as Record<string, any>;
129
+ expect(llm.profiles.bound.model).toBe(REPLACEMENT);
130
+ expect(llm.profiles.managed.model).toBe(REPLACEMENT);
131
+ });
132
+
133
+ test("leaves fragments with an explicit other provider untouched", () => {
134
+ seedRows([{ name: "byo-compat", provider: "openai-compatible" }]);
135
+ writeConfig({
136
+ llm: {
137
+ default: { provider: "openai-compatible", model: STALE },
138
+ profiles: {
139
+ byo: { provider: "openai-compatible", model: STALE },
140
+ // Entry-bound profile whose row kind is not repairable.
141
+ compatBound: { provider: "byo-compat", model: STALE },
142
+ // Entry name with no row: dangling, so nothing proves it is a
143
+ // fireworks-kind route.
144
+ dangling: { provider: "ghost", model: STALE },
145
+ },
146
+ },
147
+ });
148
+
149
+ repairRenamedFireworksDeepseekProModelIdMigration.run(workspaceDir);
150
+
151
+ const llm = readConfig().llm as Record<string, any>;
152
+ expect(llm.default.model).toBe(STALE);
153
+ expect(llm.profiles.byo.model).toBe(STALE);
154
+ expect(llm.profiles.compatBound.model).toBe(STALE);
155
+ expect(llm.profiles.dangling.model).toBe(STALE);
156
+ });
157
+
158
+ test("leaves entry-name providers untouched when no DB file exists", () => {
159
+ writeConfig({
160
+ llm: {
161
+ profiles: { bound: { provider: "my-fireworks", model: STALE } },
162
+ },
163
+ });
164
+
165
+ repairRenamedFireworksDeepseekProModelIdMigration.run(workspaceDir);
166
+
167
+ const llm = readConfig().llm as Record<string, any>;
168
+ expect(llm.profiles.bound.model).toBe(STALE);
169
+ });
170
+
171
+ test("throws when an entry-name provider needs rows and the DB is unreadable", () => {
172
+ // A DB file without the provider_connections table is unqueryable, so
173
+ // the run must fail (and retry later) instead of skipping the profile.
174
+ mkdirSync(join(workspaceDir, "data", "db"), { recursive: true });
175
+ writeFileSync(join(workspaceDir, "data", "db", "assistant.db"), "");
176
+ writeConfig({
177
+ llm: {
178
+ profiles: { bound: { provider: "my-fireworks", model: STALE } },
179
+ },
180
+ });
181
+
182
+ expect(() =>
183
+ repairRenamedFireworksDeepseekProModelIdMigration.run(workspaceDir),
184
+ ).toThrow();
185
+ const llm = readConfig().llm as Record<string, any>;
186
+ expect(llm.profiles.bound.model).toBe(STALE);
187
+
188
+ // Vendor and absent providers never need the rows, so the same broken
189
+ // DB does not block their repair.
190
+ writeConfig({
191
+ llm: {
192
+ default: { provider: "fireworks", model: STALE },
193
+ profiles: { legacy: { model: STALE } },
194
+ },
195
+ });
196
+ repairRenamedFireworksDeepseekProModelIdMigration.run(workspaceDir);
197
+ const repaired = readConfig().llm as Record<string, any>;
198
+ expect(repaired.default.model).toBe(REPLACEMENT);
199
+ expect(repaired.profiles.legacy.model).toBe(REPLACEMENT);
200
+ });
201
+
202
+ test("is idempotent and a no-op without the stale ID", () => {
203
+ writeConfig({
204
+ llm: {
205
+ default: { provider: "fireworks", model: STALE },
206
+ profiles: { fine: { provider: "fireworks", model: REPLACEMENT } },
207
+ },
208
+ });
209
+
210
+ repairRenamedFireworksDeepseekProModelIdMigration.run(workspaceDir);
211
+ const first = readConfig();
212
+ repairRenamedFireworksDeepseekProModelIdMigration.run(workspaceDir);
213
+ expect(readConfig()).toEqual(first);
214
+
215
+ const llm = first.llm as Record<string, any>;
216
+ expect(llm.default.model).toBe(REPLACEMENT);
217
+ expect(llm.profiles.fine.model).toBe(REPLACEMENT);
218
+ });
219
+
220
+ test("handles missing config, missing llm block, and invalid JSON", () => {
221
+ // No config.json at all.
222
+ expect(() =>
223
+ repairRenamedFireworksDeepseekProModelIdMigration.run(workspaceDir),
224
+ ).not.toThrow();
225
+
226
+ writeConfig({ theme: "dark" });
227
+ repairRenamedFireworksDeepseekProModelIdMigration.run(workspaceDir);
228
+ expect(readConfig()).toEqual({ theme: "dark" });
229
+
230
+ writeFileSync(join(workspaceDir, "config.json"), "{not json");
231
+ expect(() =>
232
+ repairRenamedFireworksDeepseekProModelIdMigration.run(workspaceDir),
233
+ ).not.toThrow();
234
+ });
235
+ });