@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,756 @@
1
+ import '../setup-dom';
2
+ import '../helpers/mock-sonner';
3
+ import '../helpers/mock-navigation';
4
+
5
+ import React from 'react';
6
+ import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
7
+ import { cleanup, render, fireEvent, waitFor, act } from '@testing-library/react';
8
+ import { AIAutopilotPanel } from '@/components/AIAutopilotPanel';
9
+ import { mockPostgresConnection } from '../fixtures/connections';
10
+
11
+ // ── Helpers ─────────────────────────────────────────────────────────────────
12
+
13
+ function createStreamResponse(text: string, status = 200) {
14
+ const encoder = new TextEncoder();
15
+ const stream = new ReadableStream({
16
+ start(controller) {
17
+ controller.enqueue(encoder.encode(text));
18
+ controller.close();
19
+ },
20
+ });
21
+ return new Response(stream, { status, headers: { 'Content-Type': 'text/plain' } });
22
+ }
23
+
24
+ function createJsonResponse(data: unknown, status = 200) {
25
+ return new Response(JSON.stringify(data), {
26
+ status,
27
+ headers: { 'Content-Type': 'application/json' },
28
+ });
29
+ }
30
+
31
+ const validSchemaContext = JSON.stringify([
32
+ { name: 'users', rowCount: 100, columns: [{ name: 'id', type: 'integer' }, { name: 'email', type: 'varchar' }] },
33
+ { name: 'orders', rowCount: 500, columns: [{ name: 'id', type: 'integer' }, { name: 'user_id', type: 'integer' }] },
34
+ ]);
35
+
36
+ const defaultProps = {
37
+ connection: mockPostgresConnection,
38
+ schemaContext: validSchemaContext,
39
+ };
40
+
41
+ // ── Tests ───────────────────────────────────────────────────────────────────
42
+
43
+ describe('AIAutopilotPanel', () => {
44
+ let originalFetch: typeof globalThis.fetch;
45
+ let mockWriteText: ReturnType<typeof mock>;
46
+
47
+ beforeEach(() => {
48
+ originalFetch = globalThis.fetch;
49
+ mockWriteText = mock(async () => {});
50
+ Object.defineProperty(globalThis.navigator, 'clipboard', {
51
+ value: { writeText: mockWriteText },
52
+ configurable: true,
53
+ });
54
+ });
55
+
56
+ afterEach(() => {
57
+ cleanup();
58
+ globalThis.fetch = originalFetch;
59
+ });
60
+
61
+ // ── Idle State ──────────────────────────────────────────────────────────
62
+
63
+ test('renders idle state with header and run button', () => {
64
+ globalThis.fetch = mock(() => Promise.resolve(new Response(''))) as never;
65
+ const { queryAllByText, queryByText } = render(
66
+ <AIAutopilotPanel {...defaultProps} />
67
+ );
68
+ expect(queryAllByText('AI Performance Autopilot').length).toBeGreaterThan(0);
69
+ expect(queryByText('Run Analysis')).not.toBeNull();
70
+ });
71
+
72
+ test('shows idle placeholder text when no report', () => {
73
+ globalThis.fetch = mock(() => Promise.resolve(new Response(''))) as never;
74
+ const { container } = render(
75
+ <AIAutopilotPanel {...defaultProps} />
76
+ );
77
+ expect(container.textContent).toContain('AI-powered optimization recommendations');
78
+ });
79
+
80
+ test('shows idle placeholder when connection is null', () => {
81
+ globalThis.fetch = mock(() => Promise.resolve(new Response(''))) as never;
82
+ const { container } = render(
83
+ <AIAutopilotPanel connection={null} schemaContext="" />
84
+ );
85
+ expect(container.textContent).toContain('AI-powered optimization recommendations');
86
+ });
87
+
88
+ // ── Button states ───────────────────────────────────────────────────────
89
+
90
+ test('run button is disabled when connection is null', () => {
91
+ globalThis.fetch = mock(() => Promise.resolve(new Response(''))) as never;
92
+ const { queryByText } = render(
93
+ <AIAutopilotPanel connection={null} schemaContext="" />
94
+ );
95
+ const button = queryByText('Run Analysis')!.closest('button');
96
+ expect(button).not.toBeNull();
97
+ expect(button!.disabled).toBe(true);
98
+ });
99
+
100
+ test('run button is enabled when connection is provided', () => {
101
+ globalThis.fetch = mock(() => Promise.resolve(new Response(''))) as never;
102
+ const { queryByText } = render(
103
+ <AIAutopilotPanel {...defaultProps} />
104
+ );
105
+ const button = queryByText('Run Analysis')!.closest('button');
106
+ expect(button!.disabled).toBe(false);
107
+ });
108
+
109
+ // ── runAutopilot: success path ──────────────────────────────────────────
110
+
111
+ test('shows loading state while analyzing', async () => {
112
+ let resolveAutopilot: (value: Response) => void;
113
+ const autopilotPromise = new Promise<Response>((r) => { resolveAutopilot = r; });
114
+
115
+ globalThis.fetch = mock((url: string) => {
116
+ if (url === '/api/db/monitoring') {
117
+ return Promise.resolve(createJsonResponse({ tables: [], indexes: [], slowQueries: [] }));
118
+ }
119
+ return autopilotPromise;
120
+ }) as never;
121
+
122
+ const { queryByText } = render(<AIAutopilotPanel {...defaultProps} />);
123
+
124
+ await act(async () => {
125
+ fireEvent.click(queryByText('Run Analysis')!.closest('button')!);
126
+ });
127
+
128
+ // Should show Analyzing... while waiting
129
+ expect(queryByText('Analyzing...')).not.toBeNull();
130
+
131
+ // Resolve to finish
132
+ await act(async () => {
133
+ resolveAutopilot!(createStreamResponse('done'));
134
+ });
135
+ });
136
+
137
+ test('streams report content from autopilot endpoint', async () => {
138
+ const reportText = '## Performance Report\n\nYour database is healthy.';
139
+
140
+ globalThis.fetch = mock((url: string) => {
141
+ if (url === '/api/db/monitoring') {
142
+ return Promise.resolve(createJsonResponse({
143
+ tables: [{ name: 'users', rows: 100 }],
144
+ indexes: [],
145
+ slowQueries: [],
146
+ performance: {},
147
+ overview: {},
148
+ }));
149
+ }
150
+ return Promise.resolve(createStreamResponse(reportText));
151
+ }) as never;
152
+
153
+ const { queryByText, container } = render(<AIAutopilotPanel {...defaultProps} />);
154
+
155
+ await act(async () => {
156
+ fireEvent.click(queryByText('Run Analysis')!.closest('button')!);
157
+ });
158
+
159
+ await waitFor(() => {
160
+ expect(container.textContent).toContain('Performance Report');
161
+ expect(container.textContent).toContain('Your database is healthy.');
162
+ });
163
+ });
164
+
165
+ test('shows Re-analyze after report is loaded', async () => {
166
+ globalThis.fetch = mock((url: string) => {
167
+ if (url === '/api/db/monitoring') {
168
+ return Promise.resolve(createJsonResponse({}));
169
+ }
170
+ return Promise.resolve(createStreamResponse('Report content'));
171
+ }) as never;
172
+
173
+ const { queryByText } = render(<AIAutopilotPanel {...defaultProps} />);
174
+
175
+ await act(async () => {
176
+ fireEvent.click(queryByText('Run Analysis')!.closest('button')!);
177
+ });
178
+
179
+ await waitFor(() => {
180
+ expect(queryByText('Re-analyze')).not.toBeNull();
181
+ });
182
+ });
183
+
184
+ // ── runAutopilot: does nothing without connection ───────────────────────
185
+
186
+ test('runAutopilot does nothing when connection is null', async () => {
187
+ const fetchMock = mock(() => Promise.resolve(new Response('')));
188
+ globalThis.fetch = fetchMock as never;
189
+
190
+ const { queryByText } = render(
191
+ <AIAutopilotPanel connection={null} schemaContext="" />
192
+ );
193
+
194
+ // Button is disabled, but try clicking anyway
195
+ await act(async () => {
196
+ fireEvent.click(queryByText('Run Analysis')!.closest('button')!);
197
+ });
198
+
199
+ // fetch should not have been called
200
+ expect(fetchMock).not.toHaveBeenCalled();
201
+ });
202
+
203
+ // ── runAutopilot: monitoring fetch fails gracefully ─────────────────────
204
+
205
+ test('continues when monitoring endpoint returns non-ok response', async () => {
206
+ globalThis.fetch = mock((url: string) => {
207
+ if (url === '/api/db/monitoring') {
208
+ return Promise.resolve(new Response('', { status: 500 }));
209
+ }
210
+ return Promise.resolve(createStreamResponse('## Recommendations\n\nUse indexes.'));
211
+ }) as never;
212
+
213
+ const { queryByText, container } = render(<AIAutopilotPanel {...defaultProps} />);
214
+
215
+ await act(async () => {
216
+ fireEvent.click(queryByText('Run Analysis')!.closest('button')!);
217
+ });
218
+
219
+ await waitFor(() => {
220
+ expect(container.textContent).toContain('Recommendations');
221
+ });
222
+ });
223
+
224
+ // ── runAutopilot: autopilot endpoint error ──────────────────────────────
225
+
226
+ test('shows error when autopilot endpoint returns non-ok', async () => {
227
+ globalThis.fetch = mock((url: string) => {
228
+ if (url === '/api/db/monitoring') {
229
+ return Promise.resolve(createJsonResponse({}));
230
+ }
231
+ return Promise.resolve(createJsonResponse({ error: 'AI service unavailable' }, 503));
232
+ }) as never;
233
+
234
+ const { queryByText, container } = render(<AIAutopilotPanel {...defaultProps} />);
235
+
236
+ await act(async () => {
237
+ fireEvent.click(queryByText('Run Analysis')!.closest('button')!);
238
+ });
239
+
240
+ await waitFor(() => {
241
+ expect(container.textContent).toContain('AI service unavailable');
242
+ });
243
+ });
244
+
245
+ test('shows default error message when autopilot error has no message', async () => {
246
+ globalThis.fetch = mock((url: string) => {
247
+ if (url === '/api/db/monitoring') {
248
+ return Promise.resolve(createJsonResponse({}));
249
+ }
250
+ return Promise.resolve(createJsonResponse({}, 500));
251
+ }) as never;
252
+
253
+ const { queryByText, container } = render(<AIAutopilotPanel {...defaultProps} />);
254
+
255
+ await act(async () => {
256
+ fireEvent.click(queryByText('Run Analysis')!.closest('button')!);
257
+ });
258
+
259
+ await waitFor(() => {
260
+ expect(container.textContent).toContain('Autopilot analysis failed');
261
+ });
262
+ });
263
+
264
+ // ── runAutopilot: no reader ─────────────────────────────────────────────
265
+
266
+ test('shows error when response body has no reader', async () => {
267
+ globalThis.fetch = mock((url: string) => {
268
+ if (url === '/api/db/monitoring') {
269
+ return Promise.resolve(createJsonResponse({}));
270
+ }
271
+ return Promise.resolve(new Response(null, { status: 200 }));
272
+ }) as never;
273
+
274
+ const { queryByText, container } = render(<AIAutopilotPanel {...defaultProps} />);
275
+
276
+ await act(async () => {
277
+ fireEvent.click(queryByText('Run Analysis')!.closest('button')!);
278
+ });
279
+
280
+ await waitFor(() => {
281
+ expect(container.textContent).toContain('No reader');
282
+ });
283
+ });
284
+
285
+ // ── runAutopilot: fetch throws ──────────────────────────────────────────
286
+
287
+ test('shows error when fetch throws an exception', async () => {
288
+ globalThis.fetch = mock(() => Promise.reject(new Error('Network failure'))) as never;
289
+
290
+ const { queryByText, container } = render(<AIAutopilotPanel {...defaultProps} />);
291
+
292
+ await act(async () => {
293
+ fireEvent.click(queryByText('Run Analysis')!.closest('button')!);
294
+ });
295
+
296
+ await waitFor(() => {
297
+ expect(container.textContent).toContain('Network failure');
298
+ });
299
+ });
300
+
301
+ test('shows Unknown error for non-Error throws', async () => {
302
+ globalThis.fetch = mock(() => Promise.reject('string error')) as never;
303
+
304
+ const { queryByText, container } = render(<AIAutopilotPanel {...defaultProps} />);
305
+
306
+ await act(async () => {
307
+ fireEvent.click(queryByText('Run Analysis')!.closest('button')!);
308
+ });
309
+
310
+ await waitFor(() => {
311
+ expect(container.textContent).toContain('Unknown error');
312
+ });
313
+ });
314
+
315
+ // ── schemaContext parsing ───────────────────────────────────────────────
316
+
317
+ test('handles invalid JSON schemaContext gracefully (falls back to substring)', async () => {
318
+ globalThis.fetch = mock((url: string) => {
319
+ if (url === '/api/db/monitoring') {
320
+ return Promise.resolve(createJsonResponse({}));
321
+ }
322
+ return Promise.resolve(createStreamResponse('Analysis done'));
323
+ }) as never;
324
+
325
+ const { queryByText, container } = render(
326
+ <AIAutopilotPanel connection={mockPostgresConnection} schemaContext="not valid json {{{" />
327
+ );
328
+
329
+ await act(async () => {
330
+ fireEvent.click(queryByText('Run Analysis')!.closest('button')!);
331
+ });
332
+
333
+ await waitFor(() => {
334
+ expect(container.textContent).toContain('Analysis done');
335
+ });
336
+ });
337
+
338
+ test('handles empty schemaContext', async () => {
339
+ globalThis.fetch = mock((url: string) => {
340
+ if (url === '/api/db/monitoring') {
341
+ return Promise.resolve(createJsonResponse({}));
342
+ }
343
+ return Promise.resolve(createStreamResponse('Report'));
344
+ }) as never;
345
+
346
+ const { queryByText, container } = render(
347
+ <AIAutopilotPanel connection={mockPostgresConnection} schemaContext="" />
348
+ );
349
+
350
+ await act(async () => {
351
+ fireEvent.click(queryByText('Run Analysis')!.closest('button')!);
352
+ });
353
+
354
+ await waitFor(() => {
355
+ expect(container.textContent).toContain('Report');
356
+ });
357
+ });
358
+
359
+ // ── Markdown rendering ──────────────────────────────────────────────────
360
+
361
+ test('renders ## headers', async () => {
362
+ globalThis.fetch = mock((url: string) => {
363
+ if (url === '/api/db/monitoring') return Promise.resolve(createJsonResponse({}));
364
+ return Promise.resolve(createStreamResponse('## My Header'));
365
+ }) as never;
366
+
367
+ const { queryByText, container } = render(<AIAutopilotPanel {...defaultProps} />);
368
+ await act(async () => { fireEvent.click(queryByText('Run Analysis')!.closest('button')!); });
369
+
370
+ await waitFor(() => {
371
+ const h2 = container.querySelector('h2');
372
+ expect(h2).not.toBeNull();
373
+ expect(h2!.textContent).toBe('My Header');
374
+ });
375
+ });
376
+
377
+ test('renders ### subheaders', async () => {
378
+ globalThis.fetch = mock((url: string) => {
379
+ if (url === '/api/db/monitoring') return Promise.resolve(createJsonResponse({}));
380
+ return Promise.resolve(createStreamResponse('### Sub Header'));
381
+ }) as never;
382
+
383
+ const { queryByText, container } = render(<AIAutopilotPanel {...defaultProps} />);
384
+ await act(async () => { fireEvent.click(queryByText('Run Analysis')!.closest('button')!); });
385
+
386
+ await waitFor(() => {
387
+ const h3 = container.querySelector('h3');
388
+ expect(h3).not.toBeNull();
389
+ expect(h3!.textContent).toBe('Sub Header');
390
+ });
391
+ });
392
+
393
+ test('renders bullet lists with - prefix', async () => {
394
+ globalThis.fetch = mock((url: string) => {
395
+ if (url === '/api/db/monitoring') return Promise.resolve(createJsonResponse({}));
396
+ return Promise.resolve(createStreamResponse('- First item\n- Second item'));
397
+ }) as never;
398
+
399
+ const { queryByText, container } = render(<AIAutopilotPanel {...defaultProps} />);
400
+ await act(async () => { fireEvent.click(queryByText('Run Analysis')!.closest('button')!); });
401
+
402
+ await waitFor(() => {
403
+ const items = container.querySelectorAll('li');
404
+ expect(items.length).toBe(2);
405
+ expect(items[0].textContent).toContain('First item');
406
+ expect(items[1].textContent).toContain('Second item');
407
+ });
408
+ });
409
+
410
+ test('renders numbered lists', async () => {
411
+ globalThis.fetch = mock((url: string) => {
412
+ if (url === '/api/db/monitoring') return Promise.resolve(createJsonResponse({}));
413
+ return Promise.resolve(createStreamResponse('1. Step one\n2. Step two'));
414
+ }) as never;
415
+
416
+ const { queryByText, container } = render(<AIAutopilotPanel {...defaultProps} />);
417
+ await act(async () => { fireEvent.click(queryByText('Run Analysis')!.closest('button')!); });
418
+
419
+ await waitFor(() => {
420
+ const items = container.querySelectorAll('li.list-decimal');
421
+ expect(items.length).toBe(2);
422
+ });
423
+ });
424
+
425
+ test('renders bold text with **markers**', async () => {
426
+ globalThis.fetch = mock((url: string) => {
427
+ if (url === '/api/db/monitoring') return Promise.resolve(createJsonResponse({}));
428
+ return Promise.resolve(createStreamResponse('This has **bold** text'));
429
+ }) as never;
430
+
431
+ const { queryByText, container } = render(<AIAutopilotPanel {...defaultProps} />);
432
+ await act(async () => { fireEvent.click(queryByText('Run Analysis')!.closest('button')!); });
433
+
434
+ await waitFor(() => {
435
+ const strong = container.querySelector('strong');
436
+ expect(strong).not.toBeNull();
437
+ expect(strong!.textContent).toBe('bold');
438
+ });
439
+ });
440
+
441
+ test('renders plain text paragraphs', async () => {
442
+ globalThis.fetch = mock((url: string) => {
443
+ if (url === '/api/db/monitoring') return Promise.resolve(createJsonResponse({}));
444
+ return Promise.resolve(createStreamResponse('Just a regular paragraph.'));
445
+ }) as never;
446
+
447
+ const { queryByText, container } = render(<AIAutopilotPanel {...defaultProps} />);
448
+ await act(async () => { fireEvent.click(queryByText('Run Analysis')!.closest('button')!); });
449
+
450
+ await waitFor(() => {
451
+ const p = container.querySelector('p.text-xs.text-zinc-400');
452
+ expect(p).not.toBeNull();
453
+ expect(p!.textContent).toContain('Just a regular paragraph.');
454
+ });
455
+ });
456
+
457
+ test('renders empty lines as spacers', async () => {
458
+ globalThis.fetch = mock((url: string) => {
459
+ if (url === '/api/db/monitoring') return Promise.resolve(createJsonResponse({}));
460
+ return Promise.resolve(createStreamResponse('Line one\n\nLine two'));
461
+ }) as never;
462
+
463
+ const { queryByText, container } = render(<AIAutopilotPanel {...defaultProps} />);
464
+ await act(async () => { fireEvent.click(queryByText('Run Analysis')!.closest('button')!); });
465
+
466
+ await waitFor(() => {
467
+ const spacer = container.querySelector('.h-2');
468
+ expect(spacer).not.toBeNull();
469
+ });
470
+ });
471
+
472
+ // ── Code blocks ─────────────────────────────────────────────────────────
473
+
474
+ test('renders code blocks with copy button', async () => {
475
+ const sql = 'SELECT * FROM users;';
476
+ const report = '```sql\n' + sql + '\n```';
477
+
478
+ globalThis.fetch = mock((url: string) => {
479
+ if (url === '/api/db/monitoring') return Promise.resolve(createJsonResponse({}));
480
+ return Promise.resolve(createStreamResponse(report));
481
+ }) as never;
482
+
483
+ const { queryByText, container } = render(<AIAutopilotPanel {...defaultProps} />);
484
+ await act(async () => { fireEvent.click(queryByText('Run Analysis')!.closest('button')!); });
485
+
486
+ await waitFor(() => {
487
+ const pre = container.querySelector('pre');
488
+ expect(pre).not.toBeNull();
489
+ expect(pre!.textContent).toContain('SELECT * FROM users;');
490
+
491
+ // Copy button exists
492
+ const copyBtn = container.querySelector('button[title="Copy"]');
493
+ expect(copyBtn).not.toBeNull();
494
+ });
495
+ });
496
+
497
+ test('renders execute button when onExecuteQuery is provided', async () => {
498
+ const sql = 'CREATE INDEX idx_email ON users(email);';
499
+ const report = '```sql\n' + sql + '\n```';
500
+
501
+ globalThis.fetch = mock((url: string) => {
502
+ if (url === '/api/db/monitoring') return Promise.resolve(createJsonResponse({}));
503
+ return Promise.resolve(createStreamResponse(report));
504
+ }) as never;
505
+
506
+ const onExecuteQuery = mock(() => {});
507
+ const { queryByText, container } = render(
508
+ <AIAutopilotPanel {...defaultProps} onExecuteQuery={onExecuteQuery} />
509
+ );
510
+ await act(async () => { fireEvent.click(queryByText('Run Analysis')!.closest('button')!); });
511
+
512
+ await waitFor(() => {
513
+ const execBtn = container.querySelector('button[title="Execute"]');
514
+ expect(execBtn).not.toBeNull();
515
+ });
516
+ });
517
+
518
+ test('does not render execute button when onExecuteQuery is not provided', async () => {
519
+ const report = '```sql\nSELECT 1;\n```';
520
+
521
+ globalThis.fetch = mock((url: string) => {
522
+ if (url === '/api/db/monitoring') return Promise.resolve(createJsonResponse({}));
523
+ return Promise.resolve(createStreamResponse(report));
524
+ }) as never;
525
+
526
+ const { queryByText, container } = render(
527
+ <AIAutopilotPanel {...defaultProps} />
528
+ );
529
+ await act(async () => { fireEvent.click(queryByText('Run Analysis')!.closest('button')!); });
530
+
531
+ await waitFor(() => {
532
+ const execBtn = container.querySelector('button[title="Execute"]');
533
+ expect(execBtn).toBeNull();
534
+ });
535
+ });
536
+
537
+ // ── Copy to clipboard ───────────────────────────────────────────────────
538
+
539
+ test('copy button copies SQL to clipboard', async () => {
540
+ const sql = 'EXPLAIN ANALYZE SELECT * FROM orders;';
541
+ const report = '```sql\n' + sql + '\n```';
542
+
543
+ globalThis.fetch = mock((url: string) => {
544
+ if (url === '/api/db/monitoring') return Promise.resolve(createJsonResponse({}));
545
+ return Promise.resolve(createStreamResponse(report));
546
+ }) as never;
547
+
548
+ const { queryByText, container } = render(<AIAutopilotPanel {...defaultProps} />);
549
+ await act(async () => { fireEvent.click(queryByText('Run Analysis')!.closest('button')!); });
550
+
551
+ await waitFor(() => {
552
+ const copyBtn = container.querySelector('button[title="Copy"]');
553
+ expect(copyBtn).not.toBeNull();
554
+ });
555
+
556
+ fireEvent.click(container.querySelector('button[title="Copy"]')!);
557
+ expect(mockWriteText).toHaveBeenCalledTimes(1);
558
+ expect((mockWriteText.mock.calls as unknown[][])[0][0]).toBe(sql);
559
+ });
560
+
561
+ // ── Execute button ──────────────────────────────────────────────────────
562
+
563
+ test('execute button calls onExecuteQuery with SQL', async () => {
564
+ const sql = 'VACUUM ANALYZE users;';
565
+ const report = '```sql\n' + sql + '\n```';
566
+
567
+ globalThis.fetch = mock((url: string) => {
568
+ if (url === '/api/db/monitoring') return Promise.resolve(createJsonResponse({}));
569
+ return Promise.resolve(createStreamResponse(report));
570
+ }) as never;
571
+
572
+ const onExecuteQuery = mock(() => {});
573
+ const { queryByText, container } = render(
574
+ <AIAutopilotPanel {...defaultProps} onExecuteQuery={onExecuteQuery} />
575
+ );
576
+ await act(async () => { fireEvent.click(queryByText('Run Analysis')!.closest('button')!); });
577
+
578
+ await waitFor(() => {
579
+ const execBtn = container.querySelector('button[title="Execute"]');
580
+ expect(execBtn).not.toBeNull();
581
+ });
582
+
583
+ fireEvent.click(container.querySelector('button[title="Execute"]')!);
584
+ expect(onExecuteQuery).toHaveBeenCalledTimes(1);
585
+ expect((onExecuteQuery.mock.calls as unknown[][])[0][0]).toBe(sql);
586
+ });
587
+
588
+ // ── Multiple code blocks ────────────────────────────────────────────────
589
+
590
+ test('renders multiple code blocks independently', async () => {
591
+ const report = '## Fix 1\n```sql\nSELECT 1;\n```\n## Fix 2\n```sql\nSELECT 2;\n```';
592
+
593
+ globalThis.fetch = mock((url: string) => {
594
+ if (url === '/api/db/monitoring') return Promise.resolve(createJsonResponse({}));
595
+ return Promise.resolve(createStreamResponse(report));
596
+ }) as never;
597
+
598
+ const { queryByText, container } = render(<AIAutopilotPanel {...defaultProps} />);
599
+ await act(async () => { fireEvent.click(queryByText('Run Analysis')!.closest('button')!); });
600
+
601
+ await waitFor(() => {
602
+ const preElements = container.querySelectorAll('pre');
603
+ expect(preElements.length).toBe(2);
604
+ expect(preElements[0].textContent).toContain('SELECT 1;');
605
+ expect(preElements[1].textContent).toContain('SELECT 2;');
606
+ });
607
+ });
608
+
609
+ // ── Bold in lists ───────────────────────────────────────────────────────
610
+
611
+ test('renders bold text inside bullet list items', async () => {
612
+ globalThis.fetch = mock((url: string) => {
613
+ if (url === '/api/db/monitoring') return Promise.resolve(createJsonResponse({}));
614
+ return Promise.resolve(createStreamResponse('- **Impact**: High'));
615
+ }) as never;
616
+
617
+ const { queryByText, container } = render(<AIAutopilotPanel {...defaultProps} />);
618
+ await act(async () => { fireEvent.click(queryByText('Run Analysis')!.closest('button')!); });
619
+
620
+ await waitFor(() => {
621
+ const strong = container.querySelector('li strong');
622
+ expect(strong).not.toBeNull();
623
+ expect(strong!.textContent).toBe('Impact');
624
+ });
625
+ });
626
+
627
+ // ── Schema slicing ─────────────────────────────────────────────────────
628
+
629
+ test('sends filtered schema with max 30 tables and 6 columns each', async () => {
630
+ const bigSchema = Array.from({ length: 50 }, (_, i) => ({
631
+ name: `table_${i}`,
632
+ rowCount: i * 10,
633
+ columns: Array.from({ length: 10 }, (_, j) => ({ name: `col_${j}`, type: 'text' })),
634
+ }));
635
+
636
+ let capturedBody: string | null = null;
637
+ globalThis.fetch = mock((url: string, opts?: RequestInit) => {
638
+ if (url === '/api/db/monitoring') {
639
+ return Promise.resolve(createJsonResponse({}));
640
+ }
641
+ if (url === '/api/ai/autopilot') {
642
+ capturedBody = opts?.body as string;
643
+ return Promise.resolve(createStreamResponse('Done'));
644
+ }
645
+ return Promise.resolve(new Response(''));
646
+ }) as never;
647
+
648
+ const { queryByText } = render(
649
+ <AIAutopilotPanel connection={mockPostgresConnection} schemaContext={JSON.stringify(bigSchema)} />
650
+ );
651
+
652
+ await act(async () => {
653
+ fireEvent.click(queryByText('Run Analysis')!.closest('button')!);
654
+ });
655
+
656
+ await waitFor(() => {
657
+ expect(capturedBody).not.toBeNull();
658
+ });
659
+
660
+ const parsed = JSON.parse(capturedBody!);
661
+ // schemaContext should only have 30 table lines
662
+ const lines = parsed.schemaContext.split('\n').filter((l: string) => l.trim());
663
+ expect(lines.length).toBe(30);
664
+ // Each line should have max 6 columns
665
+ const firstLine = lines[0];
666
+ // columns are joined with ", " — count commas to approximate
667
+ // 6 columns = 5 commas (in the column part)
668
+ expect(firstLine).toContain('col_0');
669
+ expect(firstLine).toContain('col_5');
670
+ expect(firstLine).not.toContain('col_6');
671
+ });
672
+
673
+ // ── Error display styling ───────────────────────────────────────────────
674
+
675
+ test('error message has red styling', async () => {
676
+ globalThis.fetch = mock(() => Promise.reject(new Error('Connection lost'))) as never;
677
+
678
+ const { queryByText, container } = render(<AIAutopilotPanel {...defaultProps} />);
679
+ await act(async () => { fireEvent.click(queryByText('Run Analysis')!.closest('button')!); });
680
+
681
+ await waitFor(() => {
682
+ const errorDiv = container.querySelector('.text-red-400');
683
+ expect(errorDiv).not.toBeNull();
684
+ expect(errorDiv!.textContent).toContain('Connection lost');
685
+ });
686
+ });
687
+
688
+ // ── Hides placeholder when report/error/loading ─────────────────────────
689
+
690
+ test('hides idle placeholder when report is shown', async () => {
691
+ globalThis.fetch = mock((url: string) => {
692
+ if (url === '/api/db/monitoring') return Promise.resolve(createJsonResponse({}));
693
+ return Promise.resolve(createStreamResponse('Report text'));
694
+ }) as never;
695
+
696
+ const { queryByText } = render(<AIAutopilotPanel {...defaultProps} />);
697
+ await act(async () => { fireEvent.click(queryByText('Run Analysis')!.closest('button')!); });
698
+
699
+ await waitFor(() => {
700
+ // Placeholder should be gone
701
+ expect(queryByText('AI-powered optimization recommendations')).toBeNull();
702
+ // Report should be visible
703
+ expect(queryByText('Report text')).not.toBeNull();
704
+ });
705
+ });
706
+
707
+ test('hides idle placeholder when error is shown', async () => {
708
+ globalThis.fetch = mock(() => Promise.reject(new Error('fail'))) as never;
709
+
710
+ const { queryByText } = render(<AIAutopilotPanel {...defaultProps} />);
711
+ await act(async () => { fireEvent.click(queryByText('Run Analysis')!.closest('button')!); });
712
+
713
+ await waitFor(() => {
714
+ expect(queryByText('AI-powered optimization recommendations')).toBeNull();
715
+ });
716
+ });
717
+
718
+ // ── Monitoring data is passed to autopilot ──────────────────────────────
719
+
720
+ test('passes monitoring data to autopilot endpoint', async () => {
721
+ const monitoringData = {
722
+ tables: [{ name: 'users', rows: 100 }],
723
+ indexes: [{ name: 'idx_1' }],
724
+ slowQueries: [{ query: 'SELECT *', duration: 5000 }],
725
+ performance: { cpu: 0.5 },
726
+ overview: { connections: 10 },
727
+ };
728
+
729
+ let capturedBody: string | null = null;
730
+ globalThis.fetch = mock((url: string, opts?: RequestInit) => {
731
+ if (url === '/api/db/monitoring') {
732
+ return Promise.resolve(createJsonResponse(monitoringData));
733
+ }
734
+ if (url === '/api/ai/autopilot') {
735
+ capturedBody = opts?.body as string;
736
+ return Promise.resolve(createStreamResponse('OK'));
737
+ }
738
+ return Promise.resolve(new Response(''));
739
+ }) as never;
740
+
741
+ const { queryByText } = render(<AIAutopilotPanel {...defaultProps} />);
742
+ await act(async () => { fireEvent.click(queryByText('Run Analysis')!.closest('button')!); });
743
+
744
+ await waitFor(() => {
745
+ expect(capturedBody).not.toBeNull();
746
+ });
747
+
748
+ const parsed = JSON.parse(capturedBody!);
749
+ expect(parsed.slowQueries).toEqual(monitoringData.slowQueries);
750
+ expect(parsed.indexStats).toEqual(monitoringData.indexes);
751
+ expect(parsed.tableStats).toEqual(monitoringData.tables);
752
+ expect(parsed.performanceMetrics).toEqual(monitoringData.performance);
753
+ expect(parsed.overview).toEqual(monitoringData.overview);
754
+ expect(parsed.databaseType).toBe('postgres');
755
+ });
756
+ });