From 81e591973551baaeeee8052ac97e171f4b059c06 Mon Sep 17 00:00:00 2001 From: haritabh-z01 Date: Wed, 12 Nov 2025 17:10:28 +0530 Subject: [PATCH] chore: handle token refresh nuances --- .gitignore | 1 + apps/desktop/src/db/schema.ts | 1 + apps/desktop/src/db/transcriptions.ts | 13 +- apps/desktop/src/services/auth-service.ts | 64 +++++--- apps/desktop/src/types/bundled-env.d.ts | 2 +- apps/desktop/tests/README.md | 24 +-- apps/desktop/tests/helpers/electron-mocks.ts | 74 +++++---- apps/desktop/tests/helpers/fixtures.ts | 108 ++++++------- apps/desktop/tests/helpers/native-mocks.ts | 74 ++++----- apps/desktop/tests/helpers/test-app.ts | 28 ++-- apps/desktop/tests/helpers/test-db.ts | 30 ++-- .../tests/services/transcriptions.test.ts | 95 ++++++------ apps/desktop/tests/setup.ts | 146 ++++++++++-------- apps/desktop/vite.main.config.mts | 8 +- apps/desktop/vitest.config.ts | 24 +-- package.json | 3 +- packages/whisper-wrapper/bin/build-addon.js | 1 + pnpm-lock.yaml | 12 +- 18 files changed, 392 insertions(+), 316 deletions(-) diff --git a/.gitignore b/.gitignore index c8086cd..edca7de 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ CLAUDE.md .serena .local .claude +.specify amical.db AGENTS.md diff --git a/apps/desktop/src/db/schema.ts b/apps/desktop/src/db/schema.ts index 84cdfd0..3252cb8 100644 --- a/apps/desktop/src/db/schema.ts +++ b/apps/desktop/src/db/schema.ts @@ -166,6 +166,7 @@ export interface AppSettingsData { isAuthenticated: boolean; idToken: string | null; refreshToken: string | null; + accessToken: string | null; expiresAt: number | null; userInfo?: { sub: string; diff --git a/apps/desktop/src/db/transcriptions.ts b/apps/desktop/src/db/transcriptions.ts index ad83bc6..bfaa1b1 100644 --- a/apps/desktop/src/db/transcriptions.ts +++ b/apps/desktop/src/db/transcriptions.ts @@ -1,4 +1,15 @@ -import { eq, desc, asc, and, ilike, count, gte, lte, sql, like } from "drizzle-orm"; +import { + eq, + desc, + asc, + and, + ilike, + count, + gte, + lte, + sql, + like, +} from "drizzle-orm"; import { db } from "."; import { transcriptions, diff --git a/apps/desktop/src/services/auth-service.ts b/apps/desktop/src/services/auth-service.ts index ecd0e1f..95b34cf 100644 --- a/apps/desktop/src/services/auth-service.ts +++ b/apps/desktop/src/services/auth-service.ts @@ -16,6 +16,7 @@ export interface AuthState { isAuthenticated: boolean; idToken: string | null; refreshToken: string | null; + accessToken: string | null; expiresAt: number | null; userInfo?: { sub: string; @@ -39,6 +40,15 @@ interface TokenResponse { id_token: string; } +interface RefreshTokenResponse { + access_token: string; + token_type: string; + expires_in: number; + refresh_token: string; + scope: string; + id_token?: string; // Optional in refresh flow +} + export class AuthService extends EventEmitter { private static instance: AuthService | null = null; private config: AuthConfig; @@ -54,8 +64,7 @@ export class AuthService extends EventEmitter { process.env.AUTHORIZATION_ENDPOINT || __BUNDLED_AUTH_AUTHORIZATION_ENDPOINT, tokenEndpoint: - process.env.AUTH_TOKEN_ENDPOINT || - __BUNDLED_AUTH_TOKEN_ENDPOINT, + process.env.AUTH_TOKEN_ENDPOINT || __BUNDLED_AUTH_TOKEN_ENDPOINT, redirectUri: "amical://oauth/callback", }; @@ -170,6 +179,7 @@ export class AuthService extends EventEmitter { isAuthenticated: true, idToken: tokenResponse.id_token, refreshToken: tokenResponse.refresh_token, + accessToken: tokenResponse.access_token, expiresAt: Date.now() + tokenResponse.expires_in * 1000, }; @@ -267,19 +277,16 @@ export class AuthService extends EventEmitter { /** * Check if user is authenticated + * Automatically refreshes tokens if they are expired or expiring soon */ async isAuthenticated(): Promise { + await this.refreshTokenIfNeeded(); + const authState = await this.getAuthState(); if (!authState || !authState.isAuthenticated) { return false; } - // Check if token is expired - if (authState.expiresAt && authState.expiresAt < Date.now()) { - // Token expired, should refresh - return false; - } - return true; } @@ -292,7 +299,8 @@ export class AuthService extends EventEmitter { } /** - * Get ID token for API requests + * Get bearer token for API requests + * Returns ID token if available, otherwise returns access token * Automatically refreshes the token if it's expiring soon */ async getIdToken(): Promise { @@ -306,7 +314,8 @@ export class AuthService extends EventEmitter { } const authState = await this.getAuthState(); - return authState?.idToken || null; + // Prefer ID token if available, otherwise use access token + return authState?.idToken || authState?.accessToken || null; } /** @@ -321,13 +330,15 @@ export class AuthService extends EventEmitter { const authState = await this.getAuthState(); if (!authState || !authState.refreshToken) { - throw new Error("No refresh token available"); + // No refresh token available - invalid state, logout user + await this.logout(); + return; } - // Check if token needs refresh (5 minutes before expiry) + // Check if token needs refresh (10 minutes before expiry) if ( authState.expiresAt && - authState.expiresAt - Date.now() > 5 * 60 * 1000 + authState.expiresAt - Date.now() > 10 * 60 * 1000 ) { // Token still valid return; @@ -337,9 +348,16 @@ export class AuthService extends EventEmitter { logger.main.info("Token needs refresh, starting refresh flow"); this.refreshPromise = this.performTokenRefresh( authState.refreshToken, - ).finally(() => { - this.refreshPromise = null; - }); + authState.idToken, + ) + .catch((error) => { + // Handle refresh errors internally - don't throw + // performTokenRefresh already handles 401/400 by logging out + logger.main.error("Token refresh failed:", error); + }) + .finally(() => { + this.refreshPromise = null; + }); return this.refreshPromise; } @@ -347,7 +365,10 @@ export class AuthService extends EventEmitter { /** * Perform the actual token refresh API call */ - private async performTokenRefresh(refreshToken: string): Promise { + private async performTokenRefresh( + refreshToken: string, + idToken: string | null, + ): Promise { try { logger.main.info("Refreshing access token"); @@ -385,7 +406,7 @@ export class AuthService extends EventEmitter { throw new Error(`Token refresh failed: ${response.statusText}`); } - const tokenResponse: TokenResponse = await response.json(); + const tokenResponse: RefreshTokenResponse = await response.json(); logger.main.info("Token refresh successful"); // Get current auth state to preserve user info @@ -394,17 +415,18 @@ export class AuthService extends EventEmitter { // Update auth state with new tokens const updatedAuthState: AuthState = { isAuthenticated: true, - idToken: tokenResponse.id_token, + idToken: tokenResponse.id_token || idToken, // Use new refresh token if provided, otherwise keep the old one refreshToken: tokenResponse.refresh_token || refreshToken, + accessToken: tokenResponse.access_token, expiresAt: Date.now() + tokenResponse.expires_in * 1000, userInfo: currentAuthState?.userInfo, }; // Update ID token user info if present - if (tokenResponse.id_token) { + if (updatedAuthState.idToken) { try { - const payload = tokenResponse.id_token.split(".")[1]; + const payload = updatedAuthState.idToken.split(".")[1]; const decoded = JSON.parse(Buffer.from(payload, "base64").toString()); updatedAuthState.userInfo = { sub: decoded.sub, diff --git a/apps/desktop/src/types/bundled-env.d.ts b/apps/desktop/src/types/bundled-env.d.ts index c163d77..7cf2e3b 100644 --- a/apps/desktop/src/types/bundled-env.d.ts +++ b/apps/desktop/src/types/bundled-env.d.ts @@ -4,4 +4,4 @@ declare const __BUNDLED_TELEMETRY_ENABLED: boolean; declare const __BUNDLED_AUTH_CLIENT_ID: string; declare const __BUNDLED_AUTH_AUTHORIZATION_ENDPOINT: string; declare const __BUNDLED_AUTH_TOKEN_ENDPOINT: string; -declare const __BUNDLED_API_ENDPOIN: string; +declare const __BUNDLED_API_ENDPOINT: string; diff --git a/apps/desktop/tests/README.md b/apps/desktop/tests/README.md index ced572e..f07e181 100644 --- a/apps/desktop/tests/README.md +++ b/apps/desktop/tests/README.md @@ -5,6 +5,7 @@ This directory contains the test setup for the Amical Desktop application's main ## 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 @@ -12,12 +13,14 @@ We use **Vitest** to test the Electron main process, specifically: ## 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 @@ -44,18 +47,18 @@ pnpm test:coverage ### Testing tRPC Procedures ```typescript -import { createTestDatabase } from '../helpers/test-db'; -import { initializeTestServices } from '../helpers/test-app'; -import { seedDatabase } from '../helpers/fixtures'; +import { createTestDatabase } from "../helpers/test-db"; +import { initializeTestServices } from "../helpers/test-app"; +import { seedDatabase } from "../helpers/fixtures"; -describe('My Service', () => { +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. + testDb = await createTestDatabase({ name: "my-test" }); + await seedDatabase(testDb, "withTranscriptions"); // or 'empty', 'full', etc. const result = await initializeTestServices(testDb); trpcCaller = result.trpcCaller; @@ -67,7 +70,7 @@ describe('My Service', () => { if (testDb) await testDb.close(); }); - it('should do something', async () => { + it("should do something", async () => { const result = await trpcCaller.myRouter.myProcedure({ input }); expect(result).toBeDefined(); }); @@ -88,8 +91,8 @@ describe('My Service', () => { ```typescript await fixtures.withCustomSettings(testDb, { - ui: { theme: 'dark' }, - transcription: { language: 'es' } + ui: { theme: "dark" }, + transcription: { language: "es" }, }); ``` @@ -104,12 +107,15 @@ await fixtures.withCustomSettings(testDb, { ## 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 diff --git a/apps/desktop/tests/helpers/electron-mocks.ts b/apps/desktop/tests/helpers/electron-mocks.ts index 3f691f1..1e01ce4 100644 --- a/apps/desktop/tests/helpers/electron-mocks.ts +++ b/apps/desktop/tests/helpers/electron-mocks.ts @@ -1,7 +1,7 @@ -import { vi } from 'vitest'; -import { EventEmitter } from 'events'; -import path from 'node:path'; -import os from 'node:os'; +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 { @@ -56,7 +56,7 @@ class FakeBrowserWindow extends EventEmitter { close() { this._isDestroyed = true; - this.emit('closed'); + this.emit("closed"); } destroy() { @@ -171,13 +171,19 @@ class FakeBrowserWindow extends EventEmitter { setTrafficLightPosition(position: { x: number; y: number }) {} // Mock methods that return values - getTitle() { return 'Test Window'; } - getNativeWindowHandle() { return Buffer.from('test'); } - getMediaSourceId() { return 'test-id'; } + 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 testUserDataPath = path.join(os.tmpdir(), "amical-test-" + Date.now()); const testAppPath = process.cwd(); // Mock app object @@ -188,19 +194,19 @@ const mockApp = { 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'), + 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'), + getName: vi.fn(() => "Amical"), + getVersion: vi.fn(() => "0.1.0-test"), isPackaged: false, isReady: vi.fn(() => true), whenReady: vi.fn(() => Promise.resolve()), @@ -212,9 +218,9 @@ const mockApp = { show: vi.fn(), setName: vi.fn(), setPath: vi.fn(), - getLocale: vi.fn(() => 'en-US'), - getLocaleCountryCode: vi.fn(() => 'US'), - getSystemLocale: vi.fn(() => 'en-US'), + getLocale: vi.fn(() => "en-US"), + getLocaleCountryCode: vi.fn(() => "US"), + getSystemLocale: vi.fn(() => "en-US"), on: vi.fn(), once: vi.fn(), removeListener: vi.fn(), @@ -276,10 +282,10 @@ const mockScreen = { // Mock systemPreferences const mockSystemPreferences = { - getMediaAccessStatus: vi.fn(() => 'granted'), + getMediaAccessStatus: vi.fn(() => "granted"), askForMediaAccess: vi.fn(() => Promise.resolve(true)), isTrustedAccessibilityClient: vi.fn(() => true), - getColor: vi.fn(() => '#000000'), + getColor: vi.fn(() => "#000000"), on: vi.fn(), removeListener: vi.fn(), }; @@ -287,7 +293,7 @@ const mockSystemPreferences = { // Mock nativeTheme const mockNativeTheme = { shouldUseDarkColors: false, - themeSource: 'system' as const, + themeSource: "system" as const, on: vi.fn(), removeListener: vi.fn(), }; @@ -309,13 +315,19 @@ class FakeTray extends EventEmitter { setImage(image: any) {} setContextMenu(menu: any) {} destroy() {} - isDestroyed() { return false; } + isDestroyed() { + return false; + } } // Mock dialog const mockDialog = { - showOpenDialog: vi.fn(() => Promise.resolve({ canceled: false, filePaths: [] })), - showSaveDialog: vi.fn(() => Promise.resolve({ canceled: false, filePath: '' })), + 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()), @@ -324,7 +336,7 @@ const mockDialog = { // Mock shell const mockShell = { openExternal: vi.fn(() => Promise.resolve()), - openPath: vi.fn(() => Promise.resolve('')), + openPath: vi.fn(() => Promise.resolve("")), showItemInFolder: vi.fn(), openItem: vi.fn(() => Promise.resolve(true)), moveItemToTrash: vi.fn(() => Promise.resolve(true)), @@ -344,9 +356,9 @@ const mockGlobalShortcut = { // Mock clipboard const mockClipboard = { - readText: vi.fn(() => ''), + readText: vi.fn(() => ""), writeText: vi.fn(), - readHTML: vi.fn(() => ''), + readHTML: vi.fn(() => ""), writeHTML: vi.fn(), readImage: vi.fn(() => ({})), writeImage: vi.fn(), diff --git a/apps/desktop/tests/helpers/fixtures.ts b/apps/desktop/tests/helpers/fixtures.ts index 79a8981..24be17c 100644 --- a/apps/desktop/tests/helpers/fixtures.ts +++ b/apps/desktop/tests/helpers/fixtures.ts @@ -1,5 +1,5 @@ -import type { TestDatabase } from './test-db'; -import * as schema from '@db/schema'; +import type { TestDatabase } from "./test-db"; +import * as schema from "@db/schema"; import type { NewTranscription, NewVocabulary, @@ -7,21 +7,21 @@ import type { NewAppSettings, NewNote, AppSettingsData, -} from '@db/schema'; +} from "@db/schema"; /** * Default app settings for testing */ export const defaultAppSettings: AppSettingsData = { formatterConfig: { - model: 'gpt-4o-mini', + model: "gpt-4o-mini", enabled: false, }, ui: { - theme: 'system', + theme: "system", }, transcription: { - language: 'en', + language: "en", autoTranscribe: true, confidenceThreshold: 0.7, enablePunctuation: true, @@ -29,23 +29,23 @@ export const defaultAppSettings: AppSettingsData = { preloadWhisperModel: false, }, recording: { - defaultFormat: 'wav', + defaultFormat: "wav", sampleRate: 16000, autoStopSilence: true, silenceThreshold: -45, maxRecordingDuration: 600, }, shortcuts: { - pushToTalk: 'CommandOrControl+Shift+Space', - toggleRecording: 'CommandOrControl+Shift+R', - toggleWindow: 'CommandOrControl+Shift+W', + pushToTalk: "CommandOrControl+Shift+Space", + toggleRecording: "CommandOrControl+Shift+R", + toggleWindow: "CommandOrControl+Shift+W", }, modelProvidersConfig: { - defaultSpeechModel: 'local-whisper:ggml-base.en', + defaultSpeechModel: "local-whisper:ggml-base.en", }, dictation: { autoDetectEnabled: true, - selectedLanguage: 'en', + selectedLanguage: "en", }, preferences: { launchAtLogin: false, @@ -68,27 +68,27 @@ export const defaultAppSettings: AppSettingsData = { */ export const sampleTranscriptions: NewTranscription[] = [ { - text: 'This is a test transcription', - language: 'en', + text: "This is a test transcription", + language: "en", confidence: 0.95, duration: 5, - speechModel: 'whisper-base', + speechModel: "whisper-base", formattingModel: null, }, { - text: 'Another test transcription with more content', - language: 'en', + text: "Another test transcription with more content", + language: "en", confidence: 0.88, duration: 8, - speechModel: 'whisper-base', - formattingModel: 'gpt-4o-mini', + speechModel: "whisper-base", + formattingModel: "gpt-4o-mini", }, { - text: 'A third transcription for comprehensive testing', - language: 'en', + text: "A third transcription for comprehensive testing", + language: "en", confidence: 0.92, duration: 6, - speechModel: 'whisper-large', + speechModel: "whisper-large", formattingModel: null, }, ]; @@ -98,20 +98,20 @@ export const sampleTranscriptions: NewTranscription[] = [ */ export const sampleVocabulary: NewVocabulary[] = [ { - word: 'Amical', + word: "Amical", replacementWord: null, isReplacement: false, usageCount: 5, }, { - word: 'API', + word: "API", replacementWord: null, isReplacement: false, usageCount: 3, }, { - word: 'teh', - replacementWord: 'the', + word: "teh", + replacementWord: "the", isReplacement: true, usageCount: 2, }, @@ -122,26 +122,26 @@ export const sampleVocabulary: NewVocabulary[] = [ */ 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', + 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', + 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', + id: "gpt-4o-mini", + provider: "openrouter", + name: "GPT-4o Mini", + type: "language", + context: "128k", + description: "Fast and efficient language model", speed: 5, accuracy: 4, }, @@ -152,14 +152,14 @@ export const sampleModels: NewModel[] = [ */ export const sampleNotes: NewNote[] = [ { - title: 'Test Note 1', - content: 'This is the first test note', - icon: '📝', + 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: '📄', + title: "Test Note 2", + content: "This is the second test note with more content", + icon: "📄", }, ]; @@ -229,7 +229,7 @@ export const fixtures = { */ withCustomSettings: async ( testDb: TestDatabase, - settings: Partial + settings: Partial, ) => { // Clear existing settings first await testDb.db.delete(schema.appSettings); @@ -248,13 +248,13 @@ export const fixtures = { await fixtures.withCustomSettings(testDb, { auth: { isAuthenticated: true, - idToken: 'test-id-token', - refreshToken: 'test-refresh-token', + 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', + sub: "test-user-123", + email: "test@example.com", + name: "Test User", }, }, }); @@ -266,9 +266,9 @@ export const fixtures = { */ export async function seedDatabase( testDb: TestDatabase, - fixture: keyof typeof fixtures | ((testDb: TestDatabase) => Promise) + fixture: keyof typeof fixtures | ((testDb: TestDatabase) => Promise), ): Promise { - if (typeof fixture === 'function') { + 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 index 137cd75..08fcacb 100644 --- a/apps/desktop/tests/helpers/native-mocks.ts +++ b/apps/desktop/tests/helpers/native-mocks.ts @@ -1,4 +1,4 @@ -import { vi } from 'vitest'; +import { vi } from "vitest"; // Mock onnxruntime-node export const mockOnnxRuntime = { @@ -11,10 +11,10 @@ export const mockOnnxRuntime = { data: new Float32Array([0.5, 0.5, 0.5]), dims: [1, 3], }, - }) + }), ), release: vi.fn(), - }) + }), ), }, Tensor: vi.fn(), @@ -30,32 +30,32 @@ export const mockWhisperWrapper = { WhisperModel: vi.fn().mockImplementation(() => ({ transcribe: vi.fn(() => Promise.resolve({ - text: 'Test transcription', + text: "Test transcription", segments: [ { start: 0, end: 1.5, - text: 'Test transcription', + text: "Test transcription", }, ], - }) + }), ), dispose: vi.fn(), })), downloadModel: vi.fn(() => Promise.resolve()), - getModelPath: vi.fn(() => '/mock/model/path'), + getModelPath: vi.fn(() => "/mock/model/path"), }; // Mock keytar (credential storage) export const mockKeytar = { getPassword: vi.fn((service: string, account: string) => - Promise.resolve(null) + Promise.resolve(null), ), setPassword: vi.fn((service: string, account: string, password: string) => - Promise.resolve() + Promise.resolve(), ), deletePassword: vi.fn((service: string, account: string) => - Promise.resolve(true) + Promise.resolve(true), ), findPassword: vi.fn((service: string) => Promise.resolve(null)), findCredentials: vi.fn((service: string) => Promise.resolve([])), @@ -69,7 +69,7 @@ export const mockLibsql = { rows: [], columns: [], rowsAffected: 0, - }) + }), ), batch: vi.fn(() => Promise.resolve([])), close: vi.fn(() => Promise.resolve()), @@ -86,7 +86,7 @@ export const mockSwiftHelper = { setSystemAudioMuted: vi.fn(() => true), isSystemAudioMuted: vi.fn(() => false), writeToClipboard: vi.fn(() => true), - readFromClipboard: vi.fn(() => ''), + readFromClipboard: vi.fn(() => ""), isRunning: vi.fn(() => true), }; @@ -102,44 +102,44 @@ export const mockWindowsHelper = { // Mock node-machine-id export const mockMachineId = { - machineIdSync: vi.fn(() => 'test-machine-id-12345'), - machineId: vi.fn(() => Promise.resolve('test-machine-id-12345')), + 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', - }) + 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', + 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', - }) + platform: "darwin", + distro: "macOS", + release: "14.0", + arch: "arm64", + }), ), }; @@ -158,15 +158,15 @@ export const mockUpdateElectronApp = vi.fn(); export function createNativeMocks() { return { - 'onnxruntime-node': mockOnnxRuntime, - '@amical/whisper-wrapper': mockWhisperWrapper, + "onnxruntime-node": mockOnnxRuntime, + "@amical/whisper-wrapper": mockWhisperWrapper, keytar: mockKeytar, libsql: mockLibsql, - '@amical/swift-helper': mockSwiftHelper, - '@amical/windows-helper': mockWindowsHelper, - 'node-machine-id': mockMachineId, + "@amical/swift-helper": mockSwiftHelper, + "@amical/windows-helper": mockWindowsHelper, + "node-machine-id": mockMachineId, systeminformation: mockSystemInformation, - 'posthog-node': mockPostHog, - 'update-electron-app': mockUpdateElectronApp, + "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 index bb391d3..0734326 100644 --- a/apps/desktop/tests/helpers/test-app.ts +++ b/apps/desktop/tests/helpers/test-app.ts @@ -1,9 +1,9 @@ -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'; +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 @@ -23,12 +23,12 @@ export async function initializeTestApp( options: { skipOnboarding?: boolean; skipWindows?: boolean; - } = {} + } = {}, ): Promise { const { skipOnboarding = true, skipWindows = false } = options; // Mock the database module to use our test database - vi.doMock('@db', () => ({ + vi.doMock("@db", () => ({ db: testDb.db, dbPath: testDb.dbPath, initializeDatabase: vi.fn().mockResolvedValue(undefined), @@ -37,7 +37,7 @@ export async function initializeTestApp( // Mock onboarding check to skip it if (skipOnboarding) { - process.env.FORCE_ONBOARDING = 'false'; + process.env.FORCE_ONBOARDING = "false"; } // Create AppManager instance @@ -49,7 +49,7 @@ export async function initializeTestApp( await appManager.initialize(); } catch (error) { // Some initialization errors are expected in test environment - console.warn('AppManager initialization warning:', error); + console.warn("AppManager initialization warning:", error); } // Get service manager @@ -83,15 +83,13 @@ export function createTestTRPCCaller(serviceManager: ServiceManager) { * Initialize just the ServiceManager without AppManager * Useful for testing services in isolation */ -export async function initializeTestServices( - testDb: TestDatabase -): Promise<{ +export async function initializeTestServices(testDb: TestDatabase): Promise<{ serviceManager: ServiceManager; trpcCaller: ReturnType; cleanup: () => Promise; }> { // Mock the database module - vi.doMock('@db', () => ({ + vi.doMock("@db", () => ({ db: testDb.db, dbPath: testDb.dbPath, initializeDatabase: vi.fn().mockResolvedValue(undefined), @@ -104,7 +102,7 @@ export async function initializeTestServices( try { await serviceManager.initialize(); } catch (error) { - console.warn('ServiceManager initialization warning:', error); + console.warn("ServiceManager initialization warning:", error); } // Create tRPC caller diff --git a/apps/desktop/tests/helpers/test-db.ts b/apps/desktop/tests/helpers/test-db.ts index 0abd11f..e397b7a 100644 --- a/apps/desktop/tests/helpers/test-db.ts +++ b/apps/desktop/tests/helpers/test-db.ts @@ -1,9 +1,9 @@ -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'; +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; @@ -21,13 +21,13 @@ 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); + const dbPath = path.join(TEST_USER_DATA_PATH, "databases", dbName); // Ensure directory exists await fs.ensureDir(path.dirname(dbPath)); @@ -41,14 +41,14 @@ export async function createTestDatabase( // Run migrations if not skipped if (!skipMigrations) { - const migrationsPath = path.join(process.cwd(), 'src', 'db', 'migrations'); + const migrationsPath = path.join(process.cwd(), "src", "db", "migrations"); // Check if migrations exist if (!fs.existsSync(migrationsPath)) { console.warn( - 'Migrations folder not found at:', + "Migrations folder not found at:", migrationsPath, - '- skipping migrations' + "- skipping migrations", ); } else { try { @@ -56,7 +56,7 @@ export async function createTestDatabase( migrationsFolder: migrationsPath, }); } catch (error) { - console.error('Failed to run migrations:', error); + console.error("Failed to run migrations:", error); throw error; } } @@ -87,7 +87,7 @@ export async function deleteTestDatabase(dbPath: string): Promise { try { await fs.remove(dbPath); } catch (error) { - console.error('Failed to delete test database:', error); + console.error("Failed to delete test database:", error); } } @@ -95,11 +95,11 @@ export async function deleteTestDatabase(dbPath: string): Promise { * Clears all test databases */ export async function clearAllTestDatabases(): Promise { - const dbDir = path.join(TEST_USER_DATA_PATH, 'databases'); + const dbDir = path.join(TEST_USER_DATA_PATH, "databases"); try { await fs.emptyDir(dbDir); } catch (error) { - console.error('Failed to clear test databases:', error); + console.error("Failed to clear test databases:", error); } } diff --git a/apps/desktop/tests/services/transcriptions.test.ts b/apps/desktop/tests/services/transcriptions.test.ts index e94e907..24a965e 100644 --- a/apps/desktop/tests/services/transcriptions.test.ts +++ b/apps/desktop/tests/services/transcriptions.test.ts @@ -1,10 +1,14 @@ -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'; +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', () => { +describe("Transcriptions Service", () => { let testDb: TestDatabase; let serviceManager: any; let trpcCaller: any; @@ -19,30 +23,30 @@ describe('Transcriptions Service', () => { } }); - describe('Get Transcriptions', () => { + describe("Get Transcriptions", () => { beforeEach(async () => { - testDb = await createTestDatabase({ name: 'get-transcriptions-test' }); + testDb = await createTestDatabase({ name: "get-transcriptions-test" }); setTestDatabase(testDb.db); - await seedDatabase(testDb, 'withTranscriptions'); + await seedDatabase(testDb, "withTranscriptions"); const result = await initializeTestServices(testDb); serviceManager = result.serviceManager; trpcCaller = result.trpcCaller; cleanup = result.cleanup; }); - it('should return all transcriptions', async () => { + 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'); + expect(transcriptions[0]).toHaveProperty("id"); + expect(transcriptions[0]).toHaveProperty("text"); + expect(transcriptions[0]).toHaveProperty("language"); }); - it('should respect limit parameter', async () => { + it("should respect limit parameter", async () => { const transcriptions = await trpcCaller.transcriptions.getTranscriptions({ limit: 2, offset: 0, @@ -51,7 +55,7 @@ describe('Transcriptions Service', () => { expect(transcriptions).toHaveLength(2); }); - it('should respect offset parameter', async () => { + it("should respect offset parameter", async () => { const allTranscriptions = await trpcCaller.transcriptions.getTranscriptions({ limit: 10, @@ -69,33 +73,34 @@ describe('Transcriptions Service', () => { }); }); - describe('Get Transcription by ID', () => { + describe("Get Transcription by ID", () => { beforeEach(async () => { - testDb = await createTestDatabase({ name: 'get-by-id-test' }); + testDb = await createTestDatabase({ name: "get-by-id-test" }); setTestDatabase(testDb.db); - await seedDatabase(testDb, 'withTranscriptions'); + 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 () => { + 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, - }); + 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 () => { + it("should return null for non-existent id", async () => { const result = await trpcCaller.transcriptions.getTranscriptionById({ id: 99999, }); @@ -103,18 +108,18 @@ describe('Transcriptions Service', () => { }); }); - describe('Delete Transcription', () => { + describe("Delete Transcription", () => { beforeEach(async () => { - testDb = await createTestDatabase({ name: 'delete-test' }); + testDb = await createTestDatabase({ name: "delete-test" }); setTestDatabase(testDb.db); - await seedDatabase(testDb, 'withTranscriptions'); + 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 () => { + it("should delete transcription by id", async () => { const transcriptions = await trpcCaller.transcriptions.getTranscriptions({ limit: 10, offset: 0, @@ -135,32 +140,32 @@ describe('Transcriptions Service', () => { }); }); - describe('Search Transcriptions', () => { + describe("Search Transcriptions", () => { beforeEach(async () => { - testDb = await createTestDatabase({ name: 'search-test' }); + testDb = await createTestDatabase({ name: "search-test" }); setTestDatabase(testDb.db); - await seedDatabase(testDb, 'withTranscriptions'); + 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 () => { + it("should search transcriptions by text", async () => { const results = await trpcCaller.transcriptions.searchTranscriptions({ - searchTerm: 'test', + searchTerm: "test", limit: 10, }); expect(results.length).toBeGreaterThan(0); results.forEach((result: any) => { - expect(result.text.toLowerCase()).toContain('test'); + expect(result.text.toLowerCase()).toContain("test"); }); }); - it('should return empty array for no matches', async () => { + it("should return empty array for no matches", async () => { const results = await trpcCaller.transcriptions.searchTranscriptions({ - searchTerm: 'nonexistentquerystring', + searchTerm: "nonexistentquerystring", limit: 10, }); @@ -168,18 +173,18 @@ describe('Transcriptions Service', () => { }); }); - describe('Empty Database', () => { + describe("Empty Database", () => { beforeEach(async () => { - testDb = await createTestDatabase({ name: 'empty-test' }); + testDb = await createTestDatabase({ name: "empty-test" }); setTestDatabase(testDb.db); - await seedDatabase(testDb, 'empty'); + 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 () => { + it("should return empty array for empty database", async () => { const transcriptions = await trpcCaller.transcriptions.getTranscriptions({ limit: 10, offset: 0, @@ -188,9 +193,9 @@ describe('Transcriptions Service', () => { expect(transcriptions).toHaveLength(0); }); - it('should handle search on empty database', async () => { + it("should handle search on empty database", async () => { const results = await trpcCaller.transcriptions.searchTranscriptions({ - searchTerm: 'test', + searchTerm: "test", limit: 10, }); @@ -198,18 +203,18 @@ describe('Transcriptions Service', () => { }); }); - describe('Get Count', () => { + describe("Get Count", () => { beforeEach(async () => { - testDb = await createTestDatabase({ name: 'count-test' }); + testDb = await createTestDatabase({ name: "count-test" }); setTestDatabase(testDb.db); - await seedDatabase(testDb, 'withTranscriptions'); + 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 () => { + it("should return total transcription count", async () => { const count = await trpcCaller.transcriptions.getTranscriptionsCount({}); expect(count).toBe(sampleTranscriptions.length); diff --git a/apps/desktop/tests/setup.ts b/apps/desktop/tests/setup.ts index 58dde69..057d76d 100644 --- a/apps/desktop/tests/setup.ts +++ b/apps/desktop/tests/setup.ts @@ -1,11 +1,11 @@ -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'; +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'; +process.env.NODE_ENV = "test"; +process.env.VITEST = "true"; // Global test database instance - will be set by each test let currentTestDb: any = null; @@ -18,35 +18,37 @@ export function setTestDatabase(db: any) { // Helper function to get the current test database export function getTestDatabase() { if (!currentTestDb) { - throw new Error('Test database not set. Call setTestDatabase() in beforeEach.'); + 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', () => ({ +vi.mock("@db", () => ({ get db() { return getTestDatabase(); }, get dbPath() { - return '/test/db/path'; + 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'); +vi.mock("electron", async () => { + const { createElectronMocks } = await import("./helpers/electron-mocks"); return createElectronMocks(); }); // Mock native modules -vi.mock('onnxruntime-node', () => ({ +vi.mock("onnxruntime-node", () => ({ InferenceSession: { - create: vi.fn(function() { + create: vi.fn(function () { return Promise.resolve({ - run: vi.fn(function() { + run: vi.fn(function () { return Promise.resolve({ output: { data: new Float32Array([0.5, 0.5, 0.5]), @@ -66,17 +68,17 @@ vi.mock('onnxruntime-node', () => ({ }, })); -vi.mock('@amical/whisper-wrapper', () => ({ - WhisperModel: vi.fn().mockImplementation(function() { +vi.mock("@amical/whisper-wrapper", () => ({ + WhisperModel: vi.fn().mockImplementation(function () { return { - transcribe: vi.fn(function() { + transcribe: vi.fn(function () { return Promise.resolve({ - text: 'Test transcription', + text: "Test transcription", segments: [ { start: 0, end: 1.5, - text: 'Test transcription', + text: "Test transcription", }, ], }); @@ -84,86 +86,100 @@ vi.mock('@amical/whisper-wrapper', () => ({ 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) { + downloadModel: vi.fn(function () { return Promise.resolve(); }), - deletePassword: vi.fn(function(service: string, account: string) { - return Promise.resolve(true); + getModelPath: vi.fn(function () { + return "/mock/model/path"; }), - findPassword: vi.fn(function(service: string) { +})); + +vi.mock("keytar", () => ({ + getPassword: vi.fn(function (service: string, account: string) { return Promise.resolve(null); }), - findCredentials: vi.fn(function(service: string) { + 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("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() { +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', + manufacturer: "Test Manufacturer", + model: "Test Model", + version: "1.0", + serial: "TEST123", + uuid: "test-uuid", + sku: "TEST-SKU", }); }), - cpu: vi.fn(function() { + cpu: vi.fn(function () { return Promise.resolve({ - manufacturer: 'Test CPU', - brand: 'Test Brand', + manufacturer: "Test CPU", + brand: "Test Brand", speed: 2.5, cores: 4, }); }), - mem: vi.fn(function() { + mem: vi.fn(function () { return Promise.resolve({ total: 16000000000, free: 8000000000, used: 8000000000, }); }), - osInfo: vi.fn(function() { + osInfo: vi.fn(function () { return Promise.resolve({ - platform: 'darwin', - distro: 'macOS', - release: '14.0', - arch: 'arm64', + platform: "darwin", + distro: "macOS", + release: "14.0", + arch: "arm64", }); }), })); -vi.mock('posthog-node', () => ({ - PostHog: vi.fn().mockImplementation(function() { +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(); }), + shutdown: vi.fn(function () { + return Promise.resolve(); + }), }; }), })); -vi.mock('update-electron-app', () => ({ +vi.mock("update-electron-app", () => ({ default: vi.fn(), })); // Mock electron-log -vi.mock('electron-log', () => ({ +vi.mock("electron-log", () => ({ default: { info: vi.fn(), error: vi.fn(), @@ -172,8 +188,8 @@ vi.mock('electron-log', () => ({ verbose: vi.fn(), silly: vi.fn(), transports: { - file: { level: 'info' }, - console: { level: 'info' }, + file: { level: "info" }, + console: { level: "info" }, }, scope: vi.fn(() => ({ info: vi.fn(), @@ -191,12 +207,12 @@ vi.mock('electron-log', () => ({ })); // Mock electron-squirrel-startup -vi.mock('electron-squirrel-startup', () => ({ +vi.mock("electron-squirrel-startup", () => ({ default: false, })); // Mock electron-trpc-experimental -vi.mock('electron-trpc-experimental/main', () => ({ +vi.mock("electron-trpc-experimental/main", () => ({ createIPCHandler: vi.fn(() => ({ handle: vi.fn(), })), @@ -206,9 +222,9 @@ vi.mock('electron-trpc-experimental/main', () => ({ 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')); + 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 @@ -217,7 +233,7 @@ afterAll(async () => { try { await fs.remove(TEST_USER_DATA_PATH); } catch (error) { - console.error('Failed to clean up test directory:', error); + console.error("Failed to clean up test directory:", error); } }); diff --git a/apps/desktop/vite.main.config.mts b/apps/desktop/vite.main.config.mts index 6fc1a45..5f5d014 100644 --- a/apps/desktop/vite.main.config.mts +++ b/apps/desktop/vite.main.config.mts @@ -12,8 +12,12 @@ export default defineConfig({ process.env.TELEMETRY_ENABLED !== "false", ), __BUNDLED_AUTH_CLIENT_ID: JSON.stringify(process.env.AUTH_CLIENT_ID || ""), - __BUNDLED_AUTH_AUTHORIZATION_ENDPOINT: JSON.stringify(process.env.AUTHORIZATION_ENDPOINT || ""), - __BUNDLED_AUTH_TOKEN_ENDPOINT: JSON.stringify(process.env.AUTH_TOKEN_ENDPOINT || ""), + __BUNDLED_AUTH_AUTHORIZATION_ENDPOINT: JSON.stringify( + process.env.AUTHORIZATION_ENDPOINT || "", + ), + __BUNDLED_AUTH_TOKEN_ENDPOINT: JSON.stringify( + process.env.AUTH_TOKEN_ENDPOINT || "", + ), __BUNDLED_API_ENDPOINT: JSON.stringify(process.env.API_ENDPOINT || ""), }, build: { diff --git a/apps/desktop/vitest.config.ts b/apps/desktop/vitest.config.ts index 7a91881..774ee40 100644 --- a/apps/desktop/vitest.config.ts +++ b/apps/desktop/vitest.config.ts @@ -1,13 +1,13 @@ -import { defineConfig } from 'vitest/config'; -import { resolve } from 'path'; +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'], + 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 @@ -17,12 +17,12 @@ export default defineConfig({ }, 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'), + "@": 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 c195085..0385cea 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "pnpm": { "overrides": { "@electron-forge/maker-dmg": "https://registry.npmjs.org/@fellow/maker-dmg/-/maker-dmg-7.4.0.tgz", - "node-abi": "4.14.0" + "node-abi": "4.14.0", + "@types/minimatch": "3.0.5" }, "ignoredBuiltDependencies": [ "@tailwindcss/oxide", diff --git a/packages/whisper-wrapper/bin/build-addon.js b/packages/whisper-wrapper/bin/build-addon.js index 5dbd13a..d423a5f 100644 --- a/packages/whisper-wrapper/bin/build-addon.js +++ b/packages/whisper-wrapper/bin/build-addon.js @@ -294,3 +294,4 @@ for (const variant of variants) { // extremely long CMake-generated paths that break Windows packaging tools. fs.rmSync(buildVariantDir, { recursive: true, force: true }); } + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6c57991..528188a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,7 @@ settings: overrides: '@electron-forge/maker-dmg': https://registry.npmjs.org/@fellow/maker-dmg/-/maker-dmg-7.4.0.tgz node-abi: 4.14.0 + '@types/minimatch': 3.0.5 importers: @@ -2607,9 +2608,8 @@ packages: '@types/keyv@3.1.4': resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} - '@types/minimatch@6.0.0': - resolution: {integrity: sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==} - deprecated: This is a stub types definition. minimatch provides its own type definitions, so you do not need this installed. + '@types/minimatch@3.0.5': + resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} '@types/node-fetch@2.6.13': resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} @@ -9007,7 +9007,7 @@ snapshots: '@types/glob@7.2.0': dependencies: - '@types/minimatch': 6.0.0 + '@types/minimatch': 3.0.5 '@types/node': 24.3.0 '@types/http-cache-semantics@4.0.4': {} @@ -9035,9 +9035,7 @@ snapshots: dependencies: '@types/node': 24.10.1 - '@types/minimatch@6.0.0': - dependencies: - minimatch: 10.0.3 + '@types/minimatch@3.0.5': {} '@types/node-fetch@2.6.13': dependencies: