@useatlas/create 0.0.1

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 (515) hide show
  1. package/README.md +231 -0
  2. package/index.ts +829 -0
  3. package/package.json +38 -0
  4. package/templates/docker/.env.example +67 -0
  5. package/templates/docker/Dockerfile +52 -0
  6. package/templates/docker/bin/__tests__/benchmark.test.ts +598 -0
  7. package/templates/docker/bin/__tests__/duckdb-ingest.test.ts +171 -0
  8. package/templates/docker/bin/__tests__/eval.test.ts +434 -0
  9. package/templates/docker/bin/__tests__/matview-partition.test.ts +615 -0
  10. package/templates/docker/bin/__tests__/multi-source.test.ts +113 -0
  11. package/templates/docker/bin/__tests__/plugin-cli.test.ts +322 -0
  12. package/templates/docker/bin/__tests__/profiler-heuristics.test.ts +608 -0
  13. package/templates/docker/bin/__tests__/query.test.ts +240 -0
  14. package/templates/docker/bin/__tests__/schema-drift.test.ts +542 -0
  15. package/templates/docker/bin/__tests__/view-yaml-generation.test.ts +146 -0
  16. package/templates/docker/bin/atlas.ts +5044 -0
  17. package/templates/docker/bin/benchmark.ts +695 -0
  18. package/templates/docker/bin/enrich.ts +559 -0
  19. package/templates/docker/bin/eval.ts +770 -0
  20. package/templates/docker/bin/smoke.ts +438 -0
  21. package/templates/docker/data/.gitkeep +0 -0
  22. package/templates/docker/data/cybersec.sql +1961 -0
  23. package/templates/docker/data/demo-semantic/catalog.yml +40 -0
  24. package/templates/docker/data/demo-semantic/entities/accounts.yml +170 -0
  25. package/templates/docker/data/demo-semantic/entities/companies.yml +207 -0
  26. package/templates/docker/data/demo-semantic/entities/people.yml +145 -0
  27. package/templates/docker/data/demo-semantic/glossary.yml +22 -0
  28. package/templates/docker/data/demo-semantic/metrics/accounts.yml +38 -0
  29. package/templates/docker/data/demo-semantic/metrics/companies.yml +89 -0
  30. package/templates/docker/data/demo.sql +373 -0
  31. package/templates/docker/data/ecommerce.sql +1690 -0
  32. package/templates/docker/data/init-demo-db.sql +8 -0
  33. package/templates/docker/docker-compose.yml +34 -0
  34. package/templates/docker/docs/deploy.md +390 -0
  35. package/templates/docker/eslint.config.mjs +18 -0
  36. package/templates/docker/gitignore +5 -0
  37. package/templates/docker/next.config.ts +9 -0
  38. package/templates/docker/package.json +59 -0
  39. package/templates/docker/postcss.config.mjs +8 -0
  40. package/templates/docker/public/.gitkeep +0 -0
  41. package/templates/docker/public/favicon.svg +4 -0
  42. package/templates/docker/railway.json +13 -0
  43. package/templates/docker/render.yaml +34 -0
  44. package/templates/docker/semantic/catalog.yml +5 -0
  45. package/templates/docker/semantic/entities/.gitkeep +0 -0
  46. package/templates/docker/semantic/glossary.yml +6 -0
  47. package/templates/docker/semantic/metrics/.gitkeep +0 -0
  48. package/templates/docker/sidecar/Dockerfile +28 -0
  49. package/templates/docker/sidecar/railway.json +14 -0
  50. package/templates/docker/sidecar/server.ts +188 -0
  51. package/templates/docker/src/api/__tests__/actions.test.ts +683 -0
  52. package/templates/docker/src/api/__tests__/admin.test.ts +820 -0
  53. package/templates/docker/src/api/__tests__/auth.test.ts +165 -0
  54. package/templates/docker/src/api/__tests__/chat.test.ts +376 -0
  55. package/templates/docker/src/api/__tests__/conversations.test.ts +555 -0
  56. package/templates/docker/src/api/__tests__/cors.test.ts +135 -0
  57. package/templates/docker/src/api/__tests__/health-plugin.test.ts +169 -0
  58. package/templates/docker/src/api/__tests__/health.test.ts +261 -0
  59. package/templates/docker/src/api/__tests__/query.test.ts +891 -0
  60. package/templates/docker/src/api/__tests__/scheduled-tasks.test.ts +601 -0
  61. package/templates/docker/src/api/__tests__/slack.test.ts +847 -0
  62. package/templates/docker/src/api/index.ts +117 -0
  63. package/templates/docker/src/api/routes/actions.ts +274 -0
  64. package/templates/docker/src/api/routes/admin.ts +757 -0
  65. package/templates/docker/src/api/routes/auth.ts +48 -0
  66. package/templates/docker/src/api/routes/chat.ts +465 -0
  67. package/templates/docker/src/api/routes/conversations.ts +266 -0
  68. package/templates/docker/src/api/routes/health.ts +287 -0
  69. package/templates/docker/src/api/routes/openapi.ts +390 -0
  70. package/templates/docker/src/api/routes/query.ts +318 -0
  71. package/templates/docker/src/api/routes/scheduled-tasks.ts +467 -0
  72. package/templates/docker/src/api/routes/slack.ts +611 -0
  73. package/templates/docker/src/api/server.ts +226 -0
  74. package/templates/docker/src/app/api/[...route]/route.ts +33 -0
  75. package/templates/docker/src/app/error.tsx +24 -0
  76. package/templates/docker/src/app/globals.css +126 -0
  77. package/templates/docker/src/app/layout.tsx +19 -0
  78. package/templates/docker/src/app/page.tsx +14 -0
  79. package/templates/docker/src/global.d.ts +1 -0
  80. package/templates/docker/src/lib/__tests__/agent-cache.test.ts +437 -0
  81. package/templates/docker/src/lib/__tests__/agent-dialect.test.ts +114 -0
  82. package/templates/docker/src/lib/__tests__/agent-health-annotations.test.ts +164 -0
  83. package/templates/docker/src/lib/__tests__/agent-integration.test.ts +514 -0
  84. package/templates/docker/src/lib/__tests__/config-actions.test.ts +166 -0
  85. package/templates/docker/src/lib/__tests__/config.test.ts +1063 -0
  86. package/templates/docker/src/lib/__tests__/conversations.test.ts +589 -0
  87. package/templates/docker/src/lib/__tests__/errors.test.ts +256 -0
  88. package/templates/docker/src/lib/__tests__/logger.test.ts +200 -0
  89. package/templates/docker/src/lib/__tests__/providers.test.ts +99 -0
  90. package/templates/docker/src/lib/__tests__/rls.test.ts +435 -0
  91. package/templates/docker/src/lib/__tests__/scheduled-task-types.test.ts +124 -0
  92. package/templates/docker/src/lib/__tests__/scheduled-tasks.test.ts +550 -0
  93. package/templates/docker/src/lib/__tests__/semantic-index.test.ts +547 -0
  94. package/templates/docker/src/lib/__tests__/semantic-multisource.test.ts +544 -0
  95. package/templates/docker/src/lib/__tests__/semantic.test.ts +363 -0
  96. package/templates/docker/src/lib/__tests__/startup-actions.test.ts +452 -0
  97. package/templates/docker/src/lib/__tests__/startup.test.ts +465 -0
  98. package/templates/docker/src/lib/__tests__/tracing.test.ts +28 -0
  99. package/templates/docker/src/lib/action-types.ts +95 -0
  100. package/templates/docker/src/lib/agent-query.ts +178 -0
  101. package/templates/docker/src/lib/agent.ts +505 -0
  102. package/templates/docker/src/lib/api-url.ts +2 -0
  103. package/templates/docker/src/lib/auth/__tests__/audit.test.ts +418 -0
  104. package/templates/docker/src/lib/auth/__tests__/byot-integration.test.ts +222 -0
  105. package/templates/docker/src/lib/auth/__tests__/byot.test.ts +366 -0
  106. package/templates/docker/src/lib/auth/__tests__/detect.test.ts +190 -0
  107. package/templates/docker/src/lib/auth/__tests__/managed.test.ts +173 -0
  108. package/templates/docker/src/lib/auth/__tests__/middleware.test.ts +456 -0
  109. package/templates/docker/src/lib/auth/__tests__/migrate.test.ts +201 -0
  110. package/templates/docker/src/lib/auth/__tests__/permissions.test.ts +225 -0
  111. package/templates/docker/src/lib/auth/__tests__/server.test.ts +34 -0
  112. package/templates/docker/src/lib/auth/__tests__/simple-key.test.ts +176 -0
  113. package/templates/docker/src/lib/auth/__tests__/types.test.ts +44 -0
  114. package/templates/docker/src/lib/auth/audit.ts +89 -0
  115. package/templates/docker/src/lib/auth/byot.ts +158 -0
  116. package/templates/docker/src/lib/auth/client.ts +35 -0
  117. package/templates/docker/src/lib/auth/detect.ts +83 -0
  118. package/templates/docker/src/lib/auth/managed.ts +73 -0
  119. package/templates/docker/src/lib/auth/middleware.ts +208 -0
  120. package/templates/docker/src/lib/auth/migrate.ts +111 -0
  121. package/templates/docker/src/lib/auth/permissions.ts +156 -0
  122. package/templates/docker/src/lib/auth/server.ts +142 -0
  123. package/templates/docker/src/lib/auth/simple-key.ts +92 -0
  124. package/templates/docker/src/lib/auth/types.ts +49 -0
  125. package/templates/docker/src/lib/config.ts +704 -0
  126. package/templates/docker/src/lib/conversation-types.ts +29 -0
  127. package/templates/docker/src/lib/conversations.ts +270 -0
  128. package/templates/docker/src/lib/db/__tests__/connection.test.ts +69 -0
  129. package/templates/docker/src/lib/db/__tests__/duckdb.test.ts +141 -0
  130. package/templates/docker/src/lib/db/__tests__/internal.test.ts +387 -0
  131. package/templates/docker/src/lib/db/__tests__/registry-health.test.ts +207 -0
  132. package/templates/docker/src/lib/db/__tests__/registry-pool-limits.test.ts +156 -0
  133. package/templates/docker/src/lib/db/__tests__/registry.test.ts +595 -0
  134. package/templates/docker/src/lib/db/__tests__/salesforce.test.ts +339 -0
  135. package/templates/docker/src/lib/db/__tests__/snowflake.test.ts +217 -0
  136. package/templates/docker/src/lib/db/__tests__/source-rate-limit.test.ts +130 -0
  137. package/templates/docker/src/lib/db/connection.ts +753 -0
  138. package/templates/docker/src/lib/db/duckdb.ts +122 -0
  139. package/templates/docker/src/lib/db/internal.ts +273 -0
  140. package/templates/docker/src/lib/db/salesforce.ts +342 -0
  141. package/templates/docker/src/lib/db/source-rate-limit.ts +191 -0
  142. package/templates/docker/src/lib/errors.ts +154 -0
  143. package/templates/docker/src/lib/logger.ts +98 -0
  144. package/templates/docker/src/lib/plugins/__tests__/hooks-integration.test.ts +202 -0
  145. package/templates/docker/src/lib/plugins/__tests__/hooks.test.ts +529 -0
  146. package/templates/docker/src/lib/plugins/__tests__/migrate.test.ts +521 -0
  147. package/templates/docker/src/lib/plugins/__tests__/registry.test.ts +346 -0
  148. package/templates/docker/src/lib/plugins/__tests__/tools.test.ts +49 -0
  149. package/templates/docker/src/lib/plugins/__tests__/wiring.test.ts +585 -0
  150. package/templates/docker/src/lib/plugins/hooks.ts +162 -0
  151. package/templates/docker/src/lib/plugins/index.ts +9 -0
  152. package/templates/docker/src/lib/plugins/migrate.ts +309 -0
  153. package/templates/docker/src/lib/plugins/registry.ts +231 -0
  154. package/templates/docker/src/lib/plugins/tools.ts +39 -0
  155. package/templates/docker/src/lib/plugins/wiring.ts +291 -0
  156. package/templates/docker/src/lib/providers.ts +102 -0
  157. package/templates/docker/src/lib/rls.ts +321 -0
  158. package/templates/docker/src/lib/scheduled-task-types.ts +132 -0
  159. package/templates/docker/src/lib/scheduled-tasks.ts +475 -0
  160. package/templates/docker/src/lib/scheduler/__tests__/delivery.test.ts +192 -0
  161. package/templates/docker/src/lib/scheduler/__tests__/engine.test.ts +248 -0
  162. package/templates/docker/src/lib/scheduler/__tests__/format-email.test.ts +96 -0
  163. package/templates/docker/src/lib/scheduler/__tests__/format-slack.test.ts +78 -0
  164. package/templates/docker/src/lib/scheduler/__tests__/format-webhook.test.ts +78 -0
  165. package/templates/docker/src/lib/scheduler/delivery.ts +248 -0
  166. package/templates/docker/src/lib/scheduler/engine.ts +317 -0
  167. package/templates/docker/src/lib/scheduler/executor.ts +73 -0
  168. package/templates/docker/src/lib/scheduler/format-email.ts +109 -0
  169. package/templates/docker/src/lib/scheduler/format-slack.ts +35 -0
  170. package/templates/docker/src/lib/scheduler/format-webhook.ts +37 -0
  171. package/templates/docker/src/lib/scheduler/index.ts +7 -0
  172. package/templates/docker/src/lib/security.ts +11 -0
  173. package/templates/docker/src/lib/semantic-index.ts +503 -0
  174. package/templates/docker/src/lib/semantic.ts +387 -0
  175. package/templates/docker/src/lib/sidecar-types.ts +16 -0
  176. package/templates/docker/src/lib/slack/__tests__/api.test.ts +160 -0
  177. package/templates/docker/src/lib/slack/__tests__/format.test.ts +237 -0
  178. package/templates/docker/src/lib/slack/__tests__/store.test.ts +188 -0
  179. package/templates/docker/src/lib/slack/__tests__/threads.test.ts +112 -0
  180. package/templates/docker/src/lib/slack/__tests__/verify.test.ts +111 -0
  181. package/templates/docker/src/lib/slack/api.ts +102 -0
  182. package/templates/docker/src/lib/slack/format.ts +209 -0
  183. package/templates/docker/src/lib/slack/store.ts +107 -0
  184. package/templates/docker/src/lib/slack/threads.ts +64 -0
  185. package/templates/docker/src/lib/slack/verify.ts +71 -0
  186. package/templates/docker/src/lib/startup.ts +730 -0
  187. package/templates/docker/src/lib/tools/__tests__/action-permissions.test.ts +594 -0
  188. package/templates/docker/src/lib/tools/__tests__/custom-validation.test.ts +238 -0
  189. package/templates/docker/src/lib/tools/__tests__/explore-backend.test.ts +267 -0
  190. package/templates/docker/src/lib/tools/__tests__/explore-nsjail.test.ts +492 -0
  191. package/templates/docker/src/lib/tools/__tests__/explore-plugin.test.ts +374 -0
  192. package/templates/docker/src/lib/tools/__tests__/explore-sdk-compat.test.ts +82 -0
  193. package/templates/docker/src/lib/tools/__tests__/explore-sidecar.test.ts +208 -0
  194. package/templates/docker/src/lib/tools/__tests__/registry-actions.test.ts +144 -0
  195. package/templates/docker/src/lib/tools/__tests__/registry.test.ts +235 -0
  196. package/templates/docker/src/lib/tools/__tests__/salesforce-tool.test.ts +154 -0
  197. package/templates/docker/src/lib/tools/__tests__/soql-validation.test.ts +303 -0
  198. package/templates/docker/src/lib/tools/__tests__/sql-audit.test.ts +225 -0
  199. package/templates/docker/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +98 -0
  200. package/templates/docker/src/lib/tools/__tests__/sql-duckdb.test.ts +233 -0
  201. package/templates/docker/src/lib/tools/__tests__/sql-ratelimit.test.ts +225 -0
  202. package/templates/docker/src/lib/tools/__tests__/sql.test.ts +1012 -0
  203. package/templates/docker/src/lib/tools/actions/__tests__/audit.test.ts +211 -0
  204. package/templates/docker/src/lib/tools/actions/__tests__/email.test.ts +378 -0
  205. package/templates/docker/src/lib/tools/actions/__tests__/handler.test.ts +681 -0
  206. package/templates/docker/src/lib/tools/actions/__tests__/jira.test.ts +427 -0
  207. package/templates/docker/src/lib/tools/actions/audit.ts +47 -0
  208. package/templates/docker/src/lib/tools/actions/email.ts +191 -0
  209. package/templates/docker/src/lib/tools/actions/handler.ts +591 -0
  210. package/templates/docker/src/lib/tools/actions/index.ts +23 -0
  211. package/templates/docker/src/lib/tools/actions/jira.ts +220 -0
  212. package/templates/docker/src/lib/tools/explore-nsjail.ts +343 -0
  213. package/templates/docker/src/lib/tools/explore-sandbox.ts +264 -0
  214. package/templates/docker/src/lib/tools/explore-sidecar.ts +163 -0
  215. package/templates/docker/src/lib/tools/explore.ts +379 -0
  216. package/templates/docker/src/lib/tools/registry.ts +221 -0
  217. package/templates/docker/src/lib/tools/salesforce.ts +138 -0
  218. package/templates/docker/src/lib/tools/soql-validation.ts +172 -0
  219. package/templates/docker/src/lib/tools/sql.ts +680 -0
  220. package/templates/docker/src/lib/tracing.ts +40 -0
  221. package/templates/docker/src/lib/utils.ts +6 -0
  222. package/templates/docker/src/test-setup.ts +38 -0
  223. package/templates/docker/src/types/vercel-sandbox.d.ts +54 -0
  224. package/templates/docker/src/ui/components/actions/action-approval-card.tsx +295 -0
  225. package/templates/docker/src/ui/components/actions/action-status-badge.tsx +50 -0
  226. package/templates/docker/src/ui/components/admin/admin-layout.tsx +26 -0
  227. package/templates/docker/src/ui/components/admin/admin-sidebar.tsx +96 -0
  228. package/templates/docker/src/ui/components/admin/empty-state.tsx +24 -0
  229. package/templates/docker/src/ui/components/admin/entity-detail.tsx +233 -0
  230. package/templates/docker/src/ui/components/admin/entity-list.tsx +96 -0
  231. package/templates/docker/src/ui/components/admin/error-banner.tsx +22 -0
  232. package/templates/docker/src/ui/components/admin/feature-disabled.tsx +44 -0
  233. package/templates/docker/src/ui/components/admin/health-badge.tsx +30 -0
  234. package/templates/docker/src/ui/components/admin/loading-state.tsx +14 -0
  235. package/templates/docker/src/ui/components/admin/stat-card.tsx +32 -0
  236. package/templates/docker/src/ui/components/atlas-chat.tsx +370 -0
  237. package/templates/docker/src/ui/components/chart/chart-detection.ts +261 -0
  238. package/templates/docker/src/ui/components/chart/result-chart.tsx +375 -0
  239. package/templates/docker/src/ui/components/chat/api-key-bar.tsx +66 -0
  240. package/templates/docker/src/ui/components/chat/copy-button.tsx +25 -0
  241. package/templates/docker/src/ui/components/chat/data-table.tsx +102 -0
  242. package/templates/docker/src/ui/components/chat/error-banner.tsx +32 -0
  243. package/templates/docker/src/ui/components/chat/explore-card.tsx +41 -0
  244. package/templates/docker/src/ui/components/chat/loading-card.tsx +10 -0
  245. package/templates/docker/src/ui/components/chat/managed-auth-card.tsx +116 -0
  246. package/templates/docker/src/ui/components/chat/markdown.tsx +72 -0
  247. package/templates/docker/src/ui/components/chat/sql-block.tsx +30 -0
  248. package/templates/docker/src/ui/components/chat/sql-result-card.tsx +144 -0
  249. package/templates/docker/src/ui/components/chat/starter-prompts.ts +6 -0
  250. package/templates/docker/src/ui/components/chat/tool-part.tsx +40 -0
  251. package/templates/docker/src/ui/components/chat/typing-indicator.tsx +19 -0
  252. package/templates/docker/src/ui/components/conversations/conversation-item.tsx +120 -0
  253. package/templates/docker/src/ui/components/conversations/conversation-list.tsx +66 -0
  254. package/templates/docker/src/ui/components/conversations/conversation-sidebar.tsx +78 -0
  255. package/templates/docker/src/ui/components/conversations/delete-confirmation.tsx +27 -0
  256. package/templates/docker/src/ui/context.tsx +78 -0
  257. package/templates/docker/src/ui/hooks/use-admin-fetch.ts +104 -0
  258. package/templates/docker/src/ui/hooks/use-conversations.ts +184 -0
  259. package/templates/docker/src/ui/hooks/use-dark-mode.ts +17 -0
  260. package/templates/docker/src/ui/lib/action-types.ts +63 -0
  261. package/templates/docker/src/ui/lib/helpers.ts +104 -0
  262. package/templates/docker/src/ui/lib/types.ts +145 -0
  263. package/templates/docker/tsconfig.json +41 -0
  264. package/templates/docker/vercel.json +3 -0
  265. package/templates/nextjs-standalone/.env.example +68 -0
  266. package/templates/nextjs-standalone/bin/__tests__/benchmark.test.ts +598 -0
  267. package/templates/nextjs-standalone/bin/__tests__/duckdb-ingest.test.ts +171 -0
  268. package/templates/nextjs-standalone/bin/__tests__/eval.test.ts +434 -0
  269. package/templates/nextjs-standalone/bin/__tests__/matview-partition.test.ts +615 -0
  270. package/templates/nextjs-standalone/bin/__tests__/multi-source.test.ts +113 -0
  271. package/templates/nextjs-standalone/bin/__tests__/plugin-cli.test.ts +322 -0
  272. package/templates/nextjs-standalone/bin/__tests__/profiler-heuristics.test.ts +608 -0
  273. package/templates/nextjs-standalone/bin/__tests__/query.test.ts +240 -0
  274. package/templates/nextjs-standalone/bin/__tests__/schema-drift.test.ts +542 -0
  275. package/templates/nextjs-standalone/bin/__tests__/view-yaml-generation.test.ts +146 -0
  276. package/templates/nextjs-standalone/bin/atlas.ts +5044 -0
  277. package/templates/nextjs-standalone/bin/benchmark.ts +695 -0
  278. package/templates/nextjs-standalone/bin/enrich.ts +559 -0
  279. package/templates/nextjs-standalone/bin/eval.ts +770 -0
  280. package/templates/nextjs-standalone/bin/smoke.ts +438 -0
  281. package/templates/nextjs-standalone/data/.gitkeep +0 -0
  282. package/templates/nextjs-standalone/data/cybersec.sql +1961 -0
  283. package/templates/nextjs-standalone/data/demo-semantic/catalog.yml +40 -0
  284. package/templates/nextjs-standalone/data/demo-semantic/entities/accounts.yml +170 -0
  285. package/templates/nextjs-standalone/data/demo-semantic/entities/companies.yml +207 -0
  286. package/templates/nextjs-standalone/data/demo-semantic/entities/people.yml +145 -0
  287. package/templates/nextjs-standalone/data/demo-semantic/glossary.yml +22 -0
  288. package/templates/nextjs-standalone/data/demo-semantic/metrics/accounts.yml +38 -0
  289. package/templates/nextjs-standalone/data/demo-semantic/metrics/companies.yml +89 -0
  290. package/templates/nextjs-standalone/data/demo.sql +373 -0
  291. package/templates/nextjs-standalone/data/ecommerce.sql +1690 -0
  292. package/templates/nextjs-standalone/data/init-demo-db.sql +8 -0
  293. package/templates/nextjs-standalone/docs/deploy.md +390 -0
  294. package/templates/nextjs-standalone/eslint.config.mjs +18 -0
  295. package/templates/nextjs-standalone/gitignore +5 -0
  296. package/templates/nextjs-standalone/next.config.ts +10 -0
  297. package/templates/nextjs-standalone/package.json +63 -0
  298. package/templates/nextjs-standalone/postcss.config.mjs +8 -0
  299. package/templates/nextjs-standalone/semantic/catalog.yml +5 -0
  300. package/templates/nextjs-standalone/semantic/entities/.gitkeep +0 -0
  301. package/templates/nextjs-standalone/semantic/glossary.yml +6 -0
  302. package/templates/nextjs-standalone/semantic/metrics/.gitkeep +0 -0
  303. package/templates/nextjs-standalone/src/api/__tests__/actions.test.ts +683 -0
  304. package/templates/nextjs-standalone/src/api/__tests__/admin.test.ts +820 -0
  305. package/templates/nextjs-standalone/src/api/__tests__/auth.test.ts +165 -0
  306. package/templates/nextjs-standalone/src/api/__tests__/chat.test.ts +376 -0
  307. package/templates/nextjs-standalone/src/api/__tests__/conversations.test.ts +555 -0
  308. package/templates/nextjs-standalone/src/api/__tests__/cors.test.ts +135 -0
  309. package/templates/nextjs-standalone/src/api/__tests__/health-plugin.test.ts +169 -0
  310. package/templates/nextjs-standalone/src/api/__tests__/health.test.ts +261 -0
  311. package/templates/nextjs-standalone/src/api/__tests__/query.test.ts +891 -0
  312. package/templates/nextjs-standalone/src/api/__tests__/scheduled-tasks.test.ts +601 -0
  313. package/templates/nextjs-standalone/src/api/__tests__/slack.test.ts +847 -0
  314. package/templates/nextjs-standalone/src/api/index.ts +117 -0
  315. package/templates/nextjs-standalone/src/api/routes/actions.ts +274 -0
  316. package/templates/nextjs-standalone/src/api/routes/admin.ts +757 -0
  317. package/templates/nextjs-standalone/src/api/routes/auth.ts +48 -0
  318. package/templates/nextjs-standalone/src/api/routes/chat.ts +465 -0
  319. package/templates/nextjs-standalone/src/api/routes/conversations.ts +266 -0
  320. package/templates/nextjs-standalone/src/api/routes/health.ts +287 -0
  321. package/templates/nextjs-standalone/src/api/routes/openapi.ts +390 -0
  322. package/templates/nextjs-standalone/src/api/routes/query.ts +318 -0
  323. package/templates/nextjs-standalone/src/api/routes/scheduled-tasks.ts +467 -0
  324. package/templates/nextjs-standalone/src/api/routes/slack.ts +611 -0
  325. package/templates/nextjs-standalone/src/api/server.ts +226 -0
  326. package/templates/nextjs-standalone/src/app/api/[...route]/route.ts +33 -0
  327. package/templates/nextjs-standalone/src/app/error.tsx +24 -0
  328. package/templates/nextjs-standalone/src/app/global-error.tsx +68 -0
  329. package/templates/nextjs-standalone/src/app/globals.css +126 -0
  330. package/templates/nextjs-standalone/src/app/layout.tsx +19 -0
  331. package/templates/nextjs-standalone/src/app/page.tsx +14 -0
  332. package/templates/nextjs-standalone/src/lib/__tests__/agent-cache.test.ts +437 -0
  333. package/templates/nextjs-standalone/src/lib/__tests__/agent-dialect.test.ts +114 -0
  334. package/templates/nextjs-standalone/src/lib/__tests__/agent-health-annotations.test.ts +164 -0
  335. package/templates/nextjs-standalone/src/lib/__tests__/agent-integration.test.ts +514 -0
  336. package/templates/nextjs-standalone/src/lib/__tests__/config-actions.test.ts +166 -0
  337. package/templates/nextjs-standalone/src/lib/__tests__/config.test.ts +1063 -0
  338. package/templates/nextjs-standalone/src/lib/__tests__/conversations.test.ts +589 -0
  339. package/templates/nextjs-standalone/src/lib/__tests__/errors.test.ts +256 -0
  340. package/templates/nextjs-standalone/src/lib/__tests__/logger.test.ts +200 -0
  341. package/templates/nextjs-standalone/src/lib/__tests__/providers.test.ts +99 -0
  342. package/templates/nextjs-standalone/src/lib/__tests__/rls.test.ts +435 -0
  343. package/templates/nextjs-standalone/src/lib/__tests__/scheduled-task-types.test.ts +124 -0
  344. package/templates/nextjs-standalone/src/lib/__tests__/scheduled-tasks.test.ts +550 -0
  345. package/templates/nextjs-standalone/src/lib/__tests__/semantic-index.test.ts +547 -0
  346. package/templates/nextjs-standalone/src/lib/__tests__/semantic-multisource.test.ts +544 -0
  347. package/templates/nextjs-standalone/src/lib/__tests__/semantic.test.ts +363 -0
  348. package/templates/nextjs-standalone/src/lib/__tests__/startup-actions.test.ts +452 -0
  349. package/templates/nextjs-standalone/src/lib/__tests__/startup.test.ts +465 -0
  350. package/templates/nextjs-standalone/src/lib/__tests__/tracing.test.ts +28 -0
  351. package/templates/nextjs-standalone/src/lib/action-types.ts +95 -0
  352. package/templates/nextjs-standalone/src/lib/agent-query.ts +178 -0
  353. package/templates/nextjs-standalone/src/lib/agent.ts +505 -0
  354. package/templates/nextjs-standalone/src/lib/api-url.ts +3 -0
  355. package/templates/nextjs-standalone/src/lib/auth/__tests__/audit.test.ts +418 -0
  356. package/templates/nextjs-standalone/src/lib/auth/__tests__/byot-integration.test.ts +222 -0
  357. package/templates/nextjs-standalone/src/lib/auth/__tests__/byot.test.ts +366 -0
  358. package/templates/nextjs-standalone/src/lib/auth/__tests__/detect.test.ts +190 -0
  359. package/templates/nextjs-standalone/src/lib/auth/__tests__/managed.test.ts +173 -0
  360. package/templates/nextjs-standalone/src/lib/auth/__tests__/middleware.test.ts +456 -0
  361. package/templates/nextjs-standalone/src/lib/auth/__tests__/migrate.test.ts +201 -0
  362. package/templates/nextjs-standalone/src/lib/auth/__tests__/permissions.test.ts +225 -0
  363. package/templates/nextjs-standalone/src/lib/auth/__tests__/server.test.ts +34 -0
  364. package/templates/nextjs-standalone/src/lib/auth/__tests__/simple-key.test.ts +176 -0
  365. package/templates/nextjs-standalone/src/lib/auth/__tests__/types.test.ts +44 -0
  366. package/templates/nextjs-standalone/src/lib/auth/audit.ts +89 -0
  367. package/templates/nextjs-standalone/src/lib/auth/byot.ts +158 -0
  368. package/templates/nextjs-standalone/src/lib/auth/client.ts +23 -0
  369. package/templates/nextjs-standalone/src/lib/auth/detect.ts +83 -0
  370. package/templates/nextjs-standalone/src/lib/auth/managed.ts +73 -0
  371. package/templates/nextjs-standalone/src/lib/auth/middleware.ts +208 -0
  372. package/templates/nextjs-standalone/src/lib/auth/migrate.ts +111 -0
  373. package/templates/nextjs-standalone/src/lib/auth/permissions.ts +156 -0
  374. package/templates/nextjs-standalone/src/lib/auth/server.ts +142 -0
  375. package/templates/nextjs-standalone/src/lib/auth/simple-key.ts +92 -0
  376. package/templates/nextjs-standalone/src/lib/auth/types.ts +49 -0
  377. package/templates/nextjs-standalone/src/lib/config.ts +704 -0
  378. package/templates/nextjs-standalone/src/lib/conversation-types.ts +29 -0
  379. package/templates/nextjs-standalone/src/lib/conversations.ts +270 -0
  380. package/templates/nextjs-standalone/src/lib/db/__tests__/connection.test.ts +69 -0
  381. package/templates/nextjs-standalone/src/lib/db/__tests__/duckdb.test.ts +141 -0
  382. package/templates/nextjs-standalone/src/lib/db/__tests__/internal.test.ts +387 -0
  383. package/templates/nextjs-standalone/src/lib/db/__tests__/registry-health.test.ts +207 -0
  384. package/templates/nextjs-standalone/src/lib/db/__tests__/registry-pool-limits.test.ts +156 -0
  385. package/templates/nextjs-standalone/src/lib/db/__tests__/registry.test.ts +595 -0
  386. package/templates/nextjs-standalone/src/lib/db/__tests__/salesforce.test.ts +339 -0
  387. package/templates/nextjs-standalone/src/lib/db/__tests__/snowflake.test.ts +217 -0
  388. package/templates/nextjs-standalone/src/lib/db/__tests__/source-rate-limit.test.ts +130 -0
  389. package/templates/nextjs-standalone/src/lib/db/connection.ts +753 -0
  390. package/templates/nextjs-standalone/src/lib/db/duckdb.ts +122 -0
  391. package/templates/nextjs-standalone/src/lib/db/internal.ts +273 -0
  392. package/templates/nextjs-standalone/src/lib/db/salesforce.ts +342 -0
  393. package/templates/nextjs-standalone/src/lib/db/source-rate-limit.ts +191 -0
  394. package/templates/nextjs-standalone/src/lib/errors.ts +154 -0
  395. package/templates/nextjs-standalone/src/lib/logger.ts +98 -0
  396. package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks-integration.test.ts +202 -0
  397. package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks.test.ts +529 -0
  398. package/templates/nextjs-standalone/src/lib/plugins/__tests__/migrate.test.ts +521 -0
  399. package/templates/nextjs-standalone/src/lib/plugins/__tests__/registry.test.ts +346 -0
  400. package/templates/nextjs-standalone/src/lib/plugins/__tests__/tools.test.ts +49 -0
  401. package/templates/nextjs-standalone/src/lib/plugins/__tests__/wiring.test.ts +585 -0
  402. package/templates/nextjs-standalone/src/lib/plugins/hooks.ts +162 -0
  403. package/templates/nextjs-standalone/src/lib/plugins/index.ts +9 -0
  404. package/templates/nextjs-standalone/src/lib/plugins/migrate.ts +309 -0
  405. package/templates/nextjs-standalone/src/lib/plugins/registry.ts +231 -0
  406. package/templates/nextjs-standalone/src/lib/plugins/tools.ts +39 -0
  407. package/templates/nextjs-standalone/src/lib/plugins/wiring.ts +291 -0
  408. package/templates/nextjs-standalone/src/lib/providers.ts +102 -0
  409. package/templates/nextjs-standalone/src/lib/rls.ts +321 -0
  410. package/templates/nextjs-standalone/src/lib/scheduled-task-types.ts +132 -0
  411. package/templates/nextjs-standalone/src/lib/scheduled-tasks.ts +475 -0
  412. package/templates/nextjs-standalone/src/lib/scheduler/__tests__/delivery.test.ts +192 -0
  413. package/templates/nextjs-standalone/src/lib/scheduler/__tests__/engine.test.ts +248 -0
  414. package/templates/nextjs-standalone/src/lib/scheduler/__tests__/format-email.test.ts +96 -0
  415. package/templates/nextjs-standalone/src/lib/scheduler/__tests__/format-slack.test.ts +78 -0
  416. package/templates/nextjs-standalone/src/lib/scheduler/__tests__/format-webhook.test.ts +78 -0
  417. package/templates/nextjs-standalone/src/lib/scheduler/delivery.ts +248 -0
  418. package/templates/nextjs-standalone/src/lib/scheduler/engine.ts +317 -0
  419. package/templates/nextjs-standalone/src/lib/scheduler/executor.ts +73 -0
  420. package/templates/nextjs-standalone/src/lib/scheduler/format-email.ts +109 -0
  421. package/templates/nextjs-standalone/src/lib/scheduler/format-slack.ts +35 -0
  422. package/templates/nextjs-standalone/src/lib/scheduler/format-webhook.ts +37 -0
  423. package/templates/nextjs-standalone/src/lib/scheduler/index.ts +7 -0
  424. package/templates/nextjs-standalone/src/lib/security.ts +11 -0
  425. package/templates/nextjs-standalone/src/lib/semantic-index.ts +503 -0
  426. package/templates/nextjs-standalone/src/lib/semantic.ts +387 -0
  427. package/templates/nextjs-standalone/src/lib/sidecar-types.ts +16 -0
  428. package/templates/nextjs-standalone/src/lib/slack/__tests__/api.test.ts +160 -0
  429. package/templates/nextjs-standalone/src/lib/slack/__tests__/format.test.ts +237 -0
  430. package/templates/nextjs-standalone/src/lib/slack/__tests__/store.test.ts +188 -0
  431. package/templates/nextjs-standalone/src/lib/slack/__tests__/threads.test.ts +112 -0
  432. package/templates/nextjs-standalone/src/lib/slack/__tests__/verify.test.ts +111 -0
  433. package/templates/nextjs-standalone/src/lib/slack/api.ts +102 -0
  434. package/templates/nextjs-standalone/src/lib/slack/format.ts +209 -0
  435. package/templates/nextjs-standalone/src/lib/slack/store.ts +107 -0
  436. package/templates/nextjs-standalone/src/lib/slack/threads.ts +64 -0
  437. package/templates/nextjs-standalone/src/lib/slack/verify.ts +71 -0
  438. package/templates/nextjs-standalone/src/lib/startup.ts +730 -0
  439. package/templates/nextjs-standalone/src/lib/tools/__tests__/action-permissions.test.ts +594 -0
  440. package/templates/nextjs-standalone/src/lib/tools/__tests__/custom-validation.test.ts +238 -0
  441. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-backend.test.ts +267 -0
  442. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-nsjail.test.ts +492 -0
  443. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-plugin.test.ts +374 -0
  444. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-sdk-compat.test.ts +82 -0
  445. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-sidecar.test.ts +208 -0
  446. package/templates/nextjs-standalone/src/lib/tools/__tests__/registry-actions.test.ts +144 -0
  447. package/templates/nextjs-standalone/src/lib/tools/__tests__/registry.test.ts +235 -0
  448. package/templates/nextjs-standalone/src/lib/tools/__tests__/salesforce-tool.test.ts +154 -0
  449. package/templates/nextjs-standalone/src/lib/tools/__tests__/soql-validation.test.ts +303 -0
  450. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-audit.test.ts +225 -0
  451. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +98 -0
  452. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-duckdb.test.ts +233 -0
  453. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-ratelimit.test.ts +225 -0
  454. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql.test.ts +1012 -0
  455. package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/audit.test.ts +211 -0
  456. package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/email.test.ts +378 -0
  457. package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/handler.test.ts +681 -0
  458. package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/jira.test.ts +427 -0
  459. package/templates/nextjs-standalone/src/lib/tools/actions/audit.ts +47 -0
  460. package/templates/nextjs-standalone/src/lib/tools/actions/email.ts +191 -0
  461. package/templates/nextjs-standalone/src/lib/tools/actions/handler.ts +591 -0
  462. package/templates/nextjs-standalone/src/lib/tools/actions/index.ts +23 -0
  463. package/templates/nextjs-standalone/src/lib/tools/actions/jira.ts +220 -0
  464. package/templates/nextjs-standalone/src/lib/tools/explore-nsjail.ts +343 -0
  465. package/templates/nextjs-standalone/src/lib/tools/explore-sandbox.ts +264 -0
  466. package/templates/nextjs-standalone/src/lib/tools/explore-sidecar.ts +163 -0
  467. package/templates/nextjs-standalone/src/lib/tools/explore.ts +379 -0
  468. package/templates/nextjs-standalone/src/lib/tools/registry.ts +221 -0
  469. package/templates/nextjs-standalone/src/lib/tools/salesforce.ts +138 -0
  470. package/templates/nextjs-standalone/src/lib/tools/soql-validation.ts +172 -0
  471. package/templates/nextjs-standalone/src/lib/tools/sql.ts +680 -0
  472. package/templates/nextjs-standalone/src/lib/tracing.ts +40 -0
  473. package/templates/nextjs-standalone/src/lib/utils.ts +6 -0
  474. package/templates/nextjs-standalone/src/test-setup.ts +38 -0
  475. package/templates/nextjs-standalone/src/ui/components/actions/action-approval-card.tsx +295 -0
  476. package/templates/nextjs-standalone/src/ui/components/actions/action-status-badge.tsx +50 -0
  477. package/templates/nextjs-standalone/src/ui/components/admin/admin-layout.tsx +26 -0
  478. package/templates/nextjs-standalone/src/ui/components/admin/admin-sidebar.tsx +96 -0
  479. package/templates/nextjs-standalone/src/ui/components/admin/empty-state.tsx +24 -0
  480. package/templates/nextjs-standalone/src/ui/components/admin/entity-detail.tsx +233 -0
  481. package/templates/nextjs-standalone/src/ui/components/admin/entity-list.tsx +96 -0
  482. package/templates/nextjs-standalone/src/ui/components/admin/error-banner.tsx +22 -0
  483. package/templates/nextjs-standalone/src/ui/components/admin/feature-disabled.tsx +44 -0
  484. package/templates/nextjs-standalone/src/ui/components/admin/health-badge.tsx +30 -0
  485. package/templates/nextjs-standalone/src/ui/components/admin/loading-state.tsx +14 -0
  486. package/templates/nextjs-standalone/src/ui/components/admin/stat-card.tsx +32 -0
  487. package/templates/nextjs-standalone/src/ui/components/atlas-chat.tsx +370 -0
  488. package/templates/nextjs-standalone/src/ui/components/chart/chart-detection.ts +261 -0
  489. package/templates/nextjs-standalone/src/ui/components/chart/result-chart.tsx +375 -0
  490. package/templates/nextjs-standalone/src/ui/components/chat/api-key-bar.tsx +66 -0
  491. package/templates/nextjs-standalone/src/ui/components/chat/copy-button.tsx +25 -0
  492. package/templates/nextjs-standalone/src/ui/components/chat/data-table.tsx +102 -0
  493. package/templates/nextjs-standalone/src/ui/components/chat/error-banner.tsx +32 -0
  494. package/templates/nextjs-standalone/src/ui/components/chat/explore-card.tsx +41 -0
  495. package/templates/nextjs-standalone/src/ui/components/chat/loading-card.tsx +10 -0
  496. package/templates/nextjs-standalone/src/ui/components/chat/managed-auth-card.tsx +116 -0
  497. package/templates/nextjs-standalone/src/ui/components/chat/markdown.tsx +72 -0
  498. package/templates/nextjs-standalone/src/ui/components/chat/sql-block.tsx +30 -0
  499. package/templates/nextjs-standalone/src/ui/components/chat/sql-result-card.tsx +144 -0
  500. package/templates/nextjs-standalone/src/ui/components/chat/starter-prompts.ts +6 -0
  501. package/templates/nextjs-standalone/src/ui/components/chat/tool-part.tsx +40 -0
  502. package/templates/nextjs-standalone/src/ui/components/chat/typing-indicator.tsx +19 -0
  503. package/templates/nextjs-standalone/src/ui/components/conversations/conversation-item.tsx +120 -0
  504. package/templates/nextjs-standalone/src/ui/components/conversations/conversation-list.tsx +66 -0
  505. package/templates/nextjs-standalone/src/ui/components/conversations/conversation-sidebar.tsx +78 -0
  506. package/templates/nextjs-standalone/src/ui/components/conversations/delete-confirmation.tsx +27 -0
  507. package/templates/nextjs-standalone/src/ui/context.tsx +78 -0
  508. package/templates/nextjs-standalone/src/ui/hooks/use-admin-fetch.ts +104 -0
  509. package/templates/nextjs-standalone/src/ui/hooks/use-conversations.ts +184 -0
  510. package/templates/nextjs-standalone/src/ui/hooks/use-dark-mode.ts +17 -0
  511. package/templates/nextjs-standalone/src/ui/lib/action-types.ts +63 -0
  512. package/templates/nextjs-standalone/src/ui/lib/helpers.ts +104 -0
  513. package/templates/nextjs-standalone/src/ui/lib/types.ts +145 -0
  514. package/templates/nextjs-standalone/tsconfig.json +32 -0
  515. package/templates/nextjs-standalone/vercel.json +4 -0
@@ -0,0 +1,891 @@
1
+ /**
2
+ * Unit tests for the POST /api/v1/query and GET /api/v1/openapi.json routes.
3
+ *
4
+ * Mocks auth, rate-limiting, startup diagnostics, and the agent to
5
+ * isolate the route wiring logic. Tests the Hono app.fetch() directly.
6
+ */
7
+
8
+ import {
9
+ describe,
10
+ it,
11
+ expect,
12
+ beforeEach,
13
+ afterEach,
14
+ mock,
15
+ type Mock,
16
+ } from "bun:test";
17
+ import { APICallError, LoadAPIKeyError, NoSuchModelError } from "ai";
18
+ import { GatewayModelNotFoundError } from "@ai-sdk/gateway";
19
+ import type { AuthResult } from "@atlas/api/lib/auth/types";
20
+
21
+ // --- Mocks ---
22
+
23
+ const mockAuthenticateRequest: Mock<
24
+ (req: Request) => Promise<AuthResult>
25
+ > = mock(() =>
26
+ Promise.resolve({
27
+ authenticated: true as const,
28
+ mode: "none" as const,
29
+ user: undefined,
30
+ }),
31
+ );
32
+
33
+ const mockCheckRateLimit: Mock<
34
+ (key: string) => { allowed: boolean; retryAfterMs?: number }
35
+ > = mock(() => ({ allowed: true }));
36
+
37
+ const mockGetClientIP: Mock<(req: Request) => string | null> = mock(
38
+ () => null,
39
+ );
40
+
41
+ mock.module("@atlas/api/lib/auth/middleware", () => ({
42
+ authenticateRequest: mockAuthenticateRequest,
43
+ checkRateLimit: mockCheckRateLimit,
44
+ getClientIP: mockGetClientIP,
45
+ }));
46
+
47
+ const mockValidateEnvironment: Mock<
48
+ () => Promise<{ message: string }[]>
49
+ > = mock(() => Promise.resolve([]));
50
+
51
+ // Helper to build a mock step with toolResults using AI SDK shape (input/output)
52
+ function mockStep(
53
+ toolResults: {
54
+ toolName: string;
55
+ input: unknown;
56
+ output: unknown;
57
+ }[],
58
+ ) {
59
+ return {
60
+ toolResults: toolResults.map((tr) => ({
61
+ type: "tool-result" as const,
62
+ toolCallId: crypto.randomUUID(),
63
+ toolName: tr.toolName,
64
+ input: tr.input,
65
+ output: tr.output,
66
+ })),
67
+ };
68
+ }
69
+
70
+ function makeAgentResult(overrides?: {
71
+ text?: string;
72
+ steps?: ReturnType<typeof mockStep>[];
73
+ inputTokens?: number;
74
+ outputTokens?: number;
75
+ }) {
76
+ return {
77
+ toUIMessageStreamResponse: () => new Response("stream", { status: 200 }),
78
+ text: Promise.resolve(overrides?.text ?? "The answer is 42."),
79
+ steps: Promise.resolve(
80
+ overrides?.steps ?? [
81
+ mockStep([
82
+ {
83
+ toolName: "executeSQL",
84
+ input: { sql: "SELECT COUNT(*) FROM users" },
85
+ output: { success: true, columns: ["count"], rows: [{ count: 42 }] },
86
+ },
87
+ ]),
88
+ ],
89
+ ),
90
+ totalUsage: Promise.resolve({
91
+ inputTokens: overrides?.inputTokens ?? 100,
92
+ outputTokens: overrides?.outputTokens ?? 50,
93
+ }),
94
+ };
95
+ }
96
+
97
+ const mockRunAgent = mock(() => Promise.resolve(makeAgentResult()));
98
+
99
+ mock.module("@atlas/api/lib/agent", () => ({
100
+ runAgent: mockRunAgent,
101
+ }));
102
+
103
+ mock.module("@atlas/api/lib/semantic", () => ({
104
+ getWhitelistedTables: () => new Set(),
105
+ _resetWhitelists: () => {},
106
+ }));
107
+
108
+ mock.module("@atlas/api/lib/tools/explore", () => ({
109
+ getExploreBackendType: () => "just-bash",
110
+ getActiveSandboxPluginId: () => null,
111
+ }));
112
+
113
+ mock.module("@atlas/api/lib/auth/detect", () => ({
114
+ detectAuthMode: () => "none",
115
+ resetAuthModeCache: () => {},
116
+ }));
117
+
118
+ mock.module("@atlas/api/lib/startup", () => ({
119
+ validateEnvironment: mockValidateEnvironment,
120
+ getStartupWarnings: () => [],
121
+ }));
122
+
123
+ const mockCreateConversationQuery = mock((): Promise<{ id: string } | null> =>
124
+ Promise.resolve({ id: "conv-query-123" }),
125
+ );
126
+ const mockAddMessageQuery = mock(() => {});
127
+ const mockGetConversationQuery = mock((): Promise<{ ok: boolean; reason?: string; data?: unknown }> => Promise.resolve({ ok: false, reason: "not_found" }));
128
+ const mockGenerateTitleQuery = mock((q: string) => q.slice(0, 80));
129
+
130
+ mock.module("@atlas/api/lib/conversations", () => ({
131
+ createConversation: mockCreateConversationQuery,
132
+ addMessage: mockAddMessageQuery,
133
+ getConversation: mockGetConversationQuery,
134
+ generateTitle: mockGenerateTitleQuery,
135
+ listConversations: mock(() => Promise.resolve({ conversations: [], total: 0 })),
136
+ deleteConversation: mock(() => Promise.resolve({ ok: false, reason: "not_found" })),
137
+ starConversation: mock(() => Promise.resolve({ ok: false, reason: "not_found" })),
138
+ }));
139
+
140
+ // Import after mocks are registered
141
+ const { app } = await import("../index");
142
+
143
+ // --- Helpers ---
144
+
145
+ function makeQueryRequest(body?: unknown): Request {
146
+ return new Request("http://localhost/api/v1/query", {
147
+ method: "POST",
148
+ headers: { "Content-Type": "application/json" },
149
+ body: JSON.stringify(body ?? { question: "How many users?" }),
150
+ });
151
+ }
152
+
153
+ // --- POST /api/v1/query ---
154
+
155
+ describe("POST /api/v1/query", () => {
156
+ const origDatasource = process.env.ATLAS_DATASOURCE_URL;
157
+ const origDatabaseUrl = process.env.DATABASE_URL;
158
+
159
+ beforeEach(() => {
160
+ process.env.ATLAS_DATASOURCE_URL =
161
+ "postgresql://test:test@localhost:5432/test";
162
+ process.env.DATABASE_URL = "postgresql://test:test@localhost:5432/test";
163
+ mockAuthenticateRequest.mockReset();
164
+ mockAuthenticateRequest.mockResolvedValue({
165
+ authenticated: true as const,
166
+ mode: "none" as const,
167
+ user: undefined,
168
+ });
169
+ mockCheckRateLimit.mockReset();
170
+ mockCheckRateLimit.mockReturnValue({ allowed: true });
171
+ mockGetClientIP.mockReset();
172
+ mockGetClientIP.mockReturnValue(null);
173
+ mockValidateEnvironment.mockReset();
174
+ mockValidateEnvironment.mockResolvedValue([]);
175
+ mockRunAgent.mockReset();
176
+ mockRunAgent.mockResolvedValue(makeAgentResult());
177
+ mockCreateConversationQuery.mockReset();
178
+ mockCreateConversationQuery.mockResolvedValue({ id: "conv-query-123" });
179
+ mockAddMessageQuery.mockReset();
180
+ mockGetConversationQuery.mockReset();
181
+ mockGetConversationQuery.mockResolvedValue({ ok: false, reason: "not_found" });
182
+ });
183
+
184
+ afterEach(() => {
185
+ if (origDatasource !== undefined)
186
+ process.env.ATLAS_DATASOURCE_URL = origDatasource;
187
+ else delete process.env.ATLAS_DATASOURCE_URL;
188
+ if (origDatabaseUrl !== undefined)
189
+ process.env.DATABASE_URL = origDatabaseUrl;
190
+ else delete process.env.DATABASE_URL;
191
+ });
192
+
193
+ it("returns structured JSON on success", async () => {
194
+ const response = await app.fetch(makeQueryRequest());
195
+ expect(response.status).toBe(200);
196
+
197
+ const body = (await response.json()) as Record<string, unknown>;
198
+ expect(body.answer).toBe("The answer is 42.");
199
+ expect(body.sql).toEqual(["SELECT COUNT(*) FROM users"]);
200
+ expect(body.data).toEqual([{ columns: ["count"], rows: [{ count: 42 }] }]);
201
+ expect(body.steps).toBe(1);
202
+ expect(body.usage).toEqual({ totalTokens: 150 });
203
+ });
204
+
205
+ it("returns 401 when unauthenticated", async () => {
206
+ mockAuthenticateRequest.mockResolvedValueOnce({
207
+ authenticated: false as const,
208
+ mode: "simple-key" as const,
209
+ status: 401 as const,
210
+ error: "API key required",
211
+ });
212
+
213
+ const response = await app.fetch(makeQueryRequest());
214
+ expect(response.status).toBe(401);
215
+
216
+ const body = (await response.json()) as Record<string, unknown>;
217
+ expect(body.error).toBe("auth_error");
218
+ expect(mockRunAgent).not.toHaveBeenCalled();
219
+ });
220
+
221
+ it("returns 500 when auth throws", async () => {
222
+ mockAuthenticateRequest.mockRejectedValueOnce(new Error("DB crashed"));
223
+
224
+ const response = await app.fetch(makeQueryRequest());
225
+ expect(response.status).toBe(500);
226
+
227
+ const body = (await response.json()) as Record<string, unknown>;
228
+ expect(body.error).toBe("auth_error");
229
+ expect(mockRunAgent).not.toHaveBeenCalled();
230
+ });
231
+
232
+ it("returns 429 with Retry-After when rate limited", async () => {
233
+ mockCheckRateLimit.mockReturnValueOnce({
234
+ allowed: false,
235
+ retryAfterMs: 30000,
236
+ });
237
+
238
+ const response = await app.fetch(makeQueryRequest());
239
+ expect(response.status).toBe(429);
240
+ expect(response.headers.get("Retry-After")).toBe("30");
241
+
242
+ const body = (await response.json()) as Record<string, unknown>;
243
+ expect(body.error).toBe("rate_limited");
244
+ expect(body.retryAfterSeconds).toBe(30);
245
+ expect(mockRunAgent).not.toHaveBeenCalled();
246
+ });
247
+
248
+ it("returns retryAfterSeconds=60 when retryAfterMs is undefined", async () => {
249
+ mockCheckRateLimit.mockReturnValueOnce({ allowed: false });
250
+ const response = await app.fetch(makeQueryRequest());
251
+ expect(response.status).toBe(429);
252
+ expect(response.headers.get("Retry-After")).toBe("60");
253
+ const body = (await response.json()) as Record<string, unknown>;
254
+ expect(body.retryAfterSeconds).toBe(60);
255
+ expect(mockRunAgent).not.toHaveBeenCalled();
256
+ });
257
+
258
+ it("returns 400 when ATLAS_DATASOURCE_URL is not set", async () => {
259
+ delete process.env.ATLAS_DATASOURCE_URL;
260
+
261
+ const response = await app.fetch(makeQueryRequest());
262
+ expect(response.status).toBe(400);
263
+
264
+ const body = (await response.json()) as Record<string, unknown>;
265
+ expect(body.error).toBe("no_datasource");
266
+ expect(mockRunAgent).not.toHaveBeenCalled();
267
+ });
268
+
269
+ it("returns 400 when validateEnvironment reports errors", async () => {
270
+ mockValidateEnvironment.mockResolvedValueOnce([
271
+ { message: "Missing API key" },
272
+ ]);
273
+
274
+ const response = await app.fetch(makeQueryRequest());
275
+ expect(response.status).toBe(400);
276
+
277
+ const body = (await response.json()) as Record<string, unknown>;
278
+ expect(body.error).toBe("configuration_error");
279
+ expect(mockRunAgent).not.toHaveBeenCalled();
280
+ });
281
+
282
+ it("returns 400 for malformed JSON", async () => {
283
+ const response = await app.fetch(
284
+ new Request("http://localhost/api/v1/query", {
285
+ method: "POST",
286
+ headers: { "Content-Type": "application/json" },
287
+ body: "not json",
288
+ }),
289
+ );
290
+ expect(response.status).toBe(400);
291
+
292
+ const body = (await response.json()) as Record<string, unknown>;
293
+ expect(body.error).toBe("invalid_request");
294
+ });
295
+
296
+ it("returns 422 for missing question field", async () => {
297
+ const response = await app.fetch(makeQueryRequest({}));
298
+ expect(response.status).toBe(422);
299
+
300
+ const body = (await response.json()) as Record<string, unknown>;
301
+ expect(body.error).toBe("validation_error");
302
+ expect(body.details).toBeDefined();
303
+ expect(mockRunAgent).not.toHaveBeenCalled();
304
+ });
305
+
306
+ it("returns 422 for empty question", async () => {
307
+ const response = await app.fetch(makeQueryRequest({ question: "" }));
308
+ expect(response.status).toBe(422);
309
+
310
+ const body = (await response.json()) as Record<string, unknown>;
311
+ expect(body.error).toBe("validation_error");
312
+ expect(mockRunAgent).not.toHaveBeenCalled();
313
+ });
314
+
315
+ it("passes question as user message to runAgent", async () => {
316
+ await app.fetch(makeQueryRequest({ question: "Top 10 users by revenue" }));
317
+ expect(mockRunAgent).toHaveBeenCalledTimes(1);
318
+
319
+ const calls = mockRunAgent.mock.calls as unknown as [
320
+ [{ messages: { parts: { text: string }[] }[] }],
321
+ ];
322
+ expect(calls[0][0].messages[0].parts[0].text).toBe(
323
+ "Top 10 users by revenue",
324
+ );
325
+ });
326
+
327
+ it("uses result.text as answer", async () => {
328
+ mockRunAgent.mockResolvedValueOnce(
329
+ makeAgentResult({
330
+ text: "Here is the raw answer.",
331
+ steps: [
332
+ mockStep([
333
+ {
334
+ toolName: "executeSQL",
335
+ input: { sql: "SELECT 1" },
336
+ output: { success: true, columns: ["?column?"], rows: [{ "?column?": 1 }] },
337
+ },
338
+ ]),
339
+ ],
340
+ inputTokens: 50,
341
+ outputTokens: 25,
342
+ }),
343
+ );
344
+
345
+ const response = await app.fetch(makeQueryRequest());
346
+ const body = (await response.json()) as Record<string, unknown>;
347
+ expect(body.answer).toBe("Here is the raw answer.");
348
+ });
349
+
350
+ it("handles agent error with 500", async () => {
351
+ mockRunAgent.mockRejectedValueOnce(new Error("Something broke"));
352
+
353
+ const response = await app.fetch(makeQueryRequest());
354
+ expect(response.status).toBe(500);
355
+
356
+ const body = (await response.json()) as Record<string, unknown>;
357
+ expect(body.error).toBe("internal_error");
358
+ });
359
+
360
+ it("handles timeout error with 504", async () => {
361
+ mockRunAgent.mockRejectedValueOnce(new Error("Request timed out"));
362
+
363
+ const response = await app.fetch(makeQueryRequest());
364
+ expect(response.status).toBe(504);
365
+
366
+ const body = (await response.json()) as Record<string, unknown>;
367
+ expect(body.error).toBe("provider_timeout");
368
+ });
369
+
370
+ it("handles connection error with 503", async () => {
371
+ mockRunAgent.mockRejectedValueOnce(
372
+ new Error("fetch failed: ECONNREFUSED"),
373
+ );
374
+
375
+ const response = await app.fetch(makeQueryRequest());
376
+ expect(response.status).toBe(503);
377
+
378
+ const body = (await response.json()) as Record<string, unknown>;
379
+ expect(body.error).toBe("provider_unreachable");
380
+ });
381
+
382
+ it("skips failed executeSQL results in data array", async () => {
383
+ mockRunAgent.mockResolvedValueOnce(
384
+ makeAgentResult({
385
+ text: "Query failed.",
386
+ steps: [
387
+ mockStep([
388
+ {
389
+ toolName: "executeSQL",
390
+ input: { sql: "SELECT bad_col FROM users" },
391
+ output: {
392
+ success: false,
393
+ error: "column bad_col does not exist",
394
+ },
395
+ },
396
+ ]),
397
+ ],
398
+ inputTokens: 50,
399
+ outputTokens: 25,
400
+ }),
401
+ );
402
+
403
+ const response = await app.fetch(makeQueryRequest());
404
+ const body = (await response.json()) as Record<string, unknown>;
405
+ expect(body.sql).toEqual(["SELECT bad_col FROM users"]);
406
+ expect(body.data).toEqual([]); // Failed queries don't produce data
407
+ });
408
+
409
+ // --- AI SDK error type tests ---
410
+
411
+ it("maps GatewayModelNotFoundError to 400 provider_model_not_found", async () => {
412
+ mockRunAgent.mockRejectedValueOnce(
413
+ new GatewayModelNotFoundError({
414
+ message: "Model not found",
415
+ modelId: "bad/model",
416
+ }),
417
+ );
418
+
419
+ const response = await app.fetch(makeQueryRequest());
420
+ expect(response.status).toBe(400);
421
+
422
+ const body = (await response.json()) as Record<string, unknown>;
423
+ expect(body.error).toBe("provider_model_not_found");
424
+ });
425
+
426
+ it("maps NoSuchModelError to 400 provider_model_not_found", async () => {
427
+ mockRunAgent.mockRejectedValueOnce(
428
+ new NoSuchModelError({
429
+ modelId: "nonexistent-model",
430
+ modelType: "languageModel",
431
+ }),
432
+ );
433
+
434
+ const response = await app.fetch(makeQueryRequest());
435
+ expect(response.status).toBe(400);
436
+
437
+ const body = (await response.json()) as Record<string, unknown>;
438
+ expect(body.error).toBe("provider_model_not_found");
439
+ });
440
+
441
+ it("maps LoadAPIKeyError to 503 provider_auth_error", async () => {
442
+ mockRunAgent.mockRejectedValueOnce(
443
+ new LoadAPIKeyError({
444
+ message: "ANTHROPIC_API_KEY environment variable is not set.",
445
+ }),
446
+ );
447
+
448
+ const response = await app.fetch(makeQueryRequest());
449
+ expect(response.status).toBe(503);
450
+
451
+ const body = (await response.json()) as Record<string, unknown>;
452
+ expect(body.error).toBe("provider_auth_error");
453
+ });
454
+
455
+ it("maps APICallError 401 to 503 provider_auth_error", async () => {
456
+ mockRunAgent.mockRejectedValueOnce(
457
+ new APICallError({
458
+ message: "Unauthorized",
459
+ url: "https://api.example.com/v1/chat",
460
+ requestBodyValues: {},
461
+ statusCode: 401,
462
+ }),
463
+ );
464
+
465
+ const response = await app.fetch(makeQueryRequest());
466
+ expect(response.status).toBe(503);
467
+
468
+ const body = (await response.json()) as Record<string, unknown>;
469
+ expect(body.error).toBe("provider_auth_error");
470
+ });
471
+
472
+ it("maps APICallError 429 to 503 provider_rate_limit", async () => {
473
+ mockRunAgent.mockRejectedValueOnce(
474
+ new APICallError({
475
+ message: "Rate limit exceeded",
476
+ url: "https://api.example.com/v1/chat",
477
+ requestBodyValues: {},
478
+ statusCode: 429,
479
+ }),
480
+ );
481
+
482
+ const response = await app.fetch(makeQueryRequest());
483
+ expect(response.status).toBe(503);
484
+
485
+ const body = (await response.json()) as Record<string, unknown>;
486
+ expect(body.error).toBe("provider_rate_limit");
487
+ });
488
+
489
+ it("maps APICallError 408 to 504 provider_timeout", async () => {
490
+ mockRunAgent.mockRejectedValueOnce(
491
+ new APICallError({
492
+ message: "Request timeout",
493
+ url: "https://api.example.com/v1/chat",
494
+ requestBodyValues: {},
495
+ statusCode: 408,
496
+ }),
497
+ );
498
+
499
+ const response = await app.fetch(makeQueryRequest());
500
+ expect(response.status).toBe(504);
501
+
502
+ const body = (await response.json()) as Record<string, unknown>;
503
+ expect(body.error).toBe("provider_timeout");
504
+ });
505
+
506
+ it("maps APICallError 500 to 502 provider_error", async () => {
507
+ mockRunAgent.mockRejectedValueOnce(
508
+ new APICallError({
509
+ message: "Internal server error",
510
+ url: "https://api.example.com/v1/chat",
511
+ requestBodyValues: {},
512
+ statusCode: 500,
513
+ }),
514
+ );
515
+
516
+ const response = await app.fetch(makeQueryRequest());
517
+ expect(response.status).toBe(502);
518
+
519
+ const body = (await response.json()) as Record<string, unknown>;
520
+ expect(body.error).toBe("provider_error");
521
+ });
522
+
523
+ // --- Edge case tests ---
524
+
525
+ it("collects SQL and data from multiple executeSQL steps", async () => {
526
+ mockRunAgent.mockResolvedValueOnce(
527
+ makeAgentResult({
528
+ text: "Two queries ran.",
529
+ steps: [
530
+ mockStep([
531
+ {
532
+ toolName: "executeSQL",
533
+ input: { sql: "SELECT COUNT(*) FROM users" },
534
+ output: { success: true, columns: ["count"], rows: [{ count: 42 }] },
535
+ },
536
+ ]),
537
+ mockStep([
538
+ {
539
+ toolName: "executeSQL",
540
+ input: { sql: "SELECT name FROM users LIMIT 5" },
541
+ output: {
542
+ success: true,
543
+ columns: ["name"],
544
+ rows: [{ name: "Alice" }, { name: "Bob" }],
545
+ },
546
+ },
547
+ ]),
548
+ ],
549
+ inputTokens: 80,
550
+ outputTokens: 40,
551
+ }),
552
+ );
553
+
554
+ const response = await app.fetch(makeQueryRequest());
555
+ expect(response.status).toBe(200);
556
+
557
+ const body = (await response.json()) as Record<string, unknown>;
558
+ expect(body.sql).toEqual([
559
+ "SELECT COUNT(*) FROM users",
560
+ "SELECT name FROM users LIMIT 5",
561
+ ]);
562
+ expect(body.data).toEqual([
563
+ { columns: ["count"], rows: [{ count: 42 }] },
564
+ { columns: ["name"], rows: [{ name: "Alice" }, { name: "Bob" }] },
565
+ ]);
566
+ expect(body.steps).toBe(2);
567
+ });
568
+
569
+ it("returns empty sql/data and steps=0 for empty steps", async () => {
570
+ mockRunAgent.mockResolvedValueOnce(
571
+ makeAgentResult({
572
+ text: "I could not help with that.",
573
+ steps: [],
574
+ inputTokens: 30,
575
+ outputTokens: 20,
576
+ }),
577
+ );
578
+
579
+ const response = await app.fetch(makeQueryRequest());
580
+ expect(response.status).toBe(200);
581
+
582
+ const body = (await response.json()) as Record<string, unknown>;
583
+ expect(body.sql).toEqual([]);
584
+ expect(body.data).toEqual([]);
585
+ expect(body.steps).toBe(0);
586
+ expect(body.answer).toBe("I could not help with that.");
587
+ });
588
+
589
+ it("maps AbortError to 504 provider_timeout", async () => {
590
+ const abortError = new Error("AbortError");
591
+ abortError.name = "AbortError";
592
+ mockRunAgent.mockRejectedValueOnce(abortError);
593
+
594
+ const response = await app.fetch(makeQueryRequest());
595
+ expect(response.status).toBe(504);
596
+
597
+ const body = (await response.json()) as Record<string, unknown>;
598
+ expect(body.error).toBe("provider_timeout");
599
+ });
600
+
601
+ it("uses provided conversationId when ownership verified", async () => {
602
+ const convId = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
603
+ mockGetConversationQuery.mockResolvedValueOnce({
604
+ ok: true,
605
+ data: { id: convId, userId: null, title: "Test", messages: [] },
606
+ });
607
+
608
+ const response = await app.fetch(
609
+ makeQueryRequest({ question: "How many users?", conversationId: convId }),
610
+ );
611
+ expect(response.status).toBe(200);
612
+
613
+ const body = (await response.json()) as Record<string, unknown>;
614
+ expect(body.conversationId).toBe(convId);
615
+ expect(mockCreateConversationQuery).not.toHaveBeenCalled();
616
+ });
617
+
618
+ it("creates new conversation when ownership check fails for provided conversationId", async () => {
619
+ const convId = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
620
+ // getConversation returns not_found — ownership check fails, falls back to new conversation
621
+ mockGetConversationQuery.mockResolvedValueOnce({ ok: false, reason: "not_found" });
622
+
623
+ const response = await app.fetch(
624
+ makeQueryRequest({ question: "How many users?", conversationId: convId }),
625
+ );
626
+ expect(response.status).toBe(200);
627
+
628
+ const body = (await response.json()) as Record<string, unknown>;
629
+ // Falls back to creating a new conversation
630
+ expect(body.conversationId).toBe("conv-query-123");
631
+ expect(mockCreateConversationQuery).toHaveBeenCalledTimes(1);
632
+ });
633
+
634
+ it("returns 200 without conversationId when persistence throws", async () => {
635
+ mockCreateConversationQuery.mockRejectedValueOnce(new Error("DB down"));
636
+ const response = await app.fetch(makeQueryRequest());
637
+ expect(response.status).toBe(200);
638
+ const body = (await response.json()) as Record<string, unknown>;
639
+ expect(body.answer).toBeDefined();
640
+ expect(body.conversationId).toBeUndefined();
641
+ });
642
+
643
+ it("includes conversationId in response when internal DB is available", async () => {
644
+ const response = await app.fetch(makeQueryRequest());
645
+ expect(response.status).toBe(200);
646
+
647
+ const body = (await response.json()) as Record<string, unknown>;
648
+ expect(body.conversationId).toBe("conv-query-123");
649
+ });
650
+
651
+ it("omits conversationId when internal DB is unavailable", async () => {
652
+ delete process.env.DATABASE_URL;
653
+
654
+ const response = await app.fetch(makeQueryRequest());
655
+ expect(response.status).toBe(200);
656
+
657
+ const body = (await response.json()) as Record<string, unknown>;
658
+ expect(body.conversationId).toBeUndefined();
659
+ });
660
+
661
+ it("includes pendingActions with approve/deny URLs when actions are pending", async () => {
662
+ mockRunAgent.mockResolvedValueOnce(
663
+ makeAgentResult({
664
+ text: "I need your approval to send a notification.",
665
+ steps: [
666
+ mockStep([
667
+ {
668
+ toolName: "sendNotification",
669
+ input: { actionType: "notification", target: "#revenue" },
670
+ output: {
671
+ status: "pending_approval",
672
+ actionId: "act-001",
673
+ summary: "Send notification to #revenue",
674
+ target: "#revenue",
675
+ },
676
+ },
677
+ ]),
678
+ ],
679
+ inputTokens: 80,
680
+ outputTokens: 40,
681
+ }),
682
+ );
683
+
684
+ const response = await app.fetch(makeQueryRequest({ question: "send a notification to #revenue" }));
685
+ expect(response.status).toBe(200);
686
+
687
+ const body = (await response.json()) as Record<string, unknown>;
688
+ expect(body.pendingActions).toBeDefined();
689
+ const actions = body.pendingActions as Array<Record<string, unknown>>;
690
+ expect(actions).toHaveLength(1);
691
+ expect(actions[0].id).toBe("act-001");
692
+ expect(actions[0].summary).toBe("Send notification to #revenue");
693
+ expect(actions[0].approveUrl).toContain("/api/v1/actions/act-001/approve");
694
+ expect(actions[0].denyUrl).toContain("/api/v1/actions/act-001/deny");
695
+ });
696
+
697
+ // --- deriveBaseUrl URL derivation tests ---
698
+
699
+ describe("pending action URL derivation", () => {
700
+ const origPublicUrl = process.env.ATLAS_PUBLIC_URL;
701
+ const origTrustProxy = process.env.ATLAS_TRUST_PROXY;
702
+
703
+ function setupPendingActionAgent() {
704
+ mockRunAgent.mockResolvedValueOnce(
705
+ makeAgentResult({
706
+ text: "I need approval.",
707
+ steps: [
708
+ mockStep([
709
+ {
710
+ toolName: "sendNotification",
711
+ input: { actionType: "notification", target: "#general" },
712
+ output: {
713
+ status: "pending_approval",
714
+ actionId: "act-url-test",
715
+ summary: "Send notification",
716
+ target: "#general",
717
+ },
718
+ },
719
+ ]),
720
+ ],
721
+ inputTokens: 50,
722
+ outputTokens: 30,
723
+ }),
724
+ );
725
+ }
726
+
727
+ afterEach(() => {
728
+ if (origPublicUrl !== undefined) process.env.ATLAS_PUBLIC_URL = origPublicUrl;
729
+ else delete process.env.ATLAS_PUBLIC_URL;
730
+ if (origTrustProxy !== undefined) process.env.ATLAS_TRUST_PROXY = origTrustProxy;
731
+ else delete process.env.ATLAS_TRUST_PROXY;
732
+ });
733
+
734
+ it("uses ATLAS_PUBLIC_URL when set", async () => {
735
+ process.env.ATLAS_PUBLIC_URL = "https://api.myapp.com";
736
+ setupPendingActionAgent();
737
+
738
+ const response = await app.fetch(makeQueryRequest({ question: "notify" }));
739
+ expect(response.status).toBe(200);
740
+
741
+ const body = (await response.json()) as Record<string, unknown>;
742
+ const actions = body.pendingActions as Array<Record<string, unknown>>;
743
+ expect(actions).toHaveLength(1);
744
+ expect(actions[0].approveUrl).toBe("https://api.myapp.com/api/v1/actions/act-url-test/approve");
745
+ expect(actions[0].denyUrl).toBe("https://api.myapp.com/api/v1/actions/act-url-test/deny");
746
+ });
747
+
748
+ it("strips trailing slash from ATLAS_PUBLIC_URL to avoid double slashes", async () => {
749
+ process.env.ATLAS_PUBLIC_URL = "https://api.myapp.com/";
750
+ setupPendingActionAgent();
751
+
752
+ const response = await app.fetch(makeQueryRequest({ question: "notify" }));
753
+ expect(response.status).toBe(200);
754
+
755
+ const body = (await response.json()) as Record<string, unknown>;
756
+ const actions = body.pendingActions as Array<Record<string, unknown>>;
757
+ expect(actions).toHaveLength(1);
758
+ // No double slash between base URL and /api/v1/...
759
+ expect(actions[0].approveUrl).toBe("https://api.myapp.com/api/v1/actions/act-url-test/approve");
760
+ expect(actions[0].denyUrl).toBe("https://api.myapp.com/api/v1/actions/act-url-test/deny");
761
+ });
762
+
763
+ it("derives URL from request when ATLAS_PUBLIC_URL is unset", async () => {
764
+ delete process.env.ATLAS_PUBLIC_URL;
765
+ delete process.env.ATLAS_TRUST_PROXY;
766
+ setupPendingActionAgent();
767
+
768
+ const response = await app.fetch(makeQueryRequest({ question: "notify" }));
769
+ expect(response.status).toBe(200);
770
+
771
+ const body = (await response.json()) as Record<string, unknown>;
772
+ const actions = body.pendingActions as Array<Record<string, unknown>>;
773
+ expect(actions).toHaveLength(1);
774
+ // Falls back to request URL — makeQueryRequest uses http://localhost
775
+ expect(actions[0].approveUrl).toBe("http://localhost/api/v1/actions/act-url-test/approve");
776
+ expect(actions[0].denyUrl).toBe("http://localhost/api/v1/actions/act-url-test/deny");
777
+ });
778
+
779
+ it("uses forwarded headers when ATLAS_TRUST_PROXY is true", async () => {
780
+ delete process.env.ATLAS_PUBLIC_URL;
781
+ process.env.ATLAS_TRUST_PROXY = "true";
782
+ setupPendingActionAgent();
783
+
784
+ const response = await app.fetch(
785
+ new Request("http://localhost/api/v1/query", {
786
+ method: "POST",
787
+ headers: {
788
+ "Content-Type": "application/json",
789
+ "X-Forwarded-Proto": "https",
790
+ "X-Forwarded-Host": "public.example.com",
791
+ },
792
+ body: JSON.stringify({ question: "notify" }),
793
+ }),
794
+ );
795
+ expect(response.status).toBe(200);
796
+
797
+ const body = (await response.json()) as Record<string, unknown>;
798
+ const actions = body.pendingActions as Array<Record<string, unknown>>;
799
+ expect(actions).toHaveLength(1);
800
+ expect(actions[0].approveUrl).toBe("https://public.example.com/api/v1/actions/act-url-test/approve");
801
+ expect(actions[0].denyUrl).toBe("https://public.example.com/api/v1/actions/act-url-test/deny");
802
+ });
803
+ });
804
+
805
+ it("omits pendingActions when there are no pending actions", async () => {
806
+ const response = await app.fetch(makeQueryRequest());
807
+ expect(response.status).toBe(200);
808
+
809
+ const body = (await response.json()) as Record<string, unknown>;
810
+ expect(body.pendingActions).toBeUndefined();
811
+ });
812
+
813
+ it("includes pendingActions alongside SQL data", async () => {
814
+ mockRunAgent.mockResolvedValueOnce(
815
+ makeAgentResult({
816
+ text: "Found 42 users. I need approval to send a report.",
817
+ steps: [
818
+ mockStep([
819
+ {
820
+ toolName: "executeSQL",
821
+ input: { sql: "SELECT COUNT(*) FROM users" },
822
+ output: { success: true, columns: ["count"], rows: [{ count: 42 }] },
823
+ },
824
+ ]),
825
+ mockStep([
826
+ {
827
+ toolName: "sendReport",
828
+ input: { actionType: "send_report", target: "email:team@company.com" },
829
+ output: {
830
+ status: "pending_approval",
831
+ actionId: "act-002",
832
+ summary: "Email report to team@company.com",
833
+ target: "email:team@company.com",
834
+ },
835
+ },
836
+ ]),
837
+ ],
838
+ inputTokens: 100,
839
+ outputTokens: 60,
840
+ }),
841
+ );
842
+
843
+ const response = await app.fetch(makeQueryRequest({ question: "count users and email report" }));
844
+ expect(response.status).toBe(200);
845
+
846
+ const body = (await response.json()) as Record<string, unknown>;
847
+ // SQL data is present
848
+ expect(body.sql).toEqual(["SELECT COUNT(*) FROM users"]);
849
+ expect(body.data).toEqual([{ columns: ["count"], rows: [{ count: 42 }] }]);
850
+ // Pending actions are also present
851
+ const actions = body.pendingActions as Array<Record<string, unknown>>;
852
+ expect(actions).toHaveLength(1);
853
+ expect(actions[0].id).toBe("act-002");
854
+ });
855
+ });
856
+
857
+ // --- GET /api/v1/openapi.json ---
858
+
859
+ describe("GET /api/v1/openapi.json", () => {
860
+ it("returns a valid OpenAPI 3.1 spec", async () => {
861
+ const response = await app.fetch(
862
+ new Request("http://localhost/api/v1/openapi.json"),
863
+ );
864
+ expect(response.status).toBe(200);
865
+
866
+ const spec = (await response.json()) as Record<string, unknown>;
867
+ expect(spec.openapi).toBe("3.1.0");
868
+ expect(spec.info).toBeDefined();
869
+ expect(spec.paths).toBeDefined();
870
+ });
871
+
872
+ it("includes the /api/v1/query path", async () => {
873
+ const response = await app.fetch(
874
+ new Request("http://localhost/api/v1/openapi.json"),
875
+ );
876
+ const spec = (await response.json()) as {
877
+ paths: Record<string, unknown>;
878
+ };
879
+ expect(spec.paths["/api/v1/query"]).toBeDefined();
880
+ });
881
+
882
+ it("includes security schemes", async () => {
883
+ const response = await app.fetch(
884
+ new Request("http://localhost/api/v1/openapi.json"),
885
+ );
886
+ const spec = (await response.json()) as {
887
+ components: { securitySchemes: Record<string, unknown> };
888
+ };
889
+ expect(spec.components.securitySchemes.bearerAuth).toBeDefined();
890
+ });
891
+ });