# Test Mocks Unified test mocks for the project, organized by process type and globally configured in test setup files. ## Overview ### Available Mocks | Process | Mock | Description | |---------|------|-------------| | Renderer | `CacheService` | Three-tier cache (memory/shared/persist) | | Renderer | `DataApiService` | HTTP client for Data API | | Renderer | `PreferenceService` | User preferences | | Renderer | `useDataApi` | Data API hooks (useQuery, useMutation, etc.) | | Renderer | `usePreference` | Preference hooks | | Renderer | `useCache` | Cache hooks | | Main | `application` | Unified mock application factory with `application.get()` | | Main | `DbService` | Database service with mock db | | Main | `CacheService` | Internal + shared cache | | Main | `DataApiService` | API coordinator | | Main | `PreferenceService` | Preference service | ### File Structure ``` tests/__mocks__/ ├── renderer/ │ ├── CacheService.ts │ ├── DataApiService.ts │ ├── PreferenceService.ts │ ├── useDataApi.ts │ ├── usePreference.ts │ └── useCache.ts ├── main/ │ ├── application.ts │ ├── CacheService.ts │ ├── DataApiService.ts │ ├── DbService.ts │ └── PreferenceService.ts ├── RendererLoggerService.ts └── MainLoggerService.ts ``` ### Test Setup Mocks are globally configured in setup files: - **Renderer**: `tests/renderer.setup.ts` - **Main**: `tests/main.setup.ts` ### Import Path Alias Use `@test-mocks/*` to import mock utilities: ```typescript import { MockCacheUtils } from '@test-mocks/renderer/CacheService' import { MockMainCacheServiceUtils } from '@test-mocks/main/CacheService' ``` --- ## Renderer Mocks ### CacheService Three-tier cache system with type-safe methods (and casual/dynamic key methods on the Memory tier only). #### Methods | Category | Method | Signature | |----------|--------|-----------| | Memory (typed) | `get` | `(key: K) => InferUseCacheValue` | | Memory (typed) | `set` | `(key: K, value, ttl?) => void` | | Memory (typed) | `has` | `(key: K) => boolean` | | Memory (typed) | `delete` | `(key: K) => boolean` | | Memory (typed) | `hasTTL` | `(key: K) => boolean` | | Memory (casual) | `getCasual` | `(key: string) => T \| undefined` | | Memory (casual) | `setCasual` | `(key, value, ttl?) => void` | | Memory (casual) | `hasCasual` | `(key: string) => boolean` | | Memory (casual) | `deleteCasual` | `(key: string) => boolean` | | Memory (casual) | `hasTTLCasual` | `(key: string) => boolean` | | Shared (typed) | `getShared` | `(key: K) => InferSharedCacheValue` | | Shared (typed) | `setShared` | `(key: K, value, ttl?) => void` | | Shared (typed) | `hasShared` | `(key: K) => boolean` | | Shared (typed) | `deleteShared` | `(key: K) => boolean` | | Shared (typed) | `hasSharedTTL` | `(key: K) => boolean` | | Persist | `getPersist` | `(key: K) => RendererPersistCacheSchema[K]` | | Persist | `setPersist` | `(key: K, value) => void` | | Persist | `hasPersist` | `(key) => boolean` | | Hook mgmt | `registerHook` | `(key: string) => void` | | Hook mgmt | `unregisterHook` | `(key: string) => void` | | Ready state | `isSharedCacheReady` | `() => boolean` | | Ready state | `onSharedCacheReady` | `(callback) => () => void` | | Lifecycle | `subscribe` | `(key, callback) => () => void` | | Lifecycle | `cleanup` | `() => void` | #### Usage ```typescript import { cacheService } from '@data/CacheService' import { MockCacheUtils } from '@test-mocks/renderer/CacheService' describe('Cache', () => { beforeEach(() => MockCacheUtils.resetMocks()) it('basic usage', () => { cacheService.setCasual('key', { data: 'value' }, 5000) expect(cacheService.getCasual('key')).toEqual({ data: 'value' }) }) it('with test utilities', () => { MockCacheUtils.setInitialState({ memory: [['key', 'value']], shared: [['shared.key', 'shared']], persist: [['persist.key', 'persist']] }) }) }) ``` --- ### DataApiService HTTP client with subscriptions and retry configuration. #### Methods | Method | Signature | |--------|-----------| | `get` | `(path, options?) => Promise` | | `post` | `(path, options) => Promise` | | `put` | `(path, options) => Promise` | | `patch` | `(path, options) => Promise` | | `delete` | `(path, options?) => Promise` | | `subscribe` | `(options, callback) => () => void` | | `configureRetry` | `(options) => void` | | `getRetryConfig` | `() => RetryOptions` | | `getRequestStats` | `() => { pendingRequests, activeSubscriptions }` | #### Usage ```typescript import { dataApiService } from '@data/DataApiService' import { MockDataApiUtils } from '@test-mocks/renderer/DataApiService' describe('API', () => { beforeEach(() => MockDataApiUtils.resetMocks()) it('basic request', async () => { const response = await dataApiService.get('/topics') expect(response.topics).toBeDefined() }) it('custom response', async () => { MockDataApiUtils.setCustomResponse('/topics', 'GET', { custom: true }) const response = await dataApiService.get('/topics') expect(response.custom).toBe(true) }) it('error simulation', async () => { MockDataApiUtils.setErrorResponse('/topics', 'GET', new Error('Failed')) await expect(dataApiService.get('/topics')).rejects.toThrow('Failed') }) }) ``` --- ### useDataApi Hooks React hooks for data operations. #### Hooks | Hook | Signature | Returns | |------|-----------|---------| | `useQuery` | `(path, options?)` | `{ data, loading, error, refetch, mutate }` | | `useMutation` | `(method, path, options?)` | `{ mutate, loading, error }` | | `usePaginatedQuery` | `(path, options?)` | `{ items, total, page, loading, error, hasMore, hasPrev, prevPage, nextPage, refresh, reset }` | | `useInvalidateCache` | `()` | `(keys?) => Promise` | | `useReadCache` | `()` | `(path, query?) => TResponse \| undefined` | | `useWriteCache` | `()` | `async (path, value, query?) => void` | #### Usage ```typescript import { useQuery, useMutation, useReadCache, useWriteCache } from '@data/hooks/useDataApi' import { MockUseDataApiUtils } from '@test-mocks/renderer/useDataApi' describe('Hooks', () => { beforeEach(() => MockUseDataApiUtils.resetMocks()) it('useQuery', () => { const { data, loading } = useQuery('/topics') expect(loading).toBe(false) expect(data).toBeDefined() }) it('useMutation', async () => { const { mutate } = useMutation('POST', '/topics') const result = await mutate({ body: { name: 'New' } }) expect(result.created).toBe(true) }) it('custom data', () => { MockUseDataApiUtils.mockQueryData('/topics', { custom: true }) const { data } = useQuery('/topics') expect(data.custom).toBe(true) }) it('useReadCache reads seeded values', () => { // Pre-populate the mock cache (key shape mirrors production: // omit `query` for [path], pass a non-empty `query` for [path, query]). MockUseDataApiUtils.seedCache('/topics', { topics: [{ id: 't1' }], total: 1 }) const read = useReadCache() expect(read('/topics')).toEqual({ topics: [{ id: 't1' }], total: 1 }) }) it('useWriteCache persists to mock store (assertable via getCachedValue)', async () => { const write = useWriteCache() await write('/topics', { topics: [], total: 0 }) expect(MockUseDataApiUtils.getCachedValue('/topics')).toEqual({ topics: [], total: 0 }) }) }) ``` > **Note:** `useReadCache`/`useWriteCache` share one in-memory `Map` under the hood. `resetMocks()` clears both call history and the cache store; use `clearCache()` if you want to drop cache entries without resetting hook mocks. --- ### useCache Hooks React hooks for cache operations. | Hook | Signature | Returns | |------|-----------|---------| | `useCache` | `(key, initValue?)` | `[value, setValue]` | | `useSharedCache` | `(key, initValue?)` | `[value, setValue]` | | `usePersistCache` | `(key)` | `[value, setValue]` | `setValue` accepts a concrete value or a functional updater `(prev) => next` (mirrors production). The mock resolves the updater against the latest mocked value with the same default fallback, so functional-update call sites run unchanged under the mock. ```typescript import { useCache } from '@data/hooks/useCache' const [value, setValue] = useCache('key', 'default') setValue('new value') setValue((prev) => prev + '!') // functional updater ``` --- ### usePreference Hooks React hooks for preferences. | Hook | Signature | Returns | |------|-----------|---------| | `usePreference` | `(key)` | `[value, setValue]` | | `useMultiplePreferences` | `(keyMap)` | `[values, setValues]` | ```typescript import { usePreference } from '@data/hooks/usePreference' const [theme, setTheme] = usePreference('ui.theme') await setTheme('dark') ``` --- ## Main Process Mocks ### Scope `tests/__mocks__/main/` holds mocks for **cross-cutting infrastructure only**: `PreferenceService`, `CacheService`, `DbService`, `DataApiService`, plus minimal `MainWindowService` / `WindowManager` stubs. All are pre-mocked globally via `tests/main.setup.ts`. **Do not add files here for feature-specific lifecycle services** (e.g., `FileProcessingTaskService`, `KnowledgeRuntimeService`). The `ServiceOverrides` type is deliberately locked to `keyof typeof defaultServiceInstances` to enforce this boundary. Stub them locally — see [Testing Other Lifecycle Services](#testing-other-lifecycle-services). | Service category | How to mock | |---|---| | Infrastructure (listed above) | Already mocked globally; override via `mockApplicationFactory({ Name: {...} })` | | Feature-specific lifecycle service | Local `vi.mock('@application')` + `MockBaseService` in the test file | | Direct-import singleton (no lifecycle) | `vi.mock('path/to/module')` directly | ### Application Mock (Unified Factory) All main-process tests get `application.get()` mocked globally via `tests/main.setup.ts`. Tests that need custom service instances can override specific services using `mockApplicationFactory(overrides)`. #### API | Export | Description | |--------|-------------| | `mockApplicationFactory(overrides?)` | Returns full mock module `{ application, serviceList }` for `vi.mock()` | | `createMockApplication(overrides?)` | Returns just the mock `application` object | | `defaultServiceInstances` | Default mock instances for all registered services | #### Usage **Global setup** (already configured in `tests/main.setup.ts`): ```typescript vi.mock('@application', async () => { const { mockApplicationFactory } = await import('./__mocks__/main/application') return mockApplicationFactory() }) ``` **Override infrastructure services** in individual test files: ```typescript const mockDb = { select: vi.fn(), insert: vi.fn() } vi.mock('@application', async () => { const { mockApplicationFactory } = await import('@test-mocks/main/application') return mockApplicationFactory({ DbService: { getDb: () => mockDb } }) }) ``` For **non-infrastructure** services, don't override here — use [Testing Other Lifecycle Services](#testing-other-lifecycle-services) instead. --- ### Main DbService Database service providing access to the mock SQLite database. #### Methods | Method | Signature | |--------|-----------| | `getDb` | `() => MockDb` | | `withWriteTx` | `(fn: (tx) => Promise) => Promise` (passthrough — calls `fn(db)`) | | `isReady` | `boolean` (getter) | ```typescript import { MockMainDbServiceUtils } from '@test-mocks/main/DbService' beforeEach(() => MockMainDbServiceUtils.resetMocks()) // Use default mock db MockMainDbServiceUtils.getDefaultMockDb() // Replace with custom db MockMainDbServiceUtils.setDb(customMockDb) ``` > **`withWriteTx`**: passthrough (`async (fn) => fn(this.db)`) — no mutex / BUSY retry. Use `vi.spyOn(dbServiceInstance, 'withWriteTx')` to inject custom behavior. Hand-rolled DbService mocks MUST include this method or production code throws `TypeError: dbService.withWriteTx is not a function`. --- ### Main CacheService Internal cache and cross-window shared cache. #### Methods | Category | Method | Signature | |----------|--------|-----------| | Lifecycle | `initialize` | `() => Promise` | | Lifecycle | `cleanup` | `() => void` | | Internal | `get` | `(key: string) => T \| undefined` | | Internal | `set` | `(key, value, ttl?) => void` | | Internal | `has` | `(key: string) => boolean` | | Internal | `delete` | `(key: string) => boolean` | | Shared | `getShared` | `(key: K) => SharedCacheSchema[K] \| undefined` | | Shared | `setShared` | `(key: K, value, ttl?) => void` | | Shared | `hasShared` | `(key: K) => boolean` | | Shared | `deleteShared` | `(key: K) => boolean` | | Subscription | `subscribeChange` | `(key, callback) => () => void` — returns a fresh `vi.fn()` unsubscribe stub | | Subscription | `subscribeSharedChange` | `(key, callback) => () => void` — returns a fresh `vi.fn()` unsubscribe stub | > **Note on subscription mocks**: `subscribeChange` / `subscribeSharedChange` are call-tracking stubs — they do **not** replicate the real fire semantics. Use them to verify `registerDisposable(cacheService.subscribeChange(...))` wiring and that subscriptions happen, not to simulate callbacks. The `setShared` / `deleteShared` mocks also record every call to `broadcastCalls` unconditionally (no `isEqual` short-circuit), keeping `getBroadcastHistory()` consumers backward-compatible. ```typescript import { MockMainCacheServiceUtils } from '@test-mocks/main/CacheService' beforeEach(() => MockMainCacheServiceUtils.resetMocks()) MockMainCacheServiceUtils.setCacheValue('key', 'value') MockMainCacheServiceUtils.setSharedCacheValue('shared.key', 'shared') ``` --- ### Main DataApiService API coordinator managing ApiServer and IpcAdapter. #### Methods | Method | Signature | |--------|-----------| | `initialize` | `() => Promise` | | `shutdown` | `() => Promise` | | `getSystemStatus` | `() => object` | | `getApiServer` | `() => ApiServer` | ```typescript import { MockMainDataApiServiceUtils } from '@test-mocks/main/DataApiService' beforeEach(() => MockMainDataApiServiceUtils.resetMocks()) MockMainDataApiServiceUtils.simulateInitializationError(new Error('Failed')) ``` --- ### Main PreferenceService Preference store with typed keys, seeded from `DefaultPreferences.default`. #### Methods | Method | Signature | |--------|-----------| | `initialize` | `() => Promise` | | `get` | `(key: K) => UnifiedPreferenceType[K]` | | `set` | `(key: K, value) => Promise` | | `getMultiple` | `(keys: K[]) => Record` | | `setMultiple` | `(values) => Promise` | | `subscribeForWindow` | `(windowId, keys) => void` | ```typescript import { MockMainPreferenceServiceUtils } from '@test-mocks/main/PreferenceService' beforeEach(() => MockMainPreferenceServiceUtils.resetMocks()) // Seed a preference value MockMainPreferenceServiceUtils.setPreferenceValue('ui.theme', 'dark') // Simulate an external change (fires main-process subscribers) MockMainPreferenceServiceUtils.simulateExternalPreferenceChange('ui.theme', 'light') ``` Utilities: `setPreferenceValue`, `getPreferenceValue`, `setMultiplePreferenceValues`, `getAllPreferenceValues`, `simulateWindowSubscription`, `simulateExternalPreferenceChange`, `getSubscriptionCounts`. --- ### Testing Other Lifecycle Services Stub feature-specific lifecycle services **locally in the test file**. A test typically needs three substitutions: `@application`, `BaseService`, and lifecycle decorators. #### Canonical Setup ```typescript import type * as LifecycleModule from '@main/core/lifecycle' import { getDependencies, getPhase } from '@main/core/lifecycle/decorators' import { Phase } from '@main/core/lifecycle/types' import { beforeEach, describe, expect, it, vi } from 'vitest' const { appGetMock, startTaskMock, getTaskMock } = vi.hoisted(() => ({ appGetMock: vi.fn(), startTaskMock: vi.fn(), getTaskMock: vi.fn() })) vi.mock('@application', () => ({ application: { get: appGetMock } })) vi.mock('@main/core/lifecycle', async (importOriginal) => { const actual = await importOriginal() class MockBaseService { ipcHandle = vi.fn() protected readonly _disposables: Array<{ dispose: () => void } | (() => void)> = [] protected registerDisposable void } | (() => void)>(d: T): T { this._disposables.push(d) return d } } return { ...actual, BaseService: MockBaseService } }) beforeEach(() => { vi.clearAllMocks() appGetMock.mockImplementation((name: string) => { if (name === 'FileProcessingTaskService') { return { startTask: startTaskMock, getTask: getTaskMock } } throw new Error(`Unexpected application.get(${name})`) }) }) // Import SUT after mocks are declared. const { FileProcessingService } = await import('../FileProcessingService') ``` #### Common Assertions Drive lifecycle hooks (`onInit` / `onStart` / `onStop` / `onDestroy`) manually — the container isn't running in tests. | Target | How | |---|---| | Phase | `expect(getPhase(MyService)).toBe(Phase.WhenReady)` | | Dependencies | `expect(getDependencies(MyService)).toEqual(['OtherService'])` | | Registered IPC channels | `const svc = new MyService(); (svc as any).onInit(); (svc as any).ipcHandle.mock.calls.map(c => c[0])` | | Single IPC handler | `ipcHandle.mock.calls.find(c => c[0] === 'channel')?.[1]`, then invoke | | Disposables | Drive lifecycle, inspect `(svc as any)._disposables` | #### Reference Implementations - `src/main/services/knowledge/__tests__/KnowledgeService.test.ts` — dispatch stub + phase/deps + per-channel handler inspection - `src/main/services/__tests__/ShortcutService.test.ts` — richer `MockBaseService` with `registerDisposable` + no-op decorator replacements --- ## Best Practices 1. Infrastructure services come pre-mocked; override via `mockApplicationFactory({ Name: {...} })`, not ad-hoc `application.get` mocks. 2. Feature-specific lifecycle services are stubbed locally — don't add them to `tests/__mocks__/main/` or `defaultServiceInstances`. 3. Each infrastructure mock exposes `MockMainServiceUtils` with `resetMocks()` plus service-specific helpers (seeding values, simulating errors). Call `resetMocks()` in `beforeEach`. ## Troubleshooting | Issue | Solution | |-------|----------| | Mock not applied | Check test runs in correct process (renderer/main in vitest.config.ts) | | Type errors | Ensure mock matches actual interface, use type assertions if needed | | State pollution | Call `resetMocks()` in `beforeEach` | | Import issues | Use path aliases (`@data/CacheService`) not relative paths |