@vellumai/assistant 0.6.0 → 0.6.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 (358) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +68 -15
  3. package/Dockerfile +2 -2
  4. package/bun.lock +6 -2
  5. package/docker-entrypoint.sh +42 -1
  6. package/docs/architecture/integrations.md +1 -1
  7. package/docs/architecture/memory.md +21 -24
  8. package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
  9. package/openapi.yaml +539 -4
  10. package/package.json +5 -1
  11. package/src/__tests__/anthropic-provider.test.ts +160 -95
  12. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  13. package/src/__tests__/app-executors.test.ts +47 -1
  14. package/src/__tests__/app-source-watcher.test.ts +159 -0
  15. package/src/__tests__/assistant-event-hub.test.ts +30 -0
  16. package/src/__tests__/checker.test.ts +138 -172
  17. package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
  18. package/src/__tests__/config-schema.test.ts +5 -0
  19. package/src/__tests__/context-overflow-approval.test.ts +5 -5
  20. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
  21. package/src/__tests__/conversation-agent-loop.test.ts +4 -51
  22. package/src/__tests__/conversation-analysis-routes.test.ts +169 -0
  23. package/src/__tests__/conversation-directories-parse.test.ts +105 -0
  24. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  25. package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
  26. package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
  27. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
  28. package/src/__tests__/conversation-wipe.test.ts +2 -6
  29. package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
  30. package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
  31. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  32. package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
  33. package/src/__tests__/credential-execution-approval-bridge.test.ts +0 -2
  34. package/src/__tests__/date-context.test.ts +76 -210
  35. package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
  36. package/src/__tests__/file-list-tool.test.ts +219 -0
  37. package/src/__tests__/first-greeting.test.ts +1 -1
  38. package/src/__tests__/heartbeat-service.test.ts +180 -3
  39. package/src/__tests__/identity-routes.test.ts +328 -0
  40. package/src/__tests__/init-feature-flag-overrides.test.ts +167 -0
  41. package/src/__tests__/injection-block.test.ts +24 -0
  42. package/src/__tests__/inline-command-runner.test.ts +7 -5
  43. package/src/__tests__/install-skill-routing.test.ts +7 -6
  44. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
  45. package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
  46. package/src/__tests__/llm-context-normalization.test.ts +18 -18
  47. package/src/__tests__/llm-context-route-provider.test.ts +101 -0
  48. package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
  49. package/src/__tests__/log-export-workspace.test.ts +257 -100
  50. package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
  51. package/src/__tests__/mcp-abort-signal.test.ts +5 -0
  52. package/src/__tests__/mcp-client-auth.test.ts +5 -0
  53. package/src/__tests__/memory-recall-log-store.test.ts +132 -0
  54. package/src/__tests__/migration-export-streaming.test.ts +304 -0
  55. package/src/__tests__/migration-import-commit-http.test.ts +11 -10
  56. package/src/__tests__/mock-fetch.ts +87 -0
  57. package/src/__tests__/navigate-settings-tab.test.ts +14 -1
  58. package/src/__tests__/notification-broadcaster.test.ts +65 -0
  59. package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
  60. package/src/__tests__/onboarding-template-contract.test.ts +63 -14
  61. package/src/__tests__/parser.test.ts +32 -0
  62. package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
  63. package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
  64. package/src/__tests__/permission-mode-sse.test.ts +418 -0
  65. package/src/__tests__/permission-mode-store.test.ts +277 -0
  66. package/src/__tests__/permission-mode.test.ts +101 -0
  67. package/src/__tests__/pkb-autoinject.test.ts +96 -0
  68. package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
  69. package/src/__tests__/profiler-routes.test.ts +502 -0
  70. package/src/__tests__/profiler-run-store.test.ts +441 -0
  71. package/src/__tests__/proxy-approval-callback.test.ts +4 -75
  72. package/src/__tests__/registry.test.ts +1 -1
  73. package/src/__tests__/require-fresh-approval.test.ts +0 -2
  74. package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
  75. package/src/__tests__/sandbox-host-parity.test.ts +5 -4
  76. package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
  77. package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
  78. package/src/__tests__/search-skills-unified.test.ts +4 -3
  79. package/src/__tests__/send-endpoint-busy.test.ts +42 -3
  80. package/src/__tests__/set-permission-mode.test.ts +274 -0
  81. package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
  82. package/src/__tests__/skill-memory.test.ts +2 -783
  83. package/src/__tests__/strip-memory-injections.test.ts +187 -0
  84. package/src/__tests__/subagent-detail.test.ts +84 -0
  85. package/src/__tests__/subagent-disposal.test.ts +308 -0
  86. package/src/__tests__/subagent-manager-notify.test.ts +19 -10
  87. package/src/__tests__/subagent-notify-parent.test.ts +390 -0
  88. package/src/__tests__/subagent-role-registry.test.ts +108 -0
  89. package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
  90. package/src/__tests__/subagent-tools.test.ts +464 -4
  91. package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
  92. package/src/__tests__/task-memory-cleanup.test.ts +12 -12
  93. package/src/__tests__/terminal-sandbox.test.ts +1 -1
  94. package/src/__tests__/terminal-tools.test.ts +16 -29
  95. package/src/__tests__/test-preload.ts +18 -0
  96. package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
  97. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
  98. package/src/__tests__/tool-executor.test.ts +4 -27
  99. package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
  100. package/src/__tests__/top-level-renderer.test.ts +10 -13
  101. package/src/__tests__/transport-hints-queue.test.ts +77 -0
  102. package/src/__tests__/trust-store.test.ts +4 -4
  103. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
  104. package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
  105. package/src/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
  106. package/src/__tests__/workspace-policy.test.ts +2 -7
  107. package/src/agent/loop.ts +6 -29
  108. package/src/approvals/guardian-request-resolvers.ts +24 -0
  109. package/src/avatar/traits-png-sync.ts +3 -3
  110. package/src/channels/types.ts +5 -0
  111. package/src/cli/__tests__/run-assistant-command.ts +56 -0
  112. package/src/cli/__tests__/unknown-command.test.ts +33 -0
  113. package/src/cli/commands/__tests__/email-download.test.ts +245 -0
  114. package/src/cli/commands/__tests__/email-list.test.ts +192 -0
  115. package/src/cli/commands/__tests__/email-register.test.ts +186 -0
  116. package/src/cli/commands/__tests__/email-send.test.ts +291 -0
  117. package/src/cli/commands/__tests__/email-status.test.ts +181 -0
  118. package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
  119. package/src/cli/commands/__tests__/routes.test.ts +562 -0
  120. package/src/cli/commands/conversations.ts +1 -8
  121. package/src/cli/commands/default-action.ts +68 -1
  122. package/src/cli/commands/email.ts +584 -835
  123. package/src/cli/commands/memory.ts +1 -34
  124. package/src/cli/commands/notifications.ts +7 -2
  125. package/src/cli/commands/oauth/__tests__/connect.test.ts +27 -0
  126. package/src/cli/commands/oauth/connect.ts +25 -5
  127. package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
  128. package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
  129. package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
  130. package/src/cli/commands/routes.ts +396 -0
  131. package/src/cli/commands/skills.ts +130 -20
  132. package/src/cli/program.ts +11 -2
  133. package/src/cli.ts +1 -120
  134. package/src/config/assistant-feature-flags.ts +59 -55
  135. package/src/config/bundled-skills/app-builder/SKILL.md +91 -5
  136. package/src/config/bundled-skills/gmail/SKILL.md +13 -8
  137. package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
  138. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
  139. package/src/config/bundled-skills/messaging/SKILL.md +7 -0
  140. package/src/config/bundled-skills/schedule/SKILL.md +22 -2
  141. package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
  142. package/src/config/bundled-skills/settings/TOOLS.json +1 -1
  143. package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
  144. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
  145. package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
  146. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
  147. package/src/config/bundled-skills/slack/SKILL.md +2 -0
  148. package/src/config/bundled-skills/subagent/SKILL.md +43 -3
  149. package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
  150. package/src/config/env-registry.ts +63 -0
  151. package/src/config/feature-flag-registry.json +17 -1
  152. package/src/config/schema.ts +8 -0
  153. package/src/config/schemas/filing.ts +51 -0
  154. package/src/config/schemas/heartbeat.ts +15 -12
  155. package/src/config/schemas/memory-lifecycle.ts +12 -0
  156. package/src/config/schemas/security.ts +14 -0
  157. package/src/config/schemas/services.ts +8 -0
  158. package/src/credential-execution/approval-bridge.ts +0 -1
  159. package/src/credential-execution/managed-catalog.ts +3 -7
  160. package/src/daemon/app-source-watcher.ts +93 -0
  161. package/src/daemon/config-watcher.ts +85 -3
  162. package/src/daemon/context-overflow-approval.ts +0 -1
  163. package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
  164. package/src/daemon/conversation-agent-loop.ts +179 -65
  165. package/src/daemon/conversation-attachments.ts +0 -1
  166. package/src/daemon/conversation-history.ts +4 -19
  167. package/src/daemon/conversation-lifecycle.ts +8 -14
  168. package/src/daemon/conversation-messaging.ts +3 -0
  169. package/src/daemon/conversation-process.ts +30 -8
  170. package/src/daemon/conversation-queue-manager.ts +8 -0
  171. package/src/daemon/conversation-runtime-assembly.ts +359 -308
  172. package/src/daemon/conversation-surfaces.ts +65 -0
  173. package/src/daemon/conversation-tool-setup.ts +44 -17
  174. package/src/daemon/conversation-workspace.ts +1 -2
  175. package/src/daemon/conversation.ts +19 -3
  176. package/src/daemon/date-context.ts +26 -53
  177. package/src/daemon/first-greeting.ts +1 -1
  178. package/src/daemon/handlers/conversations.ts +5 -7
  179. package/src/daemon/handlers/shared.test.ts +143 -0
  180. package/src/daemon/handlers/shared.ts +70 -5
  181. package/src/daemon/handlers/skills.ts +11 -18
  182. package/src/daemon/lifecycle.ts +220 -158
  183. package/src/daemon/message-types/conversations.ts +29 -6
  184. package/src/daemon/message-types/messages.ts +9 -2
  185. package/src/daemon/message-types/notifications.ts +12 -0
  186. package/src/daemon/message-types/schedules.ts +1 -0
  187. package/src/daemon/message-types/settings.ts +18 -0
  188. package/src/daemon/profiler-run-store.ts +557 -0
  189. package/src/daemon/server.ts +87 -10
  190. package/src/daemon/shutdown-handlers.ts +5 -0
  191. package/src/daemon/tool-side-effects.ts +23 -3
  192. package/src/daemon/transport-hints.ts +33 -0
  193. package/src/export/transcript-formatter.ts +148 -0
  194. package/src/filing/filing-service.ts +228 -0
  195. package/src/heartbeat/heartbeat-service.ts +96 -7
  196. package/src/index.ts +1 -1
  197. package/src/mcp/client.ts +6 -0
  198. package/src/mcp/mcp-oauth-provider.ts +149 -27
  199. package/src/memory/admin.ts +33 -32
  200. package/src/memory/app-store.ts +69 -0
  201. package/src/memory/conversation-bootstrap.ts +1 -1
  202. package/src/memory/conversation-crud.ts +151 -117
  203. package/src/memory/conversation-directories.ts +39 -0
  204. package/src/memory/conversation-group-migration.ts +66 -6
  205. package/src/memory/conversation-queries.ts +58 -12
  206. package/src/memory/conversation-title-service.ts +1 -0
  207. package/src/memory/db-init.ts +182 -376
  208. package/src/memory/embedding-local.ts +1 -1
  209. package/src/memory/graph/bootstrap.ts +75 -66
  210. package/src/memory/graph/capability-seed.ts +167 -17
  211. package/src/memory/graph/consolidation.ts +38 -4
  212. package/src/memory/graph/conversation-graph-memory.ts +133 -104
  213. package/src/memory/graph/extraction-job.ts +9 -4
  214. package/src/memory/graph/extraction.ts +66 -23
  215. package/src/memory/graph/graph-memory-state-store.ts +37 -0
  216. package/src/memory/graph/graph-search.ts +29 -15
  217. package/src/memory/graph/injection.ts +38 -8
  218. package/src/memory/graph/inspect.ts +12 -3
  219. package/src/memory/graph/retriever.ts +365 -262
  220. package/src/memory/graph/store.test.ts +48 -0
  221. package/src/memory/graph/store.ts +150 -11
  222. package/src/memory/graph/tool-handlers.ts +84 -209
  223. package/src/memory/graph/tools.ts +8 -52
  224. package/src/memory/graph/types.ts +24 -0
  225. package/src/memory/group-crud.ts +25 -9
  226. package/src/memory/job-handlers/cleanup.ts +44 -1
  227. package/src/memory/jobs-store.ts +70 -60
  228. package/src/memory/jobs-worker.ts +44 -28
  229. package/src/memory/llm-request-log-store.ts +96 -12
  230. package/src/memory/memory-recall-log-store.ts +49 -5
  231. package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
  232. package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
  233. package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
  234. package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
  235. package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
  236. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
  237. package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
  238. package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
  239. package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
  240. package/src/memory/migrations/index.ts +8 -0
  241. package/src/memory/migrations/registry.ts +8 -0
  242. package/src/memory/schema/conversations.ts +14 -0
  243. package/src/memory/schema/infrastructure.ts +8 -1
  244. package/src/memory/schema/memory-core.ts +0 -51
  245. package/src/memory/schema/memory-graph.ts +15 -0
  246. package/src/memory/task-memory-cleanup.ts +30 -11
  247. package/src/messaging/provider.ts +1 -1
  248. package/src/notifications/broadcaster.ts +6 -0
  249. package/src/notifications/conversation-pairing.ts +12 -4
  250. package/src/notifications/copy-composer.ts +86 -0
  251. package/src/notifications/decision-engine.ts +35 -0
  252. package/src/notifications/emit-signal.ts +14 -0
  253. package/src/notifications/signal.ts +11 -0
  254. package/src/oauth/platform-connection.test.ts +2 -2
  255. package/src/oauth/seed-providers.ts +1 -0
  256. package/src/permissions/checker.ts +15 -4
  257. package/src/permissions/defaults.ts +7 -8
  258. package/src/permissions/permission-mode-store.ts +180 -0
  259. package/src/permissions/permission-mode.ts +31 -0
  260. package/src/permissions/prompter.ts +0 -2
  261. package/src/permissions/workspace-policy.ts +9 -0
  262. package/src/platform/client.ts +1 -1
  263. package/src/prompts/system-prompt.ts +59 -7
  264. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
  265. package/src/prompts/templates/BOOTSTRAP.md +76 -162
  266. package/src/prompts/templates/HEARTBEAT.md +3 -1
  267. package/src/prompts/templates/SOUL.md +30 -9
  268. package/src/prompts/templates/UPDATES.md +8 -0
  269. package/src/providers/anthropic/client.ts +107 -219
  270. package/src/runtime/assistant-event-hub.ts +22 -0
  271. package/src/runtime/auth/route-policy.ts +23 -0
  272. package/src/runtime/auth/token-service.ts +8 -0
  273. package/src/runtime/http-server.ts +32 -2
  274. package/src/runtime/http-types.ts +12 -1
  275. package/src/runtime/migrations/vbundle-builder.ts +389 -3
  276. package/src/runtime/migrations/vbundle-importer.ts +8 -6
  277. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
  278. package/src/runtime/routes/app-management-routes.ts +1 -11
  279. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
  280. package/src/runtime/routes/archive-utils.ts +29 -0
  281. package/src/runtime/routes/avatar-routes.ts +2 -9
  282. package/src/runtime/routes/btw-routes.ts +14 -1
  283. package/src/runtime/routes/conversation-analysis-routes.ts +185 -0
  284. package/src/runtime/routes/conversation-management-routes.ts +1 -14
  285. package/src/runtime/routes/conversation-query-routes.ts +49 -3
  286. package/src/runtime/routes/conversation-routes.ts +270 -44
  287. package/src/runtime/routes/group-routes.ts +22 -8
  288. package/src/runtime/routes/heartbeat-routes.ts +4 -10
  289. package/src/runtime/routes/identity-routes.ts +53 -18
  290. package/src/runtime/routes/llm-context-normalization.ts +14 -10
  291. package/src/runtime/routes/log-export/AGENTS.md +104 -0
  292. package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
  293. package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
  294. package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
  295. package/src/runtime/routes/log-export-routes.ts +41 -278
  296. package/src/runtime/routes/memory-item-routes.test.ts +168 -233
  297. package/src/runtime/routes/migration-routes.ts +18 -7
  298. package/src/runtime/routes/profiler-routes.ts +350 -0
  299. package/src/runtime/routes/schedule-routes.ts +27 -12
  300. package/src/runtime/routes/settings-routes.ts +95 -8
  301. package/src/runtime/routes/subagents-routes.ts +28 -7
  302. package/src/runtime/routes/user-route-dispatcher.ts +223 -0
  303. package/src/runtime/routes/user-routes.ts +41 -0
  304. package/src/runtime/routes/workspace-routes.ts +0 -1
  305. package/src/schedule/schedule-store.ts +30 -0
  306. package/src/schedule/scheduler.ts +45 -18
  307. package/src/skills/catalog-install.ts +10 -2
  308. package/src/skills/inline-command-runner.ts +12 -14
  309. package/src/skills/managed-store.ts +2 -2
  310. package/src/skills/skill-memory.ts +1 -293
  311. package/src/subagent/index.ts +13 -3
  312. package/src/subagent/manager.ts +308 -29
  313. package/src/subagent/types.ts +68 -0
  314. package/src/tasks/task-runner.ts +4 -4
  315. package/src/tools/apps/executors.ts +29 -4
  316. package/src/tools/filesystem/list.ts +93 -0
  317. package/src/tools/permission-checker.ts +78 -18
  318. package/src/tools/registry.ts +4 -0
  319. package/src/tools/schedule/create.ts +3 -0
  320. package/src/tools/schedule/list.ts +1 -0
  321. package/src/tools/schedule/update.ts +6 -0
  322. package/src/tools/secret-detection-handler.ts +0 -1
  323. package/src/tools/shared/filesystem/errors.ts +5 -0
  324. package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
  325. package/src/tools/shared/filesystem/types.ts +17 -0
  326. package/src/tools/shared/shell-output.ts +31 -2
  327. package/src/tools/skills/sandbox-runner.ts +3 -6
  328. package/src/tools/subagent/abort.ts +12 -2
  329. package/src/tools/subagent/message.ts +9 -2
  330. package/src/tools/subagent/notify-parent.ts +79 -0
  331. package/src/tools/subagent/read.ts +29 -8
  332. package/src/tools/subagent/resolve.ts +21 -0
  333. package/src/tools/subagent/spawn.ts +2 -0
  334. package/src/tools/subagent/status.ts +11 -1
  335. package/src/tools/system/avatar-generator.ts +3 -3
  336. package/src/tools/system/register.ts +23 -0
  337. package/src/tools/system/set-permission-mode.ts +103 -0
  338. package/src/tools/terminal/parser.ts +30 -5
  339. package/src/tools/terminal/safe-env.ts +16 -1
  340. package/src/tools/terminal/sandbox-diagnostics.ts +4 -4
  341. package/src/tools/terminal/sandbox.ts +4 -1
  342. package/src/tools/terminal/shell.ts +3 -5
  343. package/src/tools/tool-manifest.ts +6 -0
  344. package/src/tools/types.ts +2 -3
  345. package/src/util/logger.ts +1 -1
  346. package/src/util/platform.ts +50 -17
  347. package/src/watcher/provider-types.ts +1 -1
  348. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
  349. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
  350. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
  351. package/src/workspace/migrations/029-seed-pkb.ts +85 -0
  352. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
  353. package/src/workspace/migrations/registry.ts +6 -0
  354. package/src/workspace/top-level-renderer.ts +5 -9
  355. package/src/__tests__/cli-memory.test.ts +0 -377
  356. package/src/__tests__/clipboard.test.ts +0 -88
  357. package/src/cli/cli-memory.ts +0 -179
  358. package/src/util/clipboard.ts +0 -34
@@ -0,0 +1,139 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ getMockFetchCalls,
5
+ mockFetch,
6
+ resetMockFetch,
7
+ } from "../../../__tests__/mock-fetch.js";
8
+ import { _setOverridesForTesting } from "../../../config/assistant-feature-flags.js";
9
+ import { setPlatformAssistantId } from "../../../config/env.js";
10
+ import { credentialKey } from "../../../security/credential-key.js";
11
+ import {
12
+ _resetBackend,
13
+ setSecureKeyAsync,
14
+ } from "../../../security/secure-keys.js";
15
+ import { runAssistantCommand } from "../../__tests__/run-assistant-command.js";
16
+
17
+ const ASSISTANT_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
18
+ const ADDRESS_ID = "550e8400-e29b-41d4-a716-446655440000";
19
+ const ADDRESS = "mybot@vellum.me";
20
+ const API_KEY_CREDENTIAL = credentialKey("vellum", "assistant_api_key");
21
+
22
+ beforeEach(async () => {
23
+ process.exitCode = 0;
24
+ _resetBackend();
25
+ resetMockFetch();
26
+ _setOverridesForTesting({ "email-channel": true });
27
+ setPlatformAssistantId(ASSISTANT_ID);
28
+ await setSecureKeyAsync(API_KEY_CREDENTIAL, "test-api-key");
29
+ });
30
+
31
+ afterEach(() => {
32
+ resetMockFetch();
33
+ _setOverridesForTesting({});
34
+ setPlatformAssistantId(undefined);
35
+ _resetBackend();
36
+ });
37
+
38
+ function standardEmailMockFetches(
39
+ deleteStatus = 204,
40
+ deleteBody: unknown = null,
41
+ ): void {
42
+ mockFetch(
43
+ "/email-addresses/",
44
+ {},
45
+ {
46
+ body: { results: [{ id: ADDRESS_ID, address: ADDRESS }] },
47
+ status: 200,
48
+ },
49
+ );
50
+ mockFetch(
51
+ `/email-addresses/${ADDRESS_ID}/`,
52
+ { method: "DELETE" },
53
+ { body: deleteBody, status: deleteStatus },
54
+ );
55
+ }
56
+
57
+ describe("assistant email unregister", () => {
58
+ test("successful unregister with --confirm lists then deletes", async () => {
59
+ standardEmailMockFetches();
60
+
61
+ await runAssistantCommand("email", "unregister", "--confirm");
62
+
63
+ const calls = getMockFetchCalls();
64
+ expect(calls).toHaveLength(2);
65
+ expect(calls[0].path).toBe(
66
+ `/v1/assistants/${ASSISTANT_ID}/email-addresses/`,
67
+ );
68
+ expect(calls[1].path).toBe(
69
+ `/v1/assistants/${ASSISTANT_ID}/email-addresses/${ADDRESS_ID}/`,
70
+ );
71
+ expect(calls[1].init.method).toBe("DELETE");
72
+ expect(process.exitCode).toBe(0);
73
+ });
74
+
75
+ test("--json outputs structured response", async () => {
76
+ standardEmailMockFetches();
77
+
78
+ const output = await runAssistantCommand("email", "--json", "unregister");
79
+
80
+ const parsed = JSON.parse(output.trim());
81
+ expect(parsed.unregistered).toBe(ADDRESS);
82
+ expect(process.exitCode).toBe(0);
83
+ });
84
+
85
+ test("no registered address returns error", async () => {
86
+ mockFetch("/email-addresses/", {}, { body: { results: [] }, status: 200 });
87
+
88
+ const output = await runAssistantCommand("email", "--json", "unregister");
89
+
90
+ expect(process.exitCode).toBe(1);
91
+ const parsed = JSON.parse(output.trim());
92
+ expect(parsed.error).toContain("No email address registered");
93
+ });
94
+
95
+ test("list endpoint failure returns error", async () => {
96
+ mockFetch(
97
+ "/email-addresses/",
98
+ {},
99
+ { body: { detail: "Internal server error" }, status: 500 },
100
+ );
101
+
102
+ const output = await runAssistantCommand("email", "--json", "unregister");
103
+
104
+ expect(process.exitCode).toBe(1);
105
+ const parsed = JSON.parse(output.trim());
106
+ expect(parsed.error).toContain("Failed to list email addresses");
107
+ });
108
+
109
+ test("delete endpoint failure returns error", async () => {
110
+ standardEmailMockFetches(500, { detail: "Cannot delete address" });
111
+
112
+ const output = await runAssistantCommand("email", "--json", "unregister");
113
+
114
+ expect(process.exitCode).toBe(1);
115
+ const parsed = JSON.parse(output.trim());
116
+ expect(parsed.error).toContain("Cannot delete address");
117
+ });
118
+
119
+ test("missing platform credentials returns error", async () => {
120
+ _resetBackend();
121
+ setPlatformAssistantId(undefined);
122
+
123
+ const output = await runAssistantCommand("email", "--json", "unregister");
124
+
125
+ expect(process.exitCode).toBe(1);
126
+ const parsed = JSON.parse(output.trim());
127
+ expect(parsed.error).toContain("Platform credentials not configured");
128
+ });
129
+
130
+ test("missing assistant ID returns error", async () => {
131
+ setPlatformAssistantId("");
132
+
133
+ const output = await runAssistantCommand("email", "--json", "unregister");
134
+
135
+ expect(process.exitCode).toBe(1);
136
+ const parsed = JSON.parse(output.trim());
137
+ expect(parsed.error).toContain("Assistant ID");
138
+ });
139
+ });
@@ -0,0 +1,562 @@
1
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
4
+
5
+ import { Command } from "commander";
6
+
7
+ import { getWorkspaceRoutesDir } from "../../../util/platform.js";
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Mock state
11
+ // ---------------------------------------------------------------------------
12
+
13
+ let mockPublicBaseUrl: string | null = null;
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Mocks
17
+ // ---------------------------------------------------------------------------
18
+
19
+ mock.module("../../../config/loader.js", () => ({
20
+ getConfig: () => ({
21
+ ingress: mockPublicBaseUrl
22
+ ? { publicBaseUrl: mockPublicBaseUrl }
23
+ : undefined,
24
+ }),
25
+ }));
26
+
27
+ mock.module("../../../inbound/public-ingress-urls.js", () => ({
28
+ getPublicBaseUrl: (config: { ingress?: { publicBaseUrl?: string } }) => {
29
+ const url = config.ingress?.publicBaseUrl;
30
+ if (!url) throw new Error("No public base URL configured");
31
+ return url;
32
+ },
33
+ }));
34
+
35
+ mock.module("../../../util/logger.js", () => ({
36
+ getLogger: () => ({
37
+ info: () => {},
38
+ warn: () => {},
39
+ error: () => {},
40
+ debug: () => {},
41
+ }),
42
+ getCliLogger: () => ({
43
+ info: () => {},
44
+ warn: () => {},
45
+ error: () => {},
46
+ debug: () => {},
47
+ }),
48
+ }));
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Import module under test (after mocks are registered)
52
+ // ---------------------------------------------------------------------------
53
+
54
+ const { registerRoutesCommand } = await import("../routes.js");
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Test helper
58
+ // ---------------------------------------------------------------------------
59
+
60
+ async function runCommand(
61
+ args: string[],
62
+ ): Promise<{ stdout: string; exitCode: number }> {
63
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
64
+ const originalStderrWrite = process.stderr.write.bind(process.stderr);
65
+ const originalConsoleLog = console.log.bind(console);
66
+ const stdoutChunks: string[] = [];
67
+
68
+ process.stdout.write = ((chunk: unknown) => {
69
+ stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk));
70
+ return true;
71
+ }) as typeof process.stdout.write;
72
+
73
+ process.stderr.write = (() => true) as typeof process.stderr.write;
74
+
75
+ console.log = (...logArgs: unknown[]) => {
76
+ stdoutChunks.push(
77
+ logArgs.map((a) => (typeof a === "string" ? a : String(a))).join(" ") +
78
+ "\n",
79
+ );
80
+ };
81
+
82
+ process.exitCode = 0;
83
+
84
+ try {
85
+ const program = new Command();
86
+ program.exitOverride();
87
+ program.configureOutput({
88
+ writeErr: () => {},
89
+ writeOut: (str: string) => stdoutChunks.push(str),
90
+ });
91
+ registerRoutesCommand(program);
92
+ await program.parseAsync(["node", "assistant", ...args]);
93
+ } catch {
94
+ if (process.exitCode === 0) process.exitCode = 1;
95
+ } finally {
96
+ process.stdout.write = originalStdoutWrite;
97
+ process.stderr.write = originalStderrWrite;
98
+ console.log = originalConsoleLog;
99
+ }
100
+
101
+ const exitCode = process.exitCode ?? 0;
102
+ process.exitCode = 0;
103
+
104
+ return { exitCode, stdout: stdoutChunks.join("") };
105
+ }
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Helpers for writing handler files into the workspace routes dir
109
+ // ---------------------------------------------------------------------------
110
+
111
+ let routesDir: string;
112
+
113
+ function writeHandler(relativePath: string, content: string): void {
114
+ const fullPath = join(routesDir, relativePath);
115
+ const dir = fullPath.substring(0, fullPath.lastIndexOf("/"));
116
+ mkdirSync(dir, { recursive: true });
117
+ writeFileSync(fullPath, content, "utf-8");
118
+ }
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // Setup / teardown
122
+ // ---------------------------------------------------------------------------
123
+
124
+ beforeEach(() => {
125
+ routesDir = getWorkspaceRoutesDir();
126
+ mkdirSync(routesDir, { recursive: true });
127
+ mockPublicBaseUrl = null;
128
+ process.exitCode = 0;
129
+ });
130
+
131
+ afterEach(() => {
132
+ try {
133
+ rmSync(routesDir, { recursive: true, force: true });
134
+ } catch {
135
+ /* best-effort cleanup */
136
+ }
137
+ });
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // routes list
141
+ // ---------------------------------------------------------------------------
142
+
143
+ describe("assistant routes list", () => {
144
+ test("empty routes dir returns zero routes in JSON", async () => {
145
+ const { exitCode, stdout } = await runCommand(["routes", "list", "--json"]);
146
+ expect(exitCode).toBe(0);
147
+ const parsed = JSON.parse(stdout);
148
+ expect(parsed.ok).toBe(true);
149
+ expect(parsed.routes).toEqual([]);
150
+ });
151
+
152
+ test("empty routes dir shows guidance in human output", async () => {
153
+ const { exitCode } = await runCommand(["routes", "list"]);
154
+ expect(exitCode).toBe(0);
155
+ });
156
+
157
+ test("discovers a single GET handler", async () => {
158
+ writeHandler(
159
+ "status.ts",
160
+ `export async function GET(req: Request) { return new Response("ok"); }`,
161
+ );
162
+
163
+ const { exitCode, stdout } = await runCommand(["routes", "list", "--json"]);
164
+ expect(exitCode).toBe(0);
165
+ const parsed = JSON.parse(stdout);
166
+ expect(parsed.ok).toBe(true);
167
+ expect(parsed.routes).toHaveLength(1);
168
+ expect(parsed.routes[0].routePath).toBe("/x/status");
169
+ expect(parsed.routes[0].methods).toEqual(["GET"]);
170
+ });
171
+
172
+ test("discovers multiple routes sorted alphabetically", async () => {
173
+ writeHandler(
174
+ "zebra.ts",
175
+ `export function GET() { return new Response("z"); }`,
176
+ );
177
+ writeHandler(
178
+ "alpha.ts",
179
+ `export function POST() { return new Response("a"); }`,
180
+ );
181
+
182
+ const { exitCode, stdout } = await runCommand(["routes", "list", "--json"]);
183
+ expect(exitCode).toBe(0);
184
+ const parsed = JSON.parse(stdout);
185
+ expect(parsed.routes).toHaveLength(2);
186
+ expect(parsed.routes[0].routePath).toBe("/x/alpha");
187
+ expect(parsed.routes[1].routePath).toBe("/x/zebra");
188
+ });
189
+
190
+ test("discovers multi-method handler", async () => {
191
+ writeHandler(
192
+ "items.ts",
193
+ [
194
+ `export function GET() { return new Response("list"); }`,
195
+ `export function POST() { return new Response("create"); }`,
196
+ `export function DELETE() { return new Response("remove"); }`,
197
+ ].join("\n"),
198
+ );
199
+
200
+ const { exitCode, stdout } = await runCommand(["routes", "list", "--json"]);
201
+ expect(exitCode).toBe(0);
202
+ const parsed = JSON.parse(stdout);
203
+ expect(parsed.routes[0].methods).toEqual(["GET", "POST", "DELETE"]);
204
+ });
205
+
206
+ test("discovers index file as directory route", async () => {
207
+ writeHandler(
208
+ "my-app/index.ts",
209
+ `export function GET() { return new Response("app"); }`,
210
+ );
211
+
212
+ const { exitCode, stdout } = await runCommand(["routes", "list", "--json"]);
213
+ expect(exitCode).toBe(0);
214
+ const parsed = JSON.parse(stdout);
215
+ expect(parsed.routes).toHaveLength(1);
216
+ expect(parsed.routes[0].routePath).toBe("/x/my-app");
217
+ });
218
+
219
+ test("discovers subdirectory routes", async () => {
220
+ writeHandler(
221
+ "api/v1/users.ts",
222
+ `export function GET() { return new Response("users"); }`,
223
+ );
224
+
225
+ const { exitCode, stdout } = await runCommand(["routes", "list", "--json"]);
226
+ expect(exitCode).toBe(0);
227
+ const parsed = JSON.parse(stdout);
228
+ expect(parsed.routes[0].routePath).toBe("/x/api/v1/users");
229
+ });
230
+
231
+ test("discovers .js handlers", async () => {
232
+ writeHandler(
233
+ "health.js",
234
+ `export function GET() { return new Response("ok"); }`,
235
+ );
236
+
237
+ const { exitCode, stdout } = await runCommand(["routes", "list", "--json"]);
238
+ expect(exitCode).toBe(0);
239
+ const parsed = JSON.parse(stdout);
240
+ expect(parsed.routes).toHaveLength(1);
241
+ expect(parsed.routes[0].routePath).toBe("/x/health");
242
+ });
243
+
244
+ test("extracts description export", async () => {
245
+ writeHandler(
246
+ "submit.ts",
247
+ [
248
+ `export const description = "Form submission handler";`,
249
+ `export function POST() { return new Response("ok"); }`,
250
+ ].join("\n"),
251
+ );
252
+
253
+ const { exitCode, stdout } = await runCommand(["routes", "list", "--json"]);
254
+ expect(exitCode).toBe(0);
255
+ const parsed = JSON.parse(stdout);
256
+ expect(parsed.routes[0].description).toBe("Form submission handler");
257
+ });
258
+
259
+ test("null description when not exported", async () => {
260
+ writeHandler(
261
+ "simple.ts",
262
+ `export function GET() { return new Response("ok"); }`,
263
+ );
264
+
265
+ const { exitCode, stdout } = await runCommand(["routes", "list", "--json"]);
266
+ expect(exitCode).toBe(0);
267
+ const parsed = JSON.parse(stdout);
268
+ expect(parsed.routes[0].description).toBeNull();
269
+ });
270
+
271
+ test("includes publicUrl when public base URL is configured", async () => {
272
+ mockPublicBaseUrl = "https://example.ngrok-free.app/v1/assistants/asst_xyz";
273
+ writeHandler(
274
+ "status.ts",
275
+ `export function GET() { return new Response("ok"); }`,
276
+ );
277
+
278
+ const { exitCode, stdout } = await runCommand(["routes", "list", "--json"]);
279
+ expect(exitCode).toBe(0);
280
+ const parsed = JSON.parse(stdout);
281
+ expect(parsed.routes[0].publicUrl).toBe(
282
+ "https://example.ngrok-free.app/v1/assistants/asst_xyz/x/status",
283
+ );
284
+ });
285
+
286
+ test("publicUrl is null when no public base URL configured", async () => {
287
+ mockPublicBaseUrl = null;
288
+ writeHandler(
289
+ "status.ts",
290
+ `export function GET() { return new Response("ok"); }`,
291
+ );
292
+
293
+ const { exitCode, stdout } = await runCommand(["routes", "list", "--json"]);
294
+ expect(exitCode).toBe(0);
295
+ const parsed = JSON.parse(stdout);
296
+ expect(parsed.routes[0].publicUrl).toBeNull();
297
+ });
298
+
299
+ test("ignores non-handler files", async () => {
300
+ writeHandler("readme.md", "# Routes\nDocumentation file");
301
+ writeHandler(
302
+ "handler.ts",
303
+ `export function GET() { return new Response("ok"); }`,
304
+ );
305
+
306
+ const { exitCode, stdout } = await runCommand(["routes", "list", "--json"]);
307
+ expect(exitCode).toBe(0);
308
+ const parsed = JSON.parse(stdout);
309
+ expect(parsed.routes).toHaveLength(1);
310
+ expect(parsed.routes[0].routePath).toBe("/x/handler");
311
+ });
312
+
313
+ test("human output runs without error for populated routes", async () => {
314
+ writeHandler(
315
+ "status.ts",
316
+ `export function GET() { return new Response("ok"); }`,
317
+ );
318
+
319
+ const { exitCode } = await runCommand(["routes", "list"]);
320
+ expect(exitCode).toBe(0);
321
+ });
322
+
323
+ test("root index file maps to /x/", async () => {
324
+ writeHandler(
325
+ "index.ts",
326
+ `export function GET() { return new Response("root"); }`,
327
+ );
328
+
329
+ const { exitCode, stdout } = await runCommand(["routes", "list", "--json"]);
330
+ expect(exitCode).toBe(0);
331
+ const parsed = JSON.parse(stdout);
332
+ expect(parsed.routes).toHaveLength(1);
333
+ expect(parsed.routes[0].routePath).toBe("/x/");
334
+ });
335
+
336
+ test("JSON output includes filePath relative to routes dir", async () => {
337
+ writeHandler(
338
+ "api/submit.ts",
339
+ `export function POST() { return new Response("ok"); }`,
340
+ );
341
+
342
+ const { exitCode, stdout } = await runCommand(["routes", "list", "--json"]);
343
+ expect(exitCode).toBe(0);
344
+ const parsed = JSON.parse(stdout);
345
+ expect(parsed.routes[0].filePath).toBe("api/submit.ts");
346
+ });
347
+ });
348
+
349
+ // ---------------------------------------------------------------------------
350
+ // routes inspect
351
+ // ---------------------------------------------------------------------------
352
+
353
+ describe("assistant routes inspect", () => {
354
+ test("inspects a handler by route path (JSON)", async () => {
355
+ writeHandler(
356
+ "status.ts",
357
+ [
358
+ `export const description = "Health check endpoint";`,
359
+ `export function GET() { return new Response("ok"); }`,
360
+ `export function POST() { return new Response("created"); }`,
361
+ ].join("\n"),
362
+ );
363
+
364
+ const { exitCode, stdout } = await runCommand([
365
+ "routes",
366
+ "inspect",
367
+ "status",
368
+ "--json",
369
+ ]);
370
+ expect(exitCode).toBe(0);
371
+ const parsed = JSON.parse(stdout);
372
+ expect(parsed.ok).toBe(true);
373
+ expect(parsed.route.routePath).toBe("/x/status");
374
+ expect(parsed.route.methods).toEqual(["GET", "POST"]);
375
+ expect(parsed.route.description).toBe("Health check endpoint");
376
+ expect(parsed.route.filePath).toContain("status.ts");
377
+ expect(parsed.route.fileSize).toBeGreaterThan(0);
378
+ expect(parsed.route.modifiedAt).toBeTruthy();
379
+ });
380
+
381
+ test("inspect resolves index file convention", async () => {
382
+ writeHandler(
383
+ "dashboard/index.ts",
384
+ `export function GET() { return new Response("dashboard"); }`,
385
+ );
386
+
387
+ const { exitCode, stdout } = await runCommand([
388
+ "routes",
389
+ "inspect",
390
+ "dashboard",
391
+ "--json",
392
+ ]);
393
+ expect(exitCode).toBe(0);
394
+ const parsed = JSON.parse(stdout);
395
+ expect(parsed.ok).toBe(true);
396
+ expect(parsed.route.routePath).toBe("/x/dashboard");
397
+ expect(parsed.route.methods).toEqual(["GET"]);
398
+ expect(parsed.route.filePath).toContain("index.ts");
399
+ });
400
+
401
+ test("inspect resolves .js files", async () => {
402
+ writeHandler(
403
+ "legacy.js",
404
+ `export function POST() { return new Response("ok"); }`,
405
+ );
406
+
407
+ const { exitCode, stdout } = await runCommand([
408
+ "routes",
409
+ "inspect",
410
+ "legacy",
411
+ "--json",
412
+ ]);
413
+ expect(exitCode).toBe(0);
414
+ const parsed = JSON.parse(stdout);
415
+ expect(parsed.ok).toBe(true);
416
+ expect(parsed.route.filePath).toContain("legacy.js");
417
+ });
418
+
419
+ test("inspect includes publicUrl when configured", async () => {
420
+ mockPublicBaseUrl = "https://example.com/v1/assistants/asst_1";
421
+ writeHandler(
422
+ "submit.ts",
423
+ `export function POST() { return new Response("ok"); }`,
424
+ );
425
+
426
+ const { exitCode, stdout } = await runCommand([
427
+ "routes",
428
+ "inspect",
429
+ "submit",
430
+ "--json",
431
+ ]);
432
+ expect(exitCode).toBe(0);
433
+ const parsed = JSON.parse(stdout);
434
+ expect(parsed.route.publicUrl).toBe(
435
+ "https://example.com/v1/assistants/asst_1/x/submit",
436
+ );
437
+ });
438
+
439
+ test("inspect publicUrl is null when not configured", async () => {
440
+ mockPublicBaseUrl = null;
441
+ writeHandler(
442
+ "submit.ts",
443
+ `export function POST() { return new Response("ok"); }`,
444
+ );
445
+
446
+ const { exitCode, stdout } = await runCommand([
447
+ "routes",
448
+ "inspect",
449
+ "submit",
450
+ "--json",
451
+ ]);
452
+ expect(exitCode).toBe(0);
453
+ const parsed = JSON.parse(stdout);
454
+ expect(parsed.route.publicUrl).toBeNull();
455
+ });
456
+
457
+ test("inspect returns error for missing handler (JSON)", async () => {
458
+ const { exitCode, stdout } = await runCommand([
459
+ "routes",
460
+ "inspect",
461
+ "nonexistent",
462
+ "--json",
463
+ ]);
464
+ expect(exitCode).toBe(1);
465
+ const parsed = JSON.parse(stdout);
466
+ expect(parsed.ok).toBe(false);
467
+ expect(parsed.error).toContain("No handler file found");
468
+ expect(parsed.error).toContain("nonexistent");
469
+ });
470
+
471
+ test("inspect returns error for missing handler (human output)", async () => {
472
+ const { exitCode } = await runCommand(["routes", "inspect", "nonexistent"]);
473
+ expect(exitCode).toBe(1);
474
+ });
475
+
476
+ test("inspect handles subdirectory routes", async () => {
477
+ writeHandler(
478
+ "api/v2/users.ts",
479
+ `export function GET() { return new Response("users"); }`,
480
+ );
481
+
482
+ const { exitCode, stdout } = await runCommand([
483
+ "routes",
484
+ "inspect",
485
+ "api/v2/users",
486
+ "--json",
487
+ ]);
488
+ expect(exitCode).toBe(0);
489
+ const parsed = JSON.parse(stdout);
490
+ expect(parsed.ok).toBe(true);
491
+ expect(parsed.route.routePath).toBe("/x/api/v2/users");
492
+ });
493
+
494
+ test("inspect human output runs without error", async () => {
495
+ writeHandler(
496
+ "check.ts",
497
+ `export function GET() { return new Response("ok"); }`,
498
+ );
499
+
500
+ const { exitCode } = await runCommand(["routes", "inspect", "check"]);
501
+ expect(exitCode).toBe(0);
502
+ });
503
+
504
+ test("inspect shows handler with no exported methods", async () => {
505
+ writeHandler("empty.ts", `export const description = "Placeholder";`);
506
+
507
+ const { exitCode, stdout } = await runCommand([
508
+ "routes",
509
+ "inspect",
510
+ "empty",
511
+ "--json",
512
+ ]);
513
+ expect(exitCode).toBe(0);
514
+ const parsed = JSON.parse(stdout);
515
+ expect(parsed.route.methods).toEqual([]);
516
+ expect(parsed.route.description).toBe("Placeholder");
517
+ });
518
+
519
+ test("inspect prefers direct file over index file", async () => {
520
+ writeHandler(
521
+ "ambiguous.ts",
522
+ `export function GET() { return new Response("direct"); }`,
523
+ );
524
+ writeHandler(
525
+ "ambiguous/index.ts",
526
+ `export function POST() { return new Response("index"); }`,
527
+ );
528
+
529
+ const { exitCode, stdout } = await runCommand([
530
+ "routes",
531
+ "inspect",
532
+ "ambiguous",
533
+ "--json",
534
+ ]);
535
+ expect(exitCode).toBe(0);
536
+ const parsed = JSON.parse(stdout);
537
+ // Direct file should be preferred over index
538
+ expect(parsed.route.methods).toEqual(["GET"]);
539
+ });
540
+
541
+ test("inspect prefers .ts over .js", async () => {
542
+ writeHandler(
543
+ "both.ts",
544
+ `export function GET() { return new Response("ts"); }`,
545
+ );
546
+ writeHandler(
547
+ "both.js",
548
+ `export function POST() { return new Response("js"); }`,
549
+ );
550
+
551
+ const { exitCode, stdout } = await runCommand([
552
+ "routes",
553
+ "inspect",
554
+ "both",
555
+ "--json",
556
+ ]);
557
+ expect(exitCode).toBe(0);
558
+ const parsed = JSON.parse(stdout);
559
+ // .ts is checked first in HANDLER_EXTENSIONS
560
+ expect(parsed.route.methods).toEqual(["GET"]);
561
+ });
562
+ });