chore: handle token refresh nuances
This commit is contained in:
parent
bae86750e0
commit
81e5919735
18 changed files with 392 additions and 316 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -49,6 +49,7 @@ CLAUDE.md
|
|||
.serena
|
||||
.local
|
||||
.claude
|
||||
.specify
|
||||
amical.db
|
||||
AGENTS.md
|
||||
|
||||
|
|
|
|||
|
|
@ -166,6 +166,7 @@ export interface AppSettingsData {
|
|||
isAuthenticated: boolean;
|
||||
idToken: string | null;
|
||||
refreshToken: string | null;
|
||||
accessToken: string | null;
|
||||
expiresAt: number | null;
|
||||
userInfo?: {
|
||||
sub: string;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<boolean> {
|
||||
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<string | null> {
|
||||
|
|
@ -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<void> {
|
||||
private async performTokenRefresh(
|
||||
refreshToken: string,
|
||||
idToken: string | null,
|
||||
): Promise<void> {
|
||||
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,
|
||||
|
|
|
|||
2
apps/desktop/src/types/bundled-env.d.ts
vendored
2
apps/desktop/src/types/bundled-env.d.ts
vendored
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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<AppSettingsData>
|
||||
settings: Partial<AppSettingsData>,
|
||||
) => {
|
||||
// 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<void>)
|
||||
fixture: keyof typeof fixtures | ((testDb: TestDatabase) => Promise<void>),
|
||||
): Promise<void> {
|
||||
if (typeof fixture === 'function') {
|
||||
if (typeof fixture === "function") {
|
||||
await fixture(testDb);
|
||||
} else {
|
||||
await fixtures[fixture](testDb);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<TestApp> {
|
||||
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<typeof router.createCaller>;
|
||||
cleanup: () => Promise<void>;
|
||||
}> {
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -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<TestDatabase> {
|
||||
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<void> {
|
|||
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<void> {
|
|||
* Clears all test databases
|
||||
*/
|
||||
export async function clearAllTestDatabases(): Promise<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
||||
|
|
|
|||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue