From bae86750e0a1c257275891693fd7928720e918ca Mon Sep 17 00:00:00 2001 From: haritabh-z01 Date: Wed, 12 Nov 2025 14:39:47 +0530 Subject: [PATCH] chore: barebones testing setup --- apps/desktop/forge.config.ts | 1 - apps/desktop/package.json | 13 +- apps/desktop/src/db/transcriptions.ts | 8 +- apps/desktop/tests/README.md | 121 +++++ apps/desktop/tests/helpers/electron-mocks.ts | 384 ++++++++++++++++ apps/desktop/tests/helpers/fixtures.ts | 276 +++++++++++ apps/desktop/tests/helpers/native-mocks.ts | 172 +++++++ apps/desktop/tests/helpers/test-app.ts | 121 +++++ apps/desktop/tests/helpers/test-db.ts | 116 +++++ .../tests/services/transcriptions.test.ts | 219 +++++++++ apps/desktop/tests/setup.ts | 235 ++++++++++ apps/desktop/vitest.config.ts | 28 ++ package.json | 5 +- pnpm-lock.yaml | 432 ++++++++++++++++-- turbo.json | 14 + 15 files changed, 2099 insertions(+), 46 deletions(-) create mode 100644 apps/desktop/tests/README.md create mode 100644 apps/desktop/tests/helpers/electron-mocks.ts create mode 100644 apps/desktop/tests/helpers/fixtures.ts create mode 100644 apps/desktop/tests/helpers/native-mocks.ts create mode 100644 apps/desktop/tests/helpers/test-app.ts create mode 100644 apps/desktop/tests/helpers/test-db.ts create mode 100644 apps/desktop/tests/services/transcriptions.test.ts create mode 100644 apps/desktop/tests/setup.ts create mode 100644 apps/desktop/vitest.config.ts diff --git a/apps/desktop/forge.config.ts b/apps/desktop/forge.config.ts index 1aae0a9..a965b82 100644 --- a/apps/desktop/forge.config.ts +++ b/apps/desktop/forge.config.ts @@ -39,7 +39,6 @@ export const EXTERNAL_DEPENDENCIES = [ "@libsql/win32-x64-msvc", "libsql", "onnxruntime-node", - "workerpool", "@amical/whisper-wrapper", // Add any other native modules you need here ]; diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 2bf5f96..a450d0e 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -33,6 +33,10 @@ "lint": "eslint --ext .ts,.tsx .", "format:check": "prettier --check \"**/*.{ts,tsx,md,json,mjs,mts,css,mdx}\" --cache --ignore-path=../../.prettierignore", "ts:check": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest watch", + "test:ui": "vitest --ui", + "test:coverage": "vitest run --coverage", "db:generate": "drizzle-kit generate", "db:push": "drizzle-kit push", "db:migrate": "drizzle-kit migrate", @@ -63,8 +67,10 @@ "@tailwindcss/vite": "^4.1.6", "@tanstack/react-router-devtools": "^1.131.41", "@tanstack/router-plugin": "^1.131.41", + "@types/node": "^24.10.1", "@types/react": "^19.1.12", "@types/react-dom": "^19.1.9", + "@vitest/ui": "^4.0.8", "bumpp": "^10.2.3", "electron": "38.1.0", "eslint": "^9.26.0", @@ -72,17 +78,19 @@ "eslint-plugin-import": "^2.31.0", "eslint-plugin-prettier": "^5.4.0", "flora-colossus": "^2.0.0", + "fs-extra": "^11.3.2", "prettier": "^3.5.3", "tailwindcss": "^4.1.6", "tsx": "^4.19.4", "typescript": "~5.8.3", - "vite": "^7.1.5" + "vite": "^7.1.5", + "vitest": "^4.0.8" }, "dependencies": { "@ai-sdk/openai": "^1.3.22", "@amical/eslint-config": "workspace:*", - "@amical/whisper-wrapper": "workspace:*", "@amical/types": "workspace:*", + "@amical/whisper-wrapper": "workspace:*", "@amical/y-libsql": "workspace:*", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", @@ -170,7 +178,6 @@ "update-electron-app": "^3.1.1", "uuid": "^11.1.0", "vaul": "^1.1.2", - "workerpool": "^9.3.3", "yjs": "^13.6.27", "zod": "^3.25.24" } diff --git a/apps/desktop/src/db/transcriptions.ts b/apps/desktop/src/db/transcriptions.ts index ae400e7..ad83bc6 100644 --- a/apps/desktop/src/db/transcriptions.ts +++ b/apps/desktop/src/db/transcriptions.ts @@ -1,4 +1,4 @@ -import { eq, desc, asc, and, ilike, count, gte, lte } from "drizzle-orm"; +import { eq, desc, asc, and, ilike, count, gte, lte, sql, like } from "drizzle-orm"; import { db } from "."; import { transcriptions, @@ -55,7 +55,7 @@ export async function getTranscriptions( return await db .select() .from(transcriptions) - .where(ilike(transcriptions.text, `%${search}%`)) + .where(sql`${transcriptions.text} LIKE ${`%${search}%`} COLLATE NOCASE`) .orderBy(orderFn(sortColumn)) .limit(limit) .offset(offset); @@ -113,7 +113,7 @@ export async function getTranscriptionsCount(search?: string) { const result = await db .select({ count: count() }) .from(transcriptions) - .where(ilike(transcriptions.text, `%${search}%`)); + .where(sql`${transcriptions.text} LIKE ${`%${search}%`} COLLATE NOCASE`); return result[0]?.count || 0; } else { const result = await db.select({ count: count() }).from(transcriptions); @@ -152,7 +152,7 @@ export async function searchTranscriptions(searchTerm: string, limit = 20) { return await db .select() .from(transcriptions) - .where(ilike(transcriptions.text, `%${searchTerm}%`)) + .where(sql`${transcriptions.text} LIKE ${`%${searchTerm}%`} COLLATE NOCASE`) .orderBy(desc(transcriptions.timestamp)) .limit(limit); } diff --git a/apps/desktop/tests/README.md b/apps/desktop/tests/README.md new file mode 100644 index 0000000..ced572e --- /dev/null +++ b/apps/desktop/tests/README.md @@ -0,0 +1,121 @@ +# Testing Guide + +This directory contains the test setup for the Amical Desktop application's main process. + +## Overview + +We use **Vitest** to test the Electron main process, specifically: +- **tRPC router procedures** - Direct testing by calling router methods +- **Service business logic** - Testing services with different database states +- **App initialization** - Testing how the app initializes with various database conditions + +## Architecture + +### Test Database +- Uses real SQLite databases (not mocked) +- Each test gets an isolated database in a temporary directory +- Migrations are applied automatically +- Fixtures for seeding test data + +### Mocking Strategy +- **Electron APIs** - Fully mocked (app, ipcMain, BrowserWindow, Menu, etc.) +- **Native Modules** - Mocked (onnxruntime, whisper, keytar, etc.) +- **Database** - Real SQLite with test fixtures +- **tRPC** - Called directly, bypassing IPC layer + +## Running Tests + +```bash +# Run all tests +pnpm test + +# Watch mode +pnpm test:watch + +# UI mode +pnpm test:ui + +# With coverage +pnpm test:coverage +``` + +## Writing Tests + +### Testing tRPC Procedures + +```typescript +import { createTestDatabase } from '../helpers/test-db'; +import { initializeTestServices } from '../helpers/test-app'; +import { seedDatabase } from '../helpers/fixtures'; + +describe('My Service', () => { + let testDb; + let trpcCaller; + let cleanup; + + beforeEach(async () => { + testDb = await createTestDatabase({ name: 'my-test' }); + await seedDatabase(testDb, 'withTranscriptions'); // or 'empty', 'full', etc. + + const result = await initializeTestServices(testDb); + trpcCaller = result.trpcCaller; + cleanup = result.cleanup; + }); + + afterEach(async () => { + if (cleanup) await cleanup(); + if (testDb) await testDb.close(); + }); + + it('should do something', async () => { + const result = await trpcCaller.myRouter.myProcedure({ input }); + expect(result).toBeDefined(); + }); +}); +``` + +### Available Fixtures + +- `empty` - Empty database with default settings +- `withTranscriptions` - Database with sample transcriptions +- `withVocabulary` - Database with vocabulary items +- `withModels` - Database with downloaded models +- `withNotes` - Database with notes +- `withAuth` - Database with authenticated user +- `full` - Database with all types of data + +### Custom Fixtures + +```typescript +await fixtures.withCustomSettings(testDb, { + ui: { theme: 'dark' }, + transcription: { language: 'es' } +}); +``` + +## Known Limitations + +1. **Full AppManager initialization** - Currently has issues with ServiceManager initialization. Use `initializeTestServices` instead for testing service business logic. + +2. **Some modules require additional mocking** - If you encounter errors about missing modules, add mocks to `tests/setup.ts`. + +3. **Database mocking** - The dynamic database mocking via `vi.doMock` doesn't work well with the existing module resolution. Tests work best when testing services directly rather than full app initialization. + +## Troubleshooting + +### "ServiceManager not initialized" +This means you're trying to use AppManager which requires more complex initialization. Use `initializeTestServices` to test services directly. + +### "No procedure found on path" +Check that the tRPC procedure name matches the actual router definition. Refer to `src/trpc/routers/` for available procedures. + +### "ENOENT: no such file or directory" +The test database or migrations folder might not be found. Ensure migrations exist at `src/db/migrations/`. + +## Future Improvements + +- Fix AppManager initialization for full integration tests +- Add more comprehensive fixtures +- Add test coverage reporting +- Add database state assertions helpers +- Create mock factories for complex objects diff --git a/apps/desktop/tests/helpers/electron-mocks.ts b/apps/desktop/tests/helpers/electron-mocks.ts new file mode 100644 index 0000000..3f691f1 --- /dev/null +++ b/apps/desktop/tests/helpers/electron-mocks.ts @@ -0,0 +1,384 @@ +import { vi } from 'vitest'; +import { EventEmitter } from 'events'; +import path from 'node:path'; +import os from 'node:os'; + +// Create a fake BrowserWindow class +class FakeBrowserWindow extends EventEmitter { + id: number; + webContents: any; + private _isDestroyed = false; + private _bounds = { x: 0, y: 0, width: 800, height: 600 }; + private _isVisible = false; + private _isMinimized = false; + private _isMaximized = false; + private _isFocused = false; + private _isFullScreen = false; + + constructor(options?: any) { + super(); + this.id = Math.floor(Math.random() * 1000000); + + // Mock webContents + this.webContents = { + send: vi.fn(), + on: vi.fn(), + once: vi.fn(), + removeListener: vi.fn(), + session: { + clearCache: vi.fn().mockResolvedValue(undefined), + clearStorageData: vi.fn().mockResolvedValue(undefined), + }, + openDevTools: vi.fn(), + closeDevTools: vi.fn(), + isDevToolsOpened: vi.fn().mockReturnValue(false), + executeJavaScript: vi.fn().mockResolvedValue(undefined), + setWindowOpenHandler: vi.fn(), + setBackgroundThrottling: vi.fn(), + }; + } + + loadURL(url: string) { + return Promise.resolve(); + } + + loadFile(filePath: string) { + return Promise.resolve(); + } + + show() { + this._isVisible = true; + } + + hide() { + this._isVisible = false; + } + + close() { + this._isDestroyed = true; + this.emit('closed'); + } + + destroy() { + this._isDestroyed = true; + } + + isDestroyed() { + return this._isDestroyed; + } + + isVisible() { + return this._isVisible; + } + + isMinimized() { + return this._isMinimized; + } + + isMaximized() { + return this._isMaximized; + } + + isFocused() { + return this._isFocused; + } + + isFullScreen() { + return this._isFullScreen; + } + + focus() { + this._isFocused = true; + } + + blur() { + this._isFocused = false; + } + + minimize() { + this._isMinimized = true; + } + + maximize() { + this._isMaximized = true; + } + + restore() { + this._isMinimized = false; + this._isMaximized = false; + } + + setFullScreen(flag: boolean) { + this._isFullScreen = flag; + } + + getBounds() { + return { ...this._bounds }; + } + + setBounds(bounds: Partial) { + this._bounds = { ...this._bounds, ...bounds }; + } + + setSize(width: number, height: number) { + this._bounds.width = width; + this._bounds.height = height; + } + + setPosition(x: number, y: number) { + this._bounds.x = x; + this._bounds.y = y; + } + + center() { + // Mock centering + } + + setResizable(resizable: boolean) {} + setMovable(movable: boolean) {} + setMinimizable(minimizable: boolean) {} + setMaximizable(maximizable: boolean) {} + setFullScreenable(fullscreenable: boolean) {} + setClosable(closable: boolean) {} + setAlwaysOnTop(flag: boolean, level?: string) {} + setVisibleOnAllWorkspaces(visible: boolean) {} + setIgnoreMouseEvents(ignore: boolean) {} + setContentProtection(enable: boolean) {} + setFocusable(focusable: boolean) {} + setParentWindow(parent: any) {} + setTitle(title: string) {} + setTitleBarOverlay(options: any) {} + setOpacity(opacity: number) {} + setShape(rects: any[]) {} + setSkipTaskbar(skip: boolean) {} + setMenu(menu: any) {} + setAutoHideMenuBar(hide: boolean) {} + setMenuBarVisibility(visible: boolean) {} + setAspectRatio(aspectRatio: number) {} + setBackgroundColor(color: string) {} + setHasShadow(hasShadow: boolean) {} + setRepresentedFilename(filename: string) {} + setDocumentEdited(edited: boolean) {} + setIcon(icon: any) {} + setProgressBar(progress: number) {} + setOverlayIcon(overlay: any, description: string) {} + setThumbarButtons(buttons: any[]) {} + setThumbnailClip(region: any) {} + setThumbnailToolTip(toolTip: string) {} + setAppDetails(options: any) {} + setVibrancy(type: string) {} + setWindowButtonVisibility(visible: boolean) {} + setTrafficLightPosition(position: { x: number; y: number }) {} + + // Mock methods that return values + getTitle() { return 'Test Window'; } + getNativeWindowHandle() { return Buffer.from('test'); } + getMediaSourceId() { return 'test-id'; } +} + +// Create test directories +const testUserDataPath = path.join(os.tmpdir(), 'amical-test-' + Date.now()); +const testAppPath = process.cwd(); + +// Mock app object +const mockApp = { + getPath: vi.fn((name: string) => { + const paths: Record = { + userData: testUserDataPath, + appData: testUserDataPath, + temp: os.tmpdir(), + home: os.homedir(), + documents: path.join(os.homedir(), 'Documents'), + downloads: path.join(os.homedir(), 'Downloads'), + desktop: path.join(os.homedir(), 'Desktop'), + music: path.join(os.homedir(), 'Music'), + pictures: path.join(os.homedir(), 'Pictures'), + videos: path.join(os.homedir(), 'Videos'), + logs: path.join(testUserDataPath, 'logs'), + crashDumps: path.join(testUserDataPath, 'crashDumps'), + }; + return paths[name] || testUserDataPath; + }), + getName: vi.fn(() => 'Amical'), + getVersion: vi.fn(() => '0.1.0-test'), + isPackaged: false, + isReady: vi.fn(() => true), + whenReady: vi.fn(() => Promise.resolve()), + quit: vi.fn(), + exit: vi.fn(), + relaunch: vi.fn(), + focus: vi.fn(), + hide: vi.fn(), + show: vi.fn(), + setName: vi.fn(), + setPath: vi.fn(), + getLocale: vi.fn(() => 'en-US'), + getLocaleCountryCode: vi.fn(() => 'US'), + getSystemLocale: vi.fn(() => 'en-US'), + on: vi.fn(), + once: vi.fn(), + removeListener: vi.fn(), + removeAllListeners: vi.fn(), + emit: vi.fn(), + setLoginItemSettings: vi.fn(), + getLoginItemSettings: vi.fn(() => ({ openAtLogin: false })), +}; + +// Mock ipcMain +const mockIpcMain = { + handle: vi.fn(), + on: vi.fn(), + once: vi.fn(), + removeHandler: vi.fn(), + removeListener: vi.fn(), + removeAllListeners: vi.fn(), + emit: vi.fn(), +}; + +// Mock screen +const mockScreen = { + getPrimaryDisplay: vi.fn(() => ({ + id: 1, + bounds: { x: 0, y: 0, width: 1920, height: 1080 }, + workArea: { x: 0, y: 0, width: 1920, height: 1040 }, + size: { width: 1920, height: 1080 }, + workAreaSize: { width: 1920, height: 1040 }, + scaleFactor: 1, + rotation: 0, + internal: false, + })), + getAllDisplays: vi.fn(() => [ + { + id: 1, + bounds: { x: 0, y: 0, width: 1920, height: 1080 }, + workArea: { x: 0, y: 0, width: 1920, height: 1040 }, + size: { width: 1920, height: 1080 }, + workAreaSize: { width: 1920, height: 1040 }, + scaleFactor: 1, + rotation: 0, + internal: false, + }, + ]), + getCursorScreenPoint: vi.fn(() => ({ x: 100, y: 100 })), + getDisplayNearestPoint: vi.fn(() => ({ + id: 1, + bounds: { x: 0, y: 0, width: 1920, height: 1080 }, + workArea: { x: 0, y: 0, width: 1920, height: 1040 }, + size: { width: 1920, height: 1080 }, + workAreaSize: { width: 1920, height: 1040 }, + scaleFactor: 1, + rotation: 0, + internal: false, + })), + on: vi.fn(), + removeListener: vi.fn(), +}; + +// Mock systemPreferences +const mockSystemPreferences = { + getMediaAccessStatus: vi.fn(() => 'granted'), + askForMediaAccess: vi.fn(() => Promise.resolve(true)), + isTrustedAccessibilityClient: vi.fn(() => true), + getColor: vi.fn(() => '#000000'), + on: vi.fn(), + removeListener: vi.fn(), +}; + +// Mock nativeTheme +const mockNativeTheme = { + shouldUseDarkColors: false, + themeSource: 'system' as const, + on: vi.fn(), + removeListener: vi.fn(), +}; + +// Mock Menu +const mockMenu = { + buildFromTemplate: vi.fn(() => ({})), + setApplicationMenu: vi.fn(), + getApplicationMenu: vi.fn(() => null), +}; + +// Mock Tray +class FakeTray extends EventEmitter { + constructor(image: any) { + super(); + } + setToolTip(toolTip: string) {} + setTitle(title: string) {} + setImage(image: any) {} + setContextMenu(menu: any) {} + destroy() {} + isDestroyed() { return false; } +} + +// Mock dialog +const mockDialog = { + showOpenDialog: vi.fn(() => Promise.resolve({ canceled: false, filePaths: [] })), + showSaveDialog: vi.fn(() => Promise.resolve({ canceled: false, filePath: '' })), + showMessageBox: vi.fn(() => Promise.resolve({ response: 0 })), + showErrorBox: vi.fn(), + showCertificateTrustDialog: vi.fn(() => Promise.resolve()), +}; + +// Mock shell +const mockShell = { + openExternal: vi.fn(() => Promise.resolve()), + openPath: vi.fn(() => Promise.resolve('')), + showItemInFolder: vi.fn(), + openItem: vi.fn(() => Promise.resolve(true)), + moveItemToTrash: vi.fn(() => Promise.resolve(true)), + beep: vi.fn(), + writeShortcutLink: vi.fn(() => true), + readShortcutLink: vi.fn(() => ({})), +}; + +// Mock globalShortcut +const mockGlobalShortcut = { + register: vi.fn(() => true), + registerAll: vi.fn(), + isRegistered: vi.fn(() => false), + unregister: vi.fn(), + unregisterAll: vi.fn(), +}; + +// Mock clipboard +const mockClipboard = { + readText: vi.fn(() => ''), + writeText: vi.fn(), + readHTML: vi.fn(() => ''), + writeHTML: vi.fn(), + readImage: vi.fn(() => ({})), + writeImage: vi.fn(), + clear: vi.fn(), + availableFormats: vi.fn(() => []), +}; + +// Mock nativeImage +const mockNativeImage = { + createEmpty: vi.fn(() => ({})), + createFromPath: vi.fn(() => ({})), + createFromBuffer: vi.fn(() => ({})), + createFromDataURL: vi.fn(() => ({})), +}; + +export function createElectronMocks() { + return { + app: mockApp, + ipcMain: mockIpcMain, + BrowserWindow: FakeBrowserWindow as any, + screen: mockScreen, + systemPreferences: mockSystemPreferences, + nativeTheme: mockNativeTheme, + Menu: mockMenu, + Tray: FakeTray as any, + dialog: mockDialog, + shell: mockShell, + globalShortcut: mockGlobalShortcut, + clipboard: mockClipboard, + nativeImage: mockNativeImage, + }; +} + +// Export test user data path for cleanup +export const TEST_USER_DATA_PATH = testUserDataPath; diff --git a/apps/desktop/tests/helpers/fixtures.ts b/apps/desktop/tests/helpers/fixtures.ts new file mode 100644 index 0000000..79a8981 --- /dev/null +++ b/apps/desktop/tests/helpers/fixtures.ts @@ -0,0 +1,276 @@ +import type { TestDatabase } from './test-db'; +import * as schema from '@db/schema'; +import type { + NewTranscription, + NewVocabulary, + NewModel, + NewAppSettings, + NewNote, + AppSettingsData, +} from '@db/schema'; + +/** + * Default app settings for testing + */ +export const defaultAppSettings: AppSettingsData = { + formatterConfig: { + model: 'gpt-4o-mini', + enabled: false, + }, + ui: { + theme: 'system', + }, + transcription: { + language: 'en', + autoTranscribe: true, + confidenceThreshold: 0.7, + enablePunctuation: true, + enableTimestamps: false, + preloadWhisperModel: false, + }, + recording: { + defaultFormat: 'wav', + sampleRate: 16000, + autoStopSilence: true, + silenceThreshold: -45, + maxRecordingDuration: 600, + }, + shortcuts: { + pushToTalk: 'CommandOrControl+Shift+Space', + toggleRecording: 'CommandOrControl+Shift+R', + toggleWindow: 'CommandOrControl+Shift+W', + }, + modelProvidersConfig: { + defaultSpeechModel: 'local-whisper:ggml-base.en', + }, + dictation: { + autoDetectEnabled: true, + selectedLanguage: 'en', + }, + preferences: { + launchAtLogin: false, + minimizeToTray: true, + showWidgetWhileInactive: true, + }, + telemetry: { + enabled: false, + }, + auth: { + isAuthenticated: false, + idToken: null, + refreshToken: null, + expiresAt: null, + }, +}; + +/** + * Sample transcriptions for testing + */ +export const sampleTranscriptions: NewTranscription[] = [ + { + text: 'This is a test transcription', + language: 'en', + confidence: 0.95, + duration: 5, + speechModel: 'whisper-base', + formattingModel: null, + }, + { + text: 'Another test transcription with more content', + language: 'en', + confidence: 0.88, + duration: 8, + speechModel: 'whisper-base', + formattingModel: 'gpt-4o-mini', + }, + { + text: 'A third transcription for comprehensive testing', + language: 'en', + confidence: 0.92, + duration: 6, + speechModel: 'whisper-large', + formattingModel: null, + }, +]; + +/** + * Sample vocabulary items for testing + */ +export const sampleVocabulary: NewVocabulary[] = [ + { + word: 'Amical', + replacementWord: null, + isReplacement: false, + usageCount: 5, + }, + { + word: 'API', + replacementWord: null, + isReplacement: false, + usageCount: 3, + }, + { + word: 'teh', + replacementWord: 'the', + isReplacement: true, + usageCount: 2, + }, +]; + +/** + * Sample models for testing + */ +export const sampleModels: NewModel[] = [ + { + id: 'ggml-base.en', + provider: 'local-whisper', + name: 'Whisper Base English', + type: 'speech', + size: '~147 MB', + description: 'Optimized for English transcription', + localPath: '/test/models/ggml-base.en.bin', + sizeBytes: 147964211, + checksum: 'test-checksum-base', + downloadedAt: new Date(), + speed: 4, + accuracy: 3, + }, + { + id: 'gpt-4o-mini', + provider: 'openrouter', + name: 'GPT-4o Mini', + type: 'language', + context: '128k', + description: 'Fast and efficient language model', + speed: 5, + accuracy: 4, + }, +]; + +/** + * Sample notes for testing + */ +export const sampleNotes: NewNote[] = [ + { + title: 'Test Note 1', + content: 'This is the first test note', + icon: '📝', + }, + { + title: 'Test Note 2', + content: 'This is the second test note with more content', + icon: '📄', + }, +]; + +/** + * Fixture presets + */ +export const fixtures = { + /** + * Empty database with only default settings + */ + empty: async (testDb: TestDatabase) => { + // Clear existing settings first + await testDb.db.delete(schema.appSettings); + // Insert default settings + await testDb.db.insert(schema.appSettings).values({ + id: 1, + data: defaultAppSettings, + version: 1, + }); + }, + + /** + * Database with existing transcriptions + */ + withTranscriptions: async (testDb: TestDatabase) => { + await fixtures.empty(testDb); + await testDb.db.insert(schema.transcriptions).values(sampleTranscriptions); + }, + + /** + * Database with vocabulary items + */ + withVocabulary: async (testDb: TestDatabase) => { + await fixtures.empty(testDb); + await testDb.db.insert(schema.vocabulary).values(sampleVocabulary); + }, + + /** + * Database with downloaded models + */ + withModels: async (testDb: TestDatabase) => { + await fixtures.empty(testDb); + await testDb.db.insert(schema.models).values(sampleModels); + }, + + /** + * Database with notes + */ + withNotes: async (testDb: TestDatabase) => { + await fixtures.empty(testDb); + await testDb.db.insert(schema.notes).values(sampleNotes); + }, + + /** + * Full database with all types of data + */ + full: async (testDb: TestDatabase) => { + await fixtures.empty(testDb); + await testDb.db.insert(schema.transcriptions).values(sampleTranscriptions); + await testDb.db.insert(schema.vocabulary).values(sampleVocabulary); + await testDb.db.insert(schema.models).values(sampleModels); + await testDb.db.insert(schema.notes).values(sampleNotes); + }, + + /** + * Database with custom settings + */ + withCustomSettings: async ( + testDb: TestDatabase, + settings: Partial + ) => { + // Clear existing settings first + await testDb.db.delete(schema.appSettings); + // Insert custom settings + await testDb.db.insert(schema.appSettings).values({ + id: 1, + data: { ...defaultAppSettings, ...settings }, + version: 1, + }); + }, + + /** + * Database with authenticated user + */ + withAuth: async (testDb: TestDatabase) => { + await fixtures.withCustomSettings(testDb, { + auth: { + isAuthenticated: true, + idToken: 'test-id-token', + refreshToken: 'test-refresh-token', + expiresAt: Date.now() + 3600000, // 1 hour from now + userInfo: { + sub: 'test-user-123', + email: 'test@example.com', + name: 'Test User', + }, + }, + }); + }, +}; + +/** + * Helper to seed specific data + */ +export async function seedDatabase( + testDb: TestDatabase, + fixture: keyof typeof fixtures | ((testDb: TestDatabase) => Promise) +): Promise { + if (typeof fixture === 'function') { + await fixture(testDb); + } else { + await fixtures[fixture](testDb); + } +} diff --git a/apps/desktop/tests/helpers/native-mocks.ts b/apps/desktop/tests/helpers/native-mocks.ts new file mode 100644 index 0000000..137cd75 --- /dev/null +++ b/apps/desktop/tests/helpers/native-mocks.ts @@ -0,0 +1,172 @@ +import { vi } from 'vitest'; + +// Mock onnxruntime-node +export const mockOnnxRuntime = { + InferenceSession: { + create: vi.fn(() => + Promise.resolve({ + run: vi.fn(() => + Promise.resolve({ + output: { + data: new Float32Array([0.5, 0.5, 0.5]), + dims: [1, 3], + }, + }) + ), + release: vi.fn(), + }) + ), + }, + Tensor: vi.fn(), + env: { + wasm: { + numThreads: 1, + }, + }, +}; + +// Mock @amical/whisper-wrapper +export const mockWhisperWrapper = { + WhisperModel: vi.fn().mockImplementation(() => ({ + transcribe: vi.fn(() => + Promise.resolve({ + text: 'Test transcription', + segments: [ + { + start: 0, + end: 1.5, + text: 'Test transcription', + }, + ], + }) + ), + dispose: vi.fn(), + })), + downloadModel: vi.fn(() => Promise.resolve()), + getModelPath: vi.fn(() => '/mock/model/path'), +}; + +// Mock keytar (credential storage) +export const mockKeytar = { + getPassword: vi.fn((service: string, account: string) => + Promise.resolve(null) + ), + setPassword: vi.fn((service: string, account: string, password: string) => + Promise.resolve() + ), + deletePassword: vi.fn((service: string, account: string) => + Promise.resolve(true) + ), + findPassword: vi.fn((service: string) => Promise.resolve(null)), + findCredentials: vi.fn((service: string) => Promise.resolve([])), +}; + +// Mock libsql native module +export const mockLibsql = { + createClient: vi.fn(() => ({ + execute: vi.fn(() => + Promise.resolve({ + rows: [], + columns: [], + rowsAffected: 0, + }) + ), + batch: vi.fn(() => Promise.resolve([])), + close: vi.fn(() => Promise.resolve()), + sync: vi.fn(() => Promise.resolve()), + })), +}; + +// Mock native helper modules +export const mockSwiftHelper = { + checkAccessibilityPermission: vi.fn(() => true), + checkMicrophonePermission: vi.fn(() => true), + requestMicrophonePermission: vi.fn(() => Promise.resolve(true)), + getSystemAudioLevel: vi.fn(() => 0.5), + setSystemAudioMuted: vi.fn(() => true), + isSystemAudioMuted: vi.fn(() => false), + writeToClipboard: vi.fn(() => true), + readFromClipboard: vi.fn(() => ''), + isRunning: vi.fn(() => true), +}; + +export const mockWindowsHelper = { + checkMicrophonePermission: vi.fn(() => true), + requestMicrophonePermission: vi.fn(() => Promise.resolve(true)), + registerGlobalShortcut: vi.fn(() => true), + unregisterGlobalShortcut: vi.fn(() => true), + isKeyPressed: vi.fn(() => false), + getSystemAudioLevel: vi.fn(() => 0.5), + isRunning: vi.fn(() => true), +}; + +// Mock node-machine-id +export const mockMachineId = { + machineIdSync: vi.fn(() => 'test-machine-id-12345'), + machineId: vi.fn(() => Promise.resolve('test-machine-id-12345')), +}; + +// Mock systeminformation +export const mockSystemInformation = { + system: vi.fn(() => + Promise.resolve({ + manufacturer: 'Test Manufacturer', + model: 'Test Model', + version: '1.0', + serial: 'TEST123', + uuid: 'test-uuid', + sku: 'TEST-SKU', + }) + ), + cpu: vi.fn(() => + Promise.resolve({ + manufacturer: 'Test CPU', + brand: 'Test Brand', + speed: 2.5, + cores: 4, + }) + ), + mem: vi.fn(() => + Promise.resolve({ + total: 16000000000, + free: 8000000000, + used: 8000000000, + }) + ), + osInfo: vi.fn(() => + Promise.resolve({ + platform: 'darwin', + distro: 'macOS', + release: '14.0', + arch: 'arm64', + }) + ), +}; + +// Mock posthog-node +export const mockPostHog = { + PostHog: vi.fn().mockImplementation(() => ({ + capture: vi.fn(), + identify: vi.fn(), + alias: vi.fn(), + shutdown: vi.fn(() => Promise.resolve()), + })), +}; + +// Mock update-electron-app +export const mockUpdateElectronApp = vi.fn(); + +export function createNativeMocks() { + return { + 'onnxruntime-node': mockOnnxRuntime, + '@amical/whisper-wrapper': mockWhisperWrapper, + keytar: mockKeytar, + libsql: mockLibsql, + '@amical/swift-helper': mockSwiftHelper, + '@amical/windows-helper': mockWindowsHelper, + 'node-machine-id': mockMachineId, + systeminformation: mockSystemInformation, + 'posthog-node': mockPostHog, + 'update-electron-app': mockUpdateElectronApp, + }; +} diff --git a/apps/desktop/tests/helpers/test-app.ts b/apps/desktop/tests/helpers/test-app.ts new file mode 100644 index 0000000..bb391d3 --- /dev/null +++ b/apps/desktop/tests/helpers/test-app.ts @@ -0,0 +1,121 @@ +import { vi } from 'vitest'; +import type { TestDatabase } from './test-db'; +import { AppManager } from '@main/core/app-manager'; +import { ServiceManager } from '@main/managers/service-manager'; +import { router } from '@trpc/router'; +import { createContext } from '@trpc/context'; + +/** + * Test wrapper for AppManager + */ +export interface TestApp { + appManager: AppManager; + serviceManager: ServiceManager; + trpcCaller: ReturnType; + cleanup: () => Promise; +} + +/** + * Initialize a test instance of AppManager with mocked database + */ +export async function initializeTestApp( + testDb: TestDatabase, + options: { + skipOnboarding?: boolean; + skipWindows?: boolean; + } = {} +): Promise { + const { skipOnboarding = true, skipWindows = false } = options; + + // Mock the database module to use our test database + vi.doMock('@db', () => ({ + db: testDb.db, + dbPath: testDb.dbPath, + initializeDatabase: vi.fn().mockResolvedValue(undefined), + closeDatabase: vi.fn().mockResolvedValue(undefined), + })); + + // Mock onboarding check to skip it + if (skipOnboarding) { + process.env.FORCE_ONBOARDING = 'false'; + } + + // Create AppManager instance + const appManager = new AppManager(); + + // Initialize the app + // Note: This will try to create windows, which are mocked + try { + await appManager.initialize(); + } catch (error) { + // Some initialization errors are expected in test environment + console.warn('AppManager initialization warning:', error); + } + + // Get service manager + const serviceManager = ServiceManager.getInstance()!; + + // Create tRPC caller for testing + const ctx = createContext(serviceManager); + const trpcCaller = router.createCaller(ctx); + + return { + appManager, + serviceManager, + trpcCaller, + cleanup: async () => { + // Clean up services + // Note: Add cleanup logic as needed + }, + }; +} + +/** + * Create a tRPC caller without initializing the full AppManager + * Useful for testing specific service methods in isolation + */ +export function createTestTRPCCaller(serviceManager: ServiceManager) { + const ctx = createContext(serviceManager); + return router.createCaller(ctx); +} + +/** + * Initialize just the ServiceManager without AppManager + * Useful for testing services in isolation + */ +export async function initializeTestServices( + testDb: TestDatabase +): Promise<{ + serviceManager: ServiceManager; + trpcCaller: ReturnType; + cleanup: () => Promise; +}> { + // Mock the database module + vi.doMock('@db', () => ({ + db: testDb.db, + dbPath: testDb.dbPath, + initializeDatabase: vi.fn().mockResolvedValue(undefined), + closeDatabase: vi.fn().mockResolvedValue(undefined), + })); + + // Create and initialize ServiceManager + const serviceManager = ServiceManager.createInstance(); + + try { + await serviceManager.initialize(); + } catch (error) { + console.warn('ServiceManager initialization warning:', error); + } + + // Create tRPC caller + const ctx = createContext(serviceManager); + const trpcCaller = router.createCaller(ctx); + + return { + serviceManager, + trpcCaller, + cleanup: async () => { + // Cleanup logic + }, + }; +} diff --git a/apps/desktop/tests/helpers/test-db.ts b/apps/desktop/tests/helpers/test-db.ts new file mode 100644 index 0000000..0abd11f --- /dev/null +++ b/apps/desktop/tests/helpers/test-db.ts @@ -0,0 +1,116 @@ +import { drizzle } from 'drizzle-orm/libsql'; +import { migrate } from 'drizzle-orm/libsql/migrator'; +import * as schema from '@db/schema'; +import path from 'node:path'; +import fs from 'fs-extra'; +import { TEST_USER_DATA_PATH } from './electron-mocks'; + +let dbCounter = 0; + +export interface TestDatabase { + db: ReturnType; + dbPath: string; + close: () => Promise; + clear: () => Promise; +} + +/** + * Creates an isolated test database with migrations applied + */ +export async function createTestDatabase( + options: { + name?: string; + skipMigrations?: boolean; + } = {} +): Promise { + const { name, skipMigrations = false } = options; + + // Create unique database path + const dbName = name || `test-${dbCounter++}-${Date.now()}.db`; + const dbPath = path.join(TEST_USER_DATA_PATH, 'databases', dbName); + + // Ensure directory exists + await fs.ensureDir(path.dirname(dbPath)); + + // Create drizzle instance + const db = drizzle(`file:${dbPath}`, { + schema: { + ...schema, + }, + }); + + // Run migrations if not skipped + if (!skipMigrations) { + const migrationsPath = path.join(process.cwd(), 'src', 'db', 'migrations'); + + // Check if migrations exist + if (!fs.existsSync(migrationsPath)) { + console.warn( + 'Migrations folder not found at:', + migrationsPath, + '- skipping migrations' + ); + } else { + try { + await migrate(db, { + migrationsFolder: migrationsPath, + }); + } catch (error) { + console.error('Failed to run migrations:', error); + throw error; + } + } + } + + return { + db, + dbPath, + close: async () => { + db.$client.close(); + }, + clear: async () => { + // Clear all tables + await db.delete(schema.transcriptions); + await db.delete(schema.vocabulary); + await db.delete(schema.models); + await db.delete(schema.appSettings); + await db.delete(schema.yjsUpdates); + await db.delete(schema.notes); + }, + }; +} + +/** + * Deletes a test database file + */ +export async function deleteTestDatabase(dbPath: string): Promise { + try { + await fs.remove(dbPath); + } catch (error) { + console.error('Failed to delete test database:', error); + } +} + +/** + * Clears all test databases + */ +export async function clearAllTestDatabases(): Promise { + const dbDir = path.join(TEST_USER_DATA_PATH, 'databases'); + try { + await fs.emptyDir(dbDir); + } catch (error) { + console.error('Failed to clear test databases:', error); + } +} + +/** + * Helper to get database instance for testing + * This bypasses the singleton pattern used in production + */ +export function createMockDb(dbPath: string) { + return drizzle(`file:${dbPath}`, { + schema: { + ...schema, + }, + }); +} diff --git a/apps/desktop/tests/services/transcriptions.test.ts b/apps/desktop/tests/services/transcriptions.test.ts new file mode 100644 index 0000000..e94e907 --- /dev/null +++ b/apps/desktop/tests/services/transcriptions.test.ts @@ -0,0 +1,219 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { createTestDatabase, type TestDatabase } from '../helpers/test-db'; +import { seedDatabase, fixtures, sampleTranscriptions } from '../helpers/fixtures'; +import { initializeTestServices } from '../helpers/test-app'; +import { setTestDatabase } from '../setup'; + +describe('Transcriptions Service', () => { + let testDb: TestDatabase; + let serviceManager: any; + let trpcCaller: any; + let cleanup: () => Promise; + + afterEach(async () => { + if (cleanup) { + await cleanup(); + } + if (testDb) { + await testDb.close(); + } + }); + + describe('Get Transcriptions', () => { + beforeEach(async () => { + testDb = await createTestDatabase({ name: 'get-transcriptions-test' }); + setTestDatabase(testDb.db); + await seedDatabase(testDb, 'withTranscriptions'); + const result = await initializeTestServices(testDb); + serviceManager = result.serviceManager; + trpcCaller = result.trpcCaller; + cleanup = result.cleanup; + }); + + it('should return all transcriptions', async () => { + const transcriptions = await trpcCaller.transcriptions.getTranscriptions({ + limit: 10, + offset: 0, + }); + + expect(transcriptions).toHaveLength(sampleTranscriptions.length); + expect(transcriptions[0]).toHaveProperty('id'); + expect(transcriptions[0]).toHaveProperty('text'); + expect(transcriptions[0]).toHaveProperty('language'); + }); + + it('should respect limit parameter', async () => { + const transcriptions = await trpcCaller.transcriptions.getTranscriptions({ + limit: 2, + offset: 0, + }); + + expect(transcriptions).toHaveLength(2); + }); + + it('should respect offset parameter', async () => { + const allTranscriptions = + await trpcCaller.transcriptions.getTranscriptions({ + limit: 10, + offset: 0, + }); + + const offsetTranscriptions = + await trpcCaller.transcriptions.getTranscriptions({ + limit: 10, + offset: 1, + }); + + expect(offsetTranscriptions).toHaveLength(allTranscriptions.length - 1); + expect(offsetTranscriptions[0].id).not.toBe(allTranscriptions[0].id); + }); + }); + + describe('Get Transcription by ID', () => { + beforeEach(async () => { + testDb = await createTestDatabase({ name: 'get-by-id-test' }); + setTestDatabase(testDb.db); + await seedDatabase(testDb, 'withTranscriptions'); + const result = await initializeTestServices(testDb); + serviceManager = result.serviceManager; + trpcCaller = result.trpcCaller; + cleanup = result.cleanup; + }); + + it('should return transcription by id', async () => { + const transcriptions = await trpcCaller.transcriptions.getTranscriptions({ + limit: 1, + offset: 0, + }); + + const transcription = await trpcCaller.transcriptions.getTranscriptionById({ + id: transcriptions[0].id, + }); + + expect(transcription).toBeDefined(); + expect(transcription.id).toBe(transcriptions[0].id); + expect(transcription.text).toBe(transcriptions[0].text); + }); + + it('should return null for non-existent id', async () => { + const result = await trpcCaller.transcriptions.getTranscriptionById({ + id: 99999, + }); + expect(result).toBeNull(); + }); + }); + + describe('Delete Transcription', () => { + beforeEach(async () => { + testDb = await createTestDatabase({ name: 'delete-test' }); + setTestDatabase(testDb.db); + await seedDatabase(testDb, 'withTranscriptions'); + const result = await initializeTestServices(testDb); + serviceManager = result.serviceManager; + trpcCaller = result.trpcCaller; + cleanup = result.cleanup; + }); + + it('should delete transcription by id', async () => { + const transcriptions = await trpcCaller.transcriptions.getTranscriptions({ + limit: 10, + offset: 0, + }); + + const initialCount = transcriptions.length; + const idToDelete = transcriptions[0].id; + + await trpcCaller.transcriptions.deleteTranscription({ id: idToDelete }); + + const afterDelete = await trpcCaller.transcriptions.getTranscriptions({ + limit: 10, + offset: 0, + }); + + expect(afterDelete).toHaveLength(initialCount - 1); + expect(afterDelete.find((t: any) => t.id === idToDelete)).toBeUndefined(); + }); + }); + + describe('Search Transcriptions', () => { + beforeEach(async () => { + testDb = await createTestDatabase({ name: 'search-test' }); + setTestDatabase(testDb.db); + await seedDatabase(testDb, 'withTranscriptions'); + const result = await initializeTestServices(testDb); + serviceManager = result.serviceManager; + trpcCaller = result.trpcCaller; + cleanup = result.cleanup; + }); + + it('should search transcriptions by text', async () => { + const results = await trpcCaller.transcriptions.searchTranscriptions({ + searchTerm: 'test', + limit: 10, + }); + + expect(results.length).toBeGreaterThan(0); + results.forEach((result: any) => { + expect(result.text.toLowerCase()).toContain('test'); + }); + }); + + it('should return empty array for no matches', async () => { + const results = await trpcCaller.transcriptions.searchTranscriptions({ + searchTerm: 'nonexistentquerystring', + limit: 10, + }); + + expect(results).toHaveLength(0); + }); + }); + + describe('Empty Database', () => { + beforeEach(async () => { + testDb = await createTestDatabase({ name: 'empty-test' }); + setTestDatabase(testDb.db); + await seedDatabase(testDb, 'empty'); + const result = await initializeTestServices(testDb); + serviceManager = result.serviceManager; + trpcCaller = result.trpcCaller; + cleanup = result.cleanup; + }); + + it('should return empty array for empty database', async () => { + const transcriptions = await trpcCaller.transcriptions.getTranscriptions({ + limit: 10, + offset: 0, + }); + + expect(transcriptions).toHaveLength(0); + }); + + it('should handle search on empty database', async () => { + const results = await trpcCaller.transcriptions.searchTranscriptions({ + searchTerm: 'test', + limit: 10, + }); + + expect(results).toHaveLength(0); + }); + }); + + describe('Get Count', () => { + beforeEach(async () => { + testDb = await createTestDatabase({ name: 'count-test' }); + setTestDatabase(testDb.db); + await seedDatabase(testDb, 'withTranscriptions'); + const result = await initializeTestServices(testDb); + serviceManager = result.serviceManager; + trpcCaller = result.trpcCaller; + cleanup = result.cleanup; + }); + + it('should return total transcription count', async () => { + const count = await trpcCaller.transcriptions.getTranscriptionsCount({}); + + expect(count).toBe(sampleTranscriptions.length); + expect(count).toBeGreaterThan(0); + }); + }); +}); diff --git a/apps/desktop/tests/setup.ts b/apps/desktop/tests/setup.ts new file mode 100644 index 0000000..58dde69 --- /dev/null +++ b/apps/desktop/tests/setup.ts @@ -0,0 +1,235 @@ +import { vi, beforeAll, afterAll, beforeEach, afterEach } from 'vitest'; +import { TEST_USER_DATA_PATH } from './helpers/electron-mocks'; +import fs from 'fs-extra'; +import path from 'path'; + +// Set test environment variable +process.env.NODE_ENV = 'test'; +process.env.VITEST = 'true'; + +// Global test database instance - will be set by each test +let currentTestDb: any = null; + +// Helper function to set the current test database +export function setTestDatabase(db: any) { + currentTestDb = db; +} + +// Helper function to get the current test database +export function getTestDatabase() { + if (!currentTestDb) { + throw new Error('Test database not set. Call setTestDatabase() in beforeEach.'); + } + return currentTestDb; +} + +// Mock the database module to return the current test database +vi.mock('@db', () => ({ + get db() { + return getTestDatabase(); + }, + get dbPath() { + return '/test/db/path'; + }, + initializeDatabase: vi.fn().mockResolvedValue(undefined), + closeDatabase: vi.fn().mockResolvedValue(undefined), +})); + +// Mock electron module +vi.mock('electron', async () => { + const { createElectronMocks } = await import('./helpers/electron-mocks'); + return createElectronMocks(); +}); + +// Mock native modules +vi.mock('onnxruntime-node', () => ({ + InferenceSession: { + create: vi.fn(function() { + return Promise.resolve({ + run: vi.fn(function() { + return Promise.resolve({ + output: { + data: new Float32Array([0.5, 0.5, 0.5]), + dims: [1, 3], + }, + }); + }), + release: vi.fn(), + }); + }), + }, + Tensor: vi.fn(), + env: { + wasm: { + numThreads: 1, + }, + }, +})); + +vi.mock('@amical/whisper-wrapper', () => ({ + WhisperModel: vi.fn().mockImplementation(function() { + return { + transcribe: vi.fn(function() { + return Promise.resolve({ + text: 'Test transcription', + segments: [ + { + start: 0, + end: 1.5, + text: 'Test transcription', + }, + ], + }); + }), + dispose: vi.fn(), + }; + }), + downloadModel: vi.fn(function() { return Promise.resolve(); }), + getModelPath: vi.fn(function() { return '/mock/model/path'; }), +})); + +vi.mock('keytar', () => ({ + getPassword: vi.fn(function(service: string, account: string) { + return Promise.resolve(null); + }), + setPassword: vi.fn(function(service: string, account: string, password: string) { + return Promise.resolve(); + }), + deletePassword: vi.fn(function(service: string, account: string) { + return Promise.resolve(true); + }), + findPassword: vi.fn(function(service: string) { + return Promise.resolve(null); + }), + findCredentials: vi.fn(function(service: string) { + return Promise.resolve([]); + }), +})); + +vi.mock('node-machine-id', () => ({ + machineIdSync: vi.fn(function() { return 'test-machine-id-12345'; }), + machineId: vi.fn(function() { return Promise.resolve('test-machine-id-12345'); }), +})); + +vi.mock('systeminformation', () => ({ + system: vi.fn(function() { + return Promise.resolve({ + manufacturer: 'Test Manufacturer', + model: 'Test Model', + version: '1.0', + serial: 'TEST123', + uuid: 'test-uuid', + sku: 'TEST-SKU', + }); + }), + cpu: vi.fn(function() { + return Promise.resolve({ + manufacturer: 'Test CPU', + brand: 'Test Brand', + speed: 2.5, + cores: 4, + }); + }), + mem: vi.fn(function() { + return Promise.resolve({ + total: 16000000000, + free: 8000000000, + used: 8000000000, + }); + }), + osInfo: vi.fn(function() { + return Promise.resolve({ + platform: 'darwin', + distro: 'macOS', + release: '14.0', + arch: 'arm64', + }); + }), +})); + +vi.mock('posthog-node', () => ({ + PostHog: vi.fn().mockImplementation(function() { + return { + capture: vi.fn(), + identify: vi.fn(), + alias: vi.fn(), + shutdown: vi.fn(function() { return Promise.resolve(); }), + }; + }), +})); + +vi.mock('update-electron-app', () => ({ + default: vi.fn(), +})); + +// Mock electron-log +vi.mock('electron-log', () => ({ + default: { + info: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + debug: vi.fn(), + verbose: vi.fn(), + silly: vi.fn(), + transports: { + file: { level: 'info' }, + console: { level: 'info' }, + }, + scope: vi.fn(() => ({ + info: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + debug: vi.fn(), + })), + }, + info: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + debug: vi.fn(), + verbose: vi.fn(), + silly: vi.fn(), +})); + +// Mock electron-squirrel-startup +vi.mock('electron-squirrel-startup', () => ({ + default: false, +})); + +// Mock electron-trpc-experimental +vi.mock('electron-trpc-experimental/main', () => ({ + createIPCHandler: vi.fn(() => ({ + handle: vi.fn(), + })), +})); + +// Global test setup +beforeAll(async () => { + // Create test user data directory + await fs.ensureDir(TEST_USER_DATA_PATH); + await fs.ensureDir(path.join(TEST_USER_DATA_PATH, 'databases')); + await fs.ensureDir(path.join(TEST_USER_DATA_PATH, 'models')); + await fs.ensureDir(path.join(TEST_USER_DATA_PATH, 'logs')); +}); + +// Global test teardown +afterAll(async () => { + // Clean up test user data directory + try { + await fs.remove(TEST_USER_DATA_PATH); + } catch (error) { + console.error('Failed to clean up test directory:', error); + } +}); + +// Reset mocks between tests +beforeEach(() => { + // Clear all mock calls and instances + vi.clearAllMocks(); +}); + +afterEach(() => { + // Additional cleanup if needed +}); + +// Export for use in tests +export { TEST_USER_DATA_PATH }; diff --git a/apps/desktop/vitest.config.ts b/apps/desktop/vitest.config.ts new file mode 100644 index 0000000..7a91881 --- /dev/null +++ b/apps/desktop/vitest.config.ts @@ -0,0 +1,28 @@ +import { defineConfig } from 'vitest/config'; +import { resolve } from 'path'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['tests/**/*.{test,spec}.{js,ts}'], + exclude: ['node_modules', '.vite', 'out'], + setupFiles: ['./tests/setup.ts'], + testTimeout: 30000, // 30 seconds for full app initialization + hookTimeout: 30000, + // Run tests sequentially to avoid database conflicts + threads: false, + // Isolate environment for each test file + isolate: true, + }, + resolve: { + alias: { + '@': resolve(__dirname, 'src'), + '@db': resolve(__dirname, 'src/db'), + '@main': resolve(__dirname, 'src/main'), + '@services': resolve(__dirname, 'src/services'), + '@utils': resolve(__dirname, 'src/utils'), + '@trpc': resolve(__dirname, 'src/trpc'), + }, + }, +}); diff --git a/package.json b/package.json index 359bf2f..c195085 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,10 @@ "lint": "turbo run lint", "format": "prettier --write \"**/*.{ts,tsx,md,json,mjs,mts,css,mdx}\"", "format:check": "turbo run format:check", - "type:check": "turbo run type:check" + "type:check": "turbo run type:check", + "test": "turbo run test", + "test:watch": "turbo run test:watch", + "test:ui": "turbo run test:ui" }, "devDependencies": { "prettier": "^3.5.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a2025e5..6c57991 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -200,7 +200,7 @@ importers: version: 0.31.4 drizzle-orm: specifier: ^0.43.1 - version: 0.43.1(@libsql/client@0.15.12)(@opentelemetry/api@1.9.0) + version: 0.43.1(@libsql/client@0.15.12)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.4.1) electron-log: specifier: ^5.4.0 version: 5.4.3 @@ -297,9 +297,6 @@ importers: vaul: specifier: ^1.1.2 version: 1.1.2(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - workerpool: - specifier: ^9.3.3 - version: 9.3.3 yjs: specifier: ^13.6.27 version: 13.6.27 @@ -345,19 +342,25 @@ importers: version: 28.0.6(rollup@4.47.1) '@tailwindcss/vite': specifier: ^4.1.6 - version: 4.1.12(vite@7.1.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1)) + version: 4.1.12(vite@7.1.5(@types/node@24.10.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1)) '@tanstack/react-router-devtools': specifier: ^1.131.41 version: 1.131.41(@tanstack/react-router@1.131.36(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@tanstack/router-core@1.131.41)(csstype@3.1.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(solid-js@1.9.9)(tiny-invariant@1.3.3) '@tanstack/router-plugin': specifier: ^1.131.41 - version: 1.131.41(@tanstack/react-router@1.131.36(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1)) + version: 1.131.41(@tanstack/react-router@1.131.36(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.5(@types/node@24.10.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1)) + '@types/node': + specifier: ^24.10.1 + version: 24.10.1 '@types/react': specifier: ^19.1.12 version: 19.1.12 '@types/react-dom': specifier: ^19.1.9 version: 19.1.9(@types/react@19.1.12) + '@vitest/ui': + specifier: ^4.0.8 + version: 4.0.8(vitest@4.0.8) bumpp: specifier: ^10.2.3 version: 10.2.3 @@ -379,6 +382,9 @@ importers: flora-colossus: specifier: ^2.0.0 version: 2.0.0 + fs-extra: + specifier: ^11.3.2 + version: 11.3.2 prettier: specifier: ^3.5.3 version: 3.6.2 @@ -393,7 +399,10 @@ importers: version: 5.8.3 vite: specifier: ^7.1.5 - version: 7.1.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1) + version: 7.1.5(@types/node@24.10.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1) + vitest: + specifier: ^4.0.8 + version: 4.0.8(@types/node@24.10.1)(@vitest/ui@4.0.8)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1) packages/eslint-config: devDependencies: @@ -1521,6 +1530,9 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@posthog/core@1.0.2': resolution: {integrity: sha512-hWk3rUtJl2crQK0WNmwg13n82hnTwB99BT99/XI5gZSvIlYZ1TPmMZE8H2dhJJ98J/rm9vYJ/UXNzw3RV5HTpQ==} @@ -2264,6 +2276,9 @@ packages: resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@standard-schema/utils@0.3.0': resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} @@ -2517,9 +2532,15 @@ packages: resolution: {integrity: sha512-TmY25GmxzgX+395Fwl/F0te6S4RHdJtYl1QjZr+wlxVvKJ0IBOACpnpAvnLM3dpTgXuQukGtSWcRz7Zi9mZqcQ==} hasBin: true + '@types/better-sqlite3@7.6.13': + resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} + '@types/cacheable-request@6.0.3': resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/d3-array@3.2.1': resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} @@ -2547,6 +2568,9 @@ packages: '@types/d3-timer@3.0.2': resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/diff-match-patch@1.0.36': resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==} @@ -2602,6 +2626,9 @@ packages: '@types/node@22.15.12': resolution: {integrity: sha512-K0fpC/ZVeb8G9rm7bH7vI0KAec4XHEhBam616nVJCV51bKzJ6oA3luG4WdKoaztxe70QaNjS/xBmcDLmr4PiGw==} + '@types/node@24.10.1': + resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} + '@types/node@24.3.0': resolution: {integrity: sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==} @@ -2699,6 +2726,40 @@ packages: resolution: {integrity: sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vitest/expect@4.0.8': + resolution: {integrity: sha512-Rv0eabdP/xjAHQGr8cjBm+NnLHNoL268lMDK85w2aAGLFoVKLd8QGnVon5lLtkXQCoYaNL0wg04EGnyKkkKhPA==} + + '@vitest/mocker@4.0.8': + resolution: {integrity: sha512-9FRM3MZCedXH3+pIh+ME5Up2NBBHDq0wqwhOKkN4VnvCiKbVxddqH9mSGPZeawjd12pCOGnl+lo/ZGHt0/dQSg==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.0.8': + resolution: {integrity: sha512-qRrjdRkINi9DaZHAimV+8ia9Gq6LeGz2CgIEmMLz3sBDYV53EsnLZbJMR1q84z1HZCMsf7s0orDgZn7ScXsZKg==} + + '@vitest/runner@4.0.8': + resolution: {integrity: sha512-mdY8Sf1gsM8hKJUQfiPT3pn1n8RF4QBcJYFslgWh41JTfrK1cbqY8whpGCFzBl45LN028g0njLCYm0d7XxSaQQ==} + + '@vitest/snapshot@4.0.8': + resolution: {integrity: sha512-Nar9OTU03KGiubrIOFhcfHg8FYaRaNT+bh5VUlNz8stFhCZPNrJvmZkhsr1jtaYvuefYFwK2Hwrq026u4uPWCw==} + + '@vitest/spy@4.0.8': + resolution: {integrity: sha512-nvGVqUunyCgZH7kmo+Ord4WgZ7lN0sOULYXUOYuHr55dvg9YvMz3izfB189Pgp28w0vWFbEEfNc/c3VTrqrXeA==} + + '@vitest/ui@4.0.8': + resolution: {integrity: sha512-F9jI5rSstNknPlTlPN2gcc4gpbaagowuRzw/OJzl368dvPun668Q182S8Q8P9PITgGCl5LAKXpzuue106eM4wA==} + peerDependencies: + vitest: 4.0.8 + + '@vitest/utils@4.0.8': + resolution: {integrity: sha512-pdk2phO5NDvEFfUTxcTP8RFYjVj/kfLSPIN5ebP2Mu9kcIMeAQTbknqcFEyBcC4z2pJlJI9aS5UQjcYfhmKAow==} + '@watchable/unpromise@1.0.2': resolution: {integrity: sha512-yGCKYzCrAfJQ9yzm76r1bl4WUIWyqmh4vqidXn5LyOfPbgdiZrKOyvW2ivqIvtmsRVb7u3ModEpc4q901VRgXw==} @@ -2871,6 +2932,10 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-types@0.13.4: resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} engines: {node: '>=4'} @@ -2926,10 +2991,17 @@ packages: before-after-hook@2.2.3: resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} + better-sqlite3@12.4.1: + resolution: {integrity: sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==} + engines: {node: 20.x || 22.x || 23.x || 24.x} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -3027,6 +3099,10 @@ packages: caniuse-lite@1.0.30001736: resolution: {integrity: sha512-ImpN5gLEY8gWeqfLUyEF4b7mYWcYoR2Si1VhnrbM4JizRFmfGaAQ12PhNykq6nvI4XvKLrsp8Xde74D5phJOSw==} + chai@6.2.1: + resolution: {integrity: sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==} + engines: {node: '>=18'} + chalk-template@0.4.0: resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==} engines: {node: '>=12'} @@ -3351,6 +3427,15 @@ packages: supports-color: optional: true + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decimal.js-light@2.5.1: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} @@ -3676,6 +3761,9 @@ packages: resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==} engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -3855,6 +3943,9 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -3885,6 +3976,10 @@ packages: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} + expect-type@1.2.2: + resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} + engines: {node: '>=12.0.0'} + exponential-backoff@3.1.2: resolution: {integrity: sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==} @@ -3943,6 +4038,9 @@ packages: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -3951,6 +4049,9 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + filename-reserved-regex@2.0.0: resolution: {integrity: sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==} engines: {node: '>=4'} @@ -4045,6 +4146,10 @@ packages: resolution: {integrity: sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==} engines: {node: '>=14.14'} + fs-extra@11.3.2: + resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==} + engines: {node: '>=14.14'} + fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -4823,6 +4928,9 @@ packages: magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} @@ -4964,6 +5072,10 @@ packages: motion-utils@12.23.6: resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==} + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} @@ -5786,6 +5898,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -5795,6 +5910,10 @@ packages: simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -5869,6 +5988,12 @@ packages: resolution: {integrity: sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -6046,9 +6171,15 @@ packages: tiny-warning@1.0.3: resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinycolor2@1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.0.1: resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} @@ -6063,6 +6194,10 @@ packages: tinygradient@1.1.5: resolution: {integrity: sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==} + tinyrainbow@3.0.3: + resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} + engines: {node: '>=14.0.0'} + title-case@2.1.1: resolution: {integrity: sha512-EkJoZ2O3zdCz3zJsYCsxyq2OC5hrxR9mfdd5I+w8h/tmFfeOxJ+vvkxsKxdmN0WtS9zLdHEgfgVOiMVgv+Po4Q==} @@ -6088,6 +6223,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -6254,6 +6393,9 @@ packages: undici-types@7.10.0: resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==} + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + unicode-properties@1.4.1: resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==} @@ -6409,6 +6551,40 @@ packages: yaml: optional: true + vitest@4.0.8: + resolution: {integrity: sha512-urzu3NCEV0Qa0Y2PwvBtRgmNtxhj5t5ULw7cuKhIHh3OrkKTLlut0lnBOv9qe5OvbkMH2g38G7KPDCTpIytBVg==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.0.8 + '@vitest/browser-preview': 4.0.8 + '@vitest/browser-webdriverio': 4.0.8 + '@vitest/ui': 4.0.8 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} @@ -6454,6 +6630,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + wide-align@1.1.5: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} @@ -6468,9 +6649,6 @@ packages: resolution: {integrity: sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg==} engines: {node: '>=12.17'} - workerpool@9.3.3: - resolution: {integrity: sha512-slxCaKbYjEdFT/o2rH9xS1hf4uRDch1w7Uo+apxhZ+sf/1d9e0ZVkn42kPNGP2dgjIx6YFvSevj0zHvbWe2jdw==} - wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -7193,7 +7371,7 @@ snapshots: debug: 4.4.1 extract-zip: 2.0.1 filenamify: 4.3.0 - fs-extra: 11.3.1 + fs-extra: 11.3.2 galactus: 1.0.0 get-package-info: 1.0.0 junk: 3.1.0 @@ -7233,7 +7411,7 @@ snapshots: '@malept/cross-spawn-promise': 2.0.0 debug: 4.4.1 dir-compare: 4.2.0 - fs-extra: 11.3.1 + fs-extra: 11.3.2 minimatch: 9.0.5 plist: 3.1.0 transitivePeerDependencies: @@ -7243,7 +7421,7 @@ snapshots: dependencies: cross-dirname: 0.1.0 debug: 4.4.1 - fs-extra: 11.3.1 + fs-extra: 11.3.2 minimist: 1.2.8 postject: 1.0.0-alpha.6 transitivePeerDependencies: @@ -7524,7 +7702,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 24.3.0 + '@types/node': 24.10.1 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -7753,6 +7931,8 @@ snapshots: '@pkgr/core@0.2.9': {} + '@polka/url@1.0.0-next.29': {} + '@posthog/core@1.0.2': {} '@radix-ui/number@1.1.1': {} @@ -8494,6 +8674,8 @@ snapshots: '@sindresorhus/is@4.6.0': {} + '@standard-schema/spec@1.0.0': {} + '@standard-schema/utils@0.3.0': {} '@szmarczak/http-timer@4.0.6': @@ -8571,12 +8753,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.12 '@tailwindcss/oxide-win32-x64-msvc': 4.1.12 - '@tailwindcss/vite@4.1.12(vite@7.1.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1))': + '@tailwindcss/vite@4.1.12(vite@7.1.5(@types/node@24.10.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1))': dependencies: '@tailwindcss/node': 4.1.12 '@tailwindcss/oxide': 4.1.12 tailwindcss: 4.1.12 - vite: 7.1.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1) + vite: 7.1.5(@types/node@24.10.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1) '@tanstack/history@1.131.2': {} @@ -8666,7 +8848,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.131.41(@tanstack/react-router@1.131.36(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1))': + '@tanstack/router-plugin@1.131.41(@tanstack/react-router@1.131.36(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.5(@types/node@24.10.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.3 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.3) @@ -8684,7 +8866,7 @@ snapshots: zod: 3.25.76 optionalDependencies: '@tanstack/react-router': 1.131.36(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - vite: 7.1.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1) + vite: 7.1.5(@types/node@24.10.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -8771,13 +8953,23 @@ snapshots: transitivePeerDependencies: - '@types/node' + '@types/better-sqlite3@7.6.13': + dependencies: + '@types/node': 24.10.1 + optional: true + '@types/cacheable-request@6.0.3': dependencies: '@types/http-cache-semantics': 4.0.4 '@types/keyv': 3.1.4 - '@types/node': 24.3.0 + '@types/node': 24.10.1 '@types/responselike': 1.0.3 + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/d3-array@3.2.1': {} '@types/d3-color@3.1.3': {} @@ -8802,13 +8994,15 @@ snapshots: '@types/d3-timer@3.0.2': {} + '@types/deep-eql@4.0.2': {} + '@types/diff-match-patch@1.0.36': {} '@types/estree@1.0.8': {} '@types/fs-extra@9.0.13': dependencies: - '@types/node': 24.3.0 + '@types/node': 24.10.1 optional: true '@types/glob@7.2.0': @@ -8839,7 +9033,7 @@ snapshots: '@types/keyv@3.1.4': dependencies: - '@types/node': 24.3.0 + '@types/node': 24.10.1 '@types/minimatch@6.0.0': dependencies: @@ -8847,7 +9041,7 @@ snapshots: '@types/node-fetch@2.6.13': dependencies: - '@types/node': 24.3.0 + '@types/node': 24.10.1 form-data: 4.0.4 '@types/node@16.18.126': {} @@ -8864,6 +9058,10 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/node@24.10.1': + dependencies: + undici-types: 7.16.0 + '@types/node@24.3.0': dependencies: undici-types: 7.10.0 @@ -8878,11 +9076,11 @@ snapshots: '@types/responselike@1.0.3': dependencies: - '@types/node': 24.3.0 + '@types/node': 24.10.1 '@types/split2@4.2.3': dependencies: - '@types/node': 24.3.0 + '@types/node': 24.10.1 '@types/through@0.0.33': dependencies: @@ -8894,7 +9092,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 24.3.0 + '@types/node': 24.10.1 '@types/yargs-parser@21.0.3': {} @@ -8904,7 +9102,7 @@ snapshots: '@types/yauzl@2.10.3': dependencies: - '@types/node': 24.3.0 + '@types/node': 24.10.1 optional: true '@typescript-eslint/eslint-plugin@8.40.0(@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)': @@ -9000,6 +9198,56 @@ snapshots: '@typescript-eslint/types': 8.40.0 eslint-visitor-keys: 4.2.1 + '@vitest/expect@4.0.8': + dependencies: + '@standard-schema/spec': 1.0.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.0.8 + '@vitest/utils': 4.0.8 + chai: 6.2.1 + tinyrainbow: 3.0.3 + + '@vitest/mocker@4.0.8(vite@7.1.5(@types/node@24.10.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1))': + dependencies: + '@vitest/spy': 4.0.8 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.1.5(@types/node@24.10.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1) + + '@vitest/pretty-format@4.0.8': + dependencies: + tinyrainbow: 3.0.3 + + '@vitest/runner@4.0.8': + dependencies: + '@vitest/utils': 4.0.8 + pathe: 2.0.3 + + '@vitest/snapshot@4.0.8': + dependencies: + '@vitest/pretty-format': 4.0.8 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.0.8': {} + + '@vitest/ui@4.0.8(vitest@4.0.8)': + dependencies: + '@vitest/utils': 4.0.8 + fflate: 0.8.2 + flatted: 3.3.3 + pathe: 2.0.3 + sirv: 3.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vitest: 4.0.8(@types/node@24.10.1)(@vitest/ui@4.0.8)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1) + + '@vitest/utils@4.0.8': + dependencies: + '@vitest/pretty-format': 4.0.8 + tinyrainbow: 3.0.3 + '@watchable/unpromise@1.0.2': {} '@xmldom/xmldom@0.8.11': {} @@ -9024,7 +9272,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -9194,6 +9442,8 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 + assertion-error@2.0.1: {} + ast-types@0.13.4: dependencies: tslib: 2.8.1 @@ -9251,8 +9501,19 @@ snapshots: before-after-hook@2.2.3: {} + better-sqlite3@12.4.1: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.3 + optional: true + binary-extensions@2.3.0: {} + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + optional: true + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -9400,6 +9661,8 @@ snapshots: caniuse-lite@1.0.30001736: {} + chai@6.2.1: {} + chalk-template@0.4.0: dependencies: chalk: 4.1.2 @@ -9729,6 +9992,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.3: + dependencies: + ms: 2.1.3 + decimal.js-light@2.5.1: {} decompress-response@6.0.0: @@ -9837,10 +10104,12 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.43.1(@libsql/client@0.15.12)(@opentelemetry/api@1.9.0): + drizzle-orm@0.43.1(@libsql/client@0.15.12)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.4.1): optionalDependencies: '@libsql/client': 0.15.12 '@opentelemetry/api': 1.9.0 + '@types/better-sqlite3': 7.6.13 + better-sqlite3: 12.4.1 ds-store@0.1.6: dependencies: @@ -10079,6 +10348,8 @@ snapshots: iterator.prototype: 1.1.5 safe-array-concat: 1.1.3 + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -10343,6 +10614,10 @@ snapshots: estree-walker@2.0.2: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + esutils@2.0.3: {} event-target-shim@5.0.1: {} @@ -10377,6 +10652,8 @@ snapshots: expand-template@2.0.3: {} + expect-type@1.2.2: {} + exponential-backoff@3.1.2: {} exsolve@1.0.7: {} @@ -10440,6 +10717,8 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 3.3.3 + fflate@0.8.2: {} + figures@3.2.0: dependencies: escape-string-regexp: 1.0.5 @@ -10448,6 +10727,9 @@ snapshots: dependencies: flat-cache: 4.0.1 + file-uri-to-path@1.0.0: + optional: true + filename-reserved-regex@2.0.0: {} filenamify@4.3.0: @@ -10544,6 +10826,12 @@ snapshots: jsonfile: 6.2.0 universalify: 2.0.1 + fs-extra@11.3.2: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -10847,7 +11135,7 @@ snapshots: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -10866,7 +11154,7 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -11164,7 +11452,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 24.3.0 + '@types/node': 24.10.1 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -11172,7 +11460,7 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 24.3.0 + '@types/node': 24.10.1 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -11394,6 +11682,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + make-error@1.3.6: {} make-fetch-happen@10.2.1: @@ -11536,6 +11828,8 @@ snapshots: motion-utils@12.23.6: {} + mrmime@2.0.1: {} + ms@2.0.0: {} ms@2.1.3: {} @@ -12475,6 +12769,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@3.0.7: {} simple-concat@1.0.1: {} @@ -12485,6 +12781,12 @@ snapshots: once: 1.4.0 simple-concat: 1.0.1 + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + slash@3.0.0: {} slice-ansi@5.0.0: @@ -12501,7 +12803,7 @@ snapshots: socks-proxy-agent@7.0.0: dependencies: agent-base: 6.0.2 - debug: 4.4.1 + debug: 4.4.3 socks: 2.8.7 transitivePeerDependencies: - supports-color @@ -12563,6 +12865,10 @@ snapshots: dependencies: minipass: 3.3.6 + stackback@0.0.2: {} + + std-env@3.10.0: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -12769,8 +13075,12 @@ snapshots: tiny-warning@1.0.3: {} + tinybench@2.9.0: {} + tinycolor2@1.6.0: {} + tinyexec@0.3.2: {} + tinyexec@1.0.1: {} tinyglobby@0.2.14: @@ -12788,6 +13098,8 @@ snapshots: '@types/tinycolor2': 1.4.6 tinycolor2: 1.6.0 + tinyrainbow@3.0.3: {} + title-case@2.1.1: dependencies: no-case: 2.3.2 @@ -12817,6 +13129,8 @@ snapshots: dependencies: is-number: 7.0.0 + totalist@3.0.1: {} + tr46@0.0.3: {} trim-repeated@1.0.0: @@ -12999,6 +13313,8 @@ snapshots: undici-types@7.10.0: {} + undici-types@7.16.0: {} + unicode-properties@1.4.1: dependencies: base64-js: 1.5.1 @@ -13126,7 +13442,7 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite@7.1.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1): + vite@7.1.5(@types/node@24.10.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) @@ -13135,13 +13451,52 @@ snapshots: rollup: 4.47.1 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 24.3.0 + '@types/node': 24.10.1 fsevents: 2.3.3 jiti: 2.5.1 lightningcss: 1.30.1 tsx: 4.20.4 yaml: 2.8.1 + vitest@4.0.8(@types/node@24.10.1)(@vitest/ui@4.0.8)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1): + dependencies: + '@vitest/expect': 4.0.8 + '@vitest/mocker': 4.0.8(vite@7.1.5(@types/node@24.10.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1)) + '@vitest/pretty-format': 4.0.8 + '@vitest/runner': 4.0.8 + '@vitest/snapshot': 4.0.8 + '@vitest/spy': 4.0.8 + '@vitest/utils': 4.0.8 + debug: 4.4.3 + es-module-lexer: 1.7.0 + expect-type: 1.2.2 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 7.1.5(@types/node@24.10.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.10.1 + '@vitest/ui': 4.0.8(vitest@4.0.8) + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + wcwidth@1.0.1: dependencies: defaults: 1.0.4 @@ -13208,6 +13563,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + wide-align@1.1.5: dependencies: string-width: 4.2.3 @@ -13218,8 +13578,6 @@ snapshots: wordwrapjs@5.1.0: {} - workerpool@9.3.3: {} - wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 diff --git a/turbo.json b/turbo.json index 7e31637..be0d15a 100644 --- a/turbo.json +++ b/turbo.json @@ -60,6 +60,20 @@ "dependsOn": ["^build"], "cache": false, "persistent": true + }, + "test": { + "dependsOn": [], + "cache": false + }, + "test:watch": { + "dependsOn": [], + "cache": false, + "persistent": true + }, + "test:ui": { + "dependsOn": [], + "cache": false, + "persistent": true } } }