chore: barebones testing setup

This commit is contained in:
haritabh-z01 2025-11-12 14:39:47 +05:30
parent 2789b5e46a
commit bae86750e0
15 changed files with 2099 additions and 46 deletions

View file

@ -39,7 +39,6 @@ export const EXTERNAL_DEPENDENCIES = [
"@libsql/win32-x64-msvc",
"libsql",
"onnxruntime-node",
"workerpool",
"@amical/whisper-wrapper",
// Add any other native modules you need here
];

View file

@ -33,6 +33,10 @@
"lint": "eslint --ext .ts,.tsx .",
"format:check": "prettier --check \"**/*.{ts,tsx,md,json,mjs,mts,css,mdx}\" --cache --ignore-path=../../.prettierignore",
"ts:check": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest watch",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage",
"db:generate": "drizzle-kit generate",
"db:push": "drizzle-kit push",
"db:migrate": "drizzle-kit migrate",
@ -63,8 +67,10 @@
"@tailwindcss/vite": "^4.1.6",
"@tanstack/react-router-devtools": "^1.131.41",
"@tanstack/router-plugin": "^1.131.41",
"@types/node": "^24.10.1",
"@types/react": "^19.1.12",
"@types/react-dom": "^19.1.9",
"@vitest/ui": "^4.0.8",
"bumpp": "^10.2.3",
"electron": "38.1.0",
"eslint": "^9.26.0",
@ -72,17 +78,19 @@
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-prettier": "^5.4.0",
"flora-colossus": "^2.0.0",
"fs-extra": "^11.3.2",
"prettier": "^3.5.3",
"tailwindcss": "^4.1.6",
"tsx": "^4.19.4",
"typescript": "~5.8.3",
"vite": "^7.1.5"
"vite": "^7.1.5",
"vitest": "^4.0.8"
},
"dependencies": {
"@ai-sdk/openai": "^1.3.22",
"@amical/eslint-config": "workspace:*",
"@amical/whisper-wrapper": "workspace:*",
"@amical/types": "workspace:*",
"@amical/whisper-wrapper": "workspace:*",
"@amical/y-libsql": "workspace:*",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
@ -170,7 +178,6 @@
"update-electron-app": "^3.1.1",
"uuid": "^11.1.0",
"vaul": "^1.1.2",
"workerpool": "^9.3.3",
"yjs": "^13.6.27",
"zod": "^3.25.24"
}

View file

@ -1,4 +1,4 @@
import { eq, desc, asc, and, ilike, count, gte, lte } from "drizzle-orm";
import { eq, desc, asc, and, ilike, count, gte, lte, sql, like } from "drizzle-orm";
import { db } from ".";
import {
transcriptions,
@ -55,7 +55,7 @@ export async function getTranscriptions(
return await db
.select()
.from(transcriptions)
.where(ilike(transcriptions.text, `%${search}%`))
.where(sql`${transcriptions.text} LIKE ${`%${search}%`} COLLATE NOCASE`)
.orderBy(orderFn(sortColumn))
.limit(limit)
.offset(offset);
@ -113,7 +113,7 @@ export async function getTranscriptionsCount(search?: string) {
const result = await db
.select({ count: count() })
.from(transcriptions)
.where(ilike(transcriptions.text, `%${search}%`));
.where(sql`${transcriptions.text} LIKE ${`%${search}%`} COLLATE NOCASE`);
return result[0]?.count || 0;
} else {
const result = await db.select({ count: count() }).from(transcriptions);
@ -152,7 +152,7 @@ export async function searchTranscriptions(searchTerm: string, limit = 20) {
return await db
.select()
.from(transcriptions)
.where(ilike(transcriptions.text, `%${searchTerm}%`))
.where(sql`${transcriptions.text} LIKE ${`%${searchTerm}%`} COLLATE NOCASE`)
.orderBy(desc(transcriptions.timestamp))
.limit(limit);
}

View file

@ -0,0 +1,121 @@
# Testing Guide
This directory contains the test setup for the Amical Desktop application's main process.
## Overview
We use **Vitest** to test the Electron main process, specifically:
- **tRPC router procedures** - Direct testing by calling router methods
- **Service business logic** - Testing services with different database states
- **App initialization** - Testing how the app initializes with various database conditions
## Architecture
### Test Database
- Uses real SQLite databases (not mocked)
- Each test gets an isolated database in a temporary directory
- Migrations are applied automatically
- Fixtures for seeding test data
### Mocking Strategy
- **Electron APIs** - Fully mocked (app, ipcMain, BrowserWindow, Menu, etc.)
- **Native Modules** - Mocked (onnxruntime, whisper, keytar, etc.)
- **Database** - Real SQLite with test fixtures
- **tRPC** - Called directly, bypassing IPC layer
## Running Tests
```bash
# Run all tests
pnpm test
# Watch mode
pnpm test:watch
# UI mode
pnpm test:ui
# With coverage
pnpm test:coverage
```
## Writing Tests
### Testing tRPC Procedures
```typescript
import { createTestDatabase } from '../helpers/test-db';
import { initializeTestServices } from '../helpers/test-app';
import { seedDatabase } from '../helpers/fixtures';
describe('My Service', () => {
let testDb;
let trpcCaller;
let cleanup;
beforeEach(async () => {
testDb = await createTestDatabase({ name: 'my-test' });
await seedDatabase(testDb, 'withTranscriptions'); // or 'empty', 'full', etc.
const result = await initializeTestServices(testDb);
trpcCaller = result.trpcCaller;
cleanup = result.cleanup;
});
afterEach(async () => {
if (cleanup) await cleanup();
if (testDb) await testDb.close();
});
it('should do something', async () => {
const result = await trpcCaller.myRouter.myProcedure({ input });
expect(result).toBeDefined();
});
});
```
### Available Fixtures
- `empty` - Empty database with default settings
- `withTranscriptions` - Database with sample transcriptions
- `withVocabulary` - Database with vocabulary items
- `withModels` - Database with downloaded models
- `withNotes` - Database with notes
- `withAuth` - Database with authenticated user
- `full` - Database with all types of data
### Custom Fixtures
```typescript
await fixtures.withCustomSettings(testDb, {
ui: { theme: 'dark' },
transcription: { language: 'es' }
});
```
## Known Limitations
1. **Full AppManager initialization** - Currently has issues with ServiceManager initialization. Use `initializeTestServices` instead for testing service business logic.
2. **Some modules require additional mocking** - If you encounter errors about missing modules, add mocks to `tests/setup.ts`.
3. **Database mocking** - The dynamic database mocking via `vi.doMock` doesn't work well with the existing module resolution. Tests work best when testing services directly rather than full app initialization.
## Troubleshooting
### "ServiceManager not initialized"
This means you're trying to use AppManager which requires more complex initialization. Use `initializeTestServices` to test services directly.
### "No procedure found on path"
Check that the tRPC procedure name matches the actual router definition. Refer to `src/trpc/routers/` for available procedures.
### "ENOENT: no such file or directory"
The test database or migrations folder might not be found. Ensure migrations exist at `src/db/migrations/`.
## Future Improvements
- Fix AppManager initialization for full integration tests
- Add more comprehensive fixtures
- Add test coverage reporting
- Add database state assertions helpers
- Create mock factories for complex objects

View file

@ -0,0 +1,384 @@
import { vi } from 'vitest';
import { EventEmitter } from 'events';
import path from 'node:path';
import os from 'node:os';
// Create a fake BrowserWindow class
class FakeBrowserWindow extends EventEmitter {
id: number;
webContents: any;
private _isDestroyed = false;
private _bounds = { x: 0, y: 0, width: 800, height: 600 };
private _isVisible = false;
private _isMinimized = false;
private _isMaximized = false;
private _isFocused = false;
private _isFullScreen = false;
constructor(options?: any) {
super();
this.id = Math.floor(Math.random() * 1000000);
// Mock webContents
this.webContents = {
send: vi.fn(),
on: vi.fn(),
once: vi.fn(),
removeListener: vi.fn(),
session: {
clearCache: vi.fn().mockResolvedValue(undefined),
clearStorageData: vi.fn().mockResolvedValue(undefined),
},
openDevTools: vi.fn(),
closeDevTools: vi.fn(),
isDevToolsOpened: vi.fn().mockReturnValue(false),
executeJavaScript: vi.fn().mockResolvedValue(undefined),
setWindowOpenHandler: vi.fn(),
setBackgroundThrottling: vi.fn(),
};
}
loadURL(url: string) {
return Promise.resolve();
}
loadFile(filePath: string) {
return Promise.resolve();
}
show() {
this._isVisible = true;
}
hide() {
this._isVisible = false;
}
close() {
this._isDestroyed = true;
this.emit('closed');
}
destroy() {
this._isDestroyed = true;
}
isDestroyed() {
return this._isDestroyed;
}
isVisible() {
return this._isVisible;
}
isMinimized() {
return this._isMinimized;
}
isMaximized() {
return this._isMaximized;
}
isFocused() {
return this._isFocused;
}
isFullScreen() {
return this._isFullScreen;
}
focus() {
this._isFocused = true;
}
blur() {
this._isFocused = false;
}
minimize() {
this._isMinimized = true;
}
maximize() {
this._isMaximized = true;
}
restore() {
this._isMinimized = false;
this._isMaximized = false;
}
setFullScreen(flag: boolean) {
this._isFullScreen = flag;
}
getBounds() {
return { ...this._bounds };
}
setBounds(bounds: Partial<typeof this._bounds>) {
this._bounds = { ...this._bounds, ...bounds };
}
setSize(width: number, height: number) {
this._bounds.width = width;
this._bounds.height = height;
}
setPosition(x: number, y: number) {
this._bounds.x = x;
this._bounds.y = y;
}
center() {
// Mock centering
}
setResizable(resizable: boolean) {}
setMovable(movable: boolean) {}
setMinimizable(minimizable: boolean) {}
setMaximizable(maximizable: boolean) {}
setFullScreenable(fullscreenable: boolean) {}
setClosable(closable: boolean) {}
setAlwaysOnTop(flag: boolean, level?: string) {}
setVisibleOnAllWorkspaces(visible: boolean) {}
setIgnoreMouseEvents(ignore: boolean) {}
setContentProtection(enable: boolean) {}
setFocusable(focusable: boolean) {}
setParentWindow(parent: any) {}
setTitle(title: string) {}
setTitleBarOverlay(options: any) {}
setOpacity(opacity: number) {}
setShape(rects: any[]) {}
setSkipTaskbar(skip: boolean) {}
setMenu(menu: any) {}
setAutoHideMenuBar(hide: boolean) {}
setMenuBarVisibility(visible: boolean) {}
setAspectRatio(aspectRatio: number) {}
setBackgroundColor(color: string) {}
setHasShadow(hasShadow: boolean) {}
setRepresentedFilename(filename: string) {}
setDocumentEdited(edited: boolean) {}
setIcon(icon: any) {}
setProgressBar(progress: number) {}
setOverlayIcon(overlay: any, description: string) {}
setThumbarButtons(buttons: any[]) {}
setThumbnailClip(region: any) {}
setThumbnailToolTip(toolTip: string) {}
setAppDetails(options: any) {}
setVibrancy(type: string) {}
setWindowButtonVisibility(visible: boolean) {}
setTrafficLightPosition(position: { x: number; y: number }) {}
// Mock methods that return values
getTitle() { return 'Test Window'; }
getNativeWindowHandle() { return Buffer.from('test'); }
getMediaSourceId() { return 'test-id'; }
}
// Create test directories
const testUserDataPath = path.join(os.tmpdir(), 'amical-test-' + Date.now());
const testAppPath = process.cwd();
// Mock app object
const mockApp = {
getPath: vi.fn((name: string) => {
const paths: Record<string, string> = {
userData: testUserDataPath,
appData: testUserDataPath,
temp: os.tmpdir(),
home: os.homedir(),
documents: path.join(os.homedir(), 'Documents'),
downloads: path.join(os.homedir(), 'Downloads'),
desktop: path.join(os.homedir(), 'Desktop'),
music: path.join(os.homedir(), 'Music'),
pictures: path.join(os.homedir(), 'Pictures'),
videos: path.join(os.homedir(), 'Videos'),
logs: path.join(testUserDataPath, 'logs'),
crashDumps: path.join(testUserDataPath, 'crashDumps'),
};
return paths[name] || testUserDataPath;
}),
getName: vi.fn(() => 'Amical'),
getVersion: vi.fn(() => '0.1.0-test'),
isPackaged: false,
isReady: vi.fn(() => true),
whenReady: vi.fn(() => Promise.resolve()),
quit: vi.fn(),
exit: vi.fn(),
relaunch: vi.fn(),
focus: vi.fn(),
hide: vi.fn(),
show: vi.fn(),
setName: vi.fn(),
setPath: vi.fn(),
getLocale: vi.fn(() => 'en-US'),
getLocaleCountryCode: vi.fn(() => 'US'),
getSystemLocale: vi.fn(() => 'en-US'),
on: vi.fn(),
once: vi.fn(),
removeListener: vi.fn(),
removeAllListeners: vi.fn(),
emit: vi.fn(),
setLoginItemSettings: vi.fn(),
getLoginItemSettings: vi.fn(() => ({ openAtLogin: false })),
};
// Mock ipcMain
const mockIpcMain = {
handle: vi.fn(),
on: vi.fn(),
once: vi.fn(),
removeHandler: vi.fn(),
removeListener: vi.fn(),
removeAllListeners: vi.fn(),
emit: vi.fn(),
};
// Mock screen
const mockScreen = {
getPrimaryDisplay: vi.fn(() => ({
id: 1,
bounds: { x: 0, y: 0, width: 1920, height: 1080 },
workArea: { x: 0, y: 0, width: 1920, height: 1040 },
size: { width: 1920, height: 1080 },
workAreaSize: { width: 1920, height: 1040 },
scaleFactor: 1,
rotation: 0,
internal: false,
})),
getAllDisplays: vi.fn(() => [
{
id: 1,
bounds: { x: 0, y: 0, width: 1920, height: 1080 },
workArea: { x: 0, y: 0, width: 1920, height: 1040 },
size: { width: 1920, height: 1080 },
workAreaSize: { width: 1920, height: 1040 },
scaleFactor: 1,
rotation: 0,
internal: false,
},
]),
getCursorScreenPoint: vi.fn(() => ({ x: 100, y: 100 })),
getDisplayNearestPoint: vi.fn(() => ({
id: 1,
bounds: { x: 0, y: 0, width: 1920, height: 1080 },
workArea: { x: 0, y: 0, width: 1920, height: 1040 },
size: { width: 1920, height: 1080 },
workAreaSize: { width: 1920, height: 1040 },
scaleFactor: 1,
rotation: 0,
internal: false,
})),
on: vi.fn(),
removeListener: vi.fn(),
};
// Mock systemPreferences
const mockSystemPreferences = {
getMediaAccessStatus: vi.fn(() => 'granted'),
askForMediaAccess: vi.fn(() => Promise.resolve(true)),
isTrustedAccessibilityClient: vi.fn(() => true),
getColor: vi.fn(() => '#000000'),
on: vi.fn(),
removeListener: vi.fn(),
};
// Mock nativeTheme
const mockNativeTheme = {
shouldUseDarkColors: false,
themeSource: 'system' as const,
on: vi.fn(),
removeListener: vi.fn(),
};
// Mock Menu
const mockMenu = {
buildFromTemplate: vi.fn(() => ({})),
setApplicationMenu: vi.fn(),
getApplicationMenu: vi.fn(() => null),
};
// Mock Tray
class FakeTray extends EventEmitter {
constructor(image: any) {
super();
}
setToolTip(toolTip: string) {}
setTitle(title: string) {}
setImage(image: any) {}
setContextMenu(menu: any) {}
destroy() {}
isDestroyed() { return false; }
}
// Mock dialog
const mockDialog = {
showOpenDialog: vi.fn(() => Promise.resolve({ canceled: false, filePaths: [] })),
showSaveDialog: vi.fn(() => Promise.resolve({ canceled: false, filePath: '' })),
showMessageBox: vi.fn(() => Promise.resolve({ response: 0 })),
showErrorBox: vi.fn(),
showCertificateTrustDialog: vi.fn(() => Promise.resolve()),
};
// Mock shell
const mockShell = {
openExternal: vi.fn(() => Promise.resolve()),
openPath: vi.fn(() => Promise.resolve('')),
showItemInFolder: vi.fn(),
openItem: vi.fn(() => Promise.resolve(true)),
moveItemToTrash: vi.fn(() => Promise.resolve(true)),
beep: vi.fn(),
writeShortcutLink: vi.fn(() => true),
readShortcutLink: vi.fn(() => ({})),
};
// Mock globalShortcut
const mockGlobalShortcut = {
register: vi.fn(() => true),
registerAll: vi.fn(),
isRegistered: vi.fn(() => false),
unregister: vi.fn(),
unregisterAll: vi.fn(),
};
// Mock clipboard
const mockClipboard = {
readText: vi.fn(() => ''),
writeText: vi.fn(),
readHTML: vi.fn(() => ''),
writeHTML: vi.fn(),
readImage: vi.fn(() => ({})),
writeImage: vi.fn(),
clear: vi.fn(),
availableFormats: vi.fn(() => []),
};
// Mock nativeImage
const mockNativeImage = {
createEmpty: vi.fn(() => ({})),
createFromPath: vi.fn(() => ({})),
createFromBuffer: vi.fn(() => ({})),
createFromDataURL: vi.fn(() => ({})),
};
export function createElectronMocks() {
return {
app: mockApp,
ipcMain: mockIpcMain,
BrowserWindow: FakeBrowserWindow as any,
screen: mockScreen,
systemPreferences: mockSystemPreferences,
nativeTheme: mockNativeTheme,
Menu: mockMenu,
Tray: FakeTray as any,
dialog: mockDialog,
shell: mockShell,
globalShortcut: mockGlobalShortcut,
clipboard: mockClipboard,
nativeImage: mockNativeImage,
};
}
// Export test user data path for cleanup
export const TEST_USER_DATA_PATH = testUserDataPath;

View file

@ -0,0 +1,276 @@
import type { TestDatabase } from './test-db';
import * as schema from '@db/schema';
import type {
NewTranscription,
NewVocabulary,
NewModel,
NewAppSettings,
NewNote,
AppSettingsData,
} from '@db/schema';
/**
* Default app settings for testing
*/
export const defaultAppSettings: AppSettingsData = {
formatterConfig: {
model: 'gpt-4o-mini',
enabled: false,
},
ui: {
theme: 'system',
},
transcription: {
language: 'en',
autoTranscribe: true,
confidenceThreshold: 0.7,
enablePunctuation: true,
enableTimestamps: false,
preloadWhisperModel: false,
},
recording: {
defaultFormat: 'wav',
sampleRate: 16000,
autoStopSilence: true,
silenceThreshold: -45,
maxRecordingDuration: 600,
},
shortcuts: {
pushToTalk: 'CommandOrControl+Shift+Space',
toggleRecording: 'CommandOrControl+Shift+R',
toggleWindow: 'CommandOrControl+Shift+W',
},
modelProvidersConfig: {
defaultSpeechModel: 'local-whisper:ggml-base.en',
},
dictation: {
autoDetectEnabled: true,
selectedLanguage: 'en',
},
preferences: {
launchAtLogin: false,
minimizeToTray: true,
showWidgetWhileInactive: true,
},
telemetry: {
enabled: false,
},
auth: {
isAuthenticated: false,
idToken: null,
refreshToken: null,
expiresAt: null,
},
};
/**
* Sample transcriptions for testing
*/
export const sampleTranscriptions: NewTranscription[] = [
{
text: 'This is a test transcription',
language: 'en',
confidence: 0.95,
duration: 5,
speechModel: 'whisper-base',
formattingModel: null,
},
{
text: 'Another test transcription with more content',
language: 'en',
confidence: 0.88,
duration: 8,
speechModel: 'whisper-base',
formattingModel: 'gpt-4o-mini',
},
{
text: 'A third transcription for comprehensive testing',
language: 'en',
confidence: 0.92,
duration: 6,
speechModel: 'whisper-large',
formattingModel: null,
},
];
/**
* Sample vocabulary items for testing
*/
export const sampleVocabulary: NewVocabulary[] = [
{
word: 'Amical',
replacementWord: null,
isReplacement: false,
usageCount: 5,
},
{
word: 'API',
replacementWord: null,
isReplacement: false,
usageCount: 3,
},
{
word: 'teh',
replacementWord: 'the',
isReplacement: true,
usageCount: 2,
},
];
/**
* Sample models for testing
*/
export const sampleModels: NewModel[] = [
{
id: 'ggml-base.en',
provider: 'local-whisper',
name: 'Whisper Base English',
type: 'speech',
size: '~147 MB',
description: 'Optimized for English transcription',
localPath: '/test/models/ggml-base.en.bin',
sizeBytes: 147964211,
checksum: 'test-checksum-base',
downloadedAt: new Date(),
speed: 4,
accuracy: 3,
},
{
id: 'gpt-4o-mini',
provider: 'openrouter',
name: 'GPT-4o Mini',
type: 'language',
context: '128k',
description: 'Fast and efficient language model',
speed: 5,
accuracy: 4,
},
];
/**
* Sample notes for testing
*/
export const sampleNotes: NewNote[] = [
{
title: 'Test Note 1',
content: 'This is the first test note',
icon: '📝',
},
{
title: 'Test Note 2',
content: 'This is the second test note with more content',
icon: '📄',
},
];
/**
* Fixture presets
*/
export const fixtures = {
/**
* Empty database with only default settings
*/
empty: async (testDb: TestDatabase) => {
// Clear existing settings first
await testDb.db.delete(schema.appSettings);
// Insert default settings
await testDb.db.insert(schema.appSettings).values({
id: 1,
data: defaultAppSettings,
version: 1,
});
},
/**
* Database with existing transcriptions
*/
withTranscriptions: async (testDb: TestDatabase) => {
await fixtures.empty(testDb);
await testDb.db.insert(schema.transcriptions).values(sampleTranscriptions);
},
/**
* Database with vocabulary items
*/
withVocabulary: async (testDb: TestDatabase) => {
await fixtures.empty(testDb);
await testDb.db.insert(schema.vocabulary).values(sampleVocabulary);
},
/**
* Database with downloaded models
*/
withModels: async (testDb: TestDatabase) => {
await fixtures.empty(testDb);
await testDb.db.insert(schema.models).values(sampleModels);
},
/**
* Database with notes
*/
withNotes: async (testDb: TestDatabase) => {
await fixtures.empty(testDb);
await testDb.db.insert(schema.notes).values(sampleNotes);
},
/**
* Full database with all types of data
*/
full: async (testDb: TestDatabase) => {
await fixtures.empty(testDb);
await testDb.db.insert(schema.transcriptions).values(sampleTranscriptions);
await testDb.db.insert(schema.vocabulary).values(sampleVocabulary);
await testDb.db.insert(schema.models).values(sampleModels);
await testDb.db.insert(schema.notes).values(sampleNotes);
},
/**
* Database with custom settings
*/
withCustomSettings: async (
testDb: TestDatabase,
settings: Partial<AppSettingsData>
) => {
// Clear existing settings first
await testDb.db.delete(schema.appSettings);
// Insert custom settings
await testDb.db.insert(schema.appSettings).values({
id: 1,
data: { ...defaultAppSettings, ...settings },
version: 1,
});
},
/**
* Database with authenticated user
*/
withAuth: async (testDb: TestDatabase) => {
await fixtures.withCustomSettings(testDb, {
auth: {
isAuthenticated: true,
idToken: 'test-id-token',
refreshToken: 'test-refresh-token',
expiresAt: Date.now() + 3600000, // 1 hour from now
userInfo: {
sub: 'test-user-123',
email: 'test@example.com',
name: 'Test User',
},
},
});
},
};
/**
* Helper to seed specific data
*/
export async function seedDatabase(
testDb: TestDatabase,
fixture: keyof typeof fixtures | ((testDb: TestDatabase) => Promise<void>)
): Promise<void> {
if (typeof fixture === 'function') {
await fixture(testDb);
} else {
await fixtures[fixture](testDb);
}
}

View file

@ -0,0 +1,172 @@
import { vi } from 'vitest';
// Mock onnxruntime-node
export const mockOnnxRuntime = {
InferenceSession: {
create: vi.fn(() =>
Promise.resolve({
run: vi.fn(() =>
Promise.resolve({
output: {
data: new Float32Array([0.5, 0.5, 0.5]),
dims: [1, 3],
},
})
),
release: vi.fn(),
})
),
},
Tensor: vi.fn(),
env: {
wasm: {
numThreads: 1,
},
},
};
// Mock @amical/whisper-wrapper
export const mockWhisperWrapper = {
WhisperModel: vi.fn().mockImplementation(() => ({
transcribe: vi.fn(() =>
Promise.resolve({
text: 'Test transcription',
segments: [
{
start: 0,
end: 1.5,
text: 'Test transcription',
},
],
})
),
dispose: vi.fn(),
})),
downloadModel: vi.fn(() => Promise.resolve()),
getModelPath: vi.fn(() => '/mock/model/path'),
};
// Mock keytar (credential storage)
export const mockKeytar = {
getPassword: vi.fn((service: string, account: string) =>
Promise.resolve(null)
),
setPassword: vi.fn((service: string, account: string, password: string) =>
Promise.resolve()
),
deletePassword: vi.fn((service: string, account: string) =>
Promise.resolve(true)
),
findPassword: vi.fn((service: string) => Promise.resolve(null)),
findCredentials: vi.fn((service: string) => Promise.resolve([])),
};
// Mock libsql native module
export const mockLibsql = {
createClient: vi.fn(() => ({
execute: vi.fn(() =>
Promise.resolve({
rows: [],
columns: [],
rowsAffected: 0,
})
),
batch: vi.fn(() => Promise.resolve([])),
close: vi.fn(() => Promise.resolve()),
sync: vi.fn(() => Promise.resolve()),
})),
};
// Mock native helper modules
export const mockSwiftHelper = {
checkAccessibilityPermission: vi.fn(() => true),
checkMicrophonePermission: vi.fn(() => true),
requestMicrophonePermission: vi.fn(() => Promise.resolve(true)),
getSystemAudioLevel: vi.fn(() => 0.5),
setSystemAudioMuted: vi.fn(() => true),
isSystemAudioMuted: vi.fn(() => false),
writeToClipboard: vi.fn(() => true),
readFromClipboard: vi.fn(() => ''),
isRunning: vi.fn(() => true),
};
export const mockWindowsHelper = {
checkMicrophonePermission: vi.fn(() => true),
requestMicrophonePermission: vi.fn(() => Promise.resolve(true)),
registerGlobalShortcut: vi.fn(() => true),
unregisterGlobalShortcut: vi.fn(() => true),
isKeyPressed: vi.fn(() => false),
getSystemAudioLevel: vi.fn(() => 0.5),
isRunning: vi.fn(() => true),
};
// Mock node-machine-id
export const mockMachineId = {
machineIdSync: vi.fn(() => 'test-machine-id-12345'),
machineId: vi.fn(() => Promise.resolve('test-machine-id-12345')),
};
// Mock systeminformation
export const mockSystemInformation = {
system: vi.fn(() =>
Promise.resolve({
manufacturer: 'Test Manufacturer',
model: 'Test Model',
version: '1.0',
serial: 'TEST123',
uuid: 'test-uuid',
sku: 'TEST-SKU',
})
),
cpu: vi.fn(() =>
Promise.resolve({
manufacturer: 'Test CPU',
brand: 'Test Brand',
speed: 2.5,
cores: 4,
})
),
mem: vi.fn(() =>
Promise.resolve({
total: 16000000000,
free: 8000000000,
used: 8000000000,
})
),
osInfo: vi.fn(() =>
Promise.resolve({
platform: 'darwin',
distro: 'macOS',
release: '14.0',
arch: 'arm64',
})
),
};
// Mock posthog-node
export const mockPostHog = {
PostHog: vi.fn().mockImplementation(() => ({
capture: vi.fn(),
identify: vi.fn(),
alias: vi.fn(),
shutdown: vi.fn(() => Promise.resolve()),
})),
};
// Mock update-electron-app
export const mockUpdateElectronApp = vi.fn();
export function createNativeMocks() {
return {
'onnxruntime-node': mockOnnxRuntime,
'@amical/whisper-wrapper': mockWhisperWrapper,
keytar: mockKeytar,
libsql: mockLibsql,
'@amical/swift-helper': mockSwiftHelper,
'@amical/windows-helper': mockWindowsHelper,
'node-machine-id': mockMachineId,
systeminformation: mockSystemInformation,
'posthog-node': mockPostHog,
'update-electron-app': mockUpdateElectronApp,
};
}

View file

@ -0,0 +1,121 @@
import { vi } from 'vitest';
import type { TestDatabase } from './test-db';
import { AppManager } from '@main/core/app-manager';
import { ServiceManager } from '@main/managers/service-manager';
import { router } from '@trpc/router';
import { createContext } from '@trpc/context';
/**
* Test wrapper for AppManager
*/
export interface TestApp {
appManager: AppManager;
serviceManager: ServiceManager;
trpcCaller: ReturnType<typeof router.createCaller>;
cleanup: () => Promise<void>;
}
/**
* Initialize a test instance of AppManager with mocked database
*/
export async function initializeTestApp(
testDb: TestDatabase,
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', () => ({
db: testDb.db,
dbPath: testDb.dbPath,
initializeDatabase: vi.fn().mockResolvedValue(undefined),
closeDatabase: vi.fn().mockResolvedValue(undefined),
}));
// Mock onboarding check to skip it
if (skipOnboarding) {
process.env.FORCE_ONBOARDING = 'false';
}
// Create AppManager instance
const appManager = new AppManager();
// Initialize the app
// Note: This will try to create windows, which are mocked
try {
await appManager.initialize();
} catch (error) {
// Some initialization errors are expected in test environment
console.warn('AppManager initialization warning:', error);
}
// Get service manager
const serviceManager = ServiceManager.getInstance()!;
// Create tRPC caller for testing
const ctx = createContext(serviceManager);
const trpcCaller = router.createCaller(ctx);
return {
appManager,
serviceManager,
trpcCaller,
cleanup: async () => {
// Clean up services
// Note: Add cleanup logic as needed
},
};
}
/**
* Create a tRPC caller without initializing the full AppManager
* Useful for testing specific service methods in isolation
*/
export function createTestTRPCCaller(serviceManager: ServiceManager) {
const ctx = createContext(serviceManager);
return router.createCaller(ctx);
}
/**
* Initialize just the ServiceManager without AppManager
* Useful for testing services in isolation
*/
export async function initializeTestServices(
testDb: TestDatabase
): Promise<{
serviceManager: ServiceManager;
trpcCaller: ReturnType<typeof router.createCaller>;
cleanup: () => Promise<void>;
}> {
// Mock the database module
vi.doMock('@db', () => ({
db: testDb.db,
dbPath: testDb.dbPath,
initializeDatabase: vi.fn().mockResolvedValue(undefined),
closeDatabase: vi.fn().mockResolvedValue(undefined),
}));
// Create and initialize ServiceManager
const serviceManager = ServiceManager.createInstance();
try {
await serviceManager.initialize();
} catch (error) {
console.warn('ServiceManager initialization warning:', error);
}
// Create tRPC caller
const ctx = createContext(serviceManager);
const trpcCaller = router.createCaller(ctx);
return {
serviceManager,
trpcCaller,
cleanup: async () => {
// Cleanup logic
},
};
}

View file

@ -0,0 +1,116 @@
import { drizzle } from 'drizzle-orm/libsql';
import { migrate } from 'drizzle-orm/libsql/migrator';
import * as schema from '@db/schema';
import path from 'node:path';
import fs from 'fs-extra';
import { TEST_USER_DATA_PATH } from './electron-mocks';
let dbCounter = 0;
export interface TestDatabase {
db: ReturnType<typeof drizzle>;
dbPath: string;
close: () => Promise<void>;
clear: () => Promise<void>;
}
/**
* Creates an isolated test database with migrations applied
*/
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);
// Ensure directory exists
await fs.ensureDir(path.dirname(dbPath));
// Create drizzle instance
const db = drizzle(`file:${dbPath}`, {
schema: {
...schema,
},
});
// Run migrations if not skipped
if (!skipMigrations) {
const migrationsPath = path.join(process.cwd(), 'src', 'db', 'migrations');
// Check if migrations exist
if (!fs.existsSync(migrationsPath)) {
console.warn(
'Migrations folder not found at:',
migrationsPath,
'- skipping migrations'
);
} else {
try {
await migrate(db, {
migrationsFolder: migrationsPath,
});
} catch (error) {
console.error('Failed to run migrations:', error);
throw error;
}
}
}
return {
db,
dbPath,
close: async () => {
db.$client.close();
},
clear: async () => {
// Clear all tables
await db.delete(schema.transcriptions);
await db.delete(schema.vocabulary);
await db.delete(schema.models);
await db.delete(schema.appSettings);
await db.delete(schema.yjsUpdates);
await db.delete(schema.notes);
},
};
}
/**
* Deletes a test database file
*/
export async function deleteTestDatabase(dbPath: string): Promise<void> {
try {
await fs.remove(dbPath);
} catch (error) {
console.error('Failed to delete test database:', error);
}
}
/**
* Clears all test databases
*/
export async function clearAllTestDatabases(): Promise<void> {
const dbDir = path.join(TEST_USER_DATA_PATH, 'databases');
try {
await fs.emptyDir(dbDir);
} catch (error) {
console.error('Failed to clear test databases:', error);
}
}
/**
* Helper to get database instance for testing
* This bypasses the singleton pattern used in production
*/
export function createMockDb(dbPath: string) {
return drizzle(`file:${dbPath}`, {
schema: {
...schema,
},
});
}

View file

@ -0,0 +1,219 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { createTestDatabase, type TestDatabase } from '../helpers/test-db';
import { seedDatabase, fixtures, sampleTranscriptions } from '../helpers/fixtures';
import { initializeTestServices } from '../helpers/test-app';
import { setTestDatabase } from '../setup';
describe('Transcriptions Service', () => {
let testDb: TestDatabase;
let serviceManager: any;
let trpcCaller: any;
let cleanup: () => Promise<void>;
afterEach(async () => {
if (cleanup) {
await cleanup();
}
if (testDb) {
await testDb.close();
}
});
describe('Get Transcriptions', () => {
beforeEach(async () => {
testDb = await createTestDatabase({ name: 'get-transcriptions-test' });
setTestDatabase(testDb.db);
await seedDatabase(testDb, 'withTranscriptions');
const result = await initializeTestServices(testDb);
serviceManager = result.serviceManager;
trpcCaller = result.trpcCaller;
cleanup = result.cleanup;
});
it('should return all transcriptions', async () => {
const transcriptions = await trpcCaller.transcriptions.getTranscriptions({
limit: 10,
offset: 0,
});
expect(transcriptions).toHaveLength(sampleTranscriptions.length);
expect(transcriptions[0]).toHaveProperty('id');
expect(transcriptions[0]).toHaveProperty('text');
expect(transcriptions[0]).toHaveProperty('language');
});
it('should respect limit parameter', async () => {
const transcriptions = await trpcCaller.transcriptions.getTranscriptions({
limit: 2,
offset: 0,
});
expect(transcriptions).toHaveLength(2);
});
it('should respect offset parameter', async () => {
const allTranscriptions =
await trpcCaller.transcriptions.getTranscriptions({
limit: 10,
offset: 0,
});
const offsetTranscriptions =
await trpcCaller.transcriptions.getTranscriptions({
limit: 10,
offset: 1,
});
expect(offsetTranscriptions).toHaveLength(allTranscriptions.length - 1);
expect(offsetTranscriptions[0].id).not.toBe(allTranscriptions[0].id);
});
});
describe('Get Transcription by ID', () => {
beforeEach(async () => {
testDb = await createTestDatabase({ name: 'get-by-id-test' });
setTestDatabase(testDb.db);
await seedDatabase(testDb, 'withTranscriptions');
const result = await initializeTestServices(testDb);
serviceManager = result.serviceManager;
trpcCaller = result.trpcCaller;
cleanup = result.cleanup;
});
it('should return transcription by id', async () => {
const transcriptions = await trpcCaller.transcriptions.getTranscriptions({
limit: 1,
offset: 0,
});
const transcription = await trpcCaller.transcriptions.getTranscriptionById({
id: transcriptions[0].id,
});
expect(transcription).toBeDefined();
expect(transcription.id).toBe(transcriptions[0].id);
expect(transcription.text).toBe(transcriptions[0].text);
});
it('should return null for non-existent id', async () => {
const result = await trpcCaller.transcriptions.getTranscriptionById({
id: 99999,
});
expect(result).toBeNull();
});
});
describe('Delete Transcription', () => {
beforeEach(async () => {
testDb = await createTestDatabase({ name: 'delete-test' });
setTestDatabase(testDb.db);
await seedDatabase(testDb, 'withTranscriptions');
const result = await initializeTestServices(testDb);
serviceManager = result.serviceManager;
trpcCaller = result.trpcCaller;
cleanup = result.cleanup;
});
it('should delete transcription by id', async () => {
const transcriptions = await trpcCaller.transcriptions.getTranscriptions({
limit: 10,
offset: 0,
});
const initialCount = transcriptions.length;
const idToDelete = transcriptions[0].id;
await trpcCaller.transcriptions.deleteTranscription({ id: idToDelete });
const afterDelete = await trpcCaller.transcriptions.getTranscriptions({
limit: 10,
offset: 0,
});
expect(afterDelete).toHaveLength(initialCount - 1);
expect(afterDelete.find((t: any) => t.id === idToDelete)).toBeUndefined();
});
});
describe('Search Transcriptions', () => {
beforeEach(async () => {
testDb = await createTestDatabase({ name: 'search-test' });
setTestDatabase(testDb.db);
await seedDatabase(testDb, 'withTranscriptions');
const result = await initializeTestServices(testDb);
serviceManager = result.serviceManager;
trpcCaller = result.trpcCaller;
cleanup = result.cleanup;
});
it('should search transcriptions by text', async () => {
const results = await trpcCaller.transcriptions.searchTranscriptions({
searchTerm: 'test',
limit: 10,
});
expect(results.length).toBeGreaterThan(0);
results.forEach((result: any) => {
expect(result.text.toLowerCase()).toContain('test');
});
});
it('should return empty array for no matches', async () => {
const results = await trpcCaller.transcriptions.searchTranscriptions({
searchTerm: 'nonexistentquerystring',
limit: 10,
});
expect(results).toHaveLength(0);
});
});
describe('Empty Database', () => {
beforeEach(async () => {
testDb = await createTestDatabase({ name: 'empty-test' });
setTestDatabase(testDb.db);
await seedDatabase(testDb, 'empty');
const result = await initializeTestServices(testDb);
serviceManager = result.serviceManager;
trpcCaller = result.trpcCaller;
cleanup = result.cleanup;
});
it('should return empty array for empty database', async () => {
const transcriptions = await trpcCaller.transcriptions.getTranscriptions({
limit: 10,
offset: 0,
});
expect(transcriptions).toHaveLength(0);
});
it('should handle search on empty database', async () => {
const results = await trpcCaller.transcriptions.searchTranscriptions({
searchTerm: 'test',
limit: 10,
});
expect(results).toHaveLength(0);
});
});
describe('Get Count', () => {
beforeEach(async () => {
testDb = await createTestDatabase({ name: 'count-test' });
setTestDatabase(testDb.db);
await seedDatabase(testDb, 'withTranscriptions');
const result = await initializeTestServices(testDb);
serviceManager = result.serviceManager;
trpcCaller = result.trpcCaller;
cleanup = result.cleanup;
});
it('should return total transcription count', async () => {
const count = await trpcCaller.transcriptions.getTranscriptionsCount({});
expect(count).toBe(sampleTranscriptions.length);
expect(count).toBeGreaterThan(0);
});
});
});

235
apps/desktop/tests/setup.ts Normal file
View file

@ -0,0 +1,235 @@
import { vi, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
import { TEST_USER_DATA_PATH } from './helpers/electron-mocks';
import fs from 'fs-extra';
import path from 'path';
// Set test environment variable
process.env.NODE_ENV = 'test';
process.env.VITEST = 'true';
// Global test database instance - will be set by each test
let currentTestDb: any = null;
// Helper function to set the current test database
export function setTestDatabase(db: any) {
currentTestDb = db;
}
// Helper function to get the current test database
export function getTestDatabase() {
if (!currentTestDb) {
throw new Error('Test database not set. Call setTestDatabase() in beforeEach.');
}
return currentTestDb;
}
// Mock the database module to return the current test database
vi.mock('@db', () => ({
get db() {
return getTestDatabase();
},
get dbPath() {
return '/test/db/path';
},
initializeDatabase: vi.fn().mockResolvedValue(undefined),
closeDatabase: vi.fn().mockResolvedValue(undefined),
}));
// Mock electron module
vi.mock('electron', async () => {
const { createElectronMocks } = await import('./helpers/electron-mocks');
return createElectronMocks();
});
// Mock native modules
vi.mock('onnxruntime-node', () => ({
InferenceSession: {
create: vi.fn(function() {
return Promise.resolve({
run: vi.fn(function() {
return Promise.resolve({
output: {
data: new Float32Array([0.5, 0.5, 0.5]),
dims: [1, 3],
},
});
}),
release: vi.fn(),
});
}),
},
Tensor: vi.fn(),
env: {
wasm: {
numThreads: 1,
},
},
}));
vi.mock('@amical/whisper-wrapper', () => ({
WhisperModel: vi.fn().mockImplementation(function() {
return {
transcribe: vi.fn(function() {
return Promise.resolve({
text: 'Test transcription',
segments: [
{
start: 0,
end: 1.5,
text: 'Test transcription',
},
],
});
}),
dispose: vi.fn(),
};
}),
downloadModel: vi.fn(function() { return Promise.resolve(); }),
getModelPath: vi.fn(function() { return '/mock/model/path'; }),
}));
vi.mock('keytar', () => ({
getPassword: vi.fn(function(service: string, account: string) {
return Promise.resolve(null);
}),
setPassword: vi.fn(function(service: string, account: string, password: string) {
return Promise.resolve();
}),
deletePassword: vi.fn(function(service: string, account: string) {
return Promise.resolve(true);
}),
findPassword: vi.fn(function(service: string) {
return Promise.resolve(null);
}),
findCredentials: vi.fn(function(service: string) {
return Promise.resolve([]);
}),
}));
vi.mock('node-machine-id', () => ({
machineIdSync: vi.fn(function() { return 'test-machine-id-12345'; }),
machineId: vi.fn(function() { return Promise.resolve('test-machine-id-12345'); }),
}));
vi.mock('systeminformation', () => ({
system: vi.fn(function() {
return Promise.resolve({
manufacturer: 'Test Manufacturer',
model: 'Test Model',
version: '1.0',
serial: 'TEST123',
uuid: 'test-uuid',
sku: 'TEST-SKU',
});
}),
cpu: vi.fn(function() {
return Promise.resolve({
manufacturer: 'Test CPU',
brand: 'Test Brand',
speed: 2.5,
cores: 4,
});
}),
mem: vi.fn(function() {
return Promise.resolve({
total: 16000000000,
free: 8000000000,
used: 8000000000,
});
}),
osInfo: vi.fn(function() {
return Promise.resolve({
platform: 'darwin',
distro: 'macOS',
release: '14.0',
arch: 'arm64',
});
}),
}));
vi.mock('posthog-node', () => ({
PostHog: vi.fn().mockImplementation(function() {
return {
capture: vi.fn(),
identify: vi.fn(),
alias: vi.fn(),
shutdown: vi.fn(function() { return Promise.resolve(); }),
};
}),
}));
vi.mock('update-electron-app', () => ({
default: vi.fn(),
}));
// Mock electron-log
vi.mock('electron-log', () => ({
default: {
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
verbose: vi.fn(),
silly: vi.fn(),
transports: {
file: { level: 'info' },
console: { level: 'info' },
},
scope: vi.fn(() => ({
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
})),
},
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
verbose: vi.fn(),
silly: vi.fn(),
}));
// Mock electron-squirrel-startup
vi.mock('electron-squirrel-startup', () => ({
default: false,
}));
// Mock electron-trpc-experimental
vi.mock('electron-trpc-experimental/main', () => ({
createIPCHandler: vi.fn(() => ({
handle: vi.fn(),
})),
}));
// Global test setup
beforeAll(async () => {
// Create test user data directory
await fs.ensureDir(TEST_USER_DATA_PATH);
await fs.ensureDir(path.join(TEST_USER_DATA_PATH, 'databases'));
await fs.ensureDir(path.join(TEST_USER_DATA_PATH, 'models'));
await fs.ensureDir(path.join(TEST_USER_DATA_PATH, 'logs'));
});
// Global test teardown
afterAll(async () => {
// Clean up test user data directory
try {
await fs.remove(TEST_USER_DATA_PATH);
} catch (error) {
console.error('Failed to clean up test directory:', error);
}
});
// Reset mocks between tests
beforeEach(() => {
// Clear all mock calls and instances
vi.clearAllMocks();
});
afterEach(() => {
// Additional cleanup if needed
});
// Export for use in tests
export { TEST_USER_DATA_PATH };

View file

@ -0,0 +1,28 @@
import { defineConfig } from 'vitest/config';
import { resolve } from 'path';
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['tests/**/*.{test,spec}.{js,ts}'],
exclude: ['node_modules', '.vite', 'out'],
setupFiles: ['./tests/setup.ts'],
testTimeout: 30000, // 30 seconds for full app initialization
hookTimeout: 30000,
// Run tests sequentially to avoid database conflicts
threads: false,
// Isolate environment for each test file
isolate: true,
},
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@db': resolve(__dirname, 'src/db'),
'@main': resolve(__dirname, 'src/main'),
'@services': resolve(__dirname, 'src/services'),
'@utils': resolve(__dirname, 'src/utils'),
'@trpc': resolve(__dirname, 'src/trpc'),
},
},
});

View file

@ -7,7 +7,10 @@
"lint": "turbo run lint",
"format": "prettier --write \"**/*.{ts,tsx,md,json,mjs,mts,css,mdx}\"",
"format:check": "turbo run format:check",
"type:check": "turbo run type:check"
"type:check": "turbo run type:check",
"test": "turbo run test",
"test:watch": "turbo run test:watch",
"test:ui": "turbo run test:ui"
},
"devDependencies": {
"prettier": "^3.5.3",

432
pnpm-lock.yaml generated
View file

@ -200,7 +200,7 @@ importers:
version: 0.31.4
drizzle-orm:
specifier: ^0.43.1
version: 0.43.1(@libsql/client@0.15.12)(@opentelemetry/api@1.9.0)
version: 0.43.1(@libsql/client@0.15.12)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.4.1)
electron-log:
specifier: ^5.4.0
version: 5.4.3
@ -297,9 +297,6 @@ importers:
vaul:
specifier: ^1.1.2
version: 1.1.2(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
workerpool:
specifier: ^9.3.3
version: 9.3.3
yjs:
specifier: ^13.6.27
version: 13.6.27
@ -345,19 +342,25 @@ importers:
version: 28.0.6(rollup@4.47.1)
'@tailwindcss/vite':
specifier: ^4.1.6
version: 4.1.12(vite@7.1.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1))
version: 4.1.12(vite@7.1.5(@types/node@24.10.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1))
'@tanstack/react-router-devtools':
specifier: ^1.131.41
version: 1.131.41(@tanstack/react-router@1.131.36(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@tanstack/router-core@1.131.41)(csstype@3.1.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(solid-js@1.9.9)(tiny-invariant@1.3.3)
'@tanstack/router-plugin':
specifier: ^1.131.41
version: 1.131.41(@tanstack/react-router@1.131.36(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1))
version: 1.131.41(@tanstack/react-router@1.131.36(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.5(@types/node@24.10.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1))
'@types/node':
specifier: ^24.10.1
version: 24.10.1
'@types/react':
specifier: ^19.1.12
version: 19.1.12
'@types/react-dom':
specifier: ^19.1.9
version: 19.1.9(@types/react@19.1.12)
'@vitest/ui':
specifier: ^4.0.8
version: 4.0.8(vitest@4.0.8)
bumpp:
specifier: ^10.2.3
version: 10.2.3
@ -379,6 +382,9 @@ importers:
flora-colossus:
specifier: ^2.0.0
version: 2.0.0
fs-extra:
specifier: ^11.3.2
version: 11.3.2
prettier:
specifier: ^3.5.3
version: 3.6.2
@ -393,7 +399,10 @@ importers:
version: 5.8.3
vite:
specifier: ^7.1.5
version: 7.1.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1)
version: 7.1.5(@types/node@24.10.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1)
vitest:
specifier: ^4.0.8
version: 4.0.8(@types/node@24.10.1)(@vitest/ui@4.0.8)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1)
packages/eslint-config:
devDependencies:
@ -1521,6 +1530,9 @@ packages:
resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
'@polka/url@1.0.0-next.29':
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
'@posthog/core@1.0.2':
resolution: {integrity: sha512-hWk3rUtJl2crQK0WNmwg13n82hnTwB99BT99/XI5gZSvIlYZ1TPmMZE8H2dhJJ98J/rm9vYJ/UXNzw3RV5HTpQ==}
@ -2264,6 +2276,9 @@ packages:
resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==}
engines: {node: '>=10'}
'@standard-schema/spec@1.0.0':
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
'@standard-schema/utils@0.3.0':
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
@ -2517,9 +2532,15 @@ packages:
resolution: {integrity: sha512-TmY25GmxzgX+395Fwl/F0te6S4RHdJtYl1QjZr+wlxVvKJ0IBOACpnpAvnLM3dpTgXuQukGtSWcRz7Zi9mZqcQ==}
hasBin: true
'@types/better-sqlite3@7.6.13':
resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==}
'@types/cacheable-request@6.0.3':
resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==}
'@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
'@types/d3-array@3.2.1':
resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
@ -2547,6 +2568,9 @@ packages:
'@types/d3-timer@3.0.2':
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
'@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
'@types/diff-match-patch@1.0.36':
resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==}
@ -2602,6 +2626,9 @@ packages:
'@types/node@22.15.12':
resolution: {integrity: sha512-K0fpC/ZVeb8G9rm7bH7vI0KAec4XHEhBam616nVJCV51bKzJ6oA3luG4WdKoaztxe70QaNjS/xBmcDLmr4PiGw==}
'@types/node@24.10.1':
resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==}
'@types/node@24.3.0':
resolution: {integrity: sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==}
@ -2699,6 +2726,40 @@ packages:
resolution: {integrity: sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@vitest/expect@4.0.8':
resolution: {integrity: sha512-Rv0eabdP/xjAHQGr8cjBm+NnLHNoL268lMDK85w2aAGLFoVKLd8QGnVon5lLtkXQCoYaNL0wg04EGnyKkkKhPA==}
'@vitest/mocker@4.0.8':
resolution: {integrity: sha512-9FRM3MZCedXH3+pIh+ME5Up2NBBHDq0wqwhOKkN4VnvCiKbVxddqH9mSGPZeawjd12pCOGnl+lo/ZGHt0/dQSg==}
peerDependencies:
msw: ^2.4.9
vite: ^6.0.0 || ^7.0.0-0
peerDependenciesMeta:
msw:
optional: true
vite:
optional: true
'@vitest/pretty-format@4.0.8':
resolution: {integrity: sha512-qRrjdRkINi9DaZHAimV+8ia9Gq6LeGz2CgIEmMLz3sBDYV53EsnLZbJMR1q84z1HZCMsf7s0orDgZn7ScXsZKg==}
'@vitest/runner@4.0.8':
resolution: {integrity: sha512-mdY8Sf1gsM8hKJUQfiPT3pn1n8RF4QBcJYFslgWh41JTfrK1cbqY8whpGCFzBl45LN028g0njLCYm0d7XxSaQQ==}
'@vitest/snapshot@4.0.8':
resolution: {integrity: sha512-Nar9OTU03KGiubrIOFhcfHg8FYaRaNT+bh5VUlNz8stFhCZPNrJvmZkhsr1jtaYvuefYFwK2Hwrq026u4uPWCw==}
'@vitest/spy@4.0.8':
resolution: {integrity: sha512-nvGVqUunyCgZH7kmo+Ord4WgZ7lN0sOULYXUOYuHr55dvg9YvMz3izfB189Pgp28w0vWFbEEfNc/c3VTrqrXeA==}
'@vitest/ui@4.0.8':
resolution: {integrity: sha512-F9jI5rSstNknPlTlPN2gcc4gpbaagowuRzw/OJzl368dvPun668Q182S8Q8P9PITgGCl5LAKXpzuue106eM4wA==}
peerDependencies:
vitest: 4.0.8
'@vitest/utils@4.0.8':
resolution: {integrity: sha512-pdk2phO5NDvEFfUTxcTP8RFYjVj/kfLSPIN5ebP2Mu9kcIMeAQTbknqcFEyBcC4z2pJlJI9aS5UQjcYfhmKAow==}
'@watchable/unpromise@1.0.2':
resolution: {integrity: sha512-yGCKYzCrAfJQ9yzm76r1bl4WUIWyqmh4vqidXn5LyOfPbgdiZrKOyvW2ivqIvtmsRVb7u3ModEpc4q901VRgXw==}
@ -2871,6 +2932,10 @@ packages:
resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==}
engines: {node: '>= 0.4'}
assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
ast-types@0.13.4:
resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==}
engines: {node: '>=4'}
@ -2926,10 +2991,17 @@ packages:
before-after-hook@2.2.3:
resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==}
better-sqlite3@12.4.1:
resolution: {integrity: sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==}
engines: {node: 20.x || 22.x || 23.x || 24.x}
binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
bindings@1.5.0:
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
@ -3027,6 +3099,10 @@ packages:
caniuse-lite@1.0.30001736:
resolution: {integrity: sha512-ImpN5gLEY8gWeqfLUyEF4b7mYWcYoR2Si1VhnrbM4JizRFmfGaAQ12PhNykq6nvI4XvKLrsp8Xde74D5phJOSw==}
chai@6.2.1:
resolution: {integrity: sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==}
engines: {node: '>=18'}
chalk-template@0.4.0:
resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==}
engines: {node: '>=12'}
@ -3351,6 +3427,15 @@ packages:
supports-color:
optional: true
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
decimal.js-light@2.5.1:
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
@ -3676,6 +3761,9 @@ packages:
resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==}
engines: {node: '>= 0.4'}
es-module-lexer@1.7.0:
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
es-object-atoms@1.1.1:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
@ -3855,6 +3943,9 @@ packages:
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
esutils@2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
@ -3885,6 +3976,10 @@ packages:
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
engines: {node: '>=6'}
expect-type@1.2.2:
resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==}
engines: {node: '>=12.0.0'}
exponential-backoff@3.1.2:
resolution: {integrity: sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==}
@ -3943,6 +4038,9 @@ packages:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
fflate@0.8.2:
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
figures@3.2.0:
resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==}
engines: {node: '>=8'}
@ -3951,6 +4049,9 @@ packages:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
file-uri-to-path@1.0.0:
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
filename-reserved-regex@2.0.0:
resolution: {integrity: sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==}
engines: {node: '>=4'}
@ -4045,6 +4146,10 @@ packages:
resolution: {integrity: sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==}
engines: {node: '>=14.14'}
fs-extra@11.3.2:
resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==}
engines: {node: '>=14.14'}
fs-extra@7.0.1:
resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==}
engines: {node: '>=6 <7 || >=8'}
@ -4823,6 +4928,9 @@ packages:
magic-string@0.30.17:
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
make-error@1.3.6:
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
@ -4964,6 +5072,10 @@ packages:
motion-utils@12.23.6:
resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==}
mrmime@2.0.1:
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
engines: {node: '>=10'}
ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
@ -5786,6 +5898,9 @@ packages:
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
engines: {node: '>= 0.4'}
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
signal-exit@3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
@ -5795,6 +5910,10 @@ packages:
simple-get@4.0.1:
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
sirv@3.0.2:
resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==}
engines: {node: '>=18'}
slash@3.0.0:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'}
@ -5869,6 +5988,12 @@ packages:
resolution: {integrity: sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==}
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
std-env@3.10.0:
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
stop-iteration-iterator@1.1.0:
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
engines: {node: '>= 0.4'}
@ -6046,9 +6171,15 @@ packages:
tiny-warning@1.0.3:
resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
tinycolor2@1.6.0:
resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==}
tinyexec@0.3.2:
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
tinyexec@1.0.1:
resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==}
@ -6063,6 +6194,10 @@ packages:
tinygradient@1.1.5:
resolution: {integrity: sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==}
tinyrainbow@3.0.3:
resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==}
engines: {node: '>=14.0.0'}
title-case@2.1.1:
resolution: {integrity: sha512-EkJoZ2O3zdCz3zJsYCsxyq2OC5hrxR9mfdd5I+w8h/tmFfeOxJ+vvkxsKxdmN0WtS9zLdHEgfgVOiMVgv+Po4Q==}
@ -6088,6 +6223,10 @@ packages:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
totalist@3.0.1:
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
engines: {node: '>=6'}
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
@ -6254,6 +6393,9 @@ packages:
undici-types@7.10.0:
resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==}
undici-types@7.16.0:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
unicode-properties@1.4.1:
resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==}
@ -6409,6 +6551,40 @@ packages:
yaml:
optional: true
vitest@4.0.8:
resolution: {integrity: sha512-urzu3NCEV0Qa0Y2PwvBtRgmNtxhj5t5ULw7cuKhIHh3OrkKTLlut0lnBOv9qe5OvbkMH2g38G7KPDCTpIytBVg==}
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@types/debug': ^4.1.12
'@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
'@vitest/browser-playwright': 4.0.8
'@vitest/browser-preview': 4.0.8
'@vitest/browser-webdriverio': 4.0.8
'@vitest/ui': 4.0.8
happy-dom: '*'
jsdom: '*'
peerDependenciesMeta:
'@edge-runtime/vm':
optional: true
'@types/debug':
optional: true
'@types/node':
optional: true
'@vitest/browser-playwright':
optional: true
'@vitest/browser-preview':
optional: true
'@vitest/browser-webdriverio':
optional: true
'@vitest/ui':
optional: true
happy-dom:
optional: true
jsdom:
optional: true
wcwidth@1.0.1:
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
@ -6454,6 +6630,11 @@ packages:
engines: {node: '>= 8'}
hasBin: true
why-is-node-running@2.3.0:
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
engines: {node: '>=8'}
hasBin: true
wide-align@1.1.5:
resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==}
@ -6468,9 +6649,6 @@ packages:
resolution: {integrity: sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg==}
engines: {node: '>=12.17'}
workerpool@9.3.3:
resolution: {integrity: sha512-slxCaKbYjEdFT/o2rH9xS1hf4uRDch1w7Uo+apxhZ+sf/1d9e0ZVkn42kPNGP2dgjIx6YFvSevj0zHvbWe2jdw==}
wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'}
@ -7193,7 +7371,7 @@ snapshots:
debug: 4.4.1
extract-zip: 2.0.1
filenamify: 4.3.0
fs-extra: 11.3.1
fs-extra: 11.3.2
galactus: 1.0.0
get-package-info: 1.0.0
junk: 3.1.0
@ -7233,7 +7411,7 @@ snapshots:
'@malept/cross-spawn-promise': 2.0.0
debug: 4.4.1
dir-compare: 4.2.0
fs-extra: 11.3.1
fs-extra: 11.3.2
minimatch: 9.0.5
plist: 3.1.0
transitivePeerDependencies:
@ -7243,7 +7421,7 @@ snapshots:
dependencies:
cross-dirname: 0.1.0
debug: 4.4.1
fs-extra: 11.3.1
fs-extra: 11.3.2
minimist: 1.2.8
postject: 1.0.0-alpha.6
transitivePeerDependencies:
@ -7524,7 +7702,7 @@ snapshots:
'@jest/schemas': 29.6.3
'@types/istanbul-lib-coverage': 2.0.6
'@types/istanbul-reports': 3.0.4
'@types/node': 24.3.0
'@types/node': 24.10.1
'@types/yargs': 17.0.33
chalk: 4.1.2
@ -7753,6 +7931,8 @@ snapshots:
'@pkgr/core@0.2.9': {}
'@polka/url@1.0.0-next.29': {}
'@posthog/core@1.0.2': {}
'@radix-ui/number@1.1.1': {}
@ -8494,6 +8674,8 @@ snapshots:
'@sindresorhus/is@4.6.0': {}
'@standard-schema/spec@1.0.0': {}
'@standard-schema/utils@0.3.0': {}
'@szmarczak/http-timer@4.0.6':
@ -8571,12 +8753,12 @@ snapshots:
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.12
'@tailwindcss/oxide-win32-x64-msvc': 4.1.12
'@tailwindcss/vite@4.1.12(vite@7.1.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1))':
'@tailwindcss/vite@4.1.12(vite@7.1.5(@types/node@24.10.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1))':
dependencies:
'@tailwindcss/node': 4.1.12
'@tailwindcss/oxide': 4.1.12
tailwindcss: 4.1.12
vite: 7.1.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1)
vite: 7.1.5(@types/node@24.10.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1)
'@tanstack/history@1.131.2': {}
@ -8666,7 +8848,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@tanstack/router-plugin@1.131.41(@tanstack/react-router@1.131.36(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1))':
'@tanstack/router-plugin@1.131.41(@tanstack/react-router@1.131.36(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.5(@types/node@24.10.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1))':
dependencies:
'@babel/core': 7.28.3
'@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.3)
@ -8684,7 +8866,7 @@ snapshots:
zod: 3.25.76
optionalDependencies:
'@tanstack/react-router': 1.131.36(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
vite: 7.1.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1)
vite: 7.1.5(@types/node@24.10.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1)
transitivePeerDependencies:
- supports-color
@ -8771,13 +8953,23 @@ snapshots:
transitivePeerDependencies:
- '@types/node'
'@types/better-sqlite3@7.6.13':
dependencies:
'@types/node': 24.10.1
optional: true
'@types/cacheable-request@6.0.3':
dependencies:
'@types/http-cache-semantics': 4.0.4
'@types/keyv': 3.1.4
'@types/node': 24.3.0
'@types/node': 24.10.1
'@types/responselike': 1.0.3
'@types/chai@5.2.3':
dependencies:
'@types/deep-eql': 4.0.2
assertion-error: 2.0.1
'@types/d3-array@3.2.1': {}
'@types/d3-color@3.1.3': {}
@ -8802,13 +8994,15 @@ snapshots:
'@types/d3-timer@3.0.2': {}
'@types/deep-eql@4.0.2': {}
'@types/diff-match-patch@1.0.36': {}
'@types/estree@1.0.8': {}
'@types/fs-extra@9.0.13':
dependencies:
'@types/node': 24.3.0
'@types/node': 24.10.1
optional: true
'@types/glob@7.2.0':
@ -8839,7 +9033,7 @@ snapshots:
'@types/keyv@3.1.4':
dependencies:
'@types/node': 24.3.0
'@types/node': 24.10.1
'@types/minimatch@6.0.0':
dependencies:
@ -8847,7 +9041,7 @@ snapshots:
'@types/node-fetch@2.6.13':
dependencies:
'@types/node': 24.3.0
'@types/node': 24.10.1
form-data: 4.0.4
'@types/node@16.18.126': {}
@ -8864,6 +9058,10 @@ snapshots:
dependencies:
undici-types: 6.21.0
'@types/node@24.10.1':
dependencies:
undici-types: 7.16.0
'@types/node@24.3.0':
dependencies:
undici-types: 7.10.0
@ -8878,11 +9076,11 @@ snapshots:
'@types/responselike@1.0.3':
dependencies:
'@types/node': 24.3.0
'@types/node': 24.10.1
'@types/split2@4.2.3':
dependencies:
'@types/node': 24.3.0
'@types/node': 24.10.1
'@types/through@0.0.33':
dependencies:
@ -8894,7 +9092,7 @@ snapshots:
'@types/ws@8.18.1':
dependencies:
'@types/node': 24.3.0
'@types/node': 24.10.1
'@types/yargs-parser@21.0.3': {}
@ -8904,7 +9102,7 @@ snapshots:
'@types/yauzl@2.10.3':
dependencies:
'@types/node': 24.3.0
'@types/node': 24.10.1
optional: true
'@typescript-eslint/eslint-plugin@8.40.0(@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.33.0(jiti@2.5.1))(typescript@5.8.3)':
@ -9000,6 +9198,56 @@ snapshots:
'@typescript-eslint/types': 8.40.0
eslint-visitor-keys: 4.2.1
'@vitest/expect@4.0.8':
dependencies:
'@standard-schema/spec': 1.0.0
'@types/chai': 5.2.3
'@vitest/spy': 4.0.8
'@vitest/utils': 4.0.8
chai: 6.2.1
tinyrainbow: 3.0.3
'@vitest/mocker@4.0.8(vite@7.1.5(@types/node@24.10.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1))':
dependencies:
'@vitest/spy': 4.0.8
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 7.1.5(@types/node@24.10.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1)
'@vitest/pretty-format@4.0.8':
dependencies:
tinyrainbow: 3.0.3
'@vitest/runner@4.0.8':
dependencies:
'@vitest/utils': 4.0.8
pathe: 2.0.3
'@vitest/snapshot@4.0.8':
dependencies:
'@vitest/pretty-format': 4.0.8
magic-string: 0.30.21
pathe: 2.0.3
'@vitest/spy@4.0.8': {}
'@vitest/ui@4.0.8(vitest@4.0.8)':
dependencies:
'@vitest/utils': 4.0.8
fflate: 0.8.2
flatted: 3.3.3
pathe: 2.0.3
sirv: 3.0.2
tinyglobby: 0.2.15
tinyrainbow: 3.0.3
vitest: 4.0.8(@types/node@24.10.1)(@vitest/ui@4.0.8)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1)
'@vitest/utils@4.0.8':
dependencies:
'@vitest/pretty-format': 4.0.8
tinyrainbow: 3.0.3
'@watchable/unpromise@1.0.2': {}
'@xmldom/xmldom@0.8.11': {}
@ -9024,7 +9272,7 @@ snapshots:
agent-base@6.0.2:
dependencies:
debug: 4.4.1
debug: 4.4.3
transitivePeerDependencies:
- supports-color
@ -9194,6 +9442,8 @@ snapshots:
get-intrinsic: 1.3.0
is-array-buffer: 3.0.5
assertion-error@2.0.1: {}
ast-types@0.13.4:
dependencies:
tslib: 2.8.1
@ -9251,8 +9501,19 @@ snapshots:
before-after-hook@2.2.3: {}
better-sqlite3@12.4.1:
dependencies:
bindings: 1.5.0
prebuild-install: 7.1.3
optional: true
binary-extensions@2.3.0: {}
bindings@1.5.0:
dependencies:
file-uri-to-path: 1.0.0
optional: true
bl@4.1.0:
dependencies:
buffer: 5.7.1
@ -9400,6 +9661,8 @@ snapshots:
caniuse-lite@1.0.30001736: {}
chai@6.2.1: {}
chalk-template@0.4.0:
dependencies:
chalk: 4.1.2
@ -9729,6 +9992,10 @@ snapshots:
dependencies:
ms: 2.1.3
debug@4.4.3:
dependencies:
ms: 2.1.3
decimal.js-light@2.5.1: {}
decompress-response@6.0.0:
@ -9837,10 +10104,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
drizzle-orm@0.43.1(@libsql/client@0.15.12)(@opentelemetry/api@1.9.0):
drizzle-orm@0.43.1(@libsql/client@0.15.12)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.4.1):
optionalDependencies:
'@libsql/client': 0.15.12
'@opentelemetry/api': 1.9.0
'@types/better-sqlite3': 7.6.13
better-sqlite3: 12.4.1
ds-store@0.1.6:
dependencies:
@ -10079,6 +10348,8 @@ snapshots:
iterator.prototype: 1.1.5
safe-array-concat: 1.1.3
es-module-lexer@1.7.0: {}
es-object-atoms@1.1.1:
dependencies:
es-errors: 1.3.0
@ -10343,6 +10614,10 @@ snapshots:
estree-walker@2.0.2: {}
estree-walker@3.0.3:
dependencies:
'@types/estree': 1.0.8
esutils@2.0.3: {}
event-target-shim@5.0.1: {}
@ -10377,6 +10652,8 @@ snapshots:
expand-template@2.0.3: {}
expect-type@1.2.2: {}
exponential-backoff@3.1.2: {}
exsolve@1.0.7: {}
@ -10440,6 +10717,8 @@ snapshots:
node-domexception: 1.0.0
web-streams-polyfill: 3.3.3
fflate@0.8.2: {}
figures@3.2.0:
dependencies:
escape-string-regexp: 1.0.5
@ -10448,6 +10727,9 @@ snapshots:
dependencies:
flat-cache: 4.0.1
file-uri-to-path@1.0.0:
optional: true
filename-reserved-regex@2.0.0: {}
filenamify@4.3.0:
@ -10544,6 +10826,12 @@ snapshots:
jsonfile: 6.2.0
universalify: 2.0.1
fs-extra@11.3.2:
dependencies:
graceful-fs: 4.2.11
jsonfile: 6.2.0
universalify: 2.0.1
fs-extra@7.0.1:
dependencies:
graceful-fs: 4.2.11
@ -10847,7 +11135,7 @@ snapshots:
dependencies:
'@tootallnate/once': 2.0.0
agent-base: 6.0.2
debug: 4.4.1
debug: 4.4.3
transitivePeerDependencies:
- supports-color
@ -10866,7 +11154,7 @@ snapshots:
https-proxy-agent@5.0.1:
dependencies:
agent-base: 6.0.2
debug: 4.4.1
debug: 4.4.3
transitivePeerDependencies:
- supports-color
@ -11164,7 +11452,7 @@ snapshots:
jest-util@29.7.0:
dependencies:
'@jest/types': 29.6.3
'@types/node': 24.3.0
'@types/node': 24.10.1
chalk: 4.1.2
ci-info: 3.9.0
graceful-fs: 4.2.11
@ -11172,7 +11460,7 @@ snapshots:
jest-worker@29.7.0:
dependencies:
'@types/node': 24.3.0
'@types/node': 24.10.1
jest-util: 29.7.0
merge-stream: 2.0.0
supports-color: 8.1.1
@ -11394,6 +11682,10 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
make-error@1.3.6: {}
make-fetch-happen@10.2.1:
@ -11536,6 +11828,8 @@ snapshots:
motion-utils@12.23.6: {}
mrmime@2.0.1: {}
ms@2.0.0: {}
ms@2.1.3: {}
@ -12475,6 +12769,8 @@ snapshots:
side-channel-map: 1.0.1
side-channel-weakmap: 1.0.2
siginfo@2.0.0: {}
signal-exit@3.0.7: {}
simple-concat@1.0.1: {}
@ -12485,6 +12781,12 @@ snapshots:
once: 1.4.0
simple-concat: 1.0.1
sirv@3.0.2:
dependencies:
'@polka/url': 1.0.0-next.29
mrmime: 2.0.1
totalist: 3.0.1
slash@3.0.0: {}
slice-ansi@5.0.0:
@ -12501,7 +12803,7 @@ snapshots:
socks-proxy-agent@7.0.0:
dependencies:
agent-base: 6.0.2
debug: 4.4.1
debug: 4.4.3
socks: 2.8.7
transitivePeerDependencies:
- supports-color
@ -12563,6 +12865,10 @@ snapshots:
dependencies:
minipass: 3.3.6
stackback@0.0.2: {}
std-env@3.10.0: {}
stop-iteration-iterator@1.1.0:
dependencies:
es-errors: 1.3.0
@ -12769,8 +13075,12 @@ snapshots:
tiny-warning@1.0.3: {}
tinybench@2.9.0: {}
tinycolor2@1.6.0: {}
tinyexec@0.3.2: {}
tinyexec@1.0.1: {}
tinyglobby@0.2.14:
@ -12788,6 +13098,8 @@ snapshots:
'@types/tinycolor2': 1.4.6
tinycolor2: 1.6.0
tinyrainbow@3.0.3: {}
title-case@2.1.1:
dependencies:
no-case: 2.3.2
@ -12817,6 +13129,8 @@ snapshots:
dependencies:
is-number: 7.0.0
totalist@3.0.1: {}
tr46@0.0.3: {}
trim-repeated@1.0.0:
@ -12999,6 +13313,8 @@ snapshots:
undici-types@7.10.0: {}
undici-types@7.16.0: {}
unicode-properties@1.4.1:
dependencies:
base64-js: 1.5.1
@ -13126,7 +13442,7 @@ snapshots:
d3-time: 3.1.0
d3-timer: 3.0.1
vite@7.1.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1):
vite@7.1.5(@types/node@24.10.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1):
dependencies:
esbuild: 0.25.9
fdir: 6.5.0(picomatch@4.0.3)
@ -13135,13 +13451,52 @@ snapshots:
rollup: 4.47.1
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 24.3.0
'@types/node': 24.10.1
fsevents: 2.3.3
jiti: 2.5.1
lightningcss: 1.30.1
tsx: 4.20.4
yaml: 2.8.1
vitest@4.0.8(@types/node@24.10.1)(@vitest/ui@4.0.8)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1):
dependencies:
'@vitest/expect': 4.0.8
'@vitest/mocker': 4.0.8(vite@7.1.5(@types/node@24.10.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1))
'@vitest/pretty-format': 4.0.8
'@vitest/runner': 4.0.8
'@vitest/snapshot': 4.0.8
'@vitest/spy': 4.0.8
'@vitest/utils': 4.0.8
debug: 4.4.3
es-module-lexer: 1.7.0
expect-type: 1.2.2
magic-string: 0.30.21
pathe: 2.0.3
picomatch: 4.0.3
std-env: 3.10.0
tinybench: 2.9.0
tinyexec: 0.3.2
tinyglobby: 0.2.15
tinyrainbow: 3.0.3
vite: 7.1.5(@types/node@24.10.1)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.4)(yaml@2.8.1)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 24.10.1
'@vitest/ui': 4.0.8(vitest@4.0.8)
transitivePeerDependencies:
- jiti
- less
- lightningcss
- msw
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
- tsx
- yaml
wcwidth@1.0.1:
dependencies:
defaults: 1.0.4
@ -13208,6 +13563,11 @@ snapshots:
dependencies:
isexe: 2.0.0
why-is-node-running@2.3.0:
dependencies:
siginfo: 2.0.0
stackback: 0.0.2
wide-align@1.1.5:
dependencies:
string-width: 4.2.3
@ -13218,8 +13578,6 @@ snapshots:
wordwrapjs@5.1.0: {}
workerpool@9.3.3: {}
wrap-ansi@6.2.0:
dependencies:
ansi-styles: 4.3.0

View file

@ -60,6 +60,20 @@
"dependsOn": ["^build"],
"cache": false,
"persistent": true
},
"test": {
"dependsOn": [],
"cache": false
},
"test:watch": {
"dependsOn": [],
"cache": false,
"persistent": true
},
"test:ui": {
"dependsOn": [],
"cache": false,
"persistent": true
}
}
}