@vellumai/assistant 0.6.5 → 0.6.6

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 (443) hide show
  1. package/AGENTS.md +9 -1
  2. package/ARCHITECTURE.md +15 -17
  3. package/Dockerfile +6 -4
  4. package/__tests__/permissions/gateway-threshold-reader.test.ts +283 -0
  5. package/docs/architecture/integrations.md +32 -39
  6. package/docs/architecture/memory.md +25 -30
  7. package/docs/architecture/security.md +7 -6
  8. package/docs/browser-use-architecture-phase2.md +63 -20
  9. package/docs/plugins.md +761 -0
  10. package/examples/plugins/echo/README.md +132 -0
  11. package/examples/plugins/echo/package.json +17 -0
  12. package/examples/plugins/echo/register.ts +187 -0
  13. package/node_modules/@vellumai/egress-proxy/src/types.ts +19 -0
  14. package/openapi.yaml +212 -68
  15. package/package.json +1 -1
  16. package/src/__tests__/app-compiler.test.ts +57 -0
  17. package/src/__tests__/approval-cascade.test.ts +7 -2
  18. package/src/__tests__/auto-analysis-end-to-end.test.ts +1 -0
  19. package/src/__tests__/avatar-generator.test.ts +4 -2
  20. package/src/__tests__/bundled-asset.test.ts +6 -6
  21. package/src/__tests__/catalog-cache.test.ts +69 -0
  22. package/src/__tests__/checker.test.ts +459 -171
  23. package/src/__tests__/circuit-breaker-pipeline.test.ts +406 -0
  24. package/src/__tests__/compaction-events.test.ts +501 -0
  25. package/src/__tests__/compaction-pipeline.test.ts +210 -0
  26. package/src/__tests__/compaction-strip-metadata-clear.test.ts +181 -0
  27. package/src/__tests__/compaction-timeout-recovery.test.ts +262 -0
  28. package/src/__tests__/config-model-image-provider.test.ts +110 -0
  29. package/src/__tests__/config-schema.test.ts +22 -9
  30. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +0 -4
  31. package/src/__tests__/contacts-tools.test.ts +26 -0
  32. package/src/__tests__/context-overflow-policy.test.ts +7 -7
  33. package/src/__tests__/context-window-manager.test.ts +355 -4
  34. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  35. package/src/__tests__/conversation-agent-loop-overflow.test.ts +26 -30
  36. package/src/__tests__/conversation-agent-loop.test.ts +30 -141
  37. package/src/__tests__/conversation-confirmation-signals.test.ts +6 -1
  38. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  39. package/src/__tests__/conversation-init.benchmark.test.ts +2 -16
  40. package/src/__tests__/conversation-pairing.test.ts +174 -10
  41. package/src/__tests__/conversation-pre-run-repair.test.ts +4 -1
  42. package/src/__tests__/conversation-process-callsite.test.ts +3 -0
  43. package/src/__tests__/conversation-provider-retry-repair.test.ts +16 -7
  44. package/src/__tests__/conversation-queue.test.ts +29 -14
  45. package/src/__tests__/conversation-routes-disk-view.test.ts +7 -6
  46. package/src/__tests__/conversation-runtime-assembly.test.ts +155 -110
  47. package/src/__tests__/conversation-runtime-workspace.test.ts +23 -38
  48. package/src/__tests__/conversation-seed-composer.test.ts +2 -2
  49. package/src/__tests__/conversation-slash-queue.test.ts +7 -2
  50. package/src/__tests__/conversation-slash-unknown.test.ts +25 -2
  51. package/src/__tests__/conversation-speed-override.test.ts +6 -1
  52. package/src/__tests__/conversation-title-service.test.ts +116 -0
  53. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +41 -2
  54. package/src/__tests__/conversation-usage.test.ts +1 -1
  55. package/src/__tests__/conversation-workspace-cache-state.test.ts +4 -1
  56. package/src/__tests__/conversation-workspace-injection.test.ts +3 -0
  57. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +4 -1
  58. package/src/__tests__/credential-health-service.test.ts +78 -9
  59. package/src/__tests__/credential-security-invariants.test.ts +2 -2
  60. package/src/__tests__/db-schedule-syntax-migration.test.ts +1 -0
  61. package/src/__tests__/empty-response-pipeline.test.ts +305 -0
  62. package/src/__tests__/extension-id-sync-guard.test.ts +3 -3
  63. package/src/__tests__/first-greeting.test.ts +247 -5
  64. package/src/__tests__/headless-browser-mode.test.ts +57 -0
  65. package/src/__tests__/history-repair-pipeline.test.ts +399 -0
  66. package/src/__tests__/host-browser-e2e-cloud.test.ts +307 -0
  67. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +3 -3
  68. package/src/__tests__/host-proxy-interface.test.ts +36 -2
  69. package/src/__tests__/image-credentials.test.ts +137 -0
  70. package/src/__tests__/image-service-dispatcher.test.ts +186 -0
  71. package/src/__tests__/injector-chain.test.ts +526 -0
  72. package/src/__tests__/intent-routing.test.ts +0 -26
  73. package/src/__tests__/llm-call-pipeline.test.ts +285 -0
  74. package/src/__tests__/llm-schema.test.ts +1 -1
  75. package/src/__tests__/media-generate-image.test.ts +119 -13
  76. package/src/__tests__/memory-retrieval-pipeline.test.ts +401 -0
  77. package/src/__tests__/memory-upsert-concurrency.test.ts +1 -0
  78. package/src/__tests__/migration-import-from-url.test.ts +5 -68
  79. package/src/__tests__/model-intents.test.ts +4 -2
  80. package/src/__tests__/notification-broadcaster.test.ts +3 -3
  81. package/src/__tests__/notification-decision-strategy.test.ts +0 -11
  82. package/src/__tests__/notification-schedule-notify-dedup.test.ts +108 -0
  83. package/src/__tests__/oauth-apps-routes.test.ts +1 -1
  84. package/src/__tests__/oauth-cli.test.ts +14 -12
  85. package/src/__tests__/oauth-connect-orchestrator.test.ts +4 -13
  86. package/src/__tests__/oauth-provider-serializer.test.ts +6 -4
  87. package/src/__tests__/oauth-provider-visibility.test.ts +3 -5
  88. package/src/__tests__/oauth-providers-routes.test.ts +3 -2
  89. package/src/__tests__/oauth-store.test.ts +41 -76
  90. package/src/__tests__/onboarding-template-contract.test.ts +16 -64
  91. package/src/__tests__/openai-image-service.test.ts +368 -0
  92. package/src/__tests__/overflow-reduce-pipeline.test.ts +676 -0
  93. package/src/__tests__/permission-checker-host-gate.test.ts +0 -24
  94. package/src/__tests__/persist-onboarding-artifacts.test.ts +266 -0
  95. package/src/__tests__/persistence-pipeline.test.ts +377 -0
  96. package/src/__tests__/pipeline-runner.test.ts +565 -0
  97. package/src/__tests__/platform.test.ts +5 -2
  98. package/src/__tests__/plugin-bootstrap.test.ts +483 -0
  99. package/src/__tests__/plugin-registry.test.ts +273 -0
  100. package/src/__tests__/plugin-route-contribution.test.ts +288 -0
  101. package/src/__tests__/plugin-skill-contribution.test.ts +367 -0
  102. package/src/__tests__/plugin-tool-contribution.test.ts +286 -0
  103. package/src/__tests__/plugin-types.test.ts +320 -0
  104. package/src/__tests__/pricing.test.ts +44 -12
  105. package/src/__tests__/proxy-approval-callback.test.ts +69 -8
  106. package/src/__tests__/reaction-persistence.test.ts +1 -0
  107. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
  108. package/src/__tests__/registry.test.ts +0 -2
  109. package/src/__tests__/schedule-routes.test.ts +131 -1
  110. package/src/__tests__/scheduler-recurrence.test.ts +14 -70
  111. package/src/__tests__/scheduler-reuse-conversation.test.ts +10 -50
  112. package/src/__tests__/secret-detection-handler.test.ts +0 -10
  113. package/src/__tests__/shell-identity.test.ts +0 -134
  114. package/src/__tests__/suggestion-routes.test.ts +103 -4
  115. package/src/__tests__/task-memory-cleanup.test.ts +1 -0
  116. package/src/__tests__/task-scheduler.test.ts +3 -15
  117. package/src/__tests__/test-preload.ts +11 -0
  118. package/src/__tests__/title-generate-pipeline.test.ts +224 -0
  119. package/src/__tests__/token-estimate-pipeline.test.ts +431 -0
  120. package/src/__tests__/tool-error-pipeline.test.ts +244 -0
  121. package/src/__tests__/tool-execute-pipeline.test.ts +431 -0
  122. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -6
  123. package/src/__tests__/tool-executor-shell-integration.test.ts +7 -10
  124. package/src/__tests__/tool-executor.test.ts +141 -0
  125. package/src/__tests__/tool-result-truncate-pipeline.test.ts +356 -0
  126. package/src/__tests__/tool-result-truncation.test.ts +0 -110
  127. package/src/__tests__/user-plugin-loader.test.ts +191 -0
  128. package/src/__tests__/workspace-migration-046-seed-conversation-starters-callsite.test.ts +185 -0
  129. package/src/__tests__/workspace-migration-049-release-notes-default-sonnet.test.ts +100 -0
  130. package/src/__tests__/workspace-migration-050-seed-main-agent-opus-callsite.test.ts +171 -0
  131. package/src/__tests__/workspace-migration-051-seed-conversation-summarization-callsite.test.ts +252 -0
  132. package/src/__tests__/workspace-migration-remove-hooks.test.ts +99 -0
  133. package/src/__tests__/workspace-policy.test.ts +21 -3
  134. package/src/agent/loop.ts +340 -102
  135. package/src/approvals/__tests__/guardian-feed-event.test.ts +304 -0
  136. package/src/approvals/guardian-request-resolvers.ts +80 -0
  137. package/src/backup/__tests__/backup-worker.test.ts +2 -13
  138. package/src/backup/backup-worker.ts +3 -15
  139. package/src/bundler/app-compiler.ts +84 -1
  140. package/src/calls/call-state.ts +2 -2
  141. package/src/channels/__tests__/types.test.ts +3 -3
  142. package/src/channels/types.ts +6 -4
  143. package/src/cli/__tests__/notifications.test.ts +87 -211
  144. package/src/cli/commands/__tests__/backup.test.ts +1 -1
  145. package/src/cli/commands/__tests__/image-generation.test.ts +255 -35
  146. package/src/cli/commands/__tests__/inference-send.test.ts +12 -0
  147. package/src/cli/commands/__tests__/tts-synthesize.test.ts +12 -0
  148. package/src/cli/commands/backup.ts +2 -2
  149. package/src/cli/commands/clients.ts +138 -0
  150. package/src/cli/commands/completions.ts +2 -9
  151. package/src/cli/commands/conversations.ts +55 -7
  152. package/src/cli/commands/image-generation.ts +33 -34
  153. package/src/cli/commands/notifications.ts +68 -103
  154. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -1
  155. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
  156. package/src/cli/commands/oauth/connect.ts +2 -2
  157. package/src/cli/commands/oauth/providers.ts +176 -8
  158. package/src/cli/commands/oauth/status.ts +46 -36
  159. package/src/cli/commands/skills.ts +3 -4
  160. package/src/cli/program.ts +25 -29
  161. package/src/config/__tests__/backup-schema.test.ts +7 -2
  162. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  163. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +10 -10
  164. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +66 -87
  165. package/src/config/bundled-skills/contacts/tools/contact-search.ts +28 -51
  166. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +22 -40
  167. package/src/config/bundled-skills/image-studio/SKILL.md +2 -1
  168. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -1
  169. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +23 -39
  170. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  171. package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +207 -0
  172. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +12 -0
  173. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +58 -0
  174. package/src/config/bundled-skills/schedule/SKILL.md +8 -3
  175. package/src/config/bundled-skills/schedule/TOOLS.json +15 -7
  176. package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +59 -0
  177. package/src/config/bundled-tool-registry.ts +0 -15
  178. package/src/config/feature-flag-registry.json +17 -1
  179. package/src/config/schema.ts +19 -0
  180. package/src/config/schemas/backup.ts +1 -1
  181. package/src/config/schemas/conversations.ts +16 -0
  182. package/src/config/schemas/llm.ts +2 -3
  183. package/src/config/schemas/security.ts +6 -6
  184. package/src/config/schemas/tts.ts +11 -0
  185. package/src/config/skill-state.ts +6 -2
  186. package/src/config/skills.ts +94 -5
  187. package/src/context/__tests__/compact-prompt.test.ts +27 -9
  188. package/src/context/prompts/compact.md +26 -12
  189. package/src/context/tool-result-truncation.ts +3 -63
  190. package/src/context/window-manager.ts +190 -16
  191. package/src/credential-health/credential-health-service.ts +19 -6
  192. package/src/daemon/__tests__/conversation-feed-event.test.ts +317 -0
  193. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +4 -12
  194. package/src/daemon/__tests__/conversation-tool-setup.test.ts +14 -15
  195. package/src/daemon/config-watcher.ts +0 -2
  196. package/src/daemon/context-overflow-policy.ts +4 -13
  197. package/src/daemon/conversation-agent-loop-handlers.ts +83 -22
  198. package/src/daemon/conversation-agent-loop.ts +984 -683
  199. package/src/daemon/conversation-history.ts +10 -19
  200. package/src/daemon/conversation-lifecycle.ts +37 -19
  201. package/src/daemon/conversation-notifiers.ts +2 -110
  202. package/src/daemon/conversation-process.ts +14 -7
  203. package/src/daemon/conversation-runtime-assembly.ts +532 -411
  204. package/src/daemon/conversation-tool-setup.ts +41 -4
  205. package/src/daemon/conversation.ts +80 -35
  206. package/src/daemon/external-plugins-bootstrap.ts +478 -0
  207. package/src/daemon/first-greeting.ts +191 -14
  208. package/src/daemon/handlers/config-model.ts +11 -0
  209. package/src/daemon/handlers/skills.ts +5 -1
  210. package/src/daemon/lifecycle.ts +33 -68
  211. package/src/daemon/message-types/computer-use.ts +2 -34
  212. package/src/daemon/message-types/conversations.ts +49 -0
  213. package/src/daemon/message-types/messages.ts +12 -0
  214. package/src/daemon/server.ts +5 -3
  215. package/src/daemon/shutdown-handlers.ts +2 -12
  216. package/src/daemon/tool-side-effects.ts +14 -56
  217. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +160 -0
  218. package/src/heartbeat/heartbeat-service.ts +24 -1
  219. package/src/home/__tests__/feed-population-integration.test.ts +312 -0
  220. package/src/home/emit-feed-event.ts +7 -0
  221. package/src/home/feed-types.ts +41 -2
  222. package/src/home/rewrite-command-preview.ts +66 -0
  223. package/src/ipc/__tests__/socket-path.test.ts +11 -50
  224. package/src/ipc/cli-client.ts +1 -1
  225. package/src/ipc/cli-server.ts +3 -3
  226. package/src/ipc/gateway-client.ts +4 -1
  227. package/src/ipc/routes/browser-context.ts +2 -0
  228. package/src/ipc/routes/browser.ts +1 -0
  229. package/src/ipc/routes/get-contact.ts +16 -0
  230. package/src/ipc/routes/index.ts +14 -0
  231. package/src/ipc/routes/list-clients.ts +31 -0
  232. package/src/ipc/routes/merge-contacts.ts +17 -0
  233. package/src/ipc/routes/notification.ts +133 -0
  234. package/src/ipc/routes/rename-conversation.ts +59 -0
  235. package/src/ipc/routes/search-contacts.ts +19 -0
  236. package/src/ipc/routes/upsert-contact.ts +25 -0
  237. package/src/ipc/socket-path.ts +14 -38
  238. package/src/media/app-icon-generator.ts +23 -46
  239. package/src/media/avatar-router.ts +26 -41
  240. package/src/media/gemini-image-service.ts +8 -41
  241. package/src/media/image-credentials.ts +73 -0
  242. package/src/media/image-service.ts +85 -0
  243. package/src/media/openai-image-service.ts +131 -0
  244. package/src/media/types.ts +46 -0
  245. package/src/memory/conversation-crud.ts +48 -18
  246. package/src/memory/conversation-queries.ts +57 -4
  247. package/src/memory/conversation-title-service.ts +25 -0
  248. package/src/memory/db-init.ts +8 -0
  249. package/src/memory/embedding-gemini.test.ts +41 -2
  250. package/src/memory/embedding-gemini.ts +6 -1
  251. package/src/memory/graph/bootstrap.test.ts +282 -0
  252. package/src/memory/graph/bootstrap.ts +8 -5
  253. package/src/memory/graph/extraction.ts +10 -2
  254. package/src/memory/graph/graph-search.test.ts +1 -0
  255. package/src/memory/graph/inspect.ts +2 -2
  256. package/src/memory/graph/retriever.ts +10 -3
  257. package/src/memory/migrations/041-approval-prompt-ts-tracker.ts +26 -0
  258. package/src/memory/migrations/149-oauth-tables.ts +1 -0
  259. package/src/memory/migrations/223-schedule-script-column.ts +11 -0
  260. package/src/memory/migrations/224-oauth-providers-managed-service-is-paid.ts +24 -0
  261. package/src/memory/migrations/225-oauth-providers-available-scopes.ts +13 -0
  262. package/src/memory/migrations/index.ts +4 -0
  263. package/src/memory/pkb/pkb-index.test.ts +1 -0
  264. package/src/memory/pkb/pkb-reconcile.test.ts +1 -0
  265. package/src/memory/pkb/pkb-search.test.ts +65 -4
  266. package/src/memory/pkb/pkb-search.ts +40 -18
  267. package/src/memory/qdrant-client.test.ts +60 -0
  268. package/src/memory/qdrant-client.ts +25 -0
  269. package/src/memory/schema/infrastructure.ts +1 -0
  270. package/src/memory/schema/oauth.ts +4 -1
  271. package/src/messaging/providers/slack/render-transcript.test.ts +77 -29
  272. package/src/messaging/providers/slack/render-transcript.ts +58 -0
  273. package/src/notifications/conversation-pairing.ts +78 -19
  274. package/src/notifications/copy-composer.ts +0 -5
  275. package/src/notifications/emit-signal.ts +1 -1
  276. package/src/notifications/signal.ts +1 -2
  277. package/src/oauth/AGENTS.md +1 -1
  278. package/src/oauth/__tests__/identity-verifier.test.ts +2 -1
  279. package/src/oauth/connect-orchestrator.ts +8 -34
  280. package/src/oauth/connect-types.ts +6 -10
  281. package/src/oauth/manual-token-connection.ts +23 -0
  282. package/src/oauth/oauth-store.ts +30 -14
  283. package/src/oauth/provider-serializer.ts +6 -1
  284. package/src/oauth/seed-providers.ts +56 -108
  285. package/src/outbound-proxy/http-forwarder.ts +9 -0
  286. package/src/permissions/approval-policy.test.ts +293 -18
  287. package/src/permissions/approval-policy.ts +110 -58
  288. package/src/permissions/arg-parser.test.ts +161 -0
  289. package/src/permissions/arg-parser.ts +141 -0
  290. package/src/permissions/bash-risk-classifier.test.ts +414 -2
  291. package/src/permissions/bash-risk-classifier.ts +303 -60
  292. package/src/permissions/checker.ts +157 -29
  293. package/src/permissions/command-registry.test.ts +239 -0
  294. package/src/permissions/command-registry.ts +234 -54
  295. package/src/permissions/defaults.ts +5 -4
  296. package/src/permissions/gateway-threshold-reader.ts +196 -0
  297. package/src/permissions/prompter.ts +4 -0
  298. package/src/permissions/risk-types.ts +61 -4
  299. package/src/permissions/schedule-risk-classifier.test.ts +129 -0
  300. package/src/permissions/schedule-risk-classifier.ts +85 -0
  301. package/src/permissions/shell-identity.ts +2 -42
  302. package/src/permissions/types.ts +2 -0
  303. package/src/permissions/workspace-policy.ts +8 -3
  304. package/src/plugins/defaults/circuit-breaker.ts +146 -0
  305. package/src/plugins/defaults/compaction.ts +145 -0
  306. package/src/plugins/defaults/empty-response.ts +126 -0
  307. package/src/plugins/defaults/history-repair.ts +85 -0
  308. package/src/plugins/defaults/index.ts +116 -0
  309. package/src/plugins/defaults/injectors.ts +491 -0
  310. package/src/plugins/defaults/llm-call.ts +82 -0
  311. package/src/plugins/defaults/memory-retrieval.ts +226 -0
  312. package/src/plugins/defaults/overflow-reduce.ts +181 -0
  313. package/src/plugins/defaults/persistence.ts +129 -0
  314. package/src/plugins/defaults/title-generate.ts +95 -0
  315. package/src/plugins/defaults/token-estimate.ts +104 -0
  316. package/src/plugins/defaults/tool-error.ts +126 -0
  317. package/src/plugins/defaults/tool-execute.ts +89 -0
  318. package/src/plugins/defaults/tool-result-truncate.ts +88 -0
  319. package/src/plugins/pipeline.ts +316 -0
  320. package/src/plugins/plugin-skill-contributions.ts +292 -0
  321. package/src/plugins/registry.ts +241 -0
  322. package/src/plugins/types.ts +1134 -0
  323. package/src/plugins/user-loader.ts +177 -0
  324. package/src/prompts/templates/BOOTSTRAP.md +27 -77
  325. package/src/providers/model-catalog.ts +52 -29
  326. package/src/providers/model-intents.ts +1 -1
  327. package/src/providers/openrouter/client.ts +5 -1
  328. package/src/providers/speech-to-text/deepgram-realtime.test.ts +61 -0
  329. package/src/providers/speech-to-text/deepgram-realtime.ts +57 -0
  330. package/src/providers/speech-to-text/xai-realtime.test.ts +72 -4
  331. package/src/providers/speech-to-text/xai-realtime.ts +39 -14
  332. package/src/runtime/AGENTS.md +25 -16
  333. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +3 -3
  334. package/src/runtime/__tests__/client-registry.test.ts +293 -0
  335. package/src/runtime/client-registry.ts +261 -0
  336. package/src/runtime/http-server.ts +77 -8
  337. package/src/runtime/http-types.ts +0 -2
  338. package/src/runtime/migrations/vbundle-builder.ts +1 -22
  339. package/src/runtime/routes/approval-prompt-ts-tracker.ts +51 -31
  340. package/src/runtime/routes/approval-routes.ts +17 -0
  341. package/src/runtime/routes/browser-extension-pair-routes.ts +27 -8
  342. package/src/runtime/routes/conversation-routes.ts +223 -116
  343. package/src/runtime/routes/inbound-message-handler.ts +88 -13
  344. package/src/runtime/routes/memory-item-routes.test.ts +1 -0
  345. package/src/runtime/routes/migration-routes.ts +0 -3
  346. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +284 -0
  347. package/src/runtime/routes/playground/__tests__/guard.test.ts +80 -0
  348. package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +294 -0
  349. package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +271 -0
  350. package/src/runtime/routes/playground/__tests__/seed-conversation.test.ts +202 -0
  351. package/src/runtime/routes/playground/__tests__/seeded-conversations.test.ts +309 -0
  352. package/src/runtime/routes/playground/__tests__/state.test.ts +224 -0
  353. package/src/runtime/routes/playground/conversation-not-found.ts +29 -0
  354. package/src/runtime/routes/playground/deps.ts +56 -0
  355. package/src/runtime/routes/playground/force-compact.ts +73 -0
  356. package/src/runtime/routes/playground/guard.ts +37 -0
  357. package/src/runtime/routes/playground/index.ts +28 -0
  358. package/src/runtime/routes/playground/inject-failures.ts +159 -0
  359. package/src/runtime/routes/playground/reset-circuit.ts +115 -0
  360. package/src/runtime/routes/playground/seed-conversation.ts +139 -0
  361. package/src/runtime/routes/playground/seeded-conversations.ts +78 -0
  362. package/src/runtime/routes/playground/state.ts +78 -0
  363. package/src/runtime/routes/schedule-routes.ts +89 -8
  364. package/src/runtime/skill-route-registry.ts +75 -15
  365. package/src/schedule/run-script.ts +68 -0
  366. package/src/schedule/schedule-store.ts +7 -1
  367. package/src/schedule/scheduler.ts +48 -8
  368. package/src/skills/catalog-cache.ts +12 -5
  369. package/src/tools/browser/__tests__/browser-status.test.ts +189 -0
  370. package/src/tools/browser/browser-execution.ts +88 -19
  371. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +230 -0
  372. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +146 -3
  373. package/src/tools/browser/cdp-client/extension-cdp-client.ts +54 -3
  374. package/src/tools/browser/cdp-client/factory.ts +15 -4
  375. package/src/tools/executor.ts +126 -74
  376. package/src/tools/network/script-proxy/session-manager.ts +37 -1
  377. package/src/tools/permission-checker.ts +98 -49
  378. package/src/tools/policy-context.ts +4 -0
  379. package/src/tools/registry.ts +140 -3
  380. package/src/tools/schedule/create.ts +23 -8
  381. package/src/tools/schedule/update.ts +3 -1
  382. package/src/tools/secret-detection-handler.ts +0 -51
  383. package/src/tools/system/avatar-generator.ts +6 -2
  384. package/src/tools/types.ts +28 -2
  385. package/src/util/platform.ts +7 -2
  386. package/src/util/pricing.ts +26 -3
  387. package/src/workspace/migrations/006-services-config.ts +2 -4
  388. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +2 -3
  389. package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +3 -4
  390. package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +108 -0
  391. package/src/workspace/migrations/047-remove-watch-callsites.ts +54 -0
  392. package/src/workspace/migrations/048-remove-workspace-hooks.ts +81 -0
  393. package/src/workspace/migrations/049-release-notes-default-sonnet.ts +80 -0
  394. package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +86 -0
  395. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +128 -0
  396. package/src/workspace/migrations/registry.ts +12 -0
  397. package/tsconfig.json +1 -1
  398. package/hook-templates/debug-prompt-logger/hook.json +0 -7
  399. package/hook-templates/debug-prompt-logger/run.sh +0 -66
  400. package/src/__tests__/compaction-circuit-breaker.test.ts +0 -336
  401. package/src/__tests__/context-overflow-approval.test.ts +0 -156
  402. package/src/__tests__/hooks-blocking.test.ts +0 -178
  403. package/src/__tests__/hooks-cli.test.ts +0 -182
  404. package/src/__tests__/hooks-config.test.ts +0 -108
  405. package/src/__tests__/hooks-discovery.test.ts +0 -211
  406. package/src/__tests__/hooks-integration.test.ts +0 -196
  407. package/src/__tests__/hooks-manager.test.ts +0 -226
  408. package/src/__tests__/hooks-runner.test.ts +0 -175
  409. package/src/__tests__/hooks-settings.test.ts +0 -160
  410. package/src/__tests__/hooks-templates.test.ts +0 -169
  411. package/src/__tests__/hooks-ts-runner.test.ts +0 -170
  412. package/src/__tests__/hooks-watch.test.ts +0 -112
  413. package/src/__tests__/notification-schedule-dedup.test.ts +0 -213
  414. package/src/__tests__/oauth-scope-policy.test.ts +0 -180
  415. package/src/__tests__/send-notification-tool.test.ts +0 -83
  416. package/src/cli/commands/shotgun.ts +0 -266
  417. package/src/config/bundled-skills/conversations/SKILL.md +0 -20
  418. package/src/config/bundled-skills/conversations/TOOLS.json +0 -23
  419. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +0 -88
  420. package/src/config/bundled-skills/heartbeat/SKILL.md +0 -43
  421. package/src/config/bundled-skills/notifications/SKILL.md +0 -40
  422. package/src/config/bundled-skills/notifications/TOOLS.json +0 -80
  423. package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -152
  424. package/src/config/bundled-skills/notifications/tools/shared.ts +0 -13
  425. package/src/config/bundled-skills/screen-watch/SKILL.md +0 -27
  426. package/src/config/bundled-skills/screen-watch/TOOLS.json +0 -35
  427. package/src/config/bundled-skills/screen-watch/tools/start-screen-watch.ts +0 -12
  428. package/src/config/bundled-skills/skills-catalog/SKILL.md +0 -84
  429. package/src/daemon/context-overflow-approval.ts +0 -52
  430. package/src/daemon/watch-handler.ts +0 -399
  431. package/src/hooks/cli.ts +0 -253
  432. package/src/hooks/config.ts +0 -100
  433. package/src/hooks/discovery.ts +0 -135
  434. package/src/hooks/manager.ts +0 -179
  435. package/src/hooks/runner.ts +0 -117
  436. package/src/hooks/templates.ts +0 -77
  437. package/src/hooks/types.ts +0 -75
  438. package/src/oauth/scope-policy.ts +0 -89
  439. package/src/runtime/gateway-internal-client.ts +0 -94
  440. package/src/runtime/routes/watch-routes.ts +0 -156
  441. package/src/signals/shotgun.ts +0 -203
  442. package/src/tools/watch/screen-watch.ts +0 -144
  443. package/src/tools/watch/watch-state.ts +0 -142
@@ -16,10 +16,10 @@ describe("onboarding template contracts", () => {
16
16
  expect(bootstrap).toMatch(/^_ Lines starting with _/);
17
17
  });
18
18
 
19
- test("contains identity discovery prompts", () => {
19
+ test("contains identity section", () => {
20
20
  const lower = bootstrap.toLowerCase();
21
21
  expect(lower).toContain("identity");
22
- expect(lower).toContain("infer");
22
+ expect(lower).toContain("colleague");
23
23
  });
24
24
 
25
25
  test("gathers user context", () => {
@@ -29,23 +29,16 @@ describe("onboarding template contracts", () => {
29
29
  expect(lower).toContain("tools");
30
30
  });
31
31
 
32
- test("contains wrapping-up criteria with deletion instructions", () => {
32
+ test("contains cleanup instructions with deletion", () => {
33
33
  const lower = bootstrap.toLowerCase();
34
- expect(lower).toContain("wrapping up");
34
+ expect(lower).toContain("wrap up");
35
35
  expect(lower).toContain("delete");
36
36
  expect(lower).toContain("bootstrap.md");
37
37
  });
38
38
 
39
- test("contains refusal policy", () => {
39
+ test("handles declined fields", () => {
40
40
  const lower = bootstrap.toLowerCase();
41
41
  expect(lower).toContain("declined");
42
- expect(lower).toContain("constraints");
43
- });
44
-
45
- test("defines field states as inferred or declined", () => {
46
- const lower = bootstrap.toLowerCase();
47
- expect(lower).toContain("inferred");
48
- expect(lower).toContain("declined");
49
42
  });
50
43
 
51
44
  test("instructs saving to IDENTITY.md, SOUL.md, and user persona file via file_edit", () => {
@@ -55,71 +48,30 @@ describe("onboarding template contracts", () => {
55
48
  expect(bootstrap).toContain("file_edit");
56
49
  });
57
50
 
58
- test("includes budget constraint", () => {
59
- expect(bootstrap).toContain("$2");
60
- expect(bootstrap).toContain("$5");
61
- });
62
-
63
- test("includes new colleague framing", () => {
64
- expect(bootstrap).toContain("new colleague");
51
+ test("contains core principle", () => {
52
+ expect(bootstrap).toContain("earns its keep");
65
53
  });
66
54
 
67
- test("contains numbered goals", () => {
55
+ test("contains opening move with onboarding context", () => {
68
56
  const lower = bootstrap.toLowerCase();
69
- expect(lower).toContain("establish mutual identity");
70
- expect(lower).toContain("prove value fast");
71
- expect(lower).toContain("infer, don't interrogate");
72
- expect(lower).toContain("surface what you learned");
73
- expect(lower).toContain("offer the next level");
74
- expect(lower).toContain("write everything immediately");
75
- expect(lower).toContain("clean up");
76
- });
77
-
78
- test("contains constraints section", () => {
79
- expect(bootstrap).toContain("## Constraints");
80
- expect(bootstrap).toContain("$2");
81
- expect(bootstrap).toContain("2 questions");
82
- expect(bootstrap.toLowerCase()).toContain("don't block on setup");
83
- expect(bootstrap).toContain("One-shot");
84
- });
85
-
86
- test("contains 'what you own' section", () => {
87
- const lower = bootstrap.toLowerCase();
88
- expect(lower).toContain("sequencing");
89
- expect(lower).toContain("pacing");
90
- });
91
-
92
- test("contains technical contract", () => {
93
- expect(bootstrap).toContain("Technical Contract");
94
- expect(bootstrap).toContain("prescribed");
57
+ expect(lower).toContain("onboarding");
58
+ expect(lower).toContain("json");
59
+ expect(lower).toContain("context");
95
60
  });
96
61
 
97
- test("contains capability unlock pattern", () => {
62
+ test("contains tone matching guidance", () => {
98
63
  const lower = bootstrap.toLowerCase();
99
- expect(lower).toContain("email");
100
- expect(lower).toContain("voice");
101
- expect(lower).toContain("slack");
102
- });
103
-
104
- test("contains tone guidance", () => {
105
- expect(bootstrap).toContain("Not servile");
106
- expect(bootstrap.toLowerCase()).toContain("match");
107
- expect(bootstrap.toLowerCase()).toContain("energy");
64
+ expect(lower).toContain("match");
65
+ expect(lower).toContain("energy");
108
66
  });
109
67
 
110
- test("contains pre-chat onboarding context section", () => {
111
- const lower = bootstrap.toLowerCase();
112
- expect(lower).toContain("onboarding");
113
- expect(lower).toContain("json");
114
- expect(lower).toContain("context");
68
+ test("is one-shot", () => {
69
+ expect(bootstrap).toContain("One-shot");
115
70
  });
116
71
 
117
72
  test("does not contain personality quiz references", () => {
118
- // BOOTSTRAP.md says "No personality quiz" as part of goal 3,
119
- // but should NOT contain instructions TO USE or SHOW a personality quiz
120
73
  expect(bootstrap).not.toMatch(/show.*personality quiz/i);
121
74
  expect(bootstrap).not.toMatch(/present.*personality quiz/i);
122
- // "dropdown" only appears in "No dropdown forms" — that's a prohibition, not an instruction
123
75
  expect(bootstrap).not.toMatch(/show.*dropdown/i);
124
76
  });
125
77
 
@@ -0,0 +1,368 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Mock openai + openai/uploads — must be before importing the service
5
+ // ---------------------------------------------------------------------------
6
+
7
+ interface FakeImagesResponse {
8
+ data?: Array<{ b64_json?: string }>;
9
+ }
10
+
11
+ let lastGenerateParams: Record<string, unknown> | null = null;
12
+ let lastEditParams: Record<string, unknown> | null = null;
13
+ let lastConstructorOptions: Record<string, unknown> | null = null;
14
+ let fakeResponse: FakeImagesResponse = { data: [] };
15
+ let shouldThrow: Error | null = null;
16
+ let generateCallCount = 0;
17
+ let editCallCount = 0;
18
+ let toFileCallCount = 0;
19
+ let toFileCalls: Array<{
20
+ input: unknown;
21
+ filename: string;
22
+ options?: { type?: string };
23
+ }> = [];
24
+
25
+ // Simulate OpenAI.APIError — the real SDK's APIError is a class attached as a
26
+ // static property on the default export.
27
+ class FakeAPIError extends Error {
28
+ status: number;
29
+ constructor(status: number, message: string) {
30
+ super(message);
31
+ this.status = status;
32
+ this.name = "APIError";
33
+ }
34
+ }
35
+
36
+ mock.module("openai", () => {
37
+ class MockOpenAI {
38
+ static APIError = FakeAPIError;
39
+ images: {
40
+ generate: (
41
+ params: Record<string, unknown>,
42
+ ) => Promise<FakeImagesResponse>;
43
+ edit: (params: Record<string, unknown>) => Promise<FakeImagesResponse>;
44
+ };
45
+ constructor(opts: Record<string, unknown>) {
46
+ lastConstructorOptions = opts;
47
+ this.images = {
48
+ generate: async (params: Record<string, unknown>) => {
49
+ lastGenerateParams = params;
50
+ generateCallCount++;
51
+ if (shouldThrow) throw shouldThrow;
52
+ return fakeResponse;
53
+ },
54
+ edit: async (params: Record<string, unknown>) => {
55
+ lastEditParams = params;
56
+ editCallCount++;
57
+ if (shouldThrow) throw shouldThrow;
58
+ return fakeResponse;
59
+ },
60
+ };
61
+ }
62
+ }
63
+ return { default: MockOpenAI, APIError: FakeAPIError };
64
+ });
65
+
66
+ // Sentinel value returned from mocked `toFile` so tests can verify it was used
67
+ // to wrap each source image before being passed to `images.edit`.
68
+ const TO_FILE_SENTINEL = Symbol("toFile-sentinel");
69
+
70
+ mock.module("openai/uploads", () => ({
71
+ toFile: async (
72
+ input: unknown,
73
+ filename: string,
74
+ options?: { type?: string },
75
+ ) => {
76
+ toFileCallCount++;
77
+ toFileCalls.push({ input, filename, options });
78
+ return { __sentinel: TO_FILE_SENTINEL, filename, options };
79
+ },
80
+ }));
81
+
82
+ // Import after mocking
83
+ import {
84
+ generateImageOpenAI,
85
+ mapOpenAIError,
86
+ } from "../media/openai-image-service.js";
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Helpers
90
+ // ---------------------------------------------------------------------------
91
+
92
+ function imageResponse(
93
+ ...entries: Array<{ b64_json?: string }>
94
+ ): FakeImagesResponse {
95
+ return { data: entries };
96
+ }
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // Tests
100
+ // ---------------------------------------------------------------------------
101
+
102
+ beforeEach(() => {
103
+ lastGenerateParams = null;
104
+ lastEditParams = null;
105
+ lastConstructorOptions = null;
106
+ fakeResponse = imageResponse({ b64_json: "abc123" });
107
+ shouldThrow = null;
108
+ generateCallCount = 0;
109
+ editCallCount = 0;
110
+ toFileCallCount = 0;
111
+ toFileCalls = [];
112
+ });
113
+
114
+ describe("generateImageOpenAI", () => {
115
+ test("generate mode returns a single variant", async () => {
116
+ fakeResponse = imageResponse({ b64_json: "abc123" });
117
+
118
+ const result = await generateImageOpenAI(
119
+ { type: "direct", apiKey: "test-key" },
120
+ { prompt: "a cat", mode: "generate" },
121
+ );
122
+
123
+ expect(generateCallCount).toBe(1);
124
+ expect(result.images).toHaveLength(1);
125
+ expect(result.images[0].mimeType).toBe("image/png");
126
+ expect(result.images[0].dataBase64).toBe("abc123");
127
+ expect(result.text).toBeUndefined();
128
+ expect(result.resolvedModel).toBe("gpt-image-2");
129
+ });
130
+
131
+ test("generate with n: 3 forwards the n param (not parallel calls)", async () => {
132
+ fakeResponse = imageResponse(
133
+ { b64_json: "a" },
134
+ { b64_json: "b" },
135
+ { b64_json: "c" },
136
+ );
137
+
138
+ const result = await generateImageOpenAI(
139
+ { type: "direct", apiKey: "test-key" },
140
+ { prompt: "a cat", mode: "generate", variants: 3 },
141
+ );
142
+
143
+ // Exactly one call; n forwarded to the SDK instead of calling thrice.
144
+ expect(generateCallCount).toBe(1);
145
+ expect((lastGenerateParams as Record<string, unknown>).n).toBe(3);
146
+ expect(result.images).toHaveLength(3);
147
+ expect(result.images.map((i) => i.dataBase64)).toEqual(["a", "b", "c"]);
148
+ });
149
+
150
+ test("variants are clamped to [1, MAX_VARIANTS]", async () => {
151
+ fakeResponse = imageResponse({ b64_json: "x" });
152
+
153
+ await generateImageOpenAI(
154
+ { type: "direct", apiKey: "test-key" },
155
+ { prompt: "test", mode: "generate", variants: 10 },
156
+ );
157
+ expect((lastGenerateParams as Record<string, unknown>).n).toBe(4);
158
+
159
+ await generateImageOpenAI(
160
+ { type: "direct", apiKey: "test-key" },
161
+ { prompt: "test", mode: "generate", variants: 0 },
162
+ );
163
+ expect((lastGenerateParams as Record<string, unknown>).n).toBe(1);
164
+ });
165
+
166
+ test("model falls back to gpt-image-2 when unknown", async () => {
167
+ fakeResponse = imageResponse({ b64_json: "x" });
168
+
169
+ await generateImageOpenAI(
170
+ { type: "direct", apiKey: "test-key" },
171
+ { prompt: "test", mode: "generate", model: "invalid-model" },
172
+ );
173
+
174
+ expect((lastGenerateParams as Record<string, unknown>).model).toBe(
175
+ "gpt-image-2",
176
+ );
177
+ });
178
+
179
+ test("edit mode with one source image calls toFile once and passes files[] to edit", async () => {
180
+ fakeResponse = imageResponse({ b64_json: "edited" });
181
+
182
+ await generateImageOpenAI(
183
+ { type: "direct", apiKey: "test-key" },
184
+ {
185
+ prompt: "remove background",
186
+ mode: "edit",
187
+ sourceImages: [{ mimeType: "image/jpeg", dataBase64: "srcdata" }],
188
+ },
189
+ );
190
+
191
+ expect(editCallCount).toBe(1);
192
+ expect(generateCallCount).toBe(0);
193
+ expect(toFileCallCount).toBe(1);
194
+ expect(toFileCalls[0].filename).toBe("input.png");
195
+ expect(toFileCalls[0].options).toEqual({ type: "image/jpeg" });
196
+
197
+ const editParams = lastEditParams as Record<string, unknown>;
198
+ expect(editParams.prompt).toBe("remove background");
199
+ const image = editParams.image as Array<Record<string, unknown>>;
200
+ expect(Array.isArray(image)).toBe(true);
201
+ expect(image).toHaveLength(1);
202
+ expect(image[0].__sentinel).toBe(TO_FILE_SENTINEL);
203
+ });
204
+
205
+ test("edit mode with multiple source images passes an array of files", async () => {
206
+ fakeResponse = imageResponse({ b64_json: "edited" });
207
+
208
+ await generateImageOpenAI(
209
+ { type: "direct", apiKey: "test-key" },
210
+ {
211
+ prompt: "merge",
212
+ mode: "edit",
213
+ sourceImages: [
214
+ { mimeType: "image/png", dataBase64: "one" },
215
+ { mimeType: "image/jpeg", dataBase64: "two" },
216
+ { mimeType: "image/webp", dataBase64: "three" },
217
+ ],
218
+ },
219
+ );
220
+
221
+ expect(toFileCallCount).toBe(3);
222
+ expect(toFileCalls[0].options?.type).toBe("image/png");
223
+ expect(toFileCalls[1].options?.type).toBe("image/jpeg");
224
+ expect(toFileCalls[2].options?.type).toBe("image/webp");
225
+
226
+ const editParams = lastEditParams as Record<string, unknown>;
227
+ const image = editParams.image as Array<Record<string, unknown>>;
228
+ expect(image).toHaveLength(3);
229
+ for (const entry of image) {
230
+ expect(entry.__sentinel).toBe(TO_FILE_SENTINEL);
231
+ }
232
+ });
233
+
234
+ test("direct credentials construct OpenAI without a baseURL", async () => {
235
+ fakeResponse = imageResponse({ b64_json: "x" });
236
+
237
+ await generateImageOpenAI(
238
+ { type: "direct", apiKey: "my-direct-key" },
239
+ { prompt: "test", mode: "generate" },
240
+ );
241
+
242
+ expect(lastConstructorOptions).not.toBeNull();
243
+ expect((lastConstructorOptions as Record<string, unknown>).apiKey).toBe(
244
+ "my-direct-key",
245
+ );
246
+ expect(
247
+ (lastConstructorOptions as Record<string, unknown>).baseURL,
248
+ ).toBeUndefined();
249
+ });
250
+
251
+ test("managed-proxy credentials set baseURL on the OpenAI client", async () => {
252
+ fakeResponse = imageResponse({ b64_json: "x" });
253
+
254
+ await generateImageOpenAI(
255
+ {
256
+ type: "managed-proxy",
257
+ assistantApiKey: "proxy-key",
258
+ baseUrl: "https://proxy.example.com/v1",
259
+ },
260
+ { prompt: "test", mode: "generate" },
261
+ );
262
+
263
+ expect(lastConstructorOptions).not.toBeNull();
264
+ expect((lastConstructorOptions as Record<string, unknown>).apiKey).toBe(
265
+ "proxy-key",
266
+ );
267
+ expect((lastConstructorOptions as Record<string, unknown>).baseURL).toBe(
268
+ "https://proxy.example.com/v1",
269
+ );
270
+ });
271
+
272
+ test("title is derived from the first 6 words of the prompt and sanitized", async () => {
273
+ fakeResponse = imageResponse({ b64_json: "one" }, { b64_json: "two" });
274
+
275
+ const result = await generateImageOpenAI(
276
+ { type: "direct", apiKey: "k" },
277
+ {
278
+ prompt: "A cute orange cat sleeping on a warm windowsill at sunset!",
279
+ mode: "generate",
280
+ variants: 2,
281
+ },
282
+ );
283
+
284
+ // First 6 words: "A cute orange cat sleeping on" -> sanitized
285
+ // (non-[\w\s-] stripped, whitespace -> '-', lowercased, sliced to 60).
286
+ expect(result.images).toHaveLength(2);
287
+ for (const img of result.images) {
288
+ expect(img.title).toBe("a-cute-orange-cat-sleeping-on");
289
+ }
290
+ });
291
+
292
+ test("title uses the whole prompt when it has fewer than 6 words", async () => {
293
+ fakeResponse = imageResponse({ b64_json: "x" });
294
+
295
+ const result = await generateImageOpenAI(
296
+ { type: "direct", apiKey: "k" },
297
+ { prompt: "tiny dog", mode: "generate" },
298
+ );
299
+
300
+ expect(result.images[0].title).toBe("tiny-dog");
301
+ });
302
+
303
+ test("entries without b64_json are skipped", async () => {
304
+ fakeResponse = imageResponse(
305
+ { b64_json: "present" },
306
+ { b64_json: undefined },
307
+ {},
308
+ );
309
+
310
+ const result = await generateImageOpenAI(
311
+ { type: "direct", apiKey: "k" },
312
+ { prompt: "test", mode: "generate" },
313
+ );
314
+
315
+ expect(result.images).toHaveLength(1);
316
+ expect(result.images[0].dataBase64).toBe("present");
317
+ });
318
+
319
+ test("empty data array returns no images", async () => {
320
+ fakeResponse = { data: [] };
321
+
322
+ const result = await generateImageOpenAI(
323
+ { type: "direct", apiKey: "k" },
324
+ { prompt: "test", mode: "generate" },
325
+ );
326
+
327
+ expect(result.images).toHaveLength(0);
328
+ expect(result.text).toBeUndefined();
329
+ expect(result.resolvedModel).toBe("gpt-image-2");
330
+ });
331
+ });
332
+
333
+ describe("mapOpenAIError", () => {
334
+ test("maps 400 status to bad request message", () => {
335
+ const msg = mapOpenAIError(new FakeAPIError(400, "bad"));
336
+ expect(msg).toContain("invalid");
337
+ });
338
+
339
+ test("maps 401 status to auth message", () => {
340
+ const msg = mapOpenAIError(new FakeAPIError(401, "unauth"));
341
+ expect(msg).toContain("Authentication");
342
+ });
343
+
344
+ test("maps 403 status to auth message", () => {
345
+ const msg = mapOpenAIError(new FakeAPIError(403, "forbidden"));
346
+ expect(msg).toContain("Authentication");
347
+ });
348
+
349
+ test("maps 429 status to rate limit message", () => {
350
+ const msg = mapOpenAIError(new FakeAPIError(429, "limit"));
351
+ expect(msg).toContain("Rate limit");
352
+ });
353
+
354
+ test("maps 500 status to server error message", () => {
355
+ const msg = mapOpenAIError(new FakeAPIError(500, "internal"));
356
+ expect(msg).toContain("temporarily unavailable");
357
+ });
358
+
359
+ test("maps generic Error to message", () => {
360
+ const msg = mapOpenAIError(new Error("network fail"));
361
+ expect(msg).toContain("network fail");
362
+ });
363
+
364
+ test("maps unknown error to generic message", () => {
365
+ const msg = mapOpenAIError("something");
366
+ expect(msg).toContain("unexpected error");
367
+ });
368
+ });