@useatlas/create 0.0.1 → 0.0.3

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 (298) hide show
  1. package/README.md +4 -18
  2. package/index.ts +191 -31
  3. package/package.json +1 -1
  4. package/templates/docker/.env.example +3 -3
  5. package/templates/docker/Dockerfile.sidecar +28 -0
  6. package/templates/docker/bin/__tests__/plugin-cli.test.ts +11 -11
  7. package/templates/docker/bin/atlas.ts +120 -56
  8. package/templates/docker/data/demo-semantic/catalog.yml +51 -27
  9. package/templates/docker/data/demo-semantic/entities/accounts.yml +95 -103
  10. package/templates/docker/data/demo-semantic/entities/companies.yml +88 -152
  11. package/templates/docker/data/demo-semantic/entities/people.yml +82 -95
  12. package/templates/docker/data/demo-semantic/glossary.yml +104 -8
  13. package/templates/docker/data/demo-semantic/metrics/accounts.yml +62 -23
  14. package/templates/docker/data/demo-semantic/metrics/companies.yml +52 -78
  15. package/templates/docker/docker-compose.yml +1 -1
  16. package/templates/docker/docs/deploy.md +4 -41
  17. package/templates/docker/package.json +17 -1
  18. package/templates/docker/semantic/catalog.yml +62 -3
  19. package/templates/docker/semantic/entities/accounts.yml +162 -0
  20. package/templates/docker/semantic/entities/companies.yml +143 -0
  21. package/templates/docker/semantic/entities/people.yml +132 -0
  22. package/templates/docker/semantic/glossary.yml +116 -4
  23. package/templates/docker/semantic/metrics/accounts.yml +77 -0
  24. package/templates/docker/semantic/metrics/companies.yml +63 -0
  25. package/templates/docker/sidecar/Dockerfile +5 -6
  26. package/templates/docker/sidecar/railway.json +1 -2
  27. package/templates/docker/src/api/__tests__/admin.test.ts +7 -7
  28. package/templates/docker/src/api/__tests__/health-plugin.test.ts +7 -0
  29. package/templates/docker/src/api/__tests__/health.test.ts +30 -8
  30. package/templates/docker/src/api/routes/admin.ts +549 -8
  31. package/templates/docker/src/api/routes/chat.ts +5 -20
  32. package/templates/docker/src/api/routes/health.ts +39 -27
  33. package/templates/docker/src/api/routes/openapi.ts +1329 -74
  34. package/templates/docker/src/api/routes/query.ts +2 -1
  35. package/templates/docker/src/api/server.ts +27 -0
  36. package/templates/docker/src/app/api/[...route]/route.ts +2 -2
  37. package/templates/docker/src/app/globals.css +13 -12
  38. package/templates/docker/src/app/layout.tsx +9 -2
  39. package/templates/docker/src/components/ui/alert-dialog.tsx +196 -0
  40. package/templates/docker/src/components/ui/badge.tsx +48 -0
  41. package/templates/docker/src/components/ui/button.tsx +64 -0
  42. package/templates/docker/src/components/ui/card.tsx +92 -0
  43. package/templates/docker/src/components/ui/collapsible.tsx +33 -0
  44. package/templates/docker/src/components/ui/command.tsx +184 -0
  45. package/templates/docker/src/components/ui/dialog.tsx +158 -0
  46. package/templates/docker/src/components/ui/dropdown-menu.tsx +257 -0
  47. package/templates/docker/src/components/ui/input.tsx +21 -0
  48. package/templates/docker/src/components/ui/scroll-area.tsx +58 -0
  49. package/templates/docker/src/components/ui/select.tsx +190 -0
  50. package/templates/docker/src/components/ui/separator.tsx +28 -0
  51. package/templates/docker/src/components/ui/sheet.tsx +143 -0
  52. package/templates/docker/src/components/ui/sidebar.tsx +726 -0
  53. package/templates/docker/src/components/ui/skeleton.tsx +13 -0
  54. package/templates/docker/src/components/ui/table.tsx +116 -0
  55. package/templates/docker/src/components/ui/tabs.tsx +91 -0
  56. package/templates/docker/src/components/ui/toggle-group.tsx +83 -0
  57. package/templates/docker/src/components/ui/toggle.tsx +47 -0
  58. package/templates/docker/src/components/ui/tooltip.tsx +57 -0
  59. package/templates/docker/src/hooks/use-mobile.ts +19 -0
  60. package/templates/docker/src/lib/__tests__/agent-cache.test.ts +2 -0
  61. package/templates/docker/src/lib/__tests__/agent-dialect.test.ts +17 -0
  62. package/templates/docker/src/lib/__tests__/agent-health-annotations.test.ts +2 -0
  63. package/templates/docker/src/lib/__tests__/agent-integration.test.ts +2 -0
  64. package/templates/docker/src/lib/__tests__/config.test.ts +69 -19
  65. package/templates/docker/src/lib/__tests__/plugin-aware-validation.test.ts +321 -0
  66. package/templates/docker/src/lib/__tests__/providers.test.ts +32 -1
  67. package/templates/docker/src/lib/__tests__/startup-actions.test.ts +9 -0
  68. package/templates/docker/src/lib/__tests__/startup-first-run.test.ts +429 -0
  69. package/templates/docker/src/lib/__tests__/startup.test.ts +5 -0
  70. package/templates/docker/src/lib/agent-query.ts +5 -23
  71. package/templates/docker/src/lib/agent.ts +32 -112
  72. package/templates/docker/src/lib/auth/__tests__/migrate.test.ts +5 -3
  73. package/templates/docker/src/lib/auth/middleware.ts +30 -4
  74. package/templates/docker/src/lib/auth/migrate.ts +97 -0
  75. package/templates/docker/src/lib/auth/server.ts +12 -1
  76. package/templates/docker/src/lib/config.ts +38 -40
  77. package/templates/docker/src/lib/db/__tests__/connection.test.ts +89 -14
  78. package/templates/docker/src/lib/db/__tests__/registry-health.test.ts +1 -18
  79. package/templates/docker/src/lib/db/__tests__/registry-pool-limits.test.ts +0 -19
  80. package/templates/docker/src/lib/db/__tests__/registry.test.ts +11 -208
  81. package/templates/docker/src/lib/db/connection.ts +87 -265
  82. package/templates/docker/src/lib/db/internal.ts +6 -1
  83. package/templates/docker/src/lib/plugins/__tests__/hooks-integration.test.ts +3 -1
  84. package/templates/docker/src/lib/plugins/__tests__/hooks.test.ts +2 -2
  85. package/templates/docker/src/lib/plugins/__tests__/migrate.test.ts +355 -1
  86. package/templates/docker/src/lib/plugins/__tests__/registry.test.ts +32 -5
  87. package/templates/docker/src/lib/plugins/__tests__/wiring.test.ts +228 -14
  88. package/templates/docker/src/lib/plugins/index.ts +4 -1
  89. package/templates/docker/src/lib/plugins/migrate.ts +104 -1
  90. package/templates/docker/src/lib/plugins/registry.ts +14 -8
  91. package/templates/docker/src/lib/plugins/wiring.ts +113 -4
  92. package/templates/docker/src/lib/providers.ts +6 -1
  93. package/templates/docker/src/lib/security.ts +24 -0
  94. package/templates/docker/src/lib/semantic.ts +2 -0
  95. package/templates/docker/src/lib/sidecar-types.ts +12 -1
  96. package/templates/docker/src/lib/startup.ts +71 -101
  97. package/templates/docker/src/lib/tools/__tests__/custom-validation.test.ts +2 -0
  98. package/templates/docker/src/lib/tools/__tests__/explore-nsjail.test.ts +32 -18
  99. package/templates/docker/src/lib/tools/__tests__/explore-plugin.test.ts +14 -14
  100. package/templates/docker/src/lib/tools/__tests__/explore-sdk-compat.test.ts +1 -1
  101. package/templates/docker/src/lib/tools/__tests__/explore-sidecar.test.ts +5 -3
  102. package/templates/docker/src/lib/tools/__tests__/python-nsjail.test.ts +515 -0
  103. package/templates/docker/src/lib/tools/__tests__/python-sandbox.test.ts +397 -0
  104. package/templates/docker/src/lib/tools/__tests__/python-sidecar.test.ts +365 -0
  105. package/templates/docker/src/lib/tools/__tests__/python.test.ts +331 -0
  106. package/templates/docker/src/lib/tools/__tests__/registry-actions.test.ts +1 -13
  107. package/templates/docker/src/lib/tools/__tests__/registry.test.ts +38 -31
  108. package/templates/docker/src/lib/tools/__tests__/sql-audit.test.ts +2 -0
  109. package/templates/docker/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +2 -0
  110. package/templates/docker/src/lib/tools/__tests__/sql-ratelimit.test.ts +2 -0
  111. package/templates/docker/src/lib/tools/__tests__/sql.test.ts +5 -308
  112. package/templates/docker/src/lib/tools/explore-nsjail.ts +17 -12
  113. package/templates/docker/src/lib/tools/explore-sidecar.ts +25 -0
  114. package/templates/docker/src/lib/tools/explore.ts +28 -32
  115. package/templates/docker/src/lib/tools/python-nsjail.ts +396 -0
  116. package/templates/docker/src/lib/tools/python-sandbox.ts +476 -0
  117. package/templates/docker/src/lib/tools/python-sidecar.ts +150 -0
  118. package/templates/docker/src/lib/tools/python.ts +367 -0
  119. package/templates/docker/src/lib/tools/registry.ts +49 -22
  120. package/templates/docker/src/lib/tools/sql.ts +88 -88
  121. package/templates/docker/src/types/vercel-sandbox.d.ts +7 -0
  122. package/templates/docker/src/ui/components/admin/admin-layout.tsx +77 -8
  123. package/templates/docker/src/ui/components/admin/admin-sidebar.tsx +25 -17
  124. package/templates/docker/src/ui/components/admin/change-password-dialog.tsx +128 -0
  125. package/templates/docker/src/ui/components/admin/entity-detail.tsx +3 -3
  126. package/templates/docker/src/ui/components/admin/semantic-file-tree.tsx +159 -0
  127. package/templates/docker/src/ui/components/atlas-chat.tsx +64 -12
  128. package/templates/docker/src/ui/components/chart/result-chart.tsx +25 -15
  129. package/templates/docker/src/ui/components/chat/markdown.tsx +88 -42
  130. package/templates/docker/src/ui/components/chat/python-result-card.tsx +244 -0
  131. package/templates/docker/src/ui/components/chat/sql-block.tsx +39 -15
  132. package/templates/docker/src/ui/components/chat/sql-result-card.tsx +6 -1
  133. package/templates/docker/src/ui/components/chat/tool-part.tsx +12 -3
  134. package/templates/docker/src/ui/components/chat/typing-indicator.tsx +5 -2
  135. package/templates/docker/src/ui/components/conversations/conversation-item.tsx +25 -20
  136. package/templates/docker/src/ui/context.tsx +1 -1
  137. package/templates/docker/src/ui/hooks/use-conversations.ts +3 -3
  138. package/templates/docker/src/ui/hooks/use-dark-mode.ts +17 -10
  139. package/templates/docker/tsconfig.json +2 -2
  140. package/templates/nextjs-standalone/.env.example +1 -1
  141. package/templates/nextjs-standalone/bin/__tests__/plugin-cli.test.ts +11 -11
  142. package/templates/nextjs-standalone/bin/atlas.ts +120 -56
  143. package/templates/nextjs-standalone/data/demo-semantic/catalog.yml +51 -27
  144. package/templates/nextjs-standalone/data/demo-semantic/entities/accounts.yml +95 -103
  145. package/templates/nextjs-standalone/data/demo-semantic/entities/companies.yml +88 -152
  146. package/templates/nextjs-standalone/data/demo-semantic/entities/people.yml +82 -95
  147. package/templates/nextjs-standalone/data/demo-semantic/glossary.yml +104 -8
  148. package/templates/nextjs-standalone/data/demo-semantic/metrics/accounts.yml +62 -23
  149. package/templates/nextjs-standalone/data/demo-semantic/metrics/companies.yml +52 -78
  150. package/templates/nextjs-standalone/docs/deploy.md +4 -41
  151. package/templates/nextjs-standalone/package.json +11 -2
  152. package/templates/nextjs-standalone/scripts/migrate-auth.ts +25 -0
  153. package/templates/nextjs-standalone/scripts/seed-demo.ts +94 -0
  154. package/templates/nextjs-standalone/semantic/catalog.yml +62 -3
  155. package/templates/nextjs-standalone/semantic/entities/accounts.yml +162 -0
  156. package/templates/nextjs-standalone/semantic/entities/companies.yml +143 -0
  157. package/templates/nextjs-standalone/semantic/entities/people.yml +132 -0
  158. package/templates/nextjs-standalone/semantic/glossary.yml +116 -4
  159. package/templates/nextjs-standalone/semantic/metrics/accounts.yml +77 -0
  160. package/templates/nextjs-standalone/semantic/metrics/companies.yml +63 -0
  161. package/templates/nextjs-standalone/src/api/__tests__/admin.test.ts +7 -7
  162. package/templates/nextjs-standalone/src/api/__tests__/health-plugin.test.ts +7 -0
  163. package/templates/nextjs-standalone/src/api/__tests__/health.test.ts +30 -8
  164. package/templates/nextjs-standalone/src/api/routes/admin.ts +549 -8
  165. package/templates/nextjs-standalone/src/api/routes/chat.ts +5 -20
  166. package/templates/nextjs-standalone/src/api/routes/health.ts +39 -27
  167. package/templates/nextjs-standalone/src/api/routes/openapi.ts +1329 -74
  168. package/templates/nextjs-standalone/src/api/routes/query.ts +2 -1
  169. package/templates/nextjs-standalone/src/api/server.ts +27 -0
  170. package/templates/nextjs-standalone/src/app/api/[...route]/route.ts +2 -2
  171. package/templates/nextjs-standalone/src/app/globals.css +13 -12
  172. package/templates/nextjs-standalone/src/app/layout.tsx +9 -2
  173. package/templates/nextjs-standalone/src/components/ui/alert-dialog.tsx +196 -0
  174. package/templates/nextjs-standalone/src/components/ui/badge.tsx +48 -0
  175. package/templates/nextjs-standalone/src/components/ui/button.tsx +64 -0
  176. package/templates/nextjs-standalone/src/components/ui/card.tsx +92 -0
  177. package/templates/nextjs-standalone/src/components/ui/collapsible.tsx +33 -0
  178. package/templates/nextjs-standalone/src/components/ui/command.tsx +184 -0
  179. package/templates/nextjs-standalone/src/components/ui/dialog.tsx +158 -0
  180. package/templates/nextjs-standalone/src/components/ui/dropdown-menu.tsx +257 -0
  181. package/templates/nextjs-standalone/src/components/ui/input.tsx +21 -0
  182. package/templates/nextjs-standalone/src/components/ui/scroll-area.tsx +58 -0
  183. package/templates/nextjs-standalone/src/components/ui/select.tsx +190 -0
  184. package/templates/nextjs-standalone/src/components/ui/separator.tsx +28 -0
  185. package/templates/nextjs-standalone/src/components/ui/sheet.tsx +143 -0
  186. package/templates/nextjs-standalone/src/components/ui/sidebar.tsx +726 -0
  187. package/templates/nextjs-standalone/src/components/ui/skeleton.tsx +13 -0
  188. package/templates/nextjs-standalone/src/components/ui/table.tsx +116 -0
  189. package/templates/nextjs-standalone/src/components/ui/tabs.tsx +91 -0
  190. package/templates/nextjs-standalone/src/components/ui/toggle-group.tsx +83 -0
  191. package/templates/nextjs-standalone/src/components/ui/toggle.tsx +47 -0
  192. package/templates/nextjs-standalone/src/components/ui/tooltip.tsx +57 -0
  193. package/templates/nextjs-standalone/src/hooks/use-mobile.ts +19 -0
  194. package/templates/nextjs-standalone/src/lib/__tests__/agent-cache.test.ts +2 -0
  195. package/templates/nextjs-standalone/src/lib/__tests__/agent-dialect.test.ts +17 -0
  196. package/templates/nextjs-standalone/src/lib/__tests__/agent-health-annotations.test.ts +2 -0
  197. package/templates/nextjs-standalone/src/lib/__tests__/agent-integration.test.ts +2 -0
  198. package/templates/nextjs-standalone/src/lib/__tests__/config.test.ts +69 -19
  199. package/templates/nextjs-standalone/src/lib/__tests__/plugin-aware-validation.test.ts +321 -0
  200. package/templates/nextjs-standalone/src/lib/__tests__/providers.test.ts +32 -1
  201. package/templates/nextjs-standalone/src/lib/__tests__/startup-actions.test.ts +9 -0
  202. package/templates/nextjs-standalone/src/lib/__tests__/startup-first-run.test.ts +429 -0
  203. package/templates/nextjs-standalone/src/lib/__tests__/startup.test.ts +5 -0
  204. package/templates/nextjs-standalone/src/lib/agent-query.ts +5 -23
  205. package/templates/nextjs-standalone/src/lib/agent.ts +32 -112
  206. package/templates/nextjs-standalone/src/lib/auth/__tests__/migrate.test.ts +5 -3
  207. package/templates/nextjs-standalone/src/lib/auth/middleware.ts +30 -4
  208. package/templates/nextjs-standalone/src/lib/auth/migrate.ts +97 -0
  209. package/templates/nextjs-standalone/src/lib/auth/server.ts +12 -1
  210. package/templates/nextjs-standalone/src/lib/config.ts +38 -40
  211. package/templates/nextjs-standalone/src/lib/db/__tests__/connection.test.ts +89 -14
  212. package/templates/nextjs-standalone/src/lib/db/__tests__/registry-health.test.ts +1 -18
  213. package/templates/nextjs-standalone/src/lib/db/__tests__/registry-pool-limits.test.ts +0 -19
  214. package/templates/nextjs-standalone/src/lib/db/__tests__/registry.test.ts +11 -208
  215. package/templates/nextjs-standalone/src/lib/db/connection.ts +87 -265
  216. package/templates/nextjs-standalone/src/lib/db/internal.ts +6 -1
  217. package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks-integration.test.ts +3 -1
  218. package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks.test.ts +2 -2
  219. package/templates/nextjs-standalone/src/lib/plugins/__tests__/migrate.test.ts +355 -1
  220. package/templates/nextjs-standalone/src/lib/plugins/__tests__/registry.test.ts +32 -5
  221. package/templates/nextjs-standalone/src/lib/plugins/__tests__/wiring.test.ts +228 -14
  222. package/templates/nextjs-standalone/src/lib/plugins/index.ts +4 -1
  223. package/templates/nextjs-standalone/src/lib/plugins/migrate.ts +104 -1
  224. package/templates/nextjs-standalone/src/lib/plugins/registry.ts +14 -8
  225. package/templates/nextjs-standalone/src/lib/plugins/wiring.ts +113 -4
  226. package/templates/nextjs-standalone/src/lib/providers.ts +6 -1
  227. package/templates/nextjs-standalone/src/lib/security.ts +24 -0
  228. package/templates/nextjs-standalone/src/lib/semantic.ts +2 -0
  229. package/templates/nextjs-standalone/src/lib/sidecar-types.ts +12 -1
  230. package/templates/nextjs-standalone/src/lib/startup.ts +71 -101
  231. package/templates/nextjs-standalone/src/lib/tools/__tests__/custom-validation.test.ts +2 -0
  232. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-nsjail.test.ts +32 -18
  233. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-plugin.test.ts +14 -14
  234. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-sdk-compat.test.ts +1 -1
  235. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-sidecar.test.ts +5 -3
  236. package/templates/nextjs-standalone/src/lib/tools/__tests__/python-nsjail.test.ts +515 -0
  237. package/templates/nextjs-standalone/src/lib/tools/__tests__/python-sandbox.test.ts +397 -0
  238. package/templates/nextjs-standalone/src/lib/tools/__tests__/python-sidecar.test.ts +365 -0
  239. package/templates/nextjs-standalone/src/lib/tools/__tests__/python.test.ts +331 -0
  240. package/templates/nextjs-standalone/src/lib/tools/__tests__/registry-actions.test.ts +1 -13
  241. package/templates/nextjs-standalone/src/lib/tools/__tests__/registry.test.ts +38 -31
  242. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-audit.test.ts +2 -0
  243. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +2 -0
  244. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-ratelimit.test.ts +2 -0
  245. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql.test.ts +5 -308
  246. package/templates/nextjs-standalone/src/lib/tools/explore-nsjail.ts +17 -12
  247. package/templates/nextjs-standalone/src/lib/tools/explore-sidecar.ts +25 -0
  248. package/templates/nextjs-standalone/src/lib/tools/explore.ts +28 -32
  249. package/templates/nextjs-standalone/src/lib/tools/python-nsjail.ts +396 -0
  250. package/templates/nextjs-standalone/src/lib/tools/python-sandbox.ts +476 -0
  251. package/templates/nextjs-standalone/src/lib/tools/python-sidecar.ts +150 -0
  252. package/templates/nextjs-standalone/src/lib/tools/python.ts +367 -0
  253. package/templates/nextjs-standalone/src/lib/tools/registry.ts +49 -22
  254. package/templates/nextjs-standalone/src/lib/tools/sql.ts +88 -88
  255. package/templates/nextjs-standalone/src/ui/components/admin/admin-layout.tsx +77 -8
  256. package/templates/nextjs-standalone/src/ui/components/admin/admin-sidebar.tsx +25 -17
  257. package/templates/nextjs-standalone/src/ui/components/admin/change-password-dialog.tsx +128 -0
  258. package/templates/nextjs-standalone/src/ui/components/admin/entity-detail.tsx +3 -3
  259. package/templates/nextjs-standalone/src/ui/components/admin/semantic-file-tree.tsx +159 -0
  260. package/templates/nextjs-standalone/src/ui/components/atlas-chat.tsx +64 -12
  261. package/templates/nextjs-standalone/src/ui/components/chart/result-chart.tsx +25 -15
  262. package/templates/nextjs-standalone/src/ui/components/chat/markdown.tsx +88 -42
  263. package/templates/nextjs-standalone/src/ui/components/chat/python-result-card.tsx +244 -0
  264. package/templates/nextjs-standalone/src/ui/components/chat/sql-block.tsx +39 -15
  265. package/templates/nextjs-standalone/src/ui/components/chat/sql-result-card.tsx +6 -1
  266. package/templates/nextjs-standalone/src/ui/components/chat/tool-part.tsx +12 -3
  267. package/templates/nextjs-standalone/src/ui/components/chat/typing-indicator.tsx +5 -2
  268. package/templates/nextjs-standalone/src/ui/components/conversations/conversation-item.tsx +25 -20
  269. package/templates/nextjs-standalone/src/ui/context.tsx +1 -1
  270. package/templates/nextjs-standalone/src/ui/hooks/use-conversations.ts +3 -3
  271. package/templates/nextjs-standalone/src/ui/hooks/use-dark-mode.ts +17 -10
  272. package/templates/nextjs-standalone/tsconfig.json +0 -1
  273. package/templates/nextjs-standalone/vercel.json +4 -1
  274. package/templates/docker/render.yaml +0 -34
  275. package/templates/docker/semantic/entities/.gitkeep +0 -0
  276. package/templates/docker/semantic/metrics/.gitkeep +0 -0
  277. package/templates/docker/src/lib/db/__tests__/duckdb.test.ts +0 -141
  278. package/templates/docker/src/lib/db/__tests__/salesforce.test.ts +0 -339
  279. package/templates/docker/src/lib/db/__tests__/snowflake.test.ts +0 -217
  280. package/templates/docker/src/lib/db/duckdb.ts +0 -122
  281. package/templates/docker/src/lib/db/salesforce.ts +0 -342
  282. package/templates/docker/src/lib/tools/__tests__/salesforce-tool.test.ts +0 -154
  283. package/templates/docker/src/lib/tools/__tests__/soql-validation.test.ts +0 -303
  284. package/templates/docker/src/lib/tools/__tests__/sql-duckdb.test.ts +0 -233
  285. package/templates/docker/src/lib/tools/salesforce.ts +0 -138
  286. package/templates/docker/src/lib/tools/soql-validation.ts +0 -172
  287. package/templates/nextjs-standalone/semantic/entities/.gitkeep +0 -0
  288. package/templates/nextjs-standalone/semantic/metrics/.gitkeep +0 -0
  289. package/templates/nextjs-standalone/src/lib/db/__tests__/duckdb.test.ts +0 -141
  290. package/templates/nextjs-standalone/src/lib/db/__tests__/salesforce.test.ts +0 -339
  291. package/templates/nextjs-standalone/src/lib/db/__tests__/snowflake.test.ts +0 -217
  292. package/templates/nextjs-standalone/src/lib/db/duckdb.ts +0 -122
  293. package/templates/nextjs-standalone/src/lib/db/salesforce.ts +0 -342
  294. package/templates/nextjs-standalone/src/lib/tools/__tests__/salesforce-tool.test.ts +0 -154
  295. package/templates/nextjs-standalone/src/lib/tools/__tests__/soql-validation.test.ts +0 -303
  296. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-duckdb.test.ts +0 -233
  297. package/templates/nextjs-standalone/src/lib/tools/salesforce.ts +0 -138
  298. package/templates/nextjs-standalone/src/lib/tools/soql-validation.ts +0 -172
@@ -0,0 +1,365 @@
1
+ import { describe, expect, it, mock, afterEach } from "bun:test";
2
+
3
+ // Mock logger to avoid side effects
4
+ mock.module("@atlas/api/lib/logger", () => ({
5
+ createLogger: () => ({
6
+ debug: () => {},
7
+ info: () => {},
8
+ warn: () => {},
9
+ error: () => {},
10
+ }),
11
+ }));
12
+
13
+ const { executePythonViaSidecar } = await import(
14
+ "@atlas/api/lib/tools/python-sidecar"
15
+ );
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Helpers
19
+ // ---------------------------------------------------------------------------
20
+
21
+ const SIDECAR_URL = "http://localhost:9999";
22
+
23
+ const savedEnv: Record<string, string | undefined> = {};
24
+
25
+ function setEnv(key: string, value: string | undefined) {
26
+ if (!(key in savedEnv)) savedEnv[key] = process.env[key];
27
+ if (value === undefined) delete process.env[key];
28
+ else process.env[key] = value;
29
+ }
30
+
31
+ // Save original fetch and provide a type-safe mock helper
32
+ const originalFetch = globalThis.fetch;
33
+
34
+ /** Override globalThis.fetch with a mock function. */
35
+ function mockFetch(fn: (input: string | URL | Request, init?: RequestInit) => Promise<Response>) {
36
+ globalThis.fetch = fn as typeof globalThis.fetch;
37
+ }
38
+
39
+ afterEach(() => {
40
+ // Restore env
41
+ for (const [key, value] of Object.entries(savedEnv)) {
42
+ if (value === undefined) delete process.env[key];
43
+ else process.env[key] = value;
44
+ }
45
+ for (const key of Object.keys(savedEnv)) delete savedEnv[key];
46
+
47
+ // Restore fetch
48
+ globalThis.fetch = originalFetch;
49
+ });
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Tests
53
+ // ---------------------------------------------------------------------------
54
+
55
+ describe("executePythonViaSidecar", () => {
56
+ describe("URL validation", () => {
57
+ it("returns error for invalid sidecar URL", async () => {
58
+ const result = await executePythonViaSidecar(
59
+ "not-a-url",
60
+ 'print("hello")',
61
+ );
62
+ expect(result.success).toBe(false);
63
+ if (!result.success) {
64
+ expect(result.error).toContain("Invalid ATLAS_SANDBOX_URL");
65
+ }
66
+ });
67
+ });
68
+
69
+ describe("connection errors", () => {
70
+ it("returns error when sidecar is unreachable", async () => {
71
+ mockFetch(() => Promise.reject(new Error("fetch failed: ECONNREFUSED")));
72
+
73
+ const result = await executePythonViaSidecar(
74
+ SIDECAR_URL,
75
+ 'print("hello")',
76
+ );
77
+ expect(result.success).toBe(false);
78
+ if (!result.success) {
79
+ expect(result.error).toContain("sidecar unreachable");
80
+ }
81
+ });
82
+
83
+ it("returns timeout error when request exceeds deadline", async () => {
84
+ mockFetch(() => Promise.reject(new Error("TimeoutError: timed out")));
85
+
86
+ const result = await executePythonViaSidecar(
87
+ SIDECAR_URL,
88
+ 'print("hello")',
89
+ );
90
+ expect(result.success).toBe(false);
91
+ if (!result.success) {
92
+ expect(result.error).toContain("timed out");
93
+ }
94
+ });
95
+
96
+ it("returns error for generic fetch failures", async () => {
97
+ mockFetch(() => Promise.reject(new Error("network error")));
98
+
99
+ const result = await executePythonViaSidecar(
100
+ SIDECAR_URL,
101
+ 'print("hello")',
102
+ );
103
+ expect(result.success).toBe(false);
104
+ if (!result.success) {
105
+ expect(result.error).toContain("Sidecar request failed");
106
+ }
107
+ });
108
+ });
109
+
110
+ describe("HTTP error responses", () => {
111
+ it("parses structured error from HTTP 500", async () => {
112
+ mockFetch(() =>
113
+ Promise.resolve(
114
+ new Response(
115
+ JSON.stringify({ success: false, error: "crash" }),
116
+ { status: 500 },
117
+ ),
118
+ ));
119
+
120
+ const result = await executePythonViaSidecar(
121
+ SIDECAR_URL,
122
+ 'print("hello")',
123
+ );
124
+ expect(result.success).toBe(false);
125
+ if (!result.success) {
126
+ expect(result.error).toBe("crash");
127
+ }
128
+ });
129
+
130
+ it("returns raw error for non-500 HTTP errors", async () => {
131
+ mockFetch(() =>
132
+ Promise.resolve(
133
+ new Response("Rate limited", { status: 429 }),
134
+ ));
135
+
136
+ const result = await executePythonViaSidecar(
137
+ SIDECAR_URL,
138
+ 'print("hello")',
139
+ );
140
+ expect(result.success).toBe(false);
141
+ if (!result.success) {
142
+ expect(result.error).toContain("HTTP 429");
143
+ expect(result.error).toContain("Rate limited");
144
+ }
145
+ });
146
+
147
+ it("returns raw error for 500 with non-JSON body", async () => {
148
+ mockFetch(() =>
149
+ Promise.resolve(
150
+ new Response("Internal Server Error", { status: 500 }),
151
+ ));
152
+
153
+ const result = await executePythonViaSidecar(
154
+ SIDECAR_URL,
155
+ 'print("hello")',
156
+ );
157
+ expect(result.success).toBe(false);
158
+ if (!result.success) {
159
+ expect(result.error).toContain("HTTP 500");
160
+ }
161
+ });
162
+ });
163
+
164
+ describe("response parsing", () => {
165
+ it("returns error when response JSON is unparseable", async () => {
166
+ mockFetch(() =>
167
+ Promise.resolve(new Response("not json", { status: 200 })));
168
+
169
+ const result = await executePythonViaSidecar(
170
+ SIDECAR_URL,
171
+ 'print("hello")',
172
+ );
173
+ expect(result.success).toBe(false);
174
+ if (!result.success) {
175
+ expect(result.error).toContain("Failed to parse sidecar response");
176
+ }
177
+ });
178
+
179
+ it("returns error when response lacks success boolean", async () => {
180
+ mockFetch(() =>
181
+ Promise.resolve(
182
+ Response.json({ output: "hello" }),
183
+ ));
184
+
185
+ const result = await executePythonViaSidecar(
186
+ SIDECAR_URL,
187
+ 'print("hello")',
188
+ );
189
+ expect(result.success).toBe(false);
190
+ if (!result.success) {
191
+ expect(result.error).toContain("unexpected response format");
192
+ }
193
+ });
194
+ });
195
+
196
+ describe("successful execution", () => {
197
+ it("returns PythonResult on success", async () => {
198
+ mockFetch(() =>
199
+ Promise.resolve(
200
+ Response.json({
201
+ success: true,
202
+ output: "hello world",
203
+ }),
204
+ ));
205
+
206
+ const result = await executePythonViaSidecar(
207
+ SIDECAR_URL,
208
+ 'print("hello world")',
209
+ );
210
+ expect(result.success).toBe(true);
211
+ if (result.success) {
212
+ expect(result.output).toBe("hello world");
213
+ }
214
+ });
215
+
216
+ it("returns result with table data", async () => {
217
+ mockFetch(() =>
218
+ Promise.resolve(
219
+ Response.json({
220
+ success: true,
221
+ table: { columns: ["x"], rows: [[1], [2]] },
222
+ }),
223
+ ));
224
+
225
+ const result = await executePythonViaSidecar(
226
+ SIDECAR_URL,
227
+ "code",
228
+ );
229
+ expect(result.success).toBe(true);
230
+ if (result.success) {
231
+ expect(result.table).toEqual({ columns: ["x"], rows: [[1], [2]] });
232
+ }
233
+ });
234
+
235
+ it("returns result with rechartsCharts", async () => {
236
+ mockFetch(() =>
237
+ Promise.resolve(
238
+ Response.json({
239
+ success: true,
240
+ rechartsCharts: [
241
+ { type: "bar", data: [{ x: 1 }], categoryKey: "x", valueKeys: ["x"] },
242
+ ],
243
+ }),
244
+ ));
245
+
246
+ const result = await executePythonViaSidecar(
247
+ SIDECAR_URL,
248
+ "code",
249
+ );
250
+ expect(result.success).toBe(true);
251
+ if (result.success) {
252
+ expect(result.rechartsCharts).toHaveLength(1);
253
+ }
254
+ });
255
+
256
+ it("returns error result from sidecar", async () => {
257
+ mockFetch(() =>
258
+ Promise.resolve(
259
+ Response.json({
260
+ success: false,
261
+ error: "ZeroDivisionError: division by zero",
262
+ }),
263
+ ));
264
+
265
+ const result = await executePythonViaSidecar(
266
+ SIDECAR_URL,
267
+ "1/0",
268
+ );
269
+ expect(result.success).toBe(false);
270
+ if (!result.success) {
271
+ expect(result.error).toContain("ZeroDivisionError");
272
+ }
273
+ });
274
+ });
275
+
276
+ describe("request construction", () => {
277
+ it("sends auth header when SIDECAR_AUTH_TOKEN is set", async () => {
278
+ setEnv("SIDECAR_AUTH_TOKEN", "test-token-123");
279
+ let capturedHeaders: Headers | null = null;
280
+
281
+ mockFetch((_input, init) => {
282
+ capturedHeaders = new Headers(init?.headers);
283
+ return Promise.resolve(Response.json({ success: true }));
284
+ });
285
+
286
+ await executePythonViaSidecar(SIDECAR_URL, "code");
287
+ expect(capturedHeaders!.get("Authorization")).toBe("Bearer test-token-123");
288
+ });
289
+
290
+ it("omits auth header when SIDECAR_AUTH_TOKEN is not set", async () => {
291
+ setEnv("SIDECAR_AUTH_TOKEN", undefined);
292
+ let capturedHeaders: Headers | null = null;
293
+
294
+ mockFetch((_input, init) => {
295
+ capturedHeaders = new Headers(init?.headers);
296
+ return Promise.resolve(Response.json({ success: true }));
297
+ });
298
+
299
+ await executePythonViaSidecar(SIDECAR_URL, "code");
300
+ expect(capturedHeaders!.get("Authorization")).toBeNull();
301
+ });
302
+
303
+ it("sends data payload when provided", async () => {
304
+ let capturedBody: string | null = null;
305
+
306
+ mockFetch((_input, init) => {
307
+ capturedBody = init?.body as string;
308
+ return Promise.resolve(Response.json({ success: true }));
309
+ });
310
+
311
+ const data = { columns: ["id"], rows: [[1], [2]] };
312
+ await executePythonViaSidecar(SIDECAR_URL, "code", data);
313
+
314
+ const parsed = JSON.parse(capturedBody!);
315
+ expect(parsed.data).toEqual(data);
316
+ expect(parsed.code).toBe("code");
317
+ });
318
+
319
+ it("omits data from request when not provided", async () => {
320
+ let capturedBody: string | null = null;
321
+
322
+ mockFetch((_input, init) => {
323
+ capturedBody = init?.body as string;
324
+ return Promise.resolve(Response.json({ success: true }));
325
+ });
326
+
327
+ await executePythonViaSidecar(SIDECAR_URL, "code");
328
+
329
+ const parsed = JSON.parse(capturedBody!);
330
+ expect(parsed.data).toBeUndefined();
331
+ });
332
+ });
333
+
334
+ describe("timeout configuration", () => {
335
+ it("uses ATLAS_PYTHON_TIMEOUT when set", async () => {
336
+ setEnv("ATLAS_PYTHON_TIMEOUT", "5000");
337
+ let capturedBody: string | null = null;
338
+
339
+ mockFetch((_input, init) => {
340
+ capturedBody = init?.body as string;
341
+ return Promise.resolve(Response.json({ success: true }));
342
+ });
343
+
344
+ await executePythonViaSidecar(SIDECAR_URL, "code");
345
+
346
+ const parsed = JSON.parse(capturedBody!);
347
+ expect(parsed.timeout).toBe(5000);
348
+ });
349
+
350
+ it("falls back to default on invalid ATLAS_PYTHON_TIMEOUT", async () => {
351
+ setEnv("ATLAS_PYTHON_TIMEOUT", "not-a-number");
352
+ let capturedBody: string | null = null;
353
+
354
+ mockFetch((_input, init) => {
355
+ capturedBody = init?.body as string;
356
+ return Promise.resolve(Response.json({ success: true }));
357
+ });
358
+
359
+ await executePythonViaSidecar(SIDECAR_URL, "code");
360
+
361
+ const parsed = JSON.parse(capturedBody!);
362
+ expect(parsed.timeout).toBe(30000); // DEFAULT_TIMEOUT_MS
363
+ });
364
+ });
365
+ });
@@ -0,0 +1,331 @@
1
+ import { describe, expect, it, mock, afterEach } from "bun:test";
2
+
3
+ // Mock logger and tracing to avoid side effects
4
+ mock.module("@atlas/api/lib/logger", () => ({
5
+ createLogger: () => ({
6
+ debug: () => {},
7
+ info: () => {},
8
+ warn: () => {},
9
+ error: () => {},
10
+ }),
11
+ }));
12
+
13
+ mock.module("@atlas/api/lib/tracing", () => ({
14
+ withSpan: async (_name: string, _attrs: unknown, fn: () => Promise<unknown>) => fn(),
15
+ }));
16
+
17
+ const { validatePythonCode } = await import(
18
+ "@atlas/api/lib/tools/python"
19
+ );
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Import guard tests
23
+ // ---------------------------------------------------------------------------
24
+
25
+ describe("validatePythonCode", () => {
26
+ describe("blocked imports", () => {
27
+ it("rejects import subprocess", async () => {
28
+ const result = await validatePythonCode("import subprocess");
29
+ expect(result.safe).toBe(false);
30
+ if (!result.safe) expect(result.reason).toContain("subprocess");
31
+ });
32
+
33
+ it("rejects from os import path", async () => {
34
+ const result = await validatePythonCode("from os import path");
35
+ expect(result.safe).toBe(false);
36
+ if (!result.safe) expect(result.reason).toContain("os");
37
+ });
38
+
39
+ it("rejects import socket", async () => {
40
+ const result = await validatePythonCode("import socket");
41
+ expect(result.safe).toBe(false);
42
+ if (!result.safe) expect(result.reason).toContain("socket");
43
+ });
44
+
45
+ it("rejects import shutil", async () => {
46
+ const result = await validatePythonCode("import shutil");
47
+ expect(result.safe).toBe(false);
48
+ if (!result.safe) expect(result.reason).toContain("shutil");
49
+ });
50
+
51
+ it("rejects import sys", async () => {
52
+ const result = await validatePythonCode("import sys");
53
+ expect(result.safe).toBe(false);
54
+ if (!result.safe) expect(result.reason).toContain("sys");
55
+ });
56
+
57
+ it("rejects import ctypes", async () => {
58
+ const result = await validatePythonCode("import ctypes");
59
+ expect(result.safe).toBe(false);
60
+ if (!result.safe) expect(result.reason).toContain("ctypes");
61
+ });
62
+
63
+ it("rejects import importlib", async () => {
64
+ const result = await validatePythonCode("import importlib");
65
+ expect(result.safe).toBe(false);
66
+ if (!result.safe) expect(result.reason).toContain("importlib");
67
+ });
68
+
69
+ it("rejects import code", async () => {
70
+ const result = await validatePythonCode("import code");
71
+ expect(result.safe).toBe(false);
72
+ if (!result.safe) expect(result.reason).toContain("code");
73
+ });
74
+
75
+ it("rejects import signal", async () => {
76
+ const result = await validatePythonCode("import signal");
77
+ expect(result.safe).toBe(false);
78
+ if (!result.safe) expect(result.reason).toContain("signal");
79
+ });
80
+
81
+ it("rejects import multiprocessing", async () => {
82
+ const result = await validatePythonCode("import multiprocessing");
83
+ expect(result.safe).toBe(false);
84
+ if (!result.safe) expect(result.reason).toContain("multiprocessing");
85
+ });
86
+
87
+ it("rejects from subprocess import run", async () => {
88
+ const result = await validatePythonCode("from subprocess import run");
89
+ expect(result.safe).toBe(false);
90
+ if (!result.safe) expect(result.reason).toContain("subprocess");
91
+ });
92
+
93
+ it("rejects os as submodule (import os.path)", async () => {
94
+ const result = await validatePythonCode("import os.path");
95
+ expect(result.safe).toBe(false);
96
+ if (!result.safe) expect(result.reason).toContain("os");
97
+ });
98
+
99
+ // Network modules (added per PR review #5)
100
+ it("rejects import http", async () => {
101
+ const result = await validatePythonCode("import http");
102
+ expect(result.safe).toBe(false);
103
+ if (!result.safe) expect(result.reason).toContain("http");
104
+ });
105
+
106
+ it("rejects import urllib", async () => {
107
+ const result = await validatePythonCode("import urllib");
108
+ expect(result.safe).toBe(false);
109
+ if (!result.safe) expect(result.reason).toContain("urllib");
110
+ });
111
+
112
+ it("rejects import requests", async () => {
113
+ const result = await validatePythonCode("import requests");
114
+ expect(result.safe).toBe(false);
115
+ if (!result.safe) expect(result.reason).toContain("requests");
116
+ });
117
+
118
+ it("rejects import pickle", async () => {
119
+ const result = await validatePythonCode("import pickle");
120
+ expect(result.safe).toBe(false);
121
+ if (!result.safe) expect(result.reason).toContain("pickle");
122
+ });
123
+
124
+ it("rejects import tempfile", async () => {
125
+ const result = await validatePythonCode("import tempfile");
126
+ expect(result.safe).toBe(false);
127
+ if (!result.safe) expect(result.reason).toContain("tempfile");
128
+ });
129
+
130
+ it("rejects import pathlib", async () => {
131
+ const result = await validatePythonCode("import pathlib");
132
+ expect(result.safe).toBe(false);
133
+ if (!result.safe) expect(result.reason).toContain("pathlib");
134
+ });
135
+ });
136
+
137
+ describe("blocked builtins", () => {
138
+ it("rejects exec()", async () => {
139
+ const result = await validatePythonCode('exec("print(1)")');
140
+ expect(result.safe).toBe(false);
141
+ if (!result.safe) expect(result.reason).toContain("exec");
142
+ });
143
+
144
+ it("rejects eval()", async () => {
145
+ const result = await validatePythonCode('eval("1+1")');
146
+ expect(result.safe).toBe(false);
147
+ if (!result.safe) expect(result.reason).toContain("eval");
148
+ });
149
+
150
+ it("rejects compile()", async () => {
151
+ const result = await validatePythonCode('compile("x=1", "<string>", "exec")');
152
+ expect(result.safe).toBe(false);
153
+ if (!result.safe) expect(result.reason).toContain("compile");
154
+ });
155
+
156
+ it("rejects __import__()", async () => {
157
+ const result = await validatePythonCode('__import__("os")');
158
+ expect(result.safe).toBe(false);
159
+ if (!result.safe) expect(result.reason).toContain("__import__");
160
+ });
161
+
162
+ it("rejects open()", async () => {
163
+ const result = await validatePythonCode('open("/etc/passwd")');
164
+ expect(result.safe).toBe(false);
165
+ if (!result.safe) expect(result.reason).toContain("open");
166
+ });
167
+
168
+ it("rejects breakpoint()", async () => {
169
+ const result = await validatePythonCode("breakpoint()");
170
+ expect(result.safe).toBe(false);
171
+ if (!result.safe) expect(result.reason).toContain("breakpoint");
172
+ });
173
+
174
+ // Guard bypass mitigations (PR review #1)
175
+ it("rejects getattr()", async () => {
176
+ const result = await validatePythonCode('getattr(__builtins__, "exec")("print(1)")');
177
+ expect(result.safe).toBe(false);
178
+ if (!result.safe) expect(result.reason).toContain("getattr");
179
+ });
180
+
181
+ it("rejects globals()", async () => {
182
+ const result = await validatePythonCode('globals()["__builtins__"]');
183
+ expect(result.safe).toBe(false);
184
+ if (!result.safe) expect(result.reason).toContain("globals");
185
+ });
186
+
187
+ it("rejects vars()", async () => {
188
+ const result = await validatePythonCode("vars()");
189
+ expect(result.safe).toBe(false);
190
+ if (!result.safe) expect(result.reason).toContain("vars");
191
+ });
192
+ });
193
+
194
+ describe("allowed imports", () => {
195
+ it("allows pandas", async () => {
196
+ const result = await validatePythonCode("import pandas as pd");
197
+ expect(result.safe).toBe(true);
198
+ });
199
+
200
+ it("allows numpy", async () => {
201
+ const result = await validatePythonCode("import numpy as np");
202
+ expect(result.safe).toBe(true);
203
+ });
204
+
205
+ it("allows matplotlib", async () => {
206
+ const result = await validatePythonCode("import matplotlib.pyplot as plt");
207
+ expect(result.safe).toBe(true);
208
+ });
209
+
210
+ it("allows json", async () => {
211
+ const result = await validatePythonCode("import json");
212
+ expect(result.safe).toBe(true);
213
+ });
214
+
215
+ it("allows math", async () => {
216
+ const result = await validatePythonCode("import math");
217
+ expect(result.safe).toBe(true);
218
+ });
219
+
220
+ it("allows datetime", async () => {
221
+ const result = await validatePythonCode("from datetime import datetime");
222
+ expect(result.safe).toBe(true);
223
+ });
224
+
225
+ it("allows statistics", async () => {
226
+ const result = await validatePythonCode("import statistics");
227
+ expect(result.safe).toBe(true);
228
+ });
229
+
230
+ it("allows collections", async () => {
231
+ const result = await validatePythonCode("from collections import Counter");
232
+ expect(result.safe).toBe(true);
233
+ });
234
+ });
235
+
236
+ describe("syntax errors", () => {
237
+ it("rejects code with syntax errors", async () => {
238
+ const result = await validatePythonCode("def foo(");
239
+ expect(result.safe).toBe(false);
240
+ if (!result.safe) expect(result.reason).toContain("SyntaxError");
241
+ });
242
+ });
243
+
244
+ describe("complex code", () => {
245
+ it("allows legitimate data analysis code", async () => {
246
+ const code = `
247
+ import json
248
+ import math
249
+ from collections import Counter
250
+
251
+ values = [1, 2, 3, 4, 5]
252
+ mean = sum(values) / len(values)
253
+ print(f"Mean: {mean}")
254
+ `;
255
+ const result = await validatePythonCode(code);
256
+ expect(result.safe).toBe(true);
257
+ });
258
+
259
+ it("rejects code with blocked import buried in logic", async () => {
260
+ const code = `
261
+ x = 1
262
+ y = 2
263
+ import subprocess
264
+ z = x + y
265
+ `;
266
+ const result = await validatePythonCode(code);
267
+ expect(result.safe).toBe(false);
268
+ if (!result.safe) expect(result.reason).toContain("subprocess");
269
+ });
270
+ });
271
+ });
272
+
273
+ // ---------------------------------------------------------------------------
274
+ // Sidecar routing tests
275
+ // ---------------------------------------------------------------------------
276
+
277
+ describe("executePython tool", () => {
278
+ const savedEnv: Record<string, string | undefined> = {};
279
+
280
+ function saveAndSetEnv(key: string, value: string | undefined) {
281
+ if (!(key in savedEnv)) {
282
+ savedEnv[key] = process.env[key];
283
+ }
284
+ if (value === undefined) {
285
+ delete process.env[key];
286
+ } else {
287
+ process.env[key] = value;
288
+ }
289
+ }
290
+
291
+ afterEach(() => {
292
+ for (const [key, value] of Object.entries(savedEnv)) {
293
+ if (value === undefined) {
294
+ delete process.env[key];
295
+ } else {
296
+ process.env[key] = value;
297
+ }
298
+ }
299
+ for (const key of Object.keys(savedEnv)) {
300
+ delete savedEnv[key];
301
+ }
302
+ });
303
+
304
+ it("rejects execution when ATLAS_SANDBOX_URL is not set", async () => {
305
+ saveAndSetEnv("ATLAS_SANDBOX_URL", undefined);
306
+
307
+ const { executePython } = await import("@atlas/api/lib/tools/python");
308
+ const execute = executePython.execute!;
309
+ const result = await execute(
310
+ { code: 'print("hello")', explanation: "test", data: undefined },
311
+ {} as never,
312
+ ) as { success: boolean; error?: string };
313
+ expect(result.success).toBe(false);
314
+ expect(result.error).toContain("ATLAS_SANDBOX_URL");
315
+ });
316
+
317
+ it("rejects code that fails import guard before hitting sidecar", async () => {
318
+ saveAndSetEnv("ATLAS_SANDBOX_URL", "http://localhost:9999");
319
+
320
+ const { executePython } = await import("@atlas/api/lib/tools/python");
321
+ const execute = executePython.execute!;
322
+ const result = await execute(
323
+ { code: "import subprocess", explanation: "test", data: undefined },
324
+ {} as never,
325
+ ) as { success: boolean; error?: string };
326
+ expect(result.success).toBe(false);
327
+ expect(result.error).toContain("subprocess");
328
+ });
329
+ });
330
+
331
+ // Registry gating (ATLAS_PYTHON_ENABLED + ATLAS_SANDBOX_URL) is tested in registry.test.ts