@libredb/studio 0.9.7

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 (572) hide show
  1. package/.claude/settings.local.json +127 -0
  2. package/.cursorrules +426 -0
  3. package/.devin/wiki.json +143 -0
  4. package/.dockerignore +80 -0
  5. package/.env.example +159 -0
  6. package/.github/ISSUE_TEMPLATE/bug_report.md +49 -0
  7. package/.github/ISSUE_TEMPLATE/feature_request.md +29 -0
  8. package/.github/PULL_REQUEST_TEMPLATE.md +57 -0
  9. package/.github/workflows/ci.yml +185 -0
  10. package/.github/workflows/codeql.yml +57 -0
  11. package/.github/workflows/docker-build-push.yml +118 -0
  12. package/.github/workflows/helm-release.yml +113 -0
  13. package/CLAUDE.md +265 -0
  14. package/CODE_OF_CONDUCT.md +124 -0
  15. package/CONTRIBUTING.md +154 -0
  16. package/Dockerfile +73 -0
  17. package/LICENSE +21 -0
  18. package/README.md +614 -0
  19. package/SECURITY.md +107 -0
  20. package/artifacthub-repo.yml +4 -0
  21. package/bun.lock +1714 -0
  22. package/bunfig.toml +3 -0
  23. package/charts/libredb-studio/.helmignore +11 -0
  24. package/charts/libredb-studio/Chart.lock +6 -0
  25. package/charts/libredb-studio/Chart.yaml +50 -0
  26. package/charts/libredb-studio/README.md +206 -0
  27. package/charts/libredb-studio/templates/NOTES.txt +59 -0
  28. package/charts/libredb-studio/templates/_helpers.tpl +135 -0
  29. package/charts/libredb-studio/templates/configmap.yaml +37 -0
  30. package/charts/libredb-studio/templates/deployment.yaml +184 -0
  31. package/charts/libredb-studio/templates/hpa.yaml +32 -0
  32. package/charts/libredb-studio/templates/ingress.yaml +41 -0
  33. package/charts/libredb-studio/templates/networkpolicy.yaml +50 -0
  34. package/charts/libredb-studio/templates/pdb.yaml +18 -0
  35. package/charts/libredb-studio/templates/pvc.yaml +23 -0
  36. package/charts/libredb-studio/templates/secret.yaml +30 -0
  37. package/charts/libredb-studio/templates/seed-configmap.yaml +11 -0
  38. package/charts/libredb-studio/templates/service.yaml +22 -0
  39. package/charts/libredb-studio/templates/serviceaccount.yaml +13 -0
  40. package/charts/libredb-studio/values.schema.json +246 -0
  41. package/charts/libredb-studio/values.yaml +286 -0
  42. package/components.json +22 -0
  43. package/conductor/code_styleguides/typescript.md +43 -0
  44. package/conductor/product-guidelines.md +43 -0
  45. package/conductor/product.md +3 -0
  46. package/conductor/setup_state.json +1 -0
  47. package/conductor/tech-stack.md +39 -0
  48. package/conductor/tracks/enhance_postgres_monitoring_20251227/metadata.json +8 -0
  49. package/conductor/tracks/enhance_postgres_monitoring_20251227/plan.md +44 -0
  50. package/conductor/tracks/enhance_postgres_monitoring_20251227/spec.md +31 -0
  51. package/conductor/tracks.md +8 -0
  52. package/conductor/workflow.md +333 -0
  53. package/database-compose.yml +55 -0
  54. package/docker/postgres-init/01-extensions.sql +10 -0
  55. package/docker/postgres-init/02-sample-data.sql +585 -0
  56. package/docker/postgres.yml +68 -0
  57. package/docker-compose.yml +38 -0
  58. package/docs/AI_PLAN.md +74 -0
  59. package/docs/API_DOCS.md +875 -0
  60. package/docs/ARCHITECTURE.md +218 -0
  61. package/docs/DATABASE_PROVIDERS.md +358 -0
  62. package/docs/FEATURES.md +116 -0
  63. package/docs/HELM_CHART.md +252 -0
  64. package/docs/LOGIN_PAGE.md +178 -0
  65. package/docs/MONACO_EDITOR_PERFORMANCE.md +315 -0
  66. package/docs/OIDC_ARCH.md +681 -0
  67. package/docs/OIDC_SETUP.md +322 -0
  68. package/docs/POSTGRES_METRICS.md +516 -0
  69. package/docs/QUERY_OPTIMIZATION.md +370 -0
  70. package/docs/SEED_CONNECTIONS.md +468 -0
  71. package/docs/SQL_ALIAS_COMPLETION.md +190 -0
  72. package/docs/STORAGE_ARCHITECTURE.md +565 -0
  73. package/docs/STORAGE_QUICK_SETUP.md +419 -0
  74. package/docs/TECHNICAL_PLAN.md +36 -0
  75. package/docs/THEMING.md +345 -0
  76. package/docs/adding-a-new-database-provider.md +642 -0
  77. package/docs/backlogs/000-PLATFORM_DATA_SYNC_DATABASE.md +360 -0
  78. package/docs/backlogs/001-INLINE_DATA_EDITING.md +118 -0
  79. package/docs/backlogs/002-DATA_IMPORT.md +215 -0
  80. package/docs/backlogs/003-QUERY_TIME_MACHINE.md +183 -0
  81. package/docs/backlogs/004-AI_DATA_STORYTELLER.md +292 -0
  82. package/docs/backlogs/005-QUERY_PLAYGROUND.md +352 -0
  83. package/docs/backlogs/006-DATA_MASKING.md +418 -0
  84. package/docs/enterprise-features.md +718 -0
  85. package/docs/kubernetes-helm-chart-artifacthub-plan.md +803 -0
  86. package/docs/medium-koyeb-article-en.md +215 -0
  87. package/docs/plans/test-plans.md +445 -0
  88. package/docs/releases/RELEASE.V0.3.0.md +22 -0
  89. package/docs/releases/RELEASE.V0.4.0.md +154 -0
  90. package/docs/releases/RELEASE.V0.5.0.md +252 -0
  91. package/docs/releases/RELEASE_v0.5.6.md +145 -0
  92. package/docs/releases/RELEASE_v0.6.1.md +303 -0
  93. package/docs/releases/RELEASE_v0.6.7.md +292 -0
  94. package/docs/releases/RELEASE_v0.7.0.md +332 -0
  95. package/docs/releases/RELEASE_v0.8.0.md +521 -0
  96. package/docs/sampledb/titanic.sql +1379 -0
  97. package/docs/superpowers/plans/2026-03-25-seed-connections.md +1362 -0
  98. package/docs/superpowers/specs/2026-03-25-seed-connections-design.md +590 -0
  99. package/e2e/admin-dashboard.spec.ts +64 -0
  100. package/e2e/connection-management.spec.ts +58 -0
  101. package/e2e/export.spec.ts +34 -0
  102. package/e2e/login.spec.ts +85 -0
  103. package/e2e/query-execution.spec.ts +35 -0
  104. package/e2e/tab-management.spec.ts +64 -0
  105. package/eslint.config.mjs +28 -0
  106. package/fly.toml +43 -0
  107. package/next.config.ts +32 -0
  108. package/package.json +130 -0
  109. package/playwright.config.ts +34 -0
  110. package/postcss.config.mjs +7 -0
  111. package/public/favicon-32x32.png +0 -0
  112. package/public/favicon.ico +0 -0
  113. package/public/file.svg +1 -0
  114. package/public/globe.svg +1 -0
  115. package/public/logo.svg +32 -0
  116. package/public/next.svg +1 -0
  117. package/public/screenshots/code-generator.png +0 -0
  118. package/public/screenshots/connection-modal.png +0 -0
  119. package/public/screenshots/data-profiler.png +0 -0
  120. package/public/screenshots/erd-diagram.png +0 -0
  121. package/public/screenshots/hero-editor.png +0 -0
  122. package/public/screenshots/nl2sql.png +0 -0
  123. package/public/vercel.svg +1 -0
  124. package/public/window.svg +1 -0
  125. package/render.yaml +58 -0
  126. package/scripts/merge-lcov.mjs +239 -0
  127. package/sonar-project.properties +16 -0
  128. package/src/app/admin/error.tsx +46 -0
  129. package/src/app/admin/page.tsx +10 -0
  130. package/src/app/api/admin/audit/route.ts +52 -0
  131. package/src/app/api/admin/fleet-health/route.ts +81 -0
  132. package/src/app/api/ai/autopilot/route.ts +105 -0
  133. package/src/app/api/ai/chat/route.ts +132 -0
  134. package/src/app/api/ai/describe-schema/route.ts +52 -0
  135. package/src/app/api/ai/explain/route.ts +86 -0
  136. package/src/app/api/ai/impact/route.ts +97 -0
  137. package/src/app/api/ai/index-advisor/route.ts +98 -0
  138. package/src/app/api/ai/nl2sql/route.ts +87 -0
  139. package/src/app/api/ai/query-safety/route.ts +87 -0
  140. package/src/app/api/auth/login/route.ts +62 -0
  141. package/src/app/api/auth/logout/route.ts +25 -0
  142. package/src/app/api/auth/me/route.ts +10 -0
  143. package/src/app/api/auth/oidc/callback/route.ts +82 -0
  144. package/src/app/api/auth/oidc/login/route.ts +43 -0
  145. package/src/app/api/connections/managed/route.ts +35 -0
  146. package/src/app/api/db/cancel/route.ts +42 -0
  147. package/src/app/api/db/disconnect/route.ts +28 -0
  148. package/src/app/api/db/health/route.ts +49 -0
  149. package/src/app/api/db/maintenance/route.ts +72 -0
  150. package/src/app/api/db/monitoring/route.ts +62 -0
  151. package/src/app/api/db/multi-query/route.ts +116 -0
  152. package/src/app/api/db/pool-stats/route.ts +37 -0
  153. package/src/app/api/db/profile/route.ts +144 -0
  154. package/src/app/api/db/provider-meta/route.ts +49 -0
  155. package/src/app/api/db/query/route.ts +50 -0
  156. package/src/app/api/db/schema/route.ts +47 -0
  157. package/src/app/api/db/schema-snapshot/route.ts +42 -0
  158. package/src/app/api/db/test-connection/route.ts +55 -0
  159. package/src/app/api/db/transaction/route.ts +111 -0
  160. package/src/app/api/storage/[collection]/route.ts +67 -0
  161. package/src/app/api/storage/config/route.ts +17 -0
  162. package/src/app/api/storage/migrate/route.ts +45 -0
  163. package/src/app/api/storage/route.ts +32 -0
  164. package/src/app/error.tsx +49 -0
  165. package/src/app/global-error.tsx +55 -0
  166. package/src/app/globals.css +146 -0
  167. package/src/app/icon.svg +42 -0
  168. package/src/app/layout.tsx +34 -0
  169. package/src/app/login/login-form.tsx +301 -0
  170. package/src/app/login/page.tsx +11 -0
  171. package/src/app/monitoring/page.tsx +8 -0
  172. package/src/app/not-found.tsx +29 -0
  173. package/src/app/page.tsx +5 -0
  174. package/src/components/AIAutopilotPanel.tsx +238 -0
  175. package/src/components/CodeGenerator.tsx +271 -0
  176. package/src/components/CommandPalette.tsx +227 -0
  177. package/src/components/ConnectionModal.tsx +759 -0
  178. package/src/components/CreateTableModal.tsx +281 -0
  179. package/src/components/DataCharts.tsx +962 -0
  180. package/src/components/DataImportModal.tsx +582 -0
  181. package/src/components/DataProfiler.tsx +335 -0
  182. package/src/components/DatabaseDocs.tsx +251 -0
  183. package/src/components/MaskingSettings.tsx +414 -0
  184. package/src/components/MobileNav.tsx +50 -0
  185. package/src/components/NL2SQLPanel.tsx +281 -0
  186. package/src/components/PivotTable.tsx +257 -0
  187. package/src/components/QueryEditor.tsx +760 -0
  188. package/src/components/QueryHistory.tsx +344 -0
  189. package/src/components/QuerySafetyDialog.tsx +290 -0
  190. package/src/components/ResultsGrid.tsx +644 -0
  191. package/src/components/SaveQueryModal.tsx +104 -0
  192. package/src/components/SavedQueries.tsx +128 -0
  193. package/src/components/SchemaDiagram.tsx +473 -0
  194. package/src/components/SchemaDiff.tsx +473 -0
  195. package/src/components/SnapshotTimeline.tsx +116 -0
  196. package/src/components/Studio.tsx +639 -0
  197. package/src/components/TestDataGenerator.tsx +261 -0
  198. package/src/components/VisualExplain.tsx +820 -0
  199. package/src/components/admin/AdminDashboard.tsx +163 -0
  200. package/src/components/admin/tabs/AuditTab.tsx +531 -0
  201. package/src/components/admin/tabs/MonitoringEmbed.tsx +11 -0
  202. package/src/components/admin/tabs/OperationsTab.tsx +646 -0
  203. package/src/components/admin/tabs/OverviewTab.tsx +1328 -0
  204. package/src/components/admin/tabs/SecurityTab.tsx +284 -0
  205. package/src/components/community-section.tsx +92 -0
  206. package/src/components/icons/db-icons.tsx +84 -0
  207. package/src/components/libredb-logo.tsx +61 -0
  208. package/src/components/monitoring/MonitoringDashboard.tsx +345 -0
  209. package/src/components/monitoring/tabs/MetricChart.tsx +82 -0
  210. package/src/components/monitoring/tabs/OverviewTab.tsx +263 -0
  211. package/src/components/monitoring/tabs/PerformanceTab.tsx +254 -0
  212. package/src/components/monitoring/tabs/PoolTab.tsx +174 -0
  213. package/src/components/monitoring/tabs/QueriesTab.tsx +287 -0
  214. package/src/components/monitoring/tabs/SessionsTab.tsx +316 -0
  215. package/src/components/monitoring/tabs/StorageTab.tsx +335 -0
  216. package/src/components/monitoring/tabs/TablesTab.tsx +300 -0
  217. package/src/components/results-grid/ResultCard.tsx +111 -0
  218. package/src/components/results-grid/RowDetailSheet.tsx +178 -0
  219. package/src/components/results-grid/StatsBar.tsx +201 -0
  220. package/src/components/results-grid/index.ts +1 -0
  221. package/src/components/results-grid/utils.ts +23 -0
  222. package/src/components/schema-explorer/ColumnList.tsx +53 -0
  223. package/src/components/schema-explorer/SchemaExplorer.tsx +182 -0
  224. package/src/components/schema-explorer/TableItem.tsx +210 -0
  225. package/src/components/schema-explorer/index.ts +1 -0
  226. package/src/components/sidebar/ConnectionItem.tsx +105 -0
  227. package/src/components/sidebar/ConnectionsList.tsx +62 -0
  228. package/src/components/sidebar/Sidebar.tsx +130 -0
  229. package/src/components/sidebar/index.ts +2 -0
  230. package/src/components/studio/BottomPanel.tsx +286 -0
  231. package/src/components/studio/QueryToolbar.tsx +180 -0
  232. package/src/components/studio/StudioDesktopHeader.tsx +114 -0
  233. package/src/components/studio/StudioMobileHeader.tsx +340 -0
  234. package/src/components/studio/StudioTabBar.tsx +82 -0
  235. package/src/components/studio/index.ts +5 -0
  236. package/src/components/ui/accordion.tsx +66 -0
  237. package/src/components/ui/alert-dialog.tsx +157 -0
  238. package/src/components/ui/alert.tsx +66 -0
  239. package/src/components/ui/aspect-ratio.tsx +11 -0
  240. package/src/components/ui/avatar.tsx +53 -0
  241. package/src/components/ui/badge.tsx +46 -0
  242. package/src/components/ui/breadcrumb.tsx +109 -0
  243. package/src/components/ui/button-group.tsx +83 -0
  244. package/src/components/ui/button.tsx +60 -0
  245. package/src/components/ui/calendar.tsx +216 -0
  246. package/src/components/ui/card.tsx +92 -0
  247. package/src/components/ui/carousel.tsx +241 -0
  248. package/src/components/ui/chart.tsx +357 -0
  249. package/src/components/ui/checkbox.tsx +32 -0
  250. package/src/components/ui/collapsible.tsx +33 -0
  251. package/src/components/ui/command.tsx +184 -0
  252. package/src/components/ui/context-menu.tsx +252 -0
  253. package/src/components/ui/dialog.tsx +143 -0
  254. package/src/components/ui/drawer.tsx +135 -0
  255. package/src/components/ui/dropdown-menu.tsx +257 -0
  256. package/src/components/ui/empty.tsx +104 -0
  257. package/src/components/ui/field.tsx +248 -0
  258. package/src/components/ui/form.tsx +167 -0
  259. package/src/components/ui/hover-card.tsx +44 -0
  260. package/src/components/ui/input-group.tsx +170 -0
  261. package/src/components/ui/input-otp.tsx +77 -0
  262. package/src/components/ui/input.tsx +21 -0
  263. package/src/components/ui/item.tsx +193 -0
  264. package/src/components/ui/kbd.tsx +28 -0
  265. package/src/components/ui/label.tsx +24 -0
  266. package/src/components/ui/menubar.tsx +276 -0
  267. package/src/components/ui/navigation-menu.tsx +168 -0
  268. package/src/components/ui/pagination.tsx +127 -0
  269. package/src/components/ui/popover.tsx +48 -0
  270. package/src/components/ui/progress.tsx +31 -0
  271. package/src/components/ui/radio-group.tsx +45 -0
  272. package/src/components/ui/resizable.tsx +56 -0
  273. package/src/components/ui/scroll-area.tsx +58 -0
  274. package/src/components/ui/select.tsx +187 -0
  275. package/src/components/ui/separator.tsx +28 -0
  276. package/src/components/ui/sheet.tsx +139 -0
  277. package/src/components/ui/sidebar.tsx +726 -0
  278. package/src/components/ui/skeleton.tsx +13 -0
  279. package/src/components/ui/slider.tsx +63 -0
  280. package/src/components/ui/sonner.tsx +40 -0
  281. package/src/components/ui/spinner.tsx +16 -0
  282. package/src/components/ui/switch.tsx +31 -0
  283. package/src/components/ui/table.tsx +116 -0
  284. package/src/components/ui/tabs.tsx +66 -0
  285. package/src/components/ui/textarea.tsx +18 -0
  286. package/src/components/ui/toggle-group.tsx +83 -0
  287. package/src/components/ui/toggle.tsx +47 -0
  288. package/src/components/ui/tooltip.tsx +61 -0
  289. package/src/exports/components.ts +15 -0
  290. package/src/exports/index.ts +4 -0
  291. package/src/exports/providers.ts +4 -0
  292. package/src/exports/types.ts +26 -0
  293. package/src/hooks/use-ai-chat.ts +182 -0
  294. package/src/hooks/use-all-connections.ts +66 -0
  295. package/src/hooks/use-api-call.ts +71 -0
  296. package/src/hooks/use-auth.ts +51 -0
  297. package/src/hooks/use-connection-form.ts +349 -0
  298. package/src/hooks/use-connection-manager.ts +169 -0
  299. package/src/hooks/use-connection-payload.ts +15 -0
  300. package/src/hooks/use-inline-editing.ts +109 -0
  301. package/src/hooks/use-mobile.ts +20 -0
  302. package/src/hooks/use-monitoring-data.ts +270 -0
  303. package/src/hooks/use-provider-metadata.ts +62 -0
  304. package/src/hooks/use-query-execution.ts +478 -0
  305. package/src/hooks/use-storage-sync.ts +259 -0
  306. package/src/hooks/use-tab-manager.ts +231 -0
  307. package/src/hooks/use-toast.ts +20 -0
  308. package/src/hooks/use-transaction-control.ts +64 -0
  309. package/src/lib/api/error-codes.ts +30 -0
  310. package/src/lib/api/errors.ts +236 -0
  311. package/src/lib/api/with-error-handler.ts +41 -0
  312. package/src/lib/audit.ts +105 -0
  313. package/src/lib/auth.ts +87 -0
  314. package/src/lib/connection-string-parser.ts +172 -0
  315. package/src/lib/data-masking.ts +385 -0
  316. package/src/lib/db/base-provider.ts +325 -0
  317. package/src/lib/db/errors.ts +317 -0
  318. package/src/lib/db/factory.ts +324 -0
  319. package/src/lib/db/index.ts +123 -0
  320. package/src/lib/db/providers/document/index.ts +6 -0
  321. package/src/lib/db/providers/document/mongodb.ts +992 -0
  322. package/src/lib/db/providers/keyvalue/redis.ts +554 -0
  323. package/src/lib/db/providers/sql/index.ts +11 -0
  324. package/src/lib/db/providers/sql/mssql.ts +1065 -0
  325. package/src/lib/db/providers/sql/mysql.ts +978 -0
  326. package/src/lib/db/providers/sql/oracle.ts +1044 -0
  327. package/src/lib/db/providers/sql/postgres.ts +1179 -0
  328. package/src/lib/db/providers/sql/sql-base.ts +174 -0
  329. package/src/lib/db/providers/sql/sqlite.ts +721 -0
  330. package/src/lib/db/types.ts +437 -0
  331. package/src/lib/db/utils/pool-manager.ts +287 -0
  332. package/src/lib/db/utils/query-limiter.ts +239 -0
  333. package/src/lib/db-ui-config.ts +86 -0
  334. package/src/lib/editor/mongodb-completions.ts +172 -0
  335. package/src/lib/editor/sql-completions.ts +280 -0
  336. package/src/lib/llm/base-provider.ts +117 -0
  337. package/src/lib/llm/factory.ts +102 -0
  338. package/src/lib/llm/index.ts +90 -0
  339. package/src/lib/llm/providers/custom.ts +181 -0
  340. package/src/lib/llm/providers/gemini.ts +126 -0
  341. package/src/lib/llm/providers/ollama.ts +154 -0
  342. package/src/lib/llm/providers/openai.ts +146 -0
  343. package/src/lib/llm/types.ts +173 -0
  344. package/src/lib/llm/utils/config.ts +187 -0
  345. package/src/lib/llm/utils/retry.ts +119 -0
  346. package/src/lib/llm/utils/streaming.ts +202 -0
  347. package/src/lib/logger.ts +127 -0
  348. package/src/lib/monitoring-thresholds.ts +44 -0
  349. package/src/lib/oidc.ts +262 -0
  350. package/src/lib/query-generators.ts +61 -0
  351. package/src/lib/schema-diff/diff-engine.ts +273 -0
  352. package/src/lib/schema-diff/migration-generator.ts +208 -0
  353. package/src/lib/schema-diff/types.ts +55 -0
  354. package/src/lib/seed/config-loader.ts +79 -0
  355. package/src/lib/seed/connection-filter.ts +49 -0
  356. package/src/lib/seed/credential-resolver.ts +62 -0
  357. package/src/lib/seed/index.ts +40 -0
  358. package/src/lib/seed/resolve-connection.ts +57 -0
  359. package/src/lib/seed/types.ts +69 -0
  360. package/src/lib/sql/alias-extractor.ts +267 -0
  361. package/src/lib/sql/index.ts +8 -0
  362. package/src/lib/sql/statement-splitter.ts +167 -0
  363. package/src/lib/sql/types.ts +40 -0
  364. package/src/lib/ssh/tunnel.ts +142 -0
  365. package/src/lib/storage/factory.ts +84 -0
  366. package/src/lib/storage/index.ts +14 -0
  367. package/src/lib/storage/local-storage.ts +99 -0
  368. package/src/lib/storage/providers/postgres.ts +225 -0
  369. package/src/lib/storage/providers/sqlite.ts +153 -0
  370. package/src/lib/storage/storage-facade.ts +272 -0
  371. package/src/lib/storage/types.ts +75 -0
  372. package/src/lib/time-series-buffer.ts +58 -0
  373. package/src/lib/types.ts +173 -0
  374. package/src/lib/utils.ts +6 -0
  375. package/src/proxy.ts +104 -0
  376. package/src/types/db-drivers.d.ts +23 -0
  377. package/src/types/html2canvas.d.ts +9 -0
  378. package/tests/api/admin/audit.test.ts +178 -0
  379. package/tests/api/admin/fleet-health.test.ts +183 -0
  380. package/tests/api/ai/autopilot.test.ts +174 -0
  381. package/tests/api/ai/chat.test.ts +250 -0
  382. package/tests/api/ai/describe-schema.test.ts +266 -0
  383. package/tests/api/ai/explain.test.ts +199 -0
  384. package/tests/api/ai/impact.test.ts +168 -0
  385. package/tests/api/ai/index-advisor.test.ts +171 -0
  386. package/tests/api/ai/nl2sql.test.ts +202 -0
  387. package/tests/api/ai/query-safety.test.ts +196 -0
  388. package/tests/api/auth/login.test.ts +170 -0
  389. package/tests/api/auth/logout.test.ts +140 -0
  390. package/tests/api/auth/me.test.ts +73 -0
  391. package/tests/api/auth/oidc-callback.test.ts +215 -0
  392. package/tests/api/auth/oidc-login.test.ts +127 -0
  393. package/tests/api/db/cancel.test.ts +198 -0
  394. package/tests/api/db/disconnect.test.ts +124 -0
  395. package/tests/api/db/health.test.ts +222 -0
  396. package/tests/api/db/maintenance.test.ts +263 -0
  397. package/tests/api/db/monitoring.test.ts +221 -0
  398. package/tests/api/db/multi-query.test.ts +316 -0
  399. package/tests/api/db/pool-stats.test.ts +135 -0
  400. package/tests/api/db/profile.test.ts +330 -0
  401. package/tests/api/db/provider-meta.test.ts +193 -0
  402. package/tests/api/db/query.test.ts +314 -0
  403. package/tests/api/db/schema-snapshot.test.ts +170 -0
  404. package/tests/api/db/schema.test.ts +191 -0
  405. package/tests/api/db/test-connection.test.ts +185 -0
  406. package/tests/api/db/transaction.test.ts +314 -0
  407. package/tests/api/proxy.test.ts +191 -0
  408. package/tests/api/seed/managed-route.test.ts +113 -0
  409. package/tests/api/storage/config.test.ts +42 -0
  410. package/tests/api/storage/storage-routes.test.ts +309 -0
  411. package/tests/components/AIAutopilotPanel.test.tsx +756 -0
  412. package/tests/components/AdminPage.test.tsx +33 -0
  413. package/tests/components/CodeGenerator.test.tsx +182 -0
  414. package/tests/components/CommandPalette.test.tsx +428 -0
  415. package/tests/components/CommunitySection.test.tsx +91 -0
  416. package/tests/components/ConnectionModal.mobile.test.tsx +284 -0
  417. package/tests/components/ConnectionModal.test.tsx +570 -0
  418. package/tests/components/CreateTableModal.test.tsx +383 -0
  419. package/tests/components/DataCharts.test.tsx +739 -0
  420. package/tests/components/DataImportModal.test.tsx +751 -0
  421. package/tests/components/DataProfiler.test.tsx +589 -0
  422. package/tests/components/DatabaseDocs.test.tsx +353 -0
  423. package/tests/components/LoginPage.test.tsx +163 -0
  424. package/tests/components/LoginPageOIDC.test.tsx +92 -0
  425. package/tests/components/MaskingSettings.test.tsx +498 -0
  426. package/tests/components/MobileNav.test.tsx +30 -0
  427. package/tests/components/MonitoringPage.test.tsx +32 -0
  428. package/tests/components/NL2SQLPanel.test.tsx +621 -0
  429. package/tests/components/Page.test.tsx +33 -0
  430. package/tests/components/PivotTable.test.tsx +350 -0
  431. package/tests/components/QueryEditor.test.tsx +1730 -0
  432. package/tests/components/QueryHistory.test.tsx +572 -0
  433. package/tests/components/QuerySafetyDialog.test.tsx +586 -0
  434. package/tests/components/ResultsGrid.test.tsx +804 -0
  435. package/tests/components/RootLayout.test.tsx +83 -0
  436. package/tests/components/SaveQueryModal.test.tsx +25 -0
  437. package/tests/components/SavedQueries.test.tsx +43 -0
  438. package/tests/components/SchemaDiagram.test.tsx +1034 -0
  439. package/tests/components/SchemaDiff.test.tsx +906 -0
  440. package/tests/components/SnapshotTimeline.test.tsx +174 -0
  441. package/tests/components/Studio.test.tsx +1030 -0
  442. package/tests/components/TestDataGenerator.test.tsx +291 -0
  443. package/tests/components/VisualExplain.test.tsx +704 -0
  444. package/tests/components/admin/AdminDashboard.test.tsx +205 -0
  445. package/tests/components/admin/AuditTab.test.tsx +220 -0
  446. package/tests/components/admin/MonitoringEmbed.test.tsx +58 -0
  447. package/tests/components/admin/OperationsTab.test.tsx +975 -0
  448. package/tests/components/admin/OverviewTab.test.tsx +254 -0
  449. package/tests/components/admin/SecurityTab.test.tsx +467 -0
  450. package/tests/components/monitoring/MetricChart.test.tsx +111 -0
  451. package/tests/components/monitoring/MonitoringDashboard.test.tsx +259 -0
  452. package/tests/components/monitoring/OverviewTab.test.tsx +78 -0
  453. package/tests/components/monitoring/PerformanceTab.test.tsx +87 -0
  454. package/tests/components/monitoring/PoolTab.test.tsx +42 -0
  455. package/tests/components/monitoring/QueriesTab.test.tsx +80 -0
  456. package/tests/components/monitoring/SessionsTab.test.tsx +154 -0
  457. package/tests/components/monitoring/StorageTab.test.tsx +127 -0
  458. package/tests/components/monitoring/TablesTab.test.tsx +153 -0
  459. package/tests/components/results-grid/ResultCard.test.tsx +105 -0
  460. package/tests/components/results-grid/RowDetailSheet.test.tsx +308 -0
  461. package/tests/components/results-grid/StatsBar.test.tsx +162 -0
  462. package/tests/components/schema-explorer/ColumnList.test.tsx +151 -0
  463. package/tests/components/schema-explorer/SchemaExplorer.test.tsx +461 -0
  464. package/tests/components/schema-explorer/TableItem.test.tsx +415 -0
  465. package/tests/components/sidebar/ConnectionItem.test.tsx +201 -0
  466. package/tests/components/sidebar/ConnectionsList.test.tsx +176 -0
  467. package/tests/components/sidebar/Sidebar.test.tsx +187 -0
  468. package/tests/components/studio/BottomPanel.test.tsx +383 -0
  469. package/tests/components/studio/QueryToolbar.test.tsx +321 -0
  470. package/tests/components/studio/StudioDesktopHeader.test.tsx +377 -0
  471. package/tests/components/studio/StudioMobileHeader.test.tsx +198 -0
  472. package/tests/components/studio/StudioTabBar.test.tsx +331 -0
  473. package/tests/fixtures/connections.ts +96 -0
  474. package/tests/fixtures/masking-configs.ts +86 -0
  475. package/tests/fixtures/query-results.ts +71 -0
  476. package/tests/fixtures/schemas.ts +64 -0
  477. package/tests/fixtures/seed-connections/invalid-config.yaml +7 -0
  478. package/tests/fixtures/seed-connections/minimal-config.yaml +8 -0
  479. package/tests/fixtures/seed-connections/mixed-credentials.yaml +23 -0
  480. package/tests/fixtures/seed-connections/multi-role-config.yaml +30 -0
  481. package/tests/fixtures/seed-connections/valid-config.json +15 -0
  482. package/tests/fixtures/seed-connections/valid-config.yaml +51 -0
  483. package/tests/helpers/mock-fetch.ts +59 -0
  484. package/tests/helpers/mock-monaco.ts +112 -0
  485. package/tests/helpers/mock-navigation.ts +28 -0
  486. package/tests/helpers/mock-next.ts +80 -0
  487. package/tests/helpers/mock-provider.ts +133 -0
  488. package/tests/helpers/mock-sonner.ts +29 -0
  489. package/tests/helpers/render-with-providers.tsx +19 -0
  490. package/tests/hooks/use-ai-chat.test.ts +600 -0
  491. package/tests/hooks/use-auth.test.ts +371 -0
  492. package/tests/hooks/use-connection-form.test.ts +743 -0
  493. package/tests/hooks/use-connection-manager.test.ts +466 -0
  494. package/tests/hooks/use-inline-editing.test.ts +321 -0
  495. package/tests/hooks/use-mobile.test.ts +177 -0
  496. package/tests/hooks/use-monitoring-data.test.ts +819 -0
  497. package/tests/hooks/use-provider-metadata.test.ts +228 -0
  498. package/tests/hooks/use-query-execution.test.ts +1212 -0
  499. package/tests/hooks/use-tab-manager.test.ts +756 -0
  500. package/tests/hooks/use-toast.test.ts +74 -0
  501. package/tests/hooks/use-transaction-control.test.ts +211 -0
  502. package/tests/integration/db/mongodb-provider.test.ts +698 -0
  503. package/tests/integration/db/mssql-provider.test.ts +840 -0
  504. package/tests/integration/db/mysql-provider.test.ts +872 -0
  505. package/tests/integration/db/oracle-provider.test.ts +843 -0
  506. package/tests/integration/db/postgres-provider.test.ts +1382 -0
  507. package/tests/integration/db/redis-provider.test.ts +526 -0
  508. package/tests/integration/db/sqlite-provider.test.ts +480 -0
  509. package/tests/integration/seed/seed-pipeline.test.ts +102 -0
  510. package/tests/isolated/factory-singleton.test.ts +150 -0
  511. package/tests/isolated/use-storage-sync.test.ts +389 -0
  512. package/tests/run-components.sh +196 -0
  513. package/tests/setup-dom.ts +58 -0
  514. package/tests/setup.ts +40 -0
  515. package/tests/unit/api-errors.test.ts +210 -0
  516. package/tests/unit/code-generator-functions.test.ts +271 -0
  517. package/tests/unit/components/column-list.test.tsx +190 -0
  518. package/tests/unit/components/data-import-modal.test.tsx +441 -0
  519. package/tests/unit/components/studio-mobile-header.test.tsx +327 -0
  520. package/tests/unit/data-charts-functions.test.ts +496 -0
  521. package/tests/unit/data-import-functions.test.ts +320 -0
  522. package/tests/unit/data-import-utils.test.ts +125 -0
  523. package/tests/unit/db/base-provider.test.ts +517 -0
  524. package/tests/unit/db/errors.test.ts +403 -0
  525. package/tests/unit/db/factory.test.ts +436 -0
  526. package/tests/unit/db/pool-manager.test.ts +440 -0
  527. package/tests/unit/db/query-limiter.test.ts +387 -0
  528. package/tests/unit/db/sql-base.test.ts +438 -0
  529. package/tests/unit/lib/api/error-codes.test.ts +39 -0
  530. package/tests/unit/lib/audit.test.ts +326 -0
  531. package/tests/unit/lib/auth.test.ts +146 -0
  532. package/tests/unit/lib/connection-string-parser.test.ts +424 -0
  533. package/tests/unit/lib/data-masking.test.ts +583 -0
  534. package/tests/unit/lib/db-icons.test.tsx +41 -0
  535. package/tests/unit/lib/monitoring-thresholds.test.ts +133 -0
  536. package/tests/unit/lib/oidc.test.ts +509 -0
  537. package/tests/unit/lib/query-generators.test.ts +127 -0
  538. package/tests/unit/lib/storage/factory.test.ts +71 -0
  539. package/tests/unit/lib/storage/local-storage.test.ts +114 -0
  540. package/tests/unit/lib/storage/providers/postgres.test.ts +312 -0
  541. package/tests/unit/lib/storage/providers/sqlite.test.ts +232 -0
  542. package/tests/unit/lib/storage/storage-facade-extended.test.ts +331 -0
  543. package/tests/unit/lib/storage/storage-facade.test.ts +184 -0
  544. package/tests/unit/lib/storage.test.ts +317 -0
  545. package/tests/unit/lib/time-series-buffer.test.ts +212 -0
  546. package/tests/unit/lib/utils.test.ts +24 -0
  547. package/tests/unit/llm/base-provider.test.ts +238 -0
  548. package/tests/unit/llm/config.test.ts +262 -0
  549. package/tests/unit/llm/custom-provider.test.ts +281 -0
  550. package/tests/unit/llm/gemini-provider.test.ts +248 -0
  551. package/tests/unit/llm/llm-factory.test.ts +155 -0
  552. package/tests/unit/llm/ollama-provider.test.ts +288 -0
  553. package/tests/unit/llm/openai-provider.test.ts +324 -0
  554. package/tests/unit/llm/retry.test.ts +180 -0
  555. package/tests/unit/llm/streaming.test.ts +355 -0
  556. package/tests/unit/logger.test.ts +198 -0
  557. package/tests/unit/mongodb-completions.test.ts +516 -0
  558. package/tests/unit/pivot-table-functions.test.ts +76 -0
  559. package/tests/unit/query-cancelled-error.test.ts +81 -0
  560. package/tests/unit/schema-diff/diff-engine.test.ts +367 -0
  561. package/tests/unit/schema-diff/migration-generator.test.ts +513 -0
  562. package/tests/unit/seed/config-loader.test.ts +73 -0
  563. package/tests/unit/seed/connection-filter.test.ts +91 -0
  564. package/tests/unit/seed/credential-resolver.test.ts +85 -0
  565. package/tests/unit/seed/index.test.ts +72 -0
  566. package/tests/unit/seed/resolve-connection.test.ts +74 -0
  567. package/tests/unit/seed/types.test.ts +129 -0
  568. package/tests/unit/sql/alias-extractor.test.ts +444 -0
  569. package/tests/unit/sql/statement-splitter.test.ts +348 -0
  570. package/tests/unit/sql-completions.test.ts +463 -0
  571. package/tests/unit/ssh-tunnel.test.ts +465 -0
  572. package/tsconfig.json +42 -0
@@ -0,0 +1,248 @@
1
+ import { describe, test, expect, mock, beforeEach } from 'bun:test';
2
+ import {
3
+ LLMAuthError,
4
+ LLMRateLimitError,
5
+ LLMSafetyError,
6
+ LLMStreamError,
7
+ LLMConfigError,
8
+ type LLMConfig,
9
+ type LLMStreamOptions,
10
+ } from '@/lib/llm/types';
11
+
12
+ // ============================================================================
13
+ // Mock State
14
+ // ============================================================================
15
+
16
+ let mockGenerateContentStream: (prompt: string) => Promise<unknown>;
17
+
18
+ // ============================================================================
19
+ // Module Mocks (must be before await import)
20
+ // ============================================================================
21
+
22
+ mock.module('@google/generative-ai', () => ({
23
+ GoogleGenerativeAI: function () {
24
+ return {
25
+ getGenerativeModel: function () {
26
+ return {
27
+ generateContentStream: async (prompt: string) =>
28
+ mockGenerateContentStream(prompt),
29
+ };
30
+ },
31
+ };
32
+ },
33
+ }));
34
+
35
+ // ============================================================================
36
+ // Import module under test (after mocks)
37
+ // ============================================================================
38
+
39
+ const { GeminiProvider } = await import('@/lib/llm/providers/gemini');
40
+
41
+ // ============================================================================
42
+ // Helpers
43
+ // ============================================================================
44
+
45
+ async function* mockStreamChunks(texts: string[]) {
46
+ for (const t of texts) {
47
+ yield { text: () => t };
48
+ }
49
+ }
50
+
51
+ async function readStream(stream: ReadableStream<Uint8Array>): Promise<string> {
52
+ const reader = stream.getReader();
53
+ const decoder = new TextDecoder();
54
+ let result = '';
55
+ while (true) {
56
+ const { done, value } = await reader.read();
57
+ if (done) break;
58
+ result += decoder.decode(value);
59
+ }
60
+ return result;
61
+ }
62
+
63
+ function makeConfig(overrides?: Partial<LLMConfig>): LLMConfig {
64
+ return {
65
+ provider: 'gemini',
66
+ apiKey: 'test-gemini-api-key',
67
+ model: 'gemini-2.0-flash',
68
+ ...overrides,
69
+ };
70
+ }
71
+
72
+ function makeStreamOptions(overrides?: Partial<LLMStreamOptions>): LLMStreamOptions {
73
+ return {
74
+ messages: [{ role: 'user', content: 'Hello' }],
75
+ ...overrides,
76
+ };
77
+ }
78
+
79
+ // ============================================================================
80
+ // Tests
81
+ // ============================================================================
82
+
83
+ describe('GeminiProvider', () => {
84
+ beforeEach(() => {
85
+ mockGenerateContentStream = async () => ({
86
+ stream: mockStreamChunks(['Hello', ' World']),
87
+ });
88
+ });
89
+
90
+ // --------------------------------------------------------------------------
91
+ // constructor
92
+ // --------------------------------------------------------------------------
93
+
94
+ describe('constructor', () => {
95
+ test('creates instance with valid config', () => {
96
+ const provider = new GeminiProvider(makeConfig());
97
+ expect(provider.name).toBe('gemini');
98
+ expect(provider.config.model).toBe('gemini-2.0-flash');
99
+ });
100
+
101
+ test('throws LLMConfigError without apiKey', () => {
102
+ expect(() => new GeminiProvider(makeConfig({ apiKey: undefined }))).toThrow(
103
+ LLMConfigError
104
+ );
105
+ });
106
+ });
107
+
108
+ // --------------------------------------------------------------------------
109
+ // stream()
110
+ // --------------------------------------------------------------------------
111
+
112
+ describe('stream()', () => {
113
+ test('returns ReadableStream on success', async () => {
114
+ const provider = new GeminiProvider(makeConfig());
115
+ const stream = await provider.stream(makeStreamOptions());
116
+
117
+ expect(stream).toBeInstanceOf(ReadableStream);
118
+ const text = await readStream(stream);
119
+ expect(text).toBe('Hello World');
120
+ });
121
+
122
+ test('passes model from options', async () => {
123
+ const provider = new GeminiProvider(makeConfig());
124
+ const stream = await provider.stream(
125
+ makeStreamOptions({ model: 'gemini-pro' })
126
+ );
127
+ expect(stream).toBeInstanceOf(ReadableStream);
128
+ });
129
+
130
+ test('passes system instruction', async () => {
131
+ const provider = new GeminiProvider(makeConfig());
132
+ const stream = await provider.stream(
133
+ makeStreamOptions({
134
+ messages: [
135
+ { role: 'system', content: 'You are helpful.' },
136
+ { role: 'user', content: 'Hello' },
137
+ ],
138
+ })
139
+ );
140
+ expect(stream).toBeInstanceOf(ReadableStream);
141
+ });
142
+
143
+ test('handles empty stream', async () => {
144
+ mockGenerateContentStream = async () => ({
145
+ stream: mockStreamChunks([]),
146
+ });
147
+
148
+ const provider = new GeminiProvider(makeConfig());
149
+ const stream = await provider.stream(makeStreamOptions());
150
+ const text = await readStream(stream);
151
+ expect(text).toBe('');
152
+ });
153
+ });
154
+
155
+ // --------------------------------------------------------------------------
156
+ // error mapping
157
+ // --------------------------------------------------------------------------
158
+
159
+ describe('error mapping', () => {
160
+ test('api key error maps to LLMAuthError', async () => {
161
+ mockGenerateContentStream = async () => {
162
+ throw new Error('Invalid API key provided');
163
+ };
164
+
165
+ const provider = new GeminiProvider(makeConfig());
166
+ await expect(provider.stream(makeStreamOptions())).rejects.toBeInstanceOf(
167
+ LLMAuthError
168
+ );
169
+ });
170
+
171
+ test('unauthorized error maps to LLMAuthError', async () => {
172
+ mockGenerateContentStream = async () => {
173
+ throw new Error('Unauthorized access');
174
+ };
175
+
176
+ const provider = new GeminiProvider(makeConfig());
177
+ await expect(provider.stream(makeStreamOptions())).rejects.toBeInstanceOf(
178
+ LLMAuthError
179
+ );
180
+ });
181
+
182
+ test('quota error maps to LLMRateLimitError', async () => {
183
+ mockGenerateContentStream = async () => {
184
+ throw new Error('Quota exceeded for this project');
185
+ };
186
+
187
+ const provider = new GeminiProvider(makeConfig());
188
+ await expect(provider.stream(makeStreamOptions())).rejects.toBeInstanceOf(
189
+ LLMRateLimitError
190
+ );
191
+ });
192
+
193
+ test('rate limit error maps to LLMRateLimitError', async () => {
194
+ mockGenerateContentStream = async () => {
195
+ throw new Error('Rate limit reached');
196
+ };
197
+
198
+ const provider = new GeminiProvider(makeConfig());
199
+ await expect(provider.stream(makeStreamOptions())).rejects.toBeInstanceOf(
200
+ LLMRateLimitError
201
+ );
202
+ });
203
+
204
+ test('safety error maps to LLMSafetyError', async () => {
205
+ mockGenerateContentStream = async () => {
206
+ throw new Error('Content blocked by safety filters');
207
+ };
208
+
209
+ const provider = new GeminiProvider(makeConfig());
210
+ await expect(provider.stream(makeStreamOptions())).rejects.toBeInstanceOf(
211
+ LLMSafetyError
212
+ );
213
+ });
214
+
215
+ test('blocked error maps to LLMSafetyError', async () => {
216
+ mockGenerateContentStream = async () => {
217
+ throw new Error('Response was blocked');
218
+ };
219
+
220
+ const provider = new GeminiProvider(makeConfig());
221
+ await expect(provider.stream(makeStreamOptions())).rejects.toBeInstanceOf(
222
+ LLMSafetyError
223
+ );
224
+ });
225
+
226
+ test('generic error maps to LLMStreamError', async () => {
227
+ mockGenerateContentStream = async () => {
228
+ throw new Error('Something unexpected happened');
229
+ };
230
+
231
+ const provider = new GeminiProvider(makeConfig());
232
+ await expect(provider.stream(makeStreamOptions())).rejects.toBeInstanceOf(
233
+ LLMStreamError
234
+ );
235
+ });
236
+
237
+ test('non-Error value maps to LLMStreamError', async () => {
238
+ mockGenerateContentStream = async () => {
239
+ throw 'string error value';
240
+ };
241
+
242
+ const provider = new GeminiProvider(makeConfig());
243
+ await expect(provider.stream(makeStreamOptions())).rejects.toBeInstanceOf(
244
+ LLMStreamError
245
+ );
246
+ });
247
+ });
248
+ });
@@ -0,0 +1,155 @@
1
+ import { describe, test, expect, mock, beforeEach, afterEach } from 'bun:test';
2
+ import { LLMConfigError } from '@/lib/llm/types';
3
+
4
+ // ============================================================================
5
+ // Environment Variable Management
6
+ // ============================================================================
7
+
8
+ let savedEnv: Record<string, string | undefined>;
9
+
10
+ const LLM_ENV_KEYS = ['LLM_PROVIDER', 'LLM_API_KEY', 'LLM_MODEL', 'LLM_API_URL'];
11
+
12
+ // ============================================================================
13
+ // Module Mocks (must be before await import)
14
+ // Mocking provider constructors to avoid real SDK initialization
15
+ // ============================================================================
16
+
17
+ mock.module('@google/generative-ai', () => ({
18
+ GoogleGenerativeAI: function () {
19
+ return {
20
+ getGenerativeModel: () => ({
21
+ generateContentStream: async () => ({ stream: (async function* () {})() }),
22
+ }),
23
+ };
24
+ },
25
+ }));
26
+
27
+ // ============================================================================
28
+ // Import module under test (after mocks)
29
+ // ============================================================================
30
+
31
+ const { createLLMProvider, getDefaultProvider, resetDefaultProvider } = await import(
32
+ '@/lib/llm/factory'
33
+ );
34
+
35
+ // ============================================================================
36
+ // Tests
37
+ // ============================================================================
38
+
39
+ describe('LLM Factory', () => {
40
+ beforeEach(() => {
41
+ savedEnv = {};
42
+ for (const key of LLM_ENV_KEYS) {
43
+ savedEnv[key] = process.env[key];
44
+ delete process.env[key];
45
+ }
46
+ resetDefaultProvider();
47
+ });
48
+
49
+ afterEach(() => {
50
+ for (const key of LLM_ENV_KEYS) {
51
+ if (savedEnv[key] !== undefined) {
52
+ process.env[key] = savedEnv[key];
53
+ } else {
54
+ delete process.env[key];
55
+ }
56
+ }
57
+ resetDefaultProvider();
58
+ });
59
+
60
+ // --------------------------------------------------------------------------
61
+ // createLLMProvider()
62
+ // --------------------------------------------------------------------------
63
+
64
+ describe('createLLMProvider()', () => {
65
+ test('creates GeminiProvider for gemini type', async () => {
66
+ const provider = await createLLMProvider({
67
+ provider: 'gemini',
68
+ apiKey: 'test-gemini-key',
69
+ model: 'gemini-2.0-flash',
70
+ });
71
+ expect(provider.name).toBe('gemini');
72
+ });
73
+
74
+ test('creates OpenAIProvider for openai type', async () => {
75
+ const provider = await createLLMProvider({
76
+ provider: 'openai',
77
+ apiKey: 'sk-test-key',
78
+ model: 'gpt-4o',
79
+ });
80
+ expect(provider.name).toBe('openai');
81
+ });
82
+
83
+ test('creates OllamaProvider for ollama type', async () => {
84
+ const provider = await createLLMProvider({
85
+ provider: 'ollama',
86
+ model: 'llama3.2',
87
+ });
88
+ expect(provider.name).toBe('ollama');
89
+ });
90
+
91
+ test('creates CustomProvider for custom type', async () => {
92
+ const provider = await createLLMProvider({
93
+ provider: 'custom',
94
+ apiUrl: 'https://my-endpoint.com/v1',
95
+ model: 'custom-model',
96
+ });
97
+ expect(provider.name).toBe('custom');
98
+ });
99
+
100
+ test('throws LLMConfigError for unknown provider', async () => {
101
+ await expect(
102
+ createLLMProvider({
103
+ provider: 'nonexistent' as 'gemini',
104
+ model: 'some-model',
105
+ })
106
+ ).rejects.toThrow(LLMConfigError);
107
+ });
108
+ });
109
+
110
+ // --------------------------------------------------------------------------
111
+ // getDefaultProvider()
112
+ // --------------------------------------------------------------------------
113
+
114
+ describe('getDefaultProvider()', () => {
115
+ test('returns singleton instance', async () => {
116
+ process.env.LLM_PROVIDER = 'gemini';
117
+ process.env.LLM_API_KEY = 'test-key-for-singleton';
118
+ process.env.LLM_MODEL = 'gemini-2.0-flash';
119
+
120
+ const provider = await getDefaultProvider();
121
+ expect(provider).toBeDefined();
122
+ expect(provider.name).toBe('gemini');
123
+ });
124
+
125
+ test('returns same instance on second call', async () => {
126
+ process.env.LLM_PROVIDER = 'gemini';
127
+ process.env.LLM_API_KEY = 'test-key-for-singleton';
128
+ process.env.LLM_MODEL = 'gemini-2.0-flash';
129
+
130
+ const first = await getDefaultProvider();
131
+ const second = await getDefaultProvider();
132
+ expect(first).toBe(second);
133
+ });
134
+ });
135
+
136
+ // --------------------------------------------------------------------------
137
+ // resetDefaultProvider()
138
+ // --------------------------------------------------------------------------
139
+
140
+ describe('resetDefaultProvider()', () => {
141
+ test('clears singleton so next call creates new', async () => {
142
+ process.env.LLM_PROVIDER = 'gemini';
143
+ process.env.LLM_API_KEY = 'test-key-for-singleton';
144
+ process.env.LLM_MODEL = 'gemini-2.0-flash';
145
+
146
+ const first = await getDefaultProvider();
147
+
148
+ resetDefaultProvider();
149
+
150
+ const second = await getDefaultProvider();
151
+ // They should be different object references
152
+ expect(first).not.toBe(second);
153
+ });
154
+ });
155
+ });
@@ -0,0 +1,288 @@
1
+ import { describe, test, expect, beforeEach, afterEach, spyOn } from 'bun:test';
2
+ import {
3
+ LLMStreamError,
4
+ LLMConfigError,
5
+ type LLMConfig,
6
+ type LLMStreamOptions,
7
+ } from '@/lib/llm/types';
8
+
9
+ // ============================================================================
10
+ // Import module under test (no mock.module needed — Ollama uses globalThis.fetch)
11
+ // ============================================================================
12
+
13
+ const { OllamaProvider } = await import('@/lib/llm/providers/ollama');
14
+
15
+ // ============================================================================
16
+ // Helpers
17
+ // ============================================================================
18
+
19
+ function makeSSEChunk(content: string): string {
20
+ return `data: ${JSON.stringify({ choices: [{ delta: { content } }] })}\n\n`;
21
+ }
22
+
23
+ function createSSEResponse(chunks: string[], status = 200): Response {
24
+ const body = new ReadableStream<Uint8Array>({
25
+ start(controller) {
26
+ for (const chunk of chunks) {
27
+ controller.enqueue(new TextEncoder().encode(chunk));
28
+ }
29
+ controller.close();
30
+ },
31
+ });
32
+ return new Response(body, {
33
+ status,
34
+ headers: { 'Content-Type': 'text/event-stream' },
35
+ });
36
+ }
37
+
38
+ function createJsonResponse(body: unknown, status: number): Response {
39
+ return new Response(JSON.stringify(body), {
40
+ status,
41
+ headers: { 'Content-Type': 'application/json' },
42
+ });
43
+ }
44
+
45
+ async function readStream(stream: ReadableStream<Uint8Array>): Promise<string> {
46
+ const reader = stream.getReader();
47
+ const decoder = new TextDecoder();
48
+ let result = '';
49
+ while (true) {
50
+ const { done, value } = await reader.read();
51
+ if (done) break;
52
+ result += decoder.decode(value);
53
+ }
54
+ return result;
55
+ }
56
+
57
+ function makeConfig(overrides?: Partial<LLMConfig>): LLMConfig {
58
+ return {
59
+ provider: 'ollama',
60
+ model: 'llama3.2',
61
+ ...overrides,
62
+ };
63
+ }
64
+
65
+ function makeStreamOptions(overrides?: Partial<LLMStreamOptions>): LLMStreamOptions {
66
+ return {
67
+ messages: [{ role: 'user', content: 'Hello' }],
68
+ ...overrides,
69
+ };
70
+ }
71
+
72
+ // ============================================================================
73
+ // Tests
74
+ // ============================================================================
75
+
76
+ describe('OllamaProvider', () => {
77
+ let fetchSpy: ReturnType<typeof spyOn>;
78
+
79
+ beforeEach(() => {
80
+ fetchSpy = spyOn(globalThis, 'fetch');
81
+ });
82
+
83
+ afterEach(() => {
84
+ fetchSpy.mockRestore();
85
+ });
86
+
87
+ // --------------------------------------------------------------------------
88
+ // constructor
89
+ // --------------------------------------------------------------------------
90
+
91
+ describe('constructor', () => {
92
+ test('creates without apiKey (not required for Ollama)', () => {
93
+ const provider = new OllamaProvider(makeConfig());
94
+ expect(provider.name).toBe('ollama');
95
+ expect(provider.config.apiKey).toBeUndefined();
96
+ });
97
+
98
+ test('uses default Ollama URL (localhost:11434)', () => {
99
+ const provider = new OllamaProvider(makeConfig());
100
+ // The default URL is set internally; config.apiUrl remains undefined
101
+ expect(provider.config.apiUrl).toBeUndefined();
102
+ });
103
+
104
+ test('uses custom apiUrl', () => {
105
+ const provider = new OllamaProvider(
106
+ makeConfig({ apiUrl: 'http://remote-ollama:11434/v1' })
107
+ );
108
+ expect(provider.config.apiUrl).toBe('http://remote-ollama:11434/v1');
109
+ });
110
+ });
111
+
112
+ // --------------------------------------------------------------------------
113
+ // validate()
114
+ // --------------------------------------------------------------------------
115
+
116
+ describe('validate()', () => {
117
+ test('throws LLMConfigError without model', () => {
118
+ // Ollama constructor does NOT call validate() automatically,
119
+ // so we call it explicitly
120
+ const provider = new OllamaProvider(
121
+ makeConfig({ model: undefined as unknown as string })
122
+ );
123
+ expect(() => provider.validate()).toThrow(LLMConfigError);
124
+ });
125
+
126
+ test('throws LLMConfigError with empty model', () => {
127
+ const provider = new OllamaProvider(makeConfig({ model: '' }));
128
+ expect(() => provider.validate()).toThrow(LLMConfigError);
129
+ });
130
+ });
131
+
132
+ // --------------------------------------------------------------------------
133
+ // stream()
134
+ // --------------------------------------------------------------------------
135
+
136
+ describe('stream()', () => {
137
+ test('sends Bearer ollama auth header', async () => {
138
+ const sseData = [makeSSEChunk('Hi'), 'data: [DONE]\n\n'];
139
+ fetchSpy.mockResolvedValueOnce(createSSEResponse(sseData));
140
+
141
+ const provider = new OllamaProvider(makeConfig());
142
+ await provider.stream(makeStreamOptions());
143
+
144
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
145
+ const [, opts] = fetchSpy.mock.calls[0] as [string, RequestInit];
146
+ const headers = opts.headers as Record<string, string>;
147
+ expect(headers.Authorization).toBe('Bearer ollama');
148
+ });
149
+
150
+ test('returns parsed stream content', async () => {
151
+ const sseData = [
152
+ makeSSEChunk('Hello'),
153
+ makeSSEChunk(' from'),
154
+ makeSSEChunk(' Ollama'),
155
+ 'data: [DONE]\n\n',
156
+ ];
157
+ fetchSpy.mockResolvedValueOnce(createSSEResponse(sseData));
158
+
159
+ const provider = new OllamaProvider(makeConfig());
160
+ const stream = await provider.stream(makeStreamOptions());
161
+
162
+ expect(stream).toBeInstanceOf(ReadableStream);
163
+ const text = await readStream(stream);
164
+ expect(text).toBe('Hello from Ollama');
165
+ });
166
+
167
+ test('passes model and messages', async () => {
168
+ const sseData = [makeSSEChunk('ok'), 'data: [DONE]\n\n'];
169
+ fetchSpy.mockResolvedValueOnce(createSSEResponse(sseData));
170
+
171
+ const provider = new OllamaProvider(makeConfig());
172
+ await provider.stream(
173
+ makeStreamOptions({
174
+ messages: [
175
+ { role: 'system', content: 'Be concise' },
176
+ { role: 'user', content: 'Hi' },
177
+ ],
178
+ })
179
+ );
180
+
181
+ const body = JSON.parse(fetchSpy.mock.calls[0][1]!.body as string);
182
+ expect(body.model).toBe('llama3.2');
183
+ expect(body.messages).toHaveLength(2);
184
+ expect(body.stream).toBe(true);
185
+ });
186
+ });
187
+
188
+ // --------------------------------------------------------------------------
189
+ // validateResponse()
190
+ // --------------------------------------------------------------------------
191
+
192
+ describe('validateResponse()', () => {
193
+ test('404 throws LLMConfigError with model pull message', async () => {
194
+ fetchSpy.mockImplementation(async () =>
195
+ createJsonResponse({ error: { message: 'model not found' } }, 404)
196
+ );
197
+
198
+ const provider = new OllamaProvider(makeConfig());
199
+ try {
200
+ await provider.stream(makeStreamOptions());
201
+ expect(true).toBe(false); // should not reach here
202
+ } catch (err) {
203
+ expect(err).toBeInstanceOf(LLMConfigError);
204
+ expect((err as Error).message).toContain('pulled');
205
+ }
206
+ });
207
+
208
+ test('500 throws LLMStreamError', async () => {
209
+ fetchSpy.mockImplementation(async () =>
210
+ createJsonResponse({ error: { message: 'Internal error' } }, 500)
211
+ );
212
+
213
+ const provider = new OllamaProvider(makeConfig());
214
+ await expect(provider.stream(makeStreamOptions())).rejects.toBeInstanceOf(
215
+ LLMStreamError
216
+ );
217
+ });
218
+ });
219
+
220
+ // --------------------------------------------------------------------------
221
+ // error mapping
222
+ // --------------------------------------------------------------------------
223
+
224
+ describe('error mapping', () => {
225
+ test('ECONNREFUSED maps to connection error', async () => {
226
+ fetchSpy.mockImplementation(async () => {
227
+ throw new Error('connect ECONNREFUSED 127.0.0.1:11434');
228
+ });
229
+
230
+ const provider = new OllamaProvider(makeConfig());
231
+ try {
232
+ await provider.stream(makeStreamOptions());
233
+ expect(true).toBe(false);
234
+ } catch (err) {
235
+ expect(err).toBeInstanceOf(LLMStreamError);
236
+ expect((err as Error).message).toContain('Cannot connect to Ollama');
237
+ }
238
+ });
239
+
240
+ test('fetch failed maps to connection error', async () => {
241
+ fetchSpy.mockImplementation(async () => {
242
+ throw new TypeError('fetch failed');
243
+ });
244
+
245
+ const provider = new OllamaProvider(makeConfig());
246
+ try {
247
+ await provider.stream(makeStreamOptions());
248
+ expect(true).toBe(false);
249
+ } catch (err) {
250
+ expect(err).toBeInstanceOf(LLMStreamError);
251
+ expect((err as Error).message).toContain('Cannot connect to Ollama');
252
+ }
253
+ });
254
+
255
+ test('LLMConfigError passes through', async () => {
256
+ fetchSpy.mockImplementation(async () =>
257
+ createJsonResponse({ error: { message: 'not found' } }, 404)
258
+ );
259
+
260
+ const provider = new OllamaProvider(makeConfig());
261
+ await expect(provider.stream(makeStreamOptions())).rejects.toBeInstanceOf(
262
+ LLMConfigError
263
+ );
264
+ });
265
+
266
+ test('generic error maps to LLMStreamError', async () => {
267
+ fetchSpy.mockImplementation(async () => {
268
+ throw new Error('Unexpected error');
269
+ });
270
+
271
+ const provider = new OllamaProvider(makeConfig());
272
+ await expect(provider.stream(makeStreamOptions())).rejects.toBeInstanceOf(
273
+ LLMStreamError
274
+ );
275
+ });
276
+
277
+ test('non-Error maps to LLMStreamError', async () => {
278
+ fetchSpy.mockImplementation(async () => {
279
+ throw 'string error value';
280
+ });
281
+
282
+ const provider = new OllamaProvider(makeConfig());
283
+ await expect(provider.stream(makeStreamOptions())).rejects.toBeInstanceOf(
284
+ LLMStreamError
285
+ );
286
+ });
287
+ });
288
+ });