Files
CherryHQ-cherry-studio/tests/__mocks__/main/CacheService.ts
SuYao 611944599f refactor(deps): replace lodash with es-toolkit/compat and drop it (#16528)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: suyao <sy20010504@gmail.com>
2026-06-29 12:28:57 +08:00

410 lines
11 KiB
TypeScript

import { DefaultMainPersistCache } from '@shared/data/cache/cacheSchemas'
import type {
InferSharedCacheValue,
MainPersistCacheKey,
MainPersistCacheSchema,
ProcessKey,
SharedCacheKey
} from '@shared/data/cache/cacheSchemas'
import type { CacheEntry, CacheSyncMessage } from '@shared/data/cache/cacheTypes'
import { isEqual } from 'es-toolkit/compat'
import { vi } from 'vitest'
/**
* Mock CacheService for main process testing
* Simulates the complete main process CacheService functionality
*/
// Mock cache storage
const mockMainCache = new Map<string, CacheEntry>()
// Mock shared cache storage
const mockSharedCache = new Map<string, CacheEntry>()
// Mock main-process persist cache storage (backs getPersist/setPersist/hasPersist)
const mockPersistCache = new Map<string, unknown>()
// Mock broadcast tracking
const mockBroadcastCalls: Array<{ message: CacheSyncMessage; senderWindowId?: number }> = []
/**
* Mock CacheService class
*/
export class MockMainCacheService {
private static instance: MockMainCacheService
private initialized = false
private constructor() {}
public static getInstance(): MockMainCacheService {
if (!MockMainCacheService.instance) {
MockMainCacheService.instance = new MockMainCacheService()
}
return MockMainCacheService.instance
}
// Mock initialization
public initialize = vi.fn(async (): Promise<void> => {
this.initialized = true
})
// Mock main process cache methods
public get = vi.fn(<T>(key: string): T | undefined => {
const entry = mockMainCache.get(key)
if (!entry) return undefined
// Check TTL (lazy cleanup)
if (entry.expireAt && Date.now() > entry.expireAt) {
mockMainCache.delete(key)
return undefined
}
return entry.value as T
})
public set = vi.fn(<T>(key: string, value: T, ttl?: number): void => {
const entry: CacheEntry<T> = {
value,
expireAt: ttl ? Date.now() + ttl : undefined
}
mockMainCache.set(key, entry)
})
public has = vi.fn((key: string): boolean => {
const entry = mockMainCache.get(key)
if (!entry) return false
// Check TTL
if (entry.expireAt && Date.now() > entry.expireAt) {
mockMainCache.delete(key)
return false
}
return true
})
public delete = vi.fn((key: string): boolean => {
return mockMainCache.delete(key)
})
// ============ Shared Cache Methods ============
public getShared = vi.fn(<K extends SharedCacheKey>(key: K): InferSharedCacheValue<K> | undefined => {
const entry = mockSharedCache.get(key)
if (!entry) return undefined
// Check TTL (lazy cleanup)
if (entry.expireAt && Date.now() > entry.expireAt) {
mockSharedCache.delete(key)
return undefined
}
return entry.value as InferSharedCacheValue<K>
})
public setShared = vi.fn(<K extends SharedCacheKey>(key: K, value: InferSharedCacheValue<K>, ttl?: number): void => {
const entry: CacheEntry = {
value,
expireAt: ttl ? Date.now() + ttl : undefined
}
mockSharedCache.set(key, entry)
// Track broadcast for testing
mockBroadcastCalls.push({
message: {
type: 'shared',
key,
value,
expireAt: entry.expireAt
}
})
})
public hasShared = vi.fn(<K extends SharedCacheKey>(key: K): boolean => {
const entry = mockSharedCache.get(key)
if (!entry) return false
// Check TTL
if (entry.expireAt && Date.now() > entry.expireAt) {
mockSharedCache.delete(key)
return false
}
return true
})
public deleteShared = vi.fn(<K extends SharedCacheKey>(key: K): boolean => {
if (!mockSharedCache.has(key)) {
return true
}
mockSharedCache.delete(key)
// Track broadcast for testing
mockBroadcastCalls.push({
message: {
type: 'shared',
key,
value: undefined
}
})
return true
})
// ============ Persist Cache Methods ============
// Faithful to the real main persist tier: getPersist returns the stored
// override else the schema default (never undefined), setPersist installs an
// override, hasPersist reports deviation from the default.
public getPersist = vi.fn(<K extends MainPersistCacheKey>(key: K): MainPersistCacheSchema[K] => {
return (
mockPersistCache.has(key) ? mockPersistCache.get(key) : DefaultMainPersistCache[key]
) as MainPersistCacheSchema[K]
})
public setPersist = vi.fn(<K extends MainPersistCacheKey>(key: K, value: MainPersistCacheSchema[K]): void => {
mockPersistCache.set(key, value)
})
public hasPersist = vi.fn(<K extends MainPersistCacheKey>(key: K): boolean => {
return mockPersistCache.has(key) && !isEqual(mockPersistCache.get(key), DefaultMainPersistCache[key])
})
// ============ Subscription Methods ============
// These are call-tracking stubs — the mock does NOT replicate fire semantics.
// Each call returns a fresh vi.fn() unsubscribe stub, useful for verifying
// `registerDisposable(cacheService.subscribeChange(...))` wiring in tests.
public subscribeChange = vi.fn(
<T = unknown>(_key: string, _callback: (newValue: T | undefined, oldValue: T | undefined) => void): (() => void) =>
vi.fn()
)
public subscribeSharedChange = vi.fn(
<K extends SharedCacheKey>(
_key: K,
_callback: (
newValue: InferSharedCacheValue<K> | undefined,
oldValue: InferSharedCacheValue<K> | undefined,
concreteKey: ProcessKey<K & string>
) => void
): (() => void) => vi.fn()
)
// Mock cleanup
public cleanup = vi.fn((): void => {
mockMainCache.clear()
mockSharedCache.clear()
mockPersistCache.clear()
mockBroadcastCalls.length = 0
})
// Private methods exposed for testing
// These methods are mocked but not exposed to avoid TypeScript unused warnings
}
// Mock singleton instance
const mockInstance = MockMainCacheService.getInstance()
/**
* Export mock service
*/
export const MockMainCacheServiceExport = {
CacheService: MockMainCacheService,
cacheService: mockInstance
}
/**
* Utility functions for testing
*/
export const MockMainCacheServiceUtils = {
/**
* Reset all mock call counts and state
*/
resetMocks: () => {
// Reset all method mocks
Object.values(mockInstance).forEach((method) => {
if (vi.isMockFunction(method)) {
method.mockClear()
}
})
// Reset cache state
mockMainCache.clear()
mockSharedCache.clear()
mockPersistCache.clear()
mockBroadcastCalls.length = 0
// Reset initialized state
mockInstance['initialized'] = false
},
/**
* Set cache value for testing
*/
setCacheValue: <T>(key: string, value: T, ttl?: number) => {
const entry: CacheEntry<T> = {
value,
expireAt: ttl ? Date.now() + ttl : undefined
}
mockMainCache.set(key, entry)
},
/**
* Get cache value for testing
*/
getCacheValue: <T>(key: string): T | undefined => {
const entry = mockMainCache.get(key)
if (!entry) return undefined
// Check TTL
if (entry.expireAt && Date.now() > entry.expireAt) {
mockMainCache.delete(key)
return undefined
}
return entry.value as T
},
/**
* Set initialization state for testing
*/
setInitialized: (initialized: boolean) => {
mockInstance['initialized'] = initialized
},
/**
* Get current initialization state
*/
isInitialized: (): boolean => {
return mockInstance['initialized']
},
/**
* Get all cache entries for testing
*/
getAllCacheEntries: (): Map<string, CacheEntry> => {
return new Map(mockMainCache)
},
// ============ Shared Cache Utilities ============
/**
* Set shared cache value for testing
*/
setSharedCacheValue: <K extends SharedCacheKey>(key: K, value: InferSharedCacheValue<K>, ttl?: number) => {
const entry: CacheEntry = {
value,
expireAt: ttl ? Date.now() + ttl : undefined
}
mockSharedCache.set(key, entry)
},
/**
* Get shared cache value for testing
*/
getSharedCacheValue: <K extends SharedCacheKey>(key: K): InferSharedCacheValue<K> | undefined => {
const entry = mockSharedCache.get(key)
if (!entry) return undefined
// Check TTL
if (entry.expireAt && Date.now() > entry.expireAt) {
mockSharedCache.delete(key)
return undefined
}
return entry.value as InferSharedCacheValue<K>
},
/**
* Get all shared cache entries for testing
*/
getAllSharedCacheEntries: (): Map<string, CacheEntry> => {
return new Map(mockSharedCache)
},
/**
* Simulate shared cache expiration for testing
*/
simulateSharedCacheExpiration: (key: string) => {
const entry = mockSharedCache.get(key)
if (entry) {
entry.expireAt = Date.now() - 1000 // Set to expired
}
},
/**
* Get broadcast call history for testing
*/
getBroadcastHistory: (): Array<{ message: CacheSyncMessage; senderWindowId?: number }> => {
return [...mockBroadcastCalls]
},
/**
* Simulate cache sync broadcast
*/
simulateCacheSync: (message: CacheSyncMessage, senderWindowId?: number) => {
mockBroadcastCalls.push({ message, senderWindowId })
},
/**
* Set multiple cache values at once
*/
setMultipleCacheValues: (values: Array<[string, any, number?]>) => {
values.forEach(([key, value, ttl]) => {
const entry: CacheEntry = {
value,
expireAt: ttl ? Date.now() + ttl : undefined
}
mockMainCache.set(key, entry)
})
},
/**
* Simulate cache expiration for testing
*/
simulateCacheExpiration: (key: string) => {
const entry = mockMainCache.get(key)
if (entry) {
entry.expireAt = Date.now() - 1000 // Set to expired
}
},
/**
* Get cache statistics
*/
getCacheStats: () => ({
totalEntries: mockMainCache.size,
sharedEntries: mockSharedCache.size,
broadcastCalls: mockBroadcastCalls.length,
keys: Array.from(mockMainCache.keys()),
sharedKeys: Array.from(mockSharedCache.keys())
}),
/**
* Mock initialization error
*/
simulateInitializationError: (error: Error) => {
mockInstance.initialize.mockRejectedValue(error)
},
/**
* Get mock call counts for debugging
*/
getMockCallCounts: () => ({
initialize: mockInstance.initialize.mock.calls.length,
get: mockInstance.get.mock.calls.length,
set: mockInstance.set.mock.calls.length,
has: mockInstance.has.mock.calls.length,
delete: mockInstance.delete.mock.calls.length,
getShared: mockInstance.getShared.mock.calls.length,
setShared: mockInstance.setShared.mock.calls.length,
hasShared: mockInstance.hasShared.mock.calls.length,
deleteShared: mockInstance.deleteShared.mock.calls.length,
subscribeChange: mockInstance.subscribeChange.mock.calls.length,
subscribeSharedChange: mockInstance.subscribeSharedChange.mock.calls.length,
cleanup: mockInstance.cleanup.mock.calls.length
})
}