Files

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:

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 <K>(key: K) => InferUseCacheValue<K>
Memory (typed) set <K>(key: K, value, ttl?) => void
Memory (typed) has <K>(key: K) => boolean
Memory (typed) delete <K>(key: K) => boolean
Memory (typed) hasTTL <K>(key: K) => boolean
Memory (casual) getCasual <T>(key: string) => T | undefined
Memory (casual) setCasual <T>(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 <K>(key: K) => InferSharedCacheValue<K>
Shared (typed) setShared <K>(key: K, value, ttl?) => void
Shared (typed) hasShared <K>(key: K) => boolean
Shared (typed) deleteShared <K>(key: K) => boolean
Shared (typed) hasSharedTTL <K>(key: K) => boolean
Persist getPersist <K>(key: K) => RendererPersistCacheSchema[K]
Persist setPersist <K>(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

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<any>
post (path, options) => Promise<any>
put (path, options) => Promise<any>
patch (path, options) => Promise<any>
delete (path, options?) => Promise<any>
subscribe (options, callback) => () => void
configureRetry (options) => void
getRetryConfig () => RetryOptions
getRequestStats () => { pendingRequests, activeSubscriptions }

Usage

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<any>
useReadCache () (path, query?) => TResponse | undefined
useWriteCache () async (path, value, query?) => void

Usage

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.

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]
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.

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

vi.mock('@application', async () => {
  const { mockApplicationFactory } = await import('./__mocks__/main/application')
  return mockApplicationFactory()
})

Override infrastructure services in individual test files:

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 instead.


Main DbService

Database service providing access to the mock SQLite database.

Methods

Method Signature
getDb () => MockDb
withWriteTx <T>(fn: (tx) => Promise<T>) => Promise<T> (passthrough — calls fn(db))
isReady boolean (getter)
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<void>
Lifecycle cleanup () => void
Internal get <T>(key: string) => T | undefined
Internal set <T>(key, value, ttl?) => void
Internal has (key: string) => boolean
Internal delete (key: string) => boolean
Shared getShared <K>(key: K) => SharedCacheSchema[K] | undefined
Shared setShared <K>(key: K, value, ttl?) => void
Shared hasShared <K>(key: K) => boolean
Shared deleteShared <K>(key: K) => boolean
Subscription subscribeChange <T>(key, callback) => () => void — returns a fresh vi.fn() unsubscribe stub
Subscription subscribeSharedChange <K>(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.

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<void>
shutdown () => Promise<void>
getSystemStatus () => object
getApiServer () => ApiServer
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<void>
get <K>(key: K) => UnifiedPreferenceType[K]
set <K>(key: K, value) => Promise<void>
getMultiple <K>(keys: K[]) => Record<K, UnifiedPreferenceType[K]>
setMultiple (values) => Promise<void>
subscribeForWindow (windowId, keys) => void
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

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<typeof LifecycleModule>()
  class MockBaseService {
    ipcHandle = vi.fn()
    protected readonly _disposables: Array<{ dispose: () => void } | (() => void)> = []
    protected registerDisposable<T extends { dispose: () => 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 MockMain<Name>ServiceUtils 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