@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.
- package/.claude/settings.local.json +127 -0
- package/.cursorrules +426 -0
- package/.devin/wiki.json +143 -0
- package/.dockerignore +80 -0
- package/.env.example +159 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +49 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +29 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +57 -0
- package/.github/workflows/ci.yml +185 -0
- package/.github/workflows/codeql.yml +57 -0
- package/.github/workflows/docker-build-push.yml +118 -0
- package/.github/workflows/helm-release.yml +113 -0
- package/CLAUDE.md +265 -0
- package/CODE_OF_CONDUCT.md +124 -0
- package/CONTRIBUTING.md +154 -0
- package/Dockerfile +73 -0
- package/LICENSE +21 -0
- package/README.md +614 -0
- package/SECURITY.md +107 -0
- package/artifacthub-repo.yml +4 -0
- package/bun.lock +1714 -0
- package/bunfig.toml +3 -0
- package/charts/libredb-studio/.helmignore +11 -0
- package/charts/libredb-studio/Chart.lock +6 -0
- package/charts/libredb-studio/Chart.yaml +50 -0
- package/charts/libredb-studio/README.md +206 -0
- package/charts/libredb-studio/templates/NOTES.txt +59 -0
- package/charts/libredb-studio/templates/_helpers.tpl +135 -0
- package/charts/libredb-studio/templates/configmap.yaml +37 -0
- package/charts/libredb-studio/templates/deployment.yaml +184 -0
- package/charts/libredb-studio/templates/hpa.yaml +32 -0
- package/charts/libredb-studio/templates/ingress.yaml +41 -0
- package/charts/libredb-studio/templates/networkpolicy.yaml +50 -0
- package/charts/libredb-studio/templates/pdb.yaml +18 -0
- package/charts/libredb-studio/templates/pvc.yaml +23 -0
- package/charts/libredb-studio/templates/secret.yaml +30 -0
- package/charts/libredb-studio/templates/seed-configmap.yaml +11 -0
- package/charts/libredb-studio/templates/service.yaml +22 -0
- package/charts/libredb-studio/templates/serviceaccount.yaml +13 -0
- package/charts/libredb-studio/values.schema.json +246 -0
- package/charts/libredb-studio/values.yaml +286 -0
- package/components.json +22 -0
- package/conductor/code_styleguides/typescript.md +43 -0
- package/conductor/product-guidelines.md +43 -0
- package/conductor/product.md +3 -0
- package/conductor/setup_state.json +1 -0
- package/conductor/tech-stack.md +39 -0
- package/conductor/tracks/enhance_postgres_monitoring_20251227/metadata.json +8 -0
- package/conductor/tracks/enhance_postgres_monitoring_20251227/plan.md +44 -0
- package/conductor/tracks/enhance_postgres_monitoring_20251227/spec.md +31 -0
- package/conductor/tracks.md +8 -0
- package/conductor/workflow.md +333 -0
- package/database-compose.yml +55 -0
- package/docker/postgres-init/01-extensions.sql +10 -0
- package/docker/postgres-init/02-sample-data.sql +585 -0
- package/docker/postgres.yml +68 -0
- package/docker-compose.yml +38 -0
- package/docs/AI_PLAN.md +74 -0
- package/docs/API_DOCS.md +875 -0
- package/docs/ARCHITECTURE.md +218 -0
- package/docs/DATABASE_PROVIDERS.md +358 -0
- package/docs/FEATURES.md +116 -0
- package/docs/HELM_CHART.md +252 -0
- package/docs/LOGIN_PAGE.md +178 -0
- package/docs/MONACO_EDITOR_PERFORMANCE.md +315 -0
- package/docs/OIDC_ARCH.md +681 -0
- package/docs/OIDC_SETUP.md +322 -0
- package/docs/POSTGRES_METRICS.md +516 -0
- package/docs/QUERY_OPTIMIZATION.md +370 -0
- package/docs/SEED_CONNECTIONS.md +468 -0
- package/docs/SQL_ALIAS_COMPLETION.md +190 -0
- package/docs/STORAGE_ARCHITECTURE.md +565 -0
- package/docs/STORAGE_QUICK_SETUP.md +419 -0
- package/docs/TECHNICAL_PLAN.md +36 -0
- package/docs/THEMING.md +345 -0
- package/docs/adding-a-new-database-provider.md +642 -0
- package/docs/backlogs/000-PLATFORM_DATA_SYNC_DATABASE.md +360 -0
- package/docs/backlogs/001-INLINE_DATA_EDITING.md +118 -0
- package/docs/backlogs/002-DATA_IMPORT.md +215 -0
- package/docs/backlogs/003-QUERY_TIME_MACHINE.md +183 -0
- package/docs/backlogs/004-AI_DATA_STORYTELLER.md +292 -0
- package/docs/backlogs/005-QUERY_PLAYGROUND.md +352 -0
- package/docs/backlogs/006-DATA_MASKING.md +418 -0
- package/docs/enterprise-features.md +718 -0
- package/docs/kubernetes-helm-chart-artifacthub-plan.md +803 -0
- package/docs/medium-koyeb-article-en.md +215 -0
- package/docs/plans/test-plans.md +445 -0
- package/docs/releases/RELEASE.V0.3.0.md +22 -0
- package/docs/releases/RELEASE.V0.4.0.md +154 -0
- package/docs/releases/RELEASE.V0.5.0.md +252 -0
- package/docs/releases/RELEASE_v0.5.6.md +145 -0
- package/docs/releases/RELEASE_v0.6.1.md +303 -0
- package/docs/releases/RELEASE_v0.6.7.md +292 -0
- package/docs/releases/RELEASE_v0.7.0.md +332 -0
- package/docs/releases/RELEASE_v0.8.0.md +521 -0
- package/docs/sampledb/titanic.sql +1379 -0
- package/docs/superpowers/plans/2026-03-25-seed-connections.md +1362 -0
- package/docs/superpowers/specs/2026-03-25-seed-connections-design.md +590 -0
- package/e2e/admin-dashboard.spec.ts +64 -0
- package/e2e/connection-management.spec.ts +58 -0
- package/e2e/export.spec.ts +34 -0
- package/e2e/login.spec.ts +85 -0
- package/e2e/query-execution.spec.ts +35 -0
- package/e2e/tab-management.spec.ts +64 -0
- package/eslint.config.mjs +28 -0
- package/fly.toml +43 -0
- package/next.config.ts +32 -0
- package/package.json +130 -0
- package/playwright.config.ts +34 -0
- package/postcss.config.mjs +7 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/logo.svg +32 -0
- package/public/next.svg +1 -0
- package/public/screenshots/code-generator.png +0 -0
- package/public/screenshots/connection-modal.png +0 -0
- package/public/screenshots/data-profiler.png +0 -0
- package/public/screenshots/erd-diagram.png +0 -0
- package/public/screenshots/hero-editor.png +0 -0
- package/public/screenshots/nl2sql.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/render.yaml +58 -0
- package/scripts/merge-lcov.mjs +239 -0
- package/sonar-project.properties +16 -0
- package/src/app/admin/error.tsx +46 -0
- package/src/app/admin/page.tsx +10 -0
- package/src/app/api/admin/audit/route.ts +52 -0
- package/src/app/api/admin/fleet-health/route.ts +81 -0
- package/src/app/api/ai/autopilot/route.ts +105 -0
- package/src/app/api/ai/chat/route.ts +132 -0
- package/src/app/api/ai/describe-schema/route.ts +52 -0
- package/src/app/api/ai/explain/route.ts +86 -0
- package/src/app/api/ai/impact/route.ts +97 -0
- package/src/app/api/ai/index-advisor/route.ts +98 -0
- package/src/app/api/ai/nl2sql/route.ts +87 -0
- package/src/app/api/ai/query-safety/route.ts +87 -0
- package/src/app/api/auth/login/route.ts +62 -0
- package/src/app/api/auth/logout/route.ts +25 -0
- package/src/app/api/auth/me/route.ts +10 -0
- package/src/app/api/auth/oidc/callback/route.ts +82 -0
- package/src/app/api/auth/oidc/login/route.ts +43 -0
- package/src/app/api/connections/managed/route.ts +35 -0
- package/src/app/api/db/cancel/route.ts +42 -0
- package/src/app/api/db/disconnect/route.ts +28 -0
- package/src/app/api/db/health/route.ts +49 -0
- package/src/app/api/db/maintenance/route.ts +72 -0
- package/src/app/api/db/monitoring/route.ts +62 -0
- package/src/app/api/db/multi-query/route.ts +116 -0
- package/src/app/api/db/pool-stats/route.ts +37 -0
- package/src/app/api/db/profile/route.ts +144 -0
- package/src/app/api/db/provider-meta/route.ts +49 -0
- package/src/app/api/db/query/route.ts +50 -0
- package/src/app/api/db/schema/route.ts +47 -0
- package/src/app/api/db/schema-snapshot/route.ts +42 -0
- package/src/app/api/db/test-connection/route.ts +55 -0
- package/src/app/api/db/transaction/route.ts +111 -0
- package/src/app/api/storage/[collection]/route.ts +67 -0
- package/src/app/api/storage/config/route.ts +17 -0
- package/src/app/api/storage/migrate/route.ts +45 -0
- package/src/app/api/storage/route.ts +32 -0
- package/src/app/error.tsx +49 -0
- package/src/app/global-error.tsx +55 -0
- package/src/app/globals.css +146 -0
- package/src/app/icon.svg +42 -0
- package/src/app/layout.tsx +34 -0
- package/src/app/login/login-form.tsx +301 -0
- package/src/app/login/page.tsx +11 -0
- package/src/app/monitoring/page.tsx +8 -0
- package/src/app/not-found.tsx +29 -0
- package/src/app/page.tsx +5 -0
- package/src/components/AIAutopilotPanel.tsx +238 -0
- package/src/components/CodeGenerator.tsx +271 -0
- package/src/components/CommandPalette.tsx +227 -0
- package/src/components/ConnectionModal.tsx +759 -0
- package/src/components/CreateTableModal.tsx +281 -0
- package/src/components/DataCharts.tsx +962 -0
- package/src/components/DataImportModal.tsx +582 -0
- package/src/components/DataProfiler.tsx +335 -0
- package/src/components/DatabaseDocs.tsx +251 -0
- package/src/components/MaskingSettings.tsx +414 -0
- package/src/components/MobileNav.tsx +50 -0
- package/src/components/NL2SQLPanel.tsx +281 -0
- package/src/components/PivotTable.tsx +257 -0
- package/src/components/QueryEditor.tsx +760 -0
- package/src/components/QueryHistory.tsx +344 -0
- package/src/components/QuerySafetyDialog.tsx +290 -0
- package/src/components/ResultsGrid.tsx +644 -0
- package/src/components/SaveQueryModal.tsx +104 -0
- package/src/components/SavedQueries.tsx +128 -0
- package/src/components/SchemaDiagram.tsx +473 -0
- package/src/components/SchemaDiff.tsx +473 -0
- package/src/components/SnapshotTimeline.tsx +116 -0
- package/src/components/Studio.tsx +639 -0
- package/src/components/TestDataGenerator.tsx +261 -0
- package/src/components/VisualExplain.tsx +820 -0
- package/src/components/admin/AdminDashboard.tsx +163 -0
- package/src/components/admin/tabs/AuditTab.tsx +531 -0
- package/src/components/admin/tabs/MonitoringEmbed.tsx +11 -0
- package/src/components/admin/tabs/OperationsTab.tsx +646 -0
- package/src/components/admin/tabs/OverviewTab.tsx +1328 -0
- package/src/components/admin/tabs/SecurityTab.tsx +284 -0
- package/src/components/community-section.tsx +92 -0
- package/src/components/icons/db-icons.tsx +84 -0
- package/src/components/libredb-logo.tsx +61 -0
- package/src/components/monitoring/MonitoringDashboard.tsx +345 -0
- package/src/components/monitoring/tabs/MetricChart.tsx +82 -0
- package/src/components/monitoring/tabs/OverviewTab.tsx +263 -0
- package/src/components/monitoring/tabs/PerformanceTab.tsx +254 -0
- package/src/components/monitoring/tabs/PoolTab.tsx +174 -0
- package/src/components/monitoring/tabs/QueriesTab.tsx +287 -0
- package/src/components/monitoring/tabs/SessionsTab.tsx +316 -0
- package/src/components/monitoring/tabs/StorageTab.tsx +335 -0
- package/src/components/monitoring/tabs/TablesTab.tsx +300 -0
- package/src/components/results-grid/ResultCard.tsx +111 -0
- package/src/components/results-grid/RowDetailSheet.tsx +178 -0
- package/src/components/results-grid/StatsBar.tsx +201 -0
- package/src/components/results-grid/index.ts +1 -0
- package/src/components/results-grid/utils.ts +23 -0
- package/src/components/schema-explorer/ColumnList.tsx +53 -0
- package/src/components/schema-explorer/SchemaExplorer.tsx +182 -0
- package/src/components/schema-explorer/TableItem.tsx +210 -0
- package/src/components/schema-explorer/index.ts +1 -0
- package/src/components/sidebar/ConnectionItem.tsx +105 -0
- package/src/components/sidebar/ConnectionsList.tsx +62 -0
- package/src/components/sidebar/Sidebar.tsx +130 -0
- package/src/components/sidebar/index.ts +2 -0
- package/src/components/studio/BottomPanel.tsx +286 -0
- package/src/components/studio/QueryToolbar.tsx +180 -0
- package/src/components/studio/StudioDesktopHeader.tsx +114 -0
- package/src/components/studio/StudioMobileHeader.tsx +340 -0
- package/src/components/studio/StudioTabBar.tsx +82 -0
- package/src/components/studio/index.ts +5 -0
- package/src/components/ui/accordion.tsx +66 -0
- package/src/components/ui/alert-dialog.tsx +157 -0
- package/src/components/ui/alert.tsx +66 -0
- package/src/components/ui/aspect-ratio.tsx +11 -0
- package/src/components/ui/avatar.tsx +53 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/breadcrumb.tsx +109 -0
- package/src/components/ui/button-group.tsx +83 -0
- package/src/components/ui/button.tsx +60 -0
- package/src/components/ui/calendar.tsx +216 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/carousel.tsx +241 -0
- package/src/components/ui/chart.tsx +357 -0
- package/src/components/ui/checkbox.tsx +32 -0
- package/src/components/ui/collapsible.tsx +33 -0
- package/src/components/ui/command.tsx +184 -0
- package/src/components/ui/context-menu.tsx +252 -0
- package/src/components/ui/dialog.tsx +143 -0
- package/src/components/ui/drawer.tsx +135 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/empty.tsx +104 -0
- package/src/components/ui/field.tsx +248 -0
- package/src/components/ui/form.tsx +167 -0
- package/src/components/ui/hover-card.tsx +44 -0
- package/src/components/ui/input-group.tsx +170 -0
- package/src/components/ui/input-otp.tsx +77 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/item.tsx +193 -0
- package/src/components/ui/kbd.tsx +28 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/menubar.tsx +276 -0
- package/src/components/ui/navigation-menu.tsx +168 -0
- package/src/components/ui/pagination.tsx +127 -0
- package/src/components/ui/popover.tsx +48 -0
- package/src/components/ui/progress.tsx +31 -0
- package/src/components/ui/radio-group.tsx +45 -0
- package/src/components/ui/resizable.tsx +56 -0
- package/src/components/ui/scroll-area.tsx +58 -0
- package/src/components/ui/select.tsx +187 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +139 -0
- package/src/components/ui/sidebar.tsx +726 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +63 -0
- package/src/components/ui/sonner.tsx +40 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/table.tsx +116 -0
- package/src/components/ui/tabs.tsx +66 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/toggle-group.tsx +83 -0
- package/src/components/ui/toggle.tsx +47 -0
- package/src/components/ui/tooltip.tsx +61 -0
- package/src/exports/components.ts +15 -0
- package/src/exports/index.ts +4 -0
- package/src/exports/providers.ts +4 -0
- package/src/exports/types.ts +26 -0
- package/src/hooks/use-ai-chat.ts +182 -0
- package/src/hooks/use-all-connections.ts +66 -0
- package/src/hooks/use-api-call.ts +71 -0
- package/src/hooks/use-auth.ts +51 -0
- package/src/hooks/use-connection-form.ts +349 -0
- package/src/hooks/use-connection-manager.ts +169 -0
- package/src/hooks/use-connection-payload.ts +15 -0
- package/src/hooks/use-inline-editing.ts +109 -0
- package/src/hooks/use-mobile.ts +20 -0
- package/src/hooks/use-monitoring-data.ts +270 -0
- package/src/hooks/use-provider-metadata.ts +62 -0
- package/src/hooks/use-query-execution.ts +478 -0
- package/src/hooks/use-storage-sync.ts +259 -0
- package/src/hooks/use-tab-manager.ts +231 -0
- package/src/hooks/use-toast.ts +20 -0
- package/src/hooks/use-transaction-control.ts +64 -0
- package/src/lib/api/error-codes.ts +30 -0
- package/src/lib/api/errors.ts +236 -0
- package/src/lib/api/with-error-handler.ts +41 -0
- package/src/lib/audit.ts +105 -0
- package/src/lib/auth.ts +87 -0
- package/src/lib/connection-string-parser.ts +172 -0
- package/src/lib/data-masking.ts +385 -0
- package/src/lib/db/base-provider.ts +325 -0
- package/src/lib/db/errors.ts +317 -0
- package/src/lib/db/factory.ts +324 -0
- package/src/lib/db/index.ts +123 -0
- package/src/lib/db/providers/document/index.ts +6 -0
- package/src/lib/db/providers/document/mongodb.ts +992 -0
- package/src/lib/db/providers/keyvalue/redis.ts +554 -0
- package/src/lib/db/providers/sql/index.ts +11 -0
- package/src/lib/db/providers/sql/mssql.ts +1065 -0
- package/src/lib/db/providers/sql/mysql.ts +978 -0
- package/src/lib/db/providers/sql/oracle.ts +1044 -0
- package/src/lib/db/providers/sql/postgres.ts +1179 -0
- package/src/lib/db/providers/sql/sql-base.ts +174 -0
- package/src/lib/db/providers/sql/sqlite.ts +721 -0
- package/src/lib/db/types.ts +437 -0
- package/src/lib/db/utils/pool-manager.ts +287 -0
- package/src/lib/db/utils/query-limiter.ts +239 -0
- package/src/lib/db-ui-config.ts +86 -0
- package/src/lib/editor/mongodb-completions.ts +172 -0
- package/src/lib/editor/sql-completions.ts +280 -0
- package/src/lib/llm/base-provider.ts +117 -0
- package/src/lib/llm/factory.ts +102 -0
- package/src/lib/llm/index.ts +90 -0
- package/src/lib/llm/providers/custom.ts +181 -0
- package/src/lib/llm/providers/gemini.ts +126 -0
- package/src/lib/llm/providers/ollama.ts +154 -0
- package/src/lib/llm/providers/openai.ts +146 -0
- package/src/lib/llm/types.ts +173 -0
- package/src/lib/llm/utils/config.ts +187 -0
- package/src/lib/llm/utils/retry.ts +119 -0
- package/src/lib/llm/utils/streaming.ts +202 -0
- package/src/lib/logger.ts +127 -0
- package/src/lib/monitoring-thresholds.ts +44 -0
- package/src/lib/oidc.ts +262 -0
- package/src/lib/query-generators.ts +61 -0
- package/src/lib/schema-diff/diff-engine.ts +273 -0
- package/src/lib/schema-diff/migration-generator.ts +208 -0
- package/src/lib/schema-diff/types.ts +55 -0
- package/src/lib/seed/config-loader.ts +79 -0
- package/src/lib/seed/connection-filter.ts +49 -0
- package/src/lib/seed/credential-resolver.ts +62 -0
- package/src/lib/seed/index.ts +40 -0
- package/src/lib/seed/resolve-connection.ts +57 -0
- package/src/lib/seed/types.ts +69 -0
- package/src/lib/sql/alias-extractor.ts +267 -0
- package/src/lib/sql/index.ts +8 -0
- package/src/lib/sql/statement-splitter.ts +167 -0
- package/src/lib/sql/types.ts +40 -0
- package/src/lib/ssh/tunnel.ts +142 -0
- package/src/lib/storage/factory.ts +84 -0
- package/src/lib/storage/index.ts +14 -0
- package/src/lib/storage/local-storage.ts +99 -0
- package/src/lib/storage/providers/postgres.ts +225 -0
- package/src/lib/storage/providers/sqlite.ts +153 -0
- package/src/lib/storage/storage-facade.ts +272 -0
- package/src/lib/storage/types.ts +75 -0
- package/src/lib/time-series-buffer.ts +58 -0
- package/src/lib/types.ts +173 -0
- package/src/lib/utils.ts +6 -0
- package/src/proxy.ts +104 -0
- package/src/types/db-drivers.d.ts +23 -0
- package/src/types/html2canvas.d.ts +9 -0
- package/tests/api/admin/audit.test.ts +178 -0
- package/tests/api/admin/fleet-health.test.ts +183 -0
- package/tests/api/ai/autopilot.test.ts +174 -0
- package/tests/api/ai/chat.test.ts +250 -0
- package/tests/api/ai/describe-schema.test.ts +266 -0
- package/tests/api/ai/explain.test.ts +199 -0
- package/tests/api/ai/impact.test.ts +168 -0
- package/tests/api/ai/index-advisor.test.ts +171 -0
- package/tests/api/ai/nl2sql.test.ts +202 -0
- package/tests/api/ai/query-safety.test.ts +196 -0
- package/tests/api/auth/login.test.ts +170 -0
- package/tests/api/auth/logout.test.ts +140 -0
- package/tests/api/auth/me.test.ts +73 -0
- package/tests/api/auth/oidc-callback.test.ts +215 -0
- package/tests/api/auth/oidc-login.test.ts +127 -0
- package/tests/api/db/cancel.test.ts +198 -0
- package/tests/api/db/disconnect.test.ts +124 -0
- package/tests/api/db/health.test.ts +222 -0
- package/tests/api/db/maintenance.test.ts +263 -0
- package/tests/api/db/monitoring.test.ts +221 -0
- package/tests/api/db/multi-query.test.ts +316 -0
- package/tests/api/db/pool-stats.test.ts +135 -0
- package/tests/api/db/profile.test.ts +330 -0
- package/tests/api/db/provider-meta.test.ts +193 -0
- package/tests/api/db/query.test.ts +314 -0
- package/tests/api/db/schema-snapshot.test.ts +170 -0
- package/tests/api/db/schema.test.ts +191 -0
- package/tests/api/db/test-connection.test.ts +185 -0
- package/tests/api/db/transaction.test.ts +314 -0
- package/tests/api/proxy.test.ts +191 -0
- package/tests/api/seed/managed-route.test.ts +113 -0
- package/tests/api/storage/config.test.ts +42 -0
- package/tests/api/storage/storage-routes.test.ts +309 -0
- package/tests/components/AIAutopilotPanel.test.tsx +756 -0
- package/tests/components/AdminPage.test.tsx +33 -0
- package/tests/components/CodeGenerator.test.tsx +182 -0
- package/tests/components/CommandPalette.test.tsx +428 -0
- package/tests/components/CommunitySection.test.tsx +91 -0
- package/tests/components/ConnectionModal.mobile.test.tsx +284 -0
- package/tests/components/ConnectionModal.test.tsx +570 -0
- package/tests/components/CreateTableModal.test.tsx +383 -0
- package/tests/components/DataCharts.test.tsx +739 -0
- package/tests/components/DataImportModal.test.tsx +751 -0
- package/tests/components/DataProfiler.test.tsx +589 -0
- package/tests/components/DatabaseDocs.test.tsx +353 -0
- package/tests/components/LoginPage.test.tsx +163 -0
- package/tests/components/LoginPageOIDC.test.tsx +92 -0
- package/tests/components/MaskingSettings.test.tsx +498 -0
- package/tests/components/MobileNav.test.tsx +30 -0
- package/tests/components/MonitoringPage.test.tsx +32 -0
- package/tests/components/NL2SQLPanel.test.tsx +621 -0
- package/tests/components/Page.test.tsx +33 -0
- package/tests/components/PivotTable.test.tsx +350 -0
- package/tests/components/QueryEditor.test.tsx +1730 -0
- package/tests/components/QueryHistory.test.tsx +572 -0
- package/tests/components/QuerySafetyDialog.test.tsx +586 -0
- package/tests/components/ResultsGrid.test.tsx +804 -0
- package/tests/components/RootLayout.test.tsx +83 -0
- package/tests/components/SaveQueryModal.test.tsx +25 -0
- package/tests/components/SavedQueries.test.tsx +43 -0
- package/tests/components/SchemaDiagram.test.tsx +1034 -0
- package/tests/components/SchemaDiff.test.tsx +906 -0
- package/tests/components/SnapshotTimeline.test.tsx +174 -0
- package/tests/components/Studio.test.tsx +1030 -0
- package/tests/components/TestDataGenerator.test.tsx +291 -0
- package/tests/components/VisualExplain.test.tsx +704 -0
- package/tests/components/admin/AdminDashboard.test.tsx +205 -0
- package/tests/components/admin/AuditTab.test.tsx +220 -0
- package/tests/components/admin/MonitoringEmbed.test.tsx +58 -0
- package/tests/components/admin/OperationsTab.test.tsx +975 -0
- package/tests/components/admin/OverviewTab.test.tsx +254 -0
- package/tests/components/admin/SecurityTab.test.tsx +467 -0
- package/tests/components/monitoring/MetricChart.test.tsx +111 -0
- package/tests/components/monitoring/MonitoringDashboard.test.tsx +259 -0
- package/tests/components/monitoring/OverviewTab.test.tsx +78 -0
- package/tests/components/monitoring/PerformanceTab.test.tsx +87 -0
- package/tests/components/monitoring/PoolTab.test.tsx +42 -0
- package/tests/components/monitoring/QueriesTab.test.tsx +80 -0
- package/tests/components/monitoring/SessionsTab.test.tsx +154 -0
- package/tests/components/monitoring/StorageTab.test.tsx +127 -0
- package/tests/components/monitoring/TablesTab.test.tsx +153 -0
- package/tests/components/results-grid/ResultCard.test.tsx +105 -0
- package/tests/components/results-grid/RowDetailSheet.test.tsx +308 -0
- package/tests/components/results-grid/StatsBar.test.tsx +162 -0
- package/tests/components/schema-explorer/ColumnList.test.tsx +151 -0
- package/tests/components/schema-explorer/SchemaExplorer.test.tsx +461 -0
- package/tests/components/schema-explorer/TableItem.test.tsx +415 -0
- package/tests/components/sidebar/ConnectionItem.test.tsx +201 -0
- package/tests/components/sidebar/ConnectionsList.test.tsx +176 -0
- package/tests/components/sidebar/Sidebar.test.tsx +187 -0
- package/tests/components/studio/BottomPanel.test.tsx +383 -0
- package/tests/components/studio/QueryToolbar.test.tsx +321 -0
- package/tests/components/studio/StudioDesktopHeader.test.tsx +377 -0
- package/tests/components/studio/StudioMobileHeader.test.tsx +198 -0
- package/tests/components/studio/StudioTabBar.test.tsx +331 -0
- package/tests/fixtures/connections.ts +96 -0
- package/tests/fixtures/masking-configs.ts +86 -0
- package/tests/fixtures/query-results.ts +71 -0
- package/tests/fixtures/schemas.ts +64 -0
- package/tests/fixtures/seed-connections/invalid-config.yaml +7 -0
- package/tests/fixtures/seed-connections/minimal-config.yaml +8 -0
- package/tests/fixtures/seed-connections/mixed-credentials.yaml +23 -0
- package/tests/fixtures/seed-connections/multi-role-config.yaml +30 -0
- package/tests/fixtures/seed-connections/valid-config.json +15 -0
- package/tests/fixtures/seed-connections/valid-config.yaml +51 -0
- package/tests/helpers/mock-fetch.ts +59 -0
- package/tests/helpers/mock-monaco.ts +112 -0
- package/tests/helpers/mock-navigation.ts +28 -0
- package/tests/helpers/mock-next.ts +80 -0
- package/tests/helpers/mock-provider.ts +133 -0
- package/tests/helpers/mock-sonner.ts +29 -0
- package/tests/helpers/render-with-providers.tsx +19 -0
- package/tests/hooks/use-ai-chat.test.ts +600 -0
- package/tests/hooks/use-auth.test.ts +371 -0
- package/tests/hooks/use-connection-form.test.ts +743 -0
- package/tests/hooks/use-connection-manager.test.ts +466 -0
- package/tests/hooks/use-inline-editing.test.ts +321 -0
- package/tests/hooks/use-mobile.test.ts +177 -0
- package/tests/hooks/use-monitoring-data.test.ts +819 -0
- package/tests/hooks/use-provider-metadata.test.ts +228 -0
- package/tests/hooks/use-query-execution.test.ts +1212 -0
- package/tests/hooks/use-tab-manager.test.ts +756 -0
- package/tests/hooks/use-toast.test.ts +74 -0
- package/tests/hooks/use-transaction-control.test.ts +211 -0
- package/tests/integration/db/mongodb-provider.test.ts +698 -0
- package/tests/integration/db/mssql-provider.test.ts +840 -0
- package/tests/integration/db/mysql-provider.test.ts +872 -0
- package/tests/integration/db/oracle-provider.test.ts +843 -0
- package/tests/integration/db/postgres-provider.test.ts +1382 -0
- package/tests/integration/db/redis-provider.test.ts +526 -0
- package/tests/integration/db/sqlite-provider.test.ts +480 -0
- package/tests/integration/seed/seed-pipeline.test.ts +102 -0
- package/tests/isolated/factory-singleton.test.ts +150 -0
- package/tests/isolated/use-storage-sync.test.ts +389 -0
- package/tests/run-components.sh +196 -0
- package/tests/setup-dom.ts +58 -0
- package/tests/setup.ts +40 -0
- package/tests/unit/api-errors.test.ts +210 -0
- package/tests/unit/code-generator-functions.test.ts +271 -0
- package/tests/unit/components/column-list.test.tsx +190 -0
- package/tests/unit/components/data-import-modal.test.tsx +441 -0
- package/tests/unit/components/studio-mobile-header.test.tsx +327 -0
- package/tests/unit/data-charts-functions.test.ts +496 -0
- package/tests/unit/data-import-functions.test.ts +320 -0
- package/tests/unit/data-import-utils.test.ts +125 -0
- package/tests/unit/db/base-provider.test.ts +517 -0
- package/tests/unit/db/errors.test.ts +403 -0
- package/tests/unit/db/factory.test.ts +436 -0
- package/tests/unit/db/pool-manager.test.ts +440 -0
- package/tests/unit/db/query-limiter.test.ts +387 -0
- package/tests/unit/db/sql-base.test.ts +438 -0
- package/tests/unit/lib/api/error-codes.test.ts +39 -0
- package/tests/unit/lib/audit.test.ts +326 -0
- package/tests/unit/lib/auth.test.ts +146 -0
- package/tests/unit/lib/connection-string-parser.test.ts +424 -0
- package/tests/unit/lib/data-masking.test.ts +583 -0
- package/tests/unit/lib/db-icons.test.tsx +41 -0
- package/tests/unit/lib/monitoring-thresholds.test.ts +133 -0
- package/tests/unit/lib/oidc.test.ts +509 -0
- package/tests/unit/lib/query-generators.test.ts +127 -0
- package/tests/unit/lib/storage/factory.test.ts +71 -0
- package/tests/unit/lib/storage/local-storage.test.ts +114 -0
- package/tests/unit/lib/storage/providers/postgres.test.ts +312 -0
- package/tests/unit/lib/storage/providers/sqlite.test.ts +232 -0
- package/tests/unit/lib/storage/storage-facade-extended.test.ts +331 -0
- package/tests/unit/lib/storage/storage-facade.test.ts +184 -0
- package/tests/unit/lib/storage.test.ts +317 -0
- package/tests/unit/lib/time-series-buffer.test.ts +212 -0
- package/tests/unit/lib/utils.test.ts +24 -0
- package/tests/unit/llm/base-provider.test.ts +238 -0
- package/tests/unit/llm/config.test.ts +262 -0
- package/tests/unit/llm/custom-provider.test.ts +281 -0
- package/tests/unit/llm/gemini-provider.test.ts +248 -0
- package/tests/unit/llm/llm-factory.test.ts +155 -0
- package/tests/unit/llm/ollama-provider.test.ts +288 -0
- package/tests/unit/llm/openai-provider.test.ts +324 -0
- package/tests/unit/llm/retry.test.ts +180 -0
- package/tests/unit/llm/streaming.test.ts +355 -0
- package/tests/unit/logger.test.ts +198 -0
- package/tests/unit/mongodb-completions.test.ts +516 -0
- package/tests/unit/pivot-table-functions.test.ts +76 -0
- package/tests/unit/query-cancelled-error.test.ts +81 -0
- package/tests/unit/schema-diff/diff-engine.test.ts +367 -0
- package/tests/unit/schema-diff/migration-generator.test.ts +513 -0
- package/tests/unit/seed/config-loader.test.ts +73 -0
- package/tests/unit/seed/connection-filter.test.ts +91 -0
- package/tests/unit/seed/credential-resolver.test.ts +85 -0
- package/tests/unit/seed/index.test.ts +72 -0
- package/tests/unit/seed/resolve-connection.test.ts +74 -0
- package/tests/unit/seed/types.test.ts +129 -0
- package/tests/unit/sql/alias-extractor.test.ts +444 -0
- package/tests/unit/sql/statement-splitter.test.ts +348 -0
- package/tests/unit/sql-completions.test.ts +463 -0
- package/tests/unit/ssh-tunnel.test.ts +465 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,621 @@
|
|
|
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 } from '@testing-library/react';
|
|
8
|
+
import userEvent from '@testing-library/user-event';
|
|
9
|
+
import { NL2SQLPanel } from '@/components/NL2SQLPanel';
|
|
10
|
+
|
|
11
|
+
const defaultProps = {
|
|
12
|
+
isOpen: true as boolean,
|
|
13
|
+
onClose: mock(() => {}),
|
|
14
|
+
onExecuteQuery: mock(() => {}),
|
|
15
|
+
onLoadQuery: mock(() => {}),
|
|
16
|
+
schemaContext: '[]',
|
|
17
|
+
databaseType: undefined as string | undefined,
|
|
18
|
+
queryLanguage: undefined as string | undefined,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function renderPanel(overrides: Partial<typeof defaultProps> = {}) {
|
|
22
|
+
const user = userEvent.setup();
|
|
23
|
+
const props = { ...defaultProps, ...overrides };
|
|
24
|
+
const result = render(<NL2SQLPanel {...props} />);
|
|
25
|
+
const input = result.queryByPlaceholderText(/Ask in plain English/) as HTMLInputElement | null;
|
|
26
|
+
const form = result.container.querySelector('form');
|
|
27
|
+
return { ...result, user, input, form, props };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Helper to create a mock streaming response
|
|
31
|
+
function mockFetchStream(body: string, ok = true, errorBody?: { error: string }) {
|
|
32
|
+
const encoder = new TextEncoder();
|
|
33
|
+
const stream = new ReadableStream({
|
|
34
|
+
start(controller) {
|
|
35
|
+
controller.enqueue(encoder.encode(body));
|
|
36
|
+
controller.close();
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return mock(() =>
|
|
41
|
+
Promise.resolve({
|
|
42
|
+
ok,
|
|
43
|
+
body: ok ? stream : null,
|
|
44
|
+
json: () => Promise.resolve(errorBody || {}),
|
|
45
|
+
})
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('NL2SQLPanel', () => {
|
|
50
|
+
let originalFetch: typeof globalThis.fetch;
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
originalFetch = globalThis.fetch;
|
|
54
|
+
defaultProps.onClose = mock(() => {});
|
|
55
|
+
defaultProps.onExecuteQuery = mock(() => {});
|
|
56
|
+
defaultProps.onLoadQuery = mock(() => {});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
globalThis.fetch = originalFetch;
|
|
61
|
+
cleanup();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// -----------------------------------------------------------------------
|
|
65
|
+
// Rendering / visibility
|
|
66
|
+
// -----------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
test('does not render when isOpen is false', () => {
|
|
69
|
+
const { container } = renderPanel({ isOpen: false });
|
|
70
|
+
expect(container.textContent).toBe('');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('renders header and empty state when open', () => {
|
|
74
|
+
const { queryByText, input } = renderPanel();
|
|
75
|
+
expect(queryByText('Natural Language Query')).not.toBeNull();
|
|
76
|
+
expect(queryByText('Ask a question in plain English')).not.toBeNull();
|
|
77
|
+
expect(input).not.toBeNull();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('shows example prompt text', () => {
|
|
81
|
+
const { queryByText } = renderPanel();
|
|
82
|
+
expect(queryByText(/Show me the top 10 employees by salary/)).not.toBeNull();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// -----------------------------------------------------------------------
|
|
86
|
+
// Close button
|
|
87
|
+
// -----------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
test('calls onClose when close button clicked', async () => {
|
|
90
|
+
const onClose = mock(() => {});
|
|
91
|
+
const { container, user } = renderPanel({ onClose });
|
|
92
|
+
const headerButtons = container.querySelectorAll('.flex.items-center.gap-1 button');
|
|
93
|
+
const closeBtnEl = headerButtons[headerButtons.length - 1];
|
|
94
|
+
await user.click(closeBtnEl);
|
|
95
|
+
expect(onClose).toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// -----------------------------------------------------------------------
|
|
99
|
+
// Submit button disabled state
|
|
100
|
+
// -----------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
test('submit button is disabled when input is empty', () => {
|
|
103
|
+
const { form } = renderPanel();
|
|
104
|
+
const submitBtn = form?.querySelector('button[type="submit"]') as HTMLButtonElement;
|
|
105
|
+
expect(submitBtn.disabled).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('submit button is enabled when input has text', async () => {
|
|
109
|
+
const { form, input, user } = renderPanel();
|
|
110
|
+
await user.type(input!, 'show all users');
|
|
111
|
+
const submitBtn = form?.querySelector('button[type="submit"]') as HTMLButtonElement;
|
|
112
|
+
expect(submitBtn.disabled).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// -----------------------------------------------------------------------
|
|
116
|
+
// Submitting a question — success flow
|
|
117
|
+
// -----------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
test('sends question to API and displays user message', async () => {
|
|
120
|
+
const responseText = 'Here is your query:\n```sql\nSELECT * FROM users;\n```';
|
|
121
|
+
globalThis.fetch = mockFetchStream(responseText) as unknown as typeof fetch;
|
|
122
|
+
|
|
123
|
+
const { input, form, user, queryByText } = renderPanel();
|
|
124
|
+
await user.type(input!, 'show all users');
|
|
125
|
+
fireEvent.submit(form!);
|
|
126
|
+
|
|
127
|
+
await waitFor(() => {
|
|
128
|
+
expect(queryByText('show all users')).not.toBeNull();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('displays assistant response with extracted query', async () => {
|
|
133
|
+
const responseText = 'Here is your query:\n```sql\nSELECT * FROM users;\n```';
|
|
134
|
+
globalThis.fetch = mockFetchStream(responseText) as unknown as typeof fetch;
|
|
135
|
+
|
|
136
|
+
const { input, form, user, container } = renderPanel();
|
|
137
|
+
await user.type(input!, 'show all users');
|
|
138
|
+
fireEvent.submit(form!);
|
|
139
|
+
|
|
140
|
+
await waitFor(() => {
|
|
141
|
+
const pre = container.querySelector('pre');
|
|
142
|
+
expect(pre).not.toBeNull();
|
|
143
|
+
expect(pre!.textContent).toContain('SELECT * FROM users;');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('shows Run and Load to Editor buttons for extracted query', async () => {
|
|
148
|
+
const responseText = '```sql\nSELECT 1;\n```';
|
|
149
|
+
globalThis.fetch = mockFetchStream(responseText) as unknown as typeof fetch;
|
|
150
|
+
|
|
151
|
+
const { input, form, user, queryByText } = renderPanel();
|
|
152
|
+
await user.type(input!, 'test');
|
|
153
|
+
fireEvent.submit(form!);
|
|
154
|
+
|
|
155
|
+
await waitFor(() => {
|
|
156
|
+
expect(queryByText('Run')).not.toBeNull();
|
|
157
|
+
expect(queryByText('Load to Editor')).not.toBeNull();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('Run button calls onExecuteQuery with extracted query', async () => {
|
|
162
|
+
const responseText = '```sql\nSELECT * FROM orders;\n```';
|
|
163
|
+
const onExecuteQuery = mock(() => {});
|
|
164
|
+
globalThis.fetch = mockFetchStream(responseText) as unknown as typeof fetch;
|
|
165
|
+
|
|
166
|
+
const { input, form, user, queryByText } = renderPanel({ onExecuteQuery });
|
|
167
|
+
await user.type(input!, 'get orders');
|
|
168
|
+
fireEvent.submit(form!);
|
|
169
|
+
|
|
170
|
+
await waitFor(() => {
|
|
171
|
+
expect(queryByText('Run')).not.toBeNull();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
await user.click(queryByText('Run')!);
|
|
175
|
+
expect(onExecuteQuery).toHaveBeenCalledWith('SELECT * FROM orders;');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('Load to Editor button calls onLoadQuery with extracted query', async () => {
|
|
179
|
+
const responseText = '```sql\nSELECT id FROM items;\n```';
|
|
180
|
+
const onLoadQuery = mock(() => {});
|
|
181
|
+
globalThis.fetch = mockFetchStream(responseText) as unknown as typeof fetch;
|
|
182
|
+
|
|
183
|
+
const { input, form, user, queryByText } = renderPanel({ onLoadQuery });
|
|
184
|
+
await user.type(input!, 'get items');
|
|
185
|
+
fireEvent.submit(form!);
|
|
186
|
+
|
|
187
|
+
await waitFor(() => {
|
|
188
|
+
expect(queryByText('Load to Editor')).not.toBeNull();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
await user.click(queryByText('Load to Editor')!);
|
|
192
|
+
expect(onLoadQuery).toHaveBeenCalledWith('SELECT id FROM items;');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// -----------------------------------------------------------------------
|
|
196
|
+
// Submitting a question — error flow
|
|
197
|
+
// -----------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
test('displays error when API returns non-ok response', async () => {
|
|
200
|
+
globalThis.fetch = mockFetchStream('', false, { error: 'AI model unavailable' }) as unknown as typeof fetch;
|
|
201
|
+
|
|
202
|
+
const { input, form, user, queryByText } = renderPanel();
|
|
203
|
+
await user.type(input!, 'test query');
|
|
204
|
+
fireEvent.submit(form!);
|
|
205
|
+
|
|
206
|
+
await waitFor(() => {
|
|
207
|
+
expect(queryByText('AI model unavailable')).not.toBeNull();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// -----------------------------------------------------------------------
|
|
212
|
+
// Empty/whitespace question
|
|
213
|
+
// -----------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
test('does not submit empty question', async () => {
|
|
216
|
+
globalThis.fetch = mock(() => Promise.resolve({ ok: true })) as unknown as typeof fetch;
|
|
217
|
+
|
|
218
|
+
const { form } = renderPanel();
|
|
219
|
+
fireEvent.submit(form!);
|
|
220
|
+
|
|
221
|
+
// fetch should not be called
|
|
222
|
+
expect(globalThis.fetch).not.toHaveBeenCalled();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// -----------------------------------------------------------------------
|
|
226
|
+
// Question counter
|
|
227
|
+
// -----------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
test('shows question count after messages exist', async () => {
|
|
230
|
+
const responseText = '```sql\nSELECT 1;\n```';
|
|
231
|
+
globalThis.fetch = mockFetchStream(responseText) as unknown as typeof fetch;
|
|
232
|
+
|
|
233
|
+
const { input, form, user, queryByText } = renderPanel();
|
|
234
|
+
await user.type(input!, 'first question');
|
|
235
|
+
fireEvent.submit(form!);
|
|
236
|
+
|
|
237
|
+
await waitFor(() => {
|
|
238
|
+
expect(queryByText('1 questions')).not.toBeNull();
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// -----------------------------------------------------------------------
|
|
243
|
+
// Clear conversation
|
|
244
|
+
// -----------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
test('clear button removes all messages', async () => {
|
|
247
|
+
const responseText = 'answer';
|
|
248
|
+
globalThis.fetch = mockFetchStream(responseText) as unknown as typeof fetch;
|
|
249
|
+
|
|
250
|
+
const { input, form, user, queryByText, container } = renderPanel();
|
|
251
|
+
await user.type(input!, 'hello');
|
|
252
|
+
fireEvent.submit(form!);
|
|
253
|
+
|
|
254
|
+
await waitFor(() => {
|
|
255
|
+
expect(queryByText('hello')).not.toBeNull();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Find and click the clear/trash button
|
|
259
|
+
const trashBtn = container.querySelector('button[title="Clear conversation"]');
|
|
260
|
+
expect(trashBtn).not.toBeNull();
|
|
261
|
+
await user.click(trashBtn!);
|
|
262
|
+
|
|
263
|
+
// Messages should be cleared, empty state should return
|
|
264
|
+
expect(queryByText('hello')).toBeNull();
|
|
265
|
+
expect(queryByText('Ask a question in plain English')).not.toBeNull();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// -----------------------------------------------------------------------
|
|
269
|
+
// extractCodeBlock (tested through component behavior)
|
|
270
|
+
// -----------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
test('extracts SQL code block from response', async () => {
|
|
273
|
+
const responseText = 'Try this:\n```sql\nSELECT name FROM employees;\n```\nThis gets all names.';
|
|
274
|
+
globalThis.fetch = mockFetchStream(responseText) as unknown as typeof fetch;
|
|
275
|
+
|
|
276
|
+
const { input, form, user, container } = renderPanel();
|
|
277
|
+
await user.type(input!, 'get names');
|
|
278
|
+
fireEvent.submit(form!);
|
|
279
|
+
|
|
280
|
+
await waitFor(() => {
|
|
281
|
+
const pre = container.querySelector('pre');
|
|
282
|
+
expect(pre!.textContent).toBe('SELECT name FROM employees;');
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('extracts JSON code block from response', async () => {
|
|
287
|
+
const responseText = '```json\n{"collection":"users","operation":"find"}\n```';
|
|
288
|
+
globalThis.fetch = mockFetchStream(responseText) as unknown as typeof fetch;
|
|
289
|
+
|
|
290
|
+
const { input, form, user, container } = renderPanel();
|
|
291
|
+
await user.type(input!, 'find users');
|
|
292
|
+
fireEvent.submit(form!);
|
|
293
|
+
|
|
294
|
+
await waitFor(() => {
|
|
295
|
+
const pre = container.querySelector('pre');
|
|
296
|
+
expect(pre!.textContent).toContain('"collection":"users"');
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test('extracts mongodb code block from response', async () => {
|
|
301
|
+
const responseText = '```mongodb\ndb.users.find({})\n```';
|
|
302
|
+
globalThis.fetch = mockFetchStream(responseText) as unknown as typeof fetch;
|
|
303
|
+
|
|
304
|
+
const { input, form, user, container } = renderPanel();
|
|
305
|
+
await user.type(input!, 'show users');
|
|
306
|
+
fireEvent.submit(form!);
|
|
307
|
+
|
|
308
|
+
await waitFor(() => {
|
|
309
|
+
const pre = container.querySelector('pre');
|
|
310
|
+
expect(pre!.textContent).toBe('db.users.find({})');
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('shows explanation text without code block markup', async () => {
|
|
315
|
+
const responseText = 'Here is the query:\n```sql\nSELECT 1;\n```\nThis selects the number one.';
|
|
316
|
+
globalThis.fetch = mockFetchStream(responseText) as unknown as typeof fetch;
|
|
317
|
+
|
|
318
|
+
const { input, form, user, queryByText } = renderPanel();
|
|
319
|
+
await user.type(input!, 'test');
|
|
320
|
+
fireEvent.submit(form!);
|
|
321
|
+
|
|
322
|
+
await waitFor(() => {
|
|
323
|
+
expect(queryByText(/This selects the number one/)).not.toBeNull();
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test('handles response without code block (no Run/Load buttons)', async () => {
|
|
328
|
+
const responseText = 'I need more information about your schema to generate a query.';
|
|
329
|
+
globalThis.fetch = mockFetchStream(responseText) as unknown as typeof fetch;
|
|
330
|
+
|
|
331
|
+
const { input, form, user, queryByText } = renderPanel();
|
|
332
|
+
await user.type(input!, 'do something');
|
|
333
|
+
fireEvent.submit(form!);
|
|
334
|
+
|
|
335
|
+
await waitFor(() => {
|
|
336
|
+
expect(queryByText(/I need more information/)).not.toBeNull();
|
|
337
|
+
});
|
|
338
|
+
expect(queryByText('Run')).toBeNull();
|
|
339
|
+
expect(queryByText('Load to Editor')).toBeNull();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// -----------------------------------------------------------------------
|
|
343
|
+
// Schema context filtering
|
|
344
|
+
// -----------------------------------------------------------------------
|
|
345
|
+
|
|
346
|
+
test('sends filtered schema context to API', async () => {
|
|
347
|
+
const schema = JSON.stringify([
|
|
348
|
+
{ name: 'users', rowCount: 100, columns: [{ name: 'id', type: 'int', isPrimary: true }] },
|
|
349
|
+
{ name: 'orders', rowCount: 500, columns: [{ name: 'id', type: 'int' }] },
|
|
350
|
+
]);
|
|
351
|
+
globalThis.fetch = mockFetchStream('response') as unknown as typeof fetch;
|
|
352
|
+
|
|
353
|
+
const { input, form, user } = renderPanel({ schemaContext: schema });
|
|
354
|
+
await user.type(input!, 'test');
|
|
355
|
+
fireEvent.submit(form!);
|
|
356
|
+
|
|
357
|
+
await waitFor(() => {
|
|
358
|
+
expect(globalThis.fetch).toHaveBeenCalled();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const fetchCall = (globalThis.fetch as unknown as ReturnType<typeof mock>).mock.calls[0] as unknown as [string, RequestInit];
|
|
362
|
+
const body = JSON.parse(fetchCall[1].body as string);
|
|
363
|
+
expect(body.question).toBe('test');
|
|
364
|
+
// Schema should be formatted, with orders first (higher rowCount)
|
|
365
|
+
expect(body.schemaContext).toContain('orders');
|
|
366
|
+
expect(body.schemaContext).toContain('users');
|
|
367
|
+
expect(body.schemaContext.indexOf('orders')).toBeLessThan(body.schemaContext.indexOf('users'));
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test('handles invalid schema JSON gracefully (truncates to 3000 chars)', async () => {
|
|
371
|
+
globalThis.fetch = mockFetchStream('response') as unknown as typeof fetch;
|
|
372
|
+
|
|
373
|
+
const { input, form, user } = renderPanel({ schemaContext: 'not valid json' });
|
|
374
|
+
await user.type(input!, 'test');
|
|
375
|
+
fireEvent.submit(form!);
|
|
376
|
+
|
|
377
|
+
await waitFor(() => {
|
|
378
|
+
expect(globalThis.fetch).toHaveBeenCalled();
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const fetchCall = (globalThis.fetch as unknown as ReturnType<typeof mock>).mock.calls[0] as unknown as [string, RequestInit];
|
|
382
|
+
const body = JSON.parse(fetchCall[1].body as string);
|
|
383
|
+
expect(body.schemaContext).toBe('not valid json');
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// -----------------------------------------------------------------------
|
|
387
|
+
// Database type and query language
|
|
388
|
+
// -----------------------------------------------------------------------
|
|
389
|
+
|
|
390
|
+
test('passes databaseType and queryLanguage to API', async () => {
|
|
391
|
+
globalThis.fetch = mockFetchStream('response') as unknown as typeof fetch;
|
|
392
|
+
|
|
393
|
+
const { input, form, user } = renderPanel({
|
|
394
|
+
databaseType: 'postgres',
|
|
395
|
+
queryLanguage: 'sql',
|
|
396
|
+
});
|
|
397
|
+
await user.type(input!, 'test');
|
|
398
|
+
fireEvent.submit(form!);
|
|
399
|
+
|
|
400
|
+
await waitFor(() => {
|
|
401
|
+
expect(globalThis.fetch).toHaveBeenCalled();
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const fetchCall = (globalThis.fetch as unknown as ReturnType<typeof mock>).mock.calls[0] as unknown as [string, RequestInit];
|
|
405
|
+
const body = JSON.parse(fetchCall[1].body as string);
|
|
406
|
+
expect(body.databaseType).toBe('postgres');
|
|
407
|
+
expect(body.queryLanguage).toBe('sql');
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// -----------------------------------------------------------------------
|
|
411
|
+
// Loading state
|
|
412
|
+
// -----------------------------------------------------------------------
|
|
413
|
+
|
|
414
|
+
test('shows loading indicator while waiting for response', async () => {
|
|
415
|
+
// Create a fetch that never resolves
|
|
416
|
+
globalThis.fetch = mock(() => new Promise(() => {})) as unknown as typeof fetch;
|
|
417
|
+
|
|
418
|
+
const { input, form, user, queryByText } = renderPanel();
|
|
419
|
+
await user.type(input!, 'test');
|
|
420
|
+
fireEvent.submit(form!);
|
|
421
|
+
|
|
422
|
+
await waitFor(() => {
|
|
423
|
+
expect(queryByText('Generating query...')).not.toBeNull();
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test('disables input while loading', async () => {
|
|
428
|
+
globalThis.fetch = mock(() => new Promise(() => {})) as unknown as typeof fetch;
|
|
429
|
+
|
|
430
|
+
const { input, form, user } = renderPanel();
|
|
431
|
+
await user.type(input!, 'test');
|
|
432
|
+
fireEvent.submit(form!);
|
|
433
|
+
|
|
434
|
+
await waitFor(() => {
|
|
435
|
+
expect(input!.disabled).toBe(true);
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// -----------------------------------------------------------------------
|
|
440
|
+
// conversationHistory sent in second API request
|
|
441
|
+
// -----------------------------------------------------------------------
|
|
442
|
+
|
|
443
|
+
test('sends conversationHistory with prior messages on second request', async () => {
|
|
444
|
+
const responseText1 = '```sql\nSELECT 1;\n```';
|
|
445
|
+
const responseText2 = '```sql\nSELECT 2;\n```';
|
|
446
|
+
let callCount = 0;
|
|
447
|
+
globalThis.fetch = mock(() => {
|
|
448
|
+
callCount++;
|
|
449
|
+
const text = callCount === 1 ? responseText1 : responseText2;
|
|
450
|
+
const encoder = new TextEncoder();
|
|
451
|
+
const stream = new ReadableStream({
|
|
452
|
+
start(controller) {
|
|
453
|
+
controller.enqueue(encoder.encode(text));
|
|
454
|
+
controller.close();
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
return Promise.resolve({ ok: true, body: stream });
|
|
458
|
+
}) as unknown as typeof fetch;
|
|
459
|
+
|
|
460
|
+
const { input, form, user } = renderPanel();
|
|
461
|
+
|
|
462
|
+
// First question
|
|
463
|
+
await user.type(input!, 'first question');
|
|
464
|
+
fireEvent.submit(form!);
|
|
465
|
+
await waitFor(() => {
|
|
466
|
+
expect(callCount).toBe(1);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// Second question
|
|
470
|
+
await user.type(input!, 'second question');
|
|
471
|
+
fireEvent.submit(form!);
|
|
472
|
+
await waitFor(() => {
|
|
473
|
+
expect(callCount).toBe(2);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
const secondCall = (globalThis.fetch as unknown as ReturnType<typeof mock>).mock.calls[1] as unknown as [string, RequestInit];
|
|
477
|
+
const body = JSON.parse(secondCall[1].body as string);
|
|
478
|
+
expect(body.conversationHistory).toBeDefined();
|
|
479
|
+
expect(body.conversationHistory.length).toBe(2);
|
|
480
|
+
expect(body.conversationHistory[0]).toEqual({ role: 'user', content: 'first question' });
|
|
481
|
+
expect(body.conversationHistory[1].role).toBe('assistant');
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// -----------------------------------------------------------------------
|
|
485
|
+
// Question count display after multiple messages
|
|
486
|
+
// -----------------------------------------------------------------------
|
|
487
|
+
|
|
488
|
+
test('displays correct question count after multiple exchanges', async () => {
|
|
489
|
+
let callCount = 0;
|
|
490
|
+
globalThis.fetch = mock(() => {
|
|
491
|
+
callCount++;
|
|
492
|
+
const text = `answer ${callCount}`;
|
|
493
|
+
const encoder = new TextEncoder();
|
|
494
|
+
const stream = new ReadableStream({
|
|
495
|
+
start(controller) {
|
|
496
|
+
controller.enqueue(encoder.encode(text));
|
|
497
|
+
controller.close();
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
return Promise.resolve({ ok: true, body: stream });
|
|
501
|
+
}) as unknown as typeof fetch;
|
|
502
|
+
|
|
503
|
+
const { input, form, user, queryByText } = renderPanel();
|
|
504
|
+
|
|
505
|
+
// First question
|
|
506
|
+
await user.type(input!, 'q1');
|
|
507
|
+
fireEvent.submit(form!);
|
|
508
|
+
await waitFor(() => {
|
|
509
|
+
expect(queryByText('1 questions')).not.toBeNull();
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// Second question
|
|
513
|
+
await user.type(input!, 'q2');
|
|
514
|
+
fireEvent.submit(form!);
|
|
515
|
+
await waitFor(() => {
|
|
516
|
+
expect(queryByText('2 questions')).not.toBeNull();
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// -----------------------------------------------------------------------
|
|
521
|
+
// Clear conversation button visibility
|
|
522
|
+
// -----------------------------------------------------------------------
|
|
523
|
+
|
|
524
|
+
test('clear conversation button is visible when messages exist', async () => {
|
|
525
|
+
const responseText = 'some response';
|
|
526
|
+
globalThis.fetch = mockFetchStream(responseText) as unknown as typeof fetch;
|
|
527
|
+
|
|
528
|
+
const { input, form, user, container } = renderPanel();
|
|
529
|
+
await user.type(input!, 'hello');
|
|
530
|
+
fireEvent.submit(form!);
|
|
531
|
+
|
|
532
|
+
await waitFor(() => {
|
|
533
|
+
const clearBtn = container.querySelector('button[title="Clear conversation"]');
|
|
534
|
+
expect(clearBtn).not.toBeNull();
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
test('clear conversation button is hidden when no messages exist', () => {
|
|
539
|
+
const { container } = renderPanel();
|
|
540
|
+
const clearBtn = container.querySelector('button[title="Clear conversation"]');
|
|
541
|
+
expect(clearBtn).toBeNull();
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// -----------------------------------------------------------------------
|
|
545
|
+
// Input focus on panel open
|
|
546
|
+
// -----------------------------------------------------------------------
|
|
547
|
+
|
|
548
|
+
test('input receives focus when panel opens', () => {
|
|
549
|
+
const { input } = renderPanel({ isOpen: true });
|
|
550
|
+
expect(input).not.toBeNull();
|
|
551
|
+
expect(document.activeElement).toBe(input);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
// -----------------------------------------------------------------------
|
|
555
|
+
// Explanation text alongside code block
|
|
556
|
+
// -----------------------------------------------------------------------
|
|
557
|
+
|
|
558
|
+
test('renders both code block and explanation text in assistant message', async () => {
|
|
559
|
+
const responseText = 'Here is the query:\n```sql\nSELECT id FROM products;\n```\nThis returns all product IDs from the table.';
|
|
560
|
+
globalThis.fetch = mockFetchStream(responseText) as unknown as typeof fetch;
|
|
561
|
+
|
|
562
|
+
const { input, form, user, container, queryByText } = renderPanel();
|
|
563
|
+
await user.type(input!, 'get product ids');
|
|
564
|
+
fireEvent.submit(form!);
|
|
565
|
+
|
|
566
|
+
await waitFor(() => {
|
|
567
|
+
// Code block is rendered
|
|
568
|
+
const pre = container.querySelector('pre');
|
|
569
|
+
expect(pre).not.toBeNull();
|
|
570
|
+
expect(pre!.textContent).toBe('SELECT id FROM products;');
|
|
571
|
+
// Explanation text is also rendered
|
|
572
|
+
expect(queryByText(/This returns all product IDs/)).not.toBeNull();
|
|
573
|
+
// Code block markup is stripped from explanation
|
|
574
|
+
expect(queryByText(/```/)).toBeNull();
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
// -----------------------------------------------------------------------
|
|
579
|
+
// Schema context parse failure fallback — truncation
|
|
580
|
+
// -----------------------------------------------------------------------
|
|
581
|
+
|
|
582
|
+
test('truncates invalid schema context to 3000 characters', async () => {
|
|
583
|
+
const longInvalidSchema = 'x'.repeat(5000);
|
|
584
|
+
globalThis.fetch = mockFetchStream('response') as unknown as typeof fetch;
|
|
585
|
+
|
|
586
|
+
const { input, form, user } = renderPanel({ schemaContext: longInvalidSchema });
|
|
587
|
+
await user.type(input!, 'test');
|
|
588
|
+
fireEvent.submit(form!);
|
|
589
|
+
|
|
590
|
+
await waitFor(() => {
|
|
591
|
+
expect(globalThis.fetch).toHaveBeenCalled();
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
const fetchCall = (globalThis.fetch as unknown as ReturnType<typeof mock>).mock.calls[0] as unknown as [string, RequestInit];
|
|
595
|
+
const body = JSON.parse(fetchCall[1].body as string);
|
|
596
|
+
expect(body.schemaContext.length).toBe(3000);
|
|
597
|
+
expect(body.schemaContext).toBe('x'.repeat(3000));
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
// -----------------------------------------------------------------------
|
|
601
|
+
// Non-ok response with fallback error message
|
|
602
|
+
// -----------------------------------------------------------------------
|
|
603
|
+
|
|
604
|
+
test('displays fallback error message when API error has no error field', async () => {
|
|
605
|
+
globalThis.fetch = mock(() =>
|
|
606
|
+
Promise.resolve({
|
|
607
|
+
ok: false,
|
|
608
|
+
body: null,
|
|
609
|
+
json: () => Promise.resolve({}),
|
|
610
|
+
})
|
|
611
|
+
) as unknown as typeof fetch;
|
|
612
|
+
|
|
613
|
+
const { input, form, user, queryByText } = renderPanel();
|
|
614
|
+
await user.type(input!, 'test query');
|
|
615
|
+
fireEvent.submit(form!);
|
|
616
|
+
|
|
617
|
+
await waitFor(() => {
|
|
618
|
+
expect(queryByText('Request failed')).not.toBeNull();
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import '../setup-dom';
|
|
2
|
+
import { mock } from 'bun:test';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
// Mock Studio to avoid its massive dependency tree
|
|
6
|
+
mock.module('@/components/Studio', () => ({
|
|
7
|
+
default: () => React.createElement('div', { 'data-testid': 'studio' }, 'Studio Mock'),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
const { default: Page } = await import('@/app/page');
|
|
11
|
+
|
|
12
|
+
import { afterEach, describe, expect, test } from 'bun:test';
|
|
13
|
+
import { cleanup, render } from '@testing-library/react';
|
|
14
|
+
|
|
15
|
+
describe('Page', () => {
|
|
16
|
+
afterEach(() => { cleanup(); });
|
|
17
|
+
|
|
18
|
+
test('renders Studio component', () => {
|
|
19
|
+
const { getByTestId } = render(<Page />);
|
|
20
|
+
expect(getByTestId('studio')).not.toBeNull();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('renders Studio content', () => {
|
|
24
|
+
const { getByText } = render(<Page />);
|
|
25
|
+
expect(getByText('Studio Mock')).not.toBeNull();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('is a valid React component (returns JSX)', () => {
|
|
29
|
+
const element = Page();
|
|
30
|
+
expect(element).not.toBeNull();
|
|
31
|
+
expect(element.type).toBeDefined();
|
|
32
|
+
});
|
|
33
|
+
});
|