diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000..2b2bee58
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,133 @@
+name: Release
+on:
+ push:
+ tags:
+ - 'v*'
+
+jobs:
+ release-linux:
+ permissions:
+ contents: write
+ runs-on: ubuntu-22.04
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: setup node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+
+ - name: setup bun
+ uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: latest
+
+ - name: install rust stable
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: install dependencies (ubuntu)
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
+
+ - name: install frontend dependencies
+ run: bun install
+ working-directory: ./apps/desktop
+
+ - name: build frontend
+ run: bun run build
+ working-directory: ./apps/desktop
+
+ - uses: tauri-apps/tauri-action@v0
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ projectPath: ./apps/desktop
+ tagName: ${{ github.ref_name }}
+ releaseName: 'App v__VERSION__'
+ releaseBody: 'See the assets to download this version and install.'
+ releaseDraft: true
+ prerelease: false
+
+ release-windows:
+ permissions:
+ contents: write
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: setup node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+
+ - name: setup bun
+ uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: latest
+
+ - name: install rust stable
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: install frontend dependencies
+ run: bun install
+ working-directory: ./apps/desktop
+
+ - name: build frontend
+ run: bun run build
+ working-directory: ./apps/desktop
+
+ - uses: tauri-apps/tauri-action@v0
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ projectPath: ./apps/desktop
+ tagName: ${{ github.ref_name }}
+ releaseName: 'App v__VERSION__'
+ releaseBody: 'See the assets to download this version and install.'
+ releaseDraft: true
+ prerelease: false
+
+ release-macos:
+ permissions:
+ contents: write
+ runs-on: macos-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: setup node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+
+ - name: setup bun
+ uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: latest
+
+ - name: install rust stable
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: install frontend dependencies
+ run: bun install
+ working-directory: ./apps/desktop
+
+ - name: build frontend
+ run: bun run build
+ working-directory: ./apps/desktop
+
+ - uses: tauri-apps/tauri-action@v0
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
+ APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
+ APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
+ APPLE_ID: ${{ secrets.APPLE_ID }}
+ APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
+ with:
+ projectPath: ./apps/desktop
+ tagName: ${{ github.ref_name }}
+ releaseName: 'App v__VERSION__'
+ releaseBody: 'See the assets to download this version and install.'
+ releaseDraft: true
+ prerelease: false
diff --git a/__tests__/apps/desktop/features/drizzle-runner/lsp-patterns.test.ts b/__tests__/apps/desktop/features/drizzle-runner/lsp-patterns.test.ts
index 015d91fd..184a958b 100644
--- a/__tests__/apps/desktop/features/drizzle-runner/lsp-patterns.test.ts
+++ b/__tests__/apps/desktop/features/drizzle-runner/lsp-patterns.test.ts
@@ -11,7 +11,8 @@ import {
isInsideJoinParens,
getTableMatch,
getColumnMatch
-} from '../../../../src/features/drizzle-runner/utils/lsp-patterns'
+} from '../../../../../apps/desktop/src/features/drizzle-runner/utils/lsp-patterns'
+
describe('Drizzle LSP Patterns', () => {
describe('getDbName', () => {
diff --git a/__tests__/apps/desktop/src/features/docker-manager/api/container-service.test.ts b/__tests__/apps/desktop/src/features/docker-manager/api/container-service.test.ts
index 2ed960af..ed6c9295 100644
--- a/__tests__/apps/desktop/src/features/docker-manager/api/container-service.test.ts
+++ b/__tests__/apps/desktop/src/features/docker-manager/api/container-service.test.ts
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
-import { seedDatabase } from '../../../../../../../apps/desktop/src/features/docker-manager/api/container-service'
+
// Mock the docker-client module
vi.mock('../../../../../../../apps/desktop/src/features/docker-manager/api/docker-client', () => ({
@@ -10,15 +10,36 @@ vi.mock('../../../../../../../apps/desktop/src/features/docker-manager/api/docke
}))
describe('seedDatabase', () => {
- beforeEach(() => {
+ // We need to keep a reference to the dynamically imported module
+ let containerService: any;
+ let dockerClient: any;
+
+ beforeEach(async () => {
+ vi.resetModules() // Important: clear cache so isTauri is re-evaluated
vi.clearAllMocks()
+
+ // Mock Tauri environment to ensure we test the real implementation
+ Object.defineProperty(window, '__TAURI_INTERNALS__', {
+ value: {},
+ writable: true,
+ configurable: true
+ })
+
+ // Re-import modules after setting up environment
+ dockerClient = await import('../../../../../../../apps/desktop/src/features/docker-manager/api/docker-client')
+ containerService = await import('../../../../../../../apps/desktop/src/features/docker-manager/api/container-service')
+ })
+
+ afterEach(() => {
+ // Cleanup environment
+ // @ts-ignore
+ delete window['__TAURI_INTERNALS__']
})
it('should successfully copy, exec, and cleanup', async () => {
- const { copyToContainer, execCommand } =
- await import('../../../../../../../apps/desktop/src/features/docker-manager/api/docker-client')
+ const { copyToContainer, execCommand } = dockerClient
- const result = await seedDatabase('container-123', '/path/to/seed.sql', {
+ const result = await containerService.seedDatabase('container-123', '/path/to/seed.sql', {
user: 'testuser',
database: 'testdb'
})
@@ -48,15 +69,15 @@ describe('seedDatabase', () => {
})
it('should handle exec failure', async () => {
- const { execCommand } =
- await import('../../../../../../../apps/desktop/src/features/docker-manager/api/docker-client')
+ const { execCommand } = dockerClient
+
vi.mocked(execCommand).mockResolvedValueOnce({
stdout: '',
stderr: 'psql error',
exitCode: 1
})
- const result = await seedDatabase('container-123', 'file.sql', { user: 'u', database: 'd' })
+ const result = await containerService.seedDatabase('container-123', 'file.sql', { user: 'u', database: 'd' })
expect(result.success).toBe(false)
expect(result.error).toContain('psql error')
diff --git a/__tests__/apps/desktop/src/features/docker-manager/api/docker-client.test.ts b/__tests__/apps/desktop/src/features/docker-manager/api/docker-client.test.ts
index 6352b263..8f76c9fb 100644
--- a/__tests__/apps/desktop/src/features/docker-manager/api/docker-client.test.ts
+++ b/__tests__/apps/desktop/src/features/docker-manager/api/docker-client.test.ts
@@ -4,42 +4,20 @@ import {
checkDockerAvailability,
startContainer,
stopContainer,
- removeContainer
+ removeContainer,
+ deps
} from '../../../../../../../apps/desktop/src/features/docker-manager/api/docker-client'
-// Mock the Tauri shell plugin
-const { mockExecute, mockCreate } = vi.hoisted(() => {
- const execute = vi.fn(() => Promise.resolve({ stdout: '', stderr: '', code: 0 }))
- const create = vi.fn(() => ({
- execute: execute,
- on: vi.fn(),
- spawn: vi.fn().mockResolvedValue({
- kill: vi.fn()
- }),
- stdout: { on: vi.fn() },
- stderr: { on: vi.fn() }
- }))
- return { mockExecute: execute, mockCreate: create }
-})
-
-vi.mock('@tauri-apps/plugin-shell', () => ({
- Command: {
- create: (cmd: string, args: string[]) => mockCreate(cmd, args)
- }
-}))
-
-// Mock Tauri API core directly to prevent "invoke" errors if leakage occurs
+// Mock Tauri API core
vi.mock('@tauri-apps/api/core', () => ({
invoke: vi.fn().mockResolvedValue('')
}))
-// Mock window to simulate Tauri environment
Object.defineProperty(window, 'Tauri', {
value: {},
writable: true
})
-// Mock Tauri internals required by some real modules if they slip through
Object.defineProperty(window, '__TAURI_INTERNALS__', {
value: {
invoke: vi.fn()
@@ -48,9 +26,27 @@ Object.defineProperty(window, '__TAURI_INTERNALS__', {
})
describe('docker-client', () => {
+ let mockExecute: any
+ let mockCreate: any
+
beforeEach(() => {
vi.clearAllMocks()
- mockExecute.mockResolvedValue({ stdout: '', stderr: '', code: 0 })
+ mockExecute = vi.fn().mockResolvedValue({ stdout: '', stderr: '', code: 0 })
+ mockCreate = vi.fn()
+
+ // Inject mock
+ deps.getCommand = async () => ({
+ create: (cmd: string, args: string[]) => {
+ mockCreate(cmd, args)
+ return {
+ execute: mockExecute,
+ on: vi.fn(),
+ spawn: vi.fn().mockResolvedValue({ kill: vi.fn() }),
+ stdout: { on: vi.fn() },
+ stderr: { on: vi.fn() }
+ }
+ }
+ } as any)
})
describe('checkDockerAvailability', () => {
@@ -65,9 +61,9 @@ describe('docker-client', () => {
expect(result).toEqual({ available: true, version: '20.10.21' })
expect(mockCreate).toHaveBeenCalledWith('docker', [
- 'version',
+ 'info',
'--format',
- '{{.Server.Version}}'
+ '{{.ServerVersion}}'
])
})
@@ -86,14 +82,13 @@ describe('docker-client', () => {
describe('listContainers', () => {
it('should list and parse containers correctly including Env', async () => {
- // Mock 'ps' command
mockExecute.mockResolvedValueOnce({
- stdout: '{"ID":"123","Names":"test-container","Image":"postgres:14","State":"running","Status":"Up 2 hours","Ports":"0.0.0.0:5432->5432/tcp","Labels":"","CreatedAt":"2023-01-01"}\n',
+ stdout:
+ '{"ID":"123","Names":"test-container","Image":"postgres:14","State":"running","Status":"Up 2 hours","Ports":"0.0.0.0:5432->5432/tcp","Labels":"","CreatedAt":"2023-01-01"}\n',
stderr: '',
code: 0
})
- // Mock 'inspect' command
mockExecute.mockResolvedValueOnce({
stdout: JSON.stringify([
{
diff --git a/__tests__/apps/desktop/src/features/docker-manager/components/docker-view.test.tsx b/__tests__/apps/desktop/src/features/docker-manager/components/docker-view.test.tsx
index 65255e84..96f6db09 100644
--- a/__tests__/apps/desktop/src/features/docker-manager/components/docker-view.test.tsx
+++ b/__tests__/apps/desktop/src/features/docker-manager/components/docker-view.test.tsx
@@ -1,15 +1,16 @@
-import { render, screen, fireEvent, waitFor } from '@testing-library/react'
+import { render, screen, fireEvent } from '@testing-library/react'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { DockerView } from '../../../../../../../apps/desktop/src/features/docker-manager/components/docker-view'
import * as useContainersModule from '../../../../../../../apps/desktop/src/features/docker-manager/api/queries/use-containers'
import * as useCreateContainerModule from '../../../../../../../apps/desktop/src/features/docker-manager/api/mutations/use-create-container'
+import { TooltipProvider } from '@/shared/ui/tooltip'
// Mock UI components to avoid dependency issues and focus on logic
vi.mock('@/components/ui/use-toast', () => ({
useToast: () => ({ toast: vi.fn() })
}))
-vi.mock('./container-list', () => ({
+vi.mock('../../../../../../../apps/desktop/src/features/docker-manager/components/container-list', () => ({
ContainerList: ({ containers, isLoading }: any) => (
{isLoading ? 'Loading containers...' : `Containers: ${containers?.length || 0}`}
@@ -17,15 +18,15 @@ vi.mock('./container-list', () => ({
)
}))
-vi.mock('./container-details-panel', () => ({
+vi.mock('../../../../../../../apps/desktop/src/features/docker-manager/components/container-details-panel', () => ({
ContainerDetailsPanel: () =>
}))
-vi.mock('./create-container-dialog', () => ({
+vi.mock('../../../../../../../apps/desktop/src/features/docker-manager/components/create-container-dialog', () => ({
CreateContainerDialog: ({ open }: any) => (open ?
: null)
}))
-vi.mock('./sandbox-indicator', () => ({
+vi.mock('../../../../../../../apps/desktop/src/features/docker-manager/components/sandbox-indicator', () => ({
SandboxIndicator: () =>
}))
@@ -44,6 +45,14 @@ describe('DockerView', () => {
)
})
+ const renderWithProviders = (component: any) => {
+ return render(
+
+ {component}
+
+ )
+ }
+
it('shows loading state when checking docker availability', () => {
vi.spyOn(useContainersModule, 'useDockerAvailability').mockReturnValue({
data: undefined,
@@ -58,7 +67,7 @@ describe('DockerView', () => {
// Mock useContainerSearch to return empty array
vi.spyOn(useContainersModule, 'useContainerSearch').mockReturnValue([])
- render(
)
+ renderWithProviders(
)
expect(screen.getByText('Checking Docker status...')).toBeInTheDocument()
})
@@ -75,7 +84,7 @@ describe('DockerView', () => {
vi.spyOn(useContainersModule, 'useContainerSearch').mockReturnValue([])
- render(
)
+ renderWithProviders(
)
expect(screen.getByText('Docker Not Available')).toBeInTheDocument()
expect(screen.getByText('Connection failed')).toBeInTheDocument()
})
@@ -87,8 +96,8 @@ describe('DockerView', () => {
} as any)
const mockContainers = [
- { id: '1', name: 'test-1' },
- { id: '2', name: 'test-2' }
+ { id: '1', names: ['test-1'], state: 'running', created: Date.now() },
+ { id: '2', names: ['test-2'], state: 'exited', created: Date.now() }
]
vi.spyOn(useContainersModule, 'useContainers').mockReturnValue({
@@ -99,7 +108,7 @@ describe('DockerView', () => {
// Mock search to return all containers
vi.spyOn(useContainersModule, 'useContainerSearch').mockReturnValue(mockContainers as any)
- render(
)
+ renderWithProviders(
)
expect(screen.getByText('Containers: 2')).toBeInTheDocument()
expect(screen.getByText('Docker Containers')).toBeInTheDocument()
})
@@ -117,7 +126,7 @@ describe('DockerView', () => {
vi.spyOn(useContainersModule, 'useContainerSearch').mockReturnValue([])
- render(
)
+ renderWithProviders(
)
const newButton = screen.getByText('New Container')
fireEvent.click(newButton)
diff --git a/__tests__/setup/vitest.setup.ts b/__tests__/setup/vitest.setup.ts
index 16c43313..5472f719 100644
--- a/__tests__/setup/vitest.setup.ts
+++ b/__tests__/setup/vitest.setup.ts
@@ -1,4 +1,5 @@
import { cleanup } from '@testing-library/react'
+import '@testing-library/jest-dom/vitest'
import { expect, afterEach } from 'vitest'
// Cleanup after each test
diff --git a/apps/desktop/package.json b/apps/desktop/package.json
index 7d2acfa7..da46fe35 100644
--- a/apps/desktop/package.json
+++ b/apps/desktop/package.json
@@ -19,22 +19,14 @@
"@faker-js/faker": "^10.2.0",
"@hookform/resolvers": "^3.10.0",
"@monaco-editor/react": "^4.6.0",
- "@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-alert-dialog": "^1.1.14",
- "@radix-ui/react-aspect-ratio": "^1.1.7",
- "@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15",
- "@radix-ui/react-hover-card": "^1.1.14",
"@radix-ui/react-label": "^2.1.7",
- "@radix-ui/react-menubar": "^1.1.15",
- "@radix-ui/react-navigation-menu": "^1.2.13",
"@radix-ui/react-popover": "^1.1.14",
- "@radix-ui/react-progress": "^1.1.7",
- "@radix-ui/react-radio-group": "^1.3.7",
"@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.7",
@@ -43,8 +35,6 @@
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-toast": "^1.2.14",
- "@radix-ui/react-toggle": "^1.1.9",
- "@radix-ui/react-toggle-group": "^1.1.10",
"@radix-ui/react-tooltip": "^1.2.7",
"@tanstack/react-query": "^5.83.0",
"@tauri-apps/api": "^2.9.1",
@@ -53,26 +43,18 @@
"@tauri-apps/plugin-shell": "^2.3.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
- "cmdk": "^1.1.1",
"date-fns": "^3.6.0",
- "embla-carousel-react": "^8.6.0",
"framer-motion": "^12.29.0",
- "input-otp": "^1.4.2",
"lucide-react": "^0.462.0",
"monaco-vim": "^0.4.4",
"next-themes": "^0.3.0",
"react": "^18.3.1",
- "react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
- "react-hook-form": "^7.61.1",
"react-resizable-panels": "^2.1.9",
"react-router-dom": "^6.30.1",
- "recharts": "^2.15.4",
"sonner": "^1.7.4",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
- "vaul": "^0.9.9",
- "zod": "^3.25.76",
"zustand": "^5.0.10"
},
"devDependencies": {
@@ -89,6 +71,7 @@
"happy-dom": "^20.3.7",
"postcss": "^8.5.6",
"rolldown": "^1.0.0-beta.57",
+ "rollup-plugin-visualizer": "^6.0.5",
"tailwindcss": "^4.1.18",
"typescript": "^5.8.3",
"vite": "npm:rolldown-vite@latest",
diff --git a/apps/desktop/src-tauri/src/database/commands.rs b/apps/desktop/src-tauri/src/database/commands.rs
index 403f76da..baaf1915 100644
--- a/apps/desktop/src-tauri/src/database/commands.rs
+++ b/apps/desktop/src-tauri/src/database/commands.rs
@@ -245,14 +245,13 @@ pub async fn fetch_page(
query_id: usize,
page_index: usize,
state: State<'_, AppState>,
-) -> Result