chore: handle token refresh nuances

This commit is contained in:
haritabh-z01 2025-11-12 17:10:28 +05:30
parent bae86750e0
commit 81e5919735
18 changed files with 392 additions and 316 deletions

1
.gitignore vendored
View file

@ -49,6 +49,7 @@ CLAUDE.md
.serena
.local
.claude
.specify
amical.db
AGENTS.md

View file

@ -166,6 +166,7 @@ export interface AppSettingsData {
isAuthenticated: boolean;
idToken: string | null;
refreshToken: string | null;
accessToken: string | null;
expiresAt: number | null;
userInfo?: {
sub: string;

View file

@ -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,

View file

@ -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,7 +348,14 @@ export class AuthService extends EventEmitter {
logger.main.info("Token needs refresh, starting refresh flow");
this.refreshPromise = this.performTokenRefresh(
authState.refreshToken,
).finally(() => {
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;
});
@ -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,

View file

@ -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;

View file

@ -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

View file

@ -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(),

View file

@ -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);

View file

@ -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,
};
}

View file

@ -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

View file

@ -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);
}
}

View file

@ -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,24 +73,25 @@ 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({
const transcription =
await trpcCaller.transcriptions.getTranscriptionById({
id: transcriptions[0].id,
});
@ -95,7 +100,7 @@ describe('Transcriptions Service', () => {
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);

View file

@ -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);
}
});

View file

@ -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: {

View file

@ -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"),
},
},
});

View file

@ -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",

View file

@ -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
View file

@ -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: