/**
* Mock helpers for indexing tests
*/
import { vi } from 'vitest'
// Constants matching the actual implementation
export const BATCH_SIZE = 32
export const BATCH_DELAY_MS = 100
export const EMBEDDING_DIM = 384
export const MAC_ABSOLUTE_EPOCH = 978307200
/**
* Creates an in-memory LanceDB mock with table simulation
*/
export function createLanceDBMock() {
const tables = new Map()
const createTableMock = (tableData) => ({
query: () => ({
select: (columns) => ({
toArray: vi.fn().mockResolvedValue(tableData?.records || [])
}),
limit: (n) => ({
toArray: vi.fn().mockResolvedValue((tableData?.records || []).slice(0, n))
}),
toArray: vi.fn().mockResolvedValue(tableData?.records || [])
}),
search: vi.fn().mockImplementation((vector) => ({
limit: (n) => ({
toArray: vi.fn().mockResolvedValue(
(tableData?.records || [])
.slice(0, n)
.map(r => ({ ...r, _distance: Math.random() * 0.5 }))
)
})
})),
add: vi.fn().mockImplementation((records) => {
if (tableData) {
tableData.records.push(...records)
}
return Promise.resolve()
}),
delete: vi.fn().mockResolvedValue(undefined)
})
return {
connect: vi.fn().mockResolvedValue({
tableNames: vi.fn().mockImplementation(() => Promise.resolve([...tables.keys()])),
createTable: vi.fn().mockImplementation((name, records) => {
tables.set(name, { records: [...records] })
return Promise.resolve(createTableMock(tables.get(name)))
}),
openTable: vi.fn().mockImplementation((name) => {
return Promise.resolve(createTableMock(tables.get(name)))
}),
dropTable: vi.fn().mockImplementation((name) => {
tables.delete(name)
return Promise.resolve()
})
}),
tables,
createTableMock
}
}
/**
* Creates a mock embedding pipeline that returns 384-dim vectors
*/
export function createEmbeddingMock() {
const mockEmbedder = vi.fn().mockImplementation((texts, options) => {
const isArray = Array.isArray(texts)
const count = isArray ? texts.length : 1
// Generate deterministic vectors based on text content for consistency
const data = new Float32Array(count * EMBEDDING_DIM)
for (let i = 0; i < count; i++) {
const text = isArray ? texts[i] : texts
const seed = hashString(text)
for (let j = 0; j < EMBEDDING_DIM; j++) {
data[i * EMBEDDING_DIM + j] = (Math.sin(seed + j) + 1) / 2 * 0.2
}
}
return Promise.resolve({ data })
})
return {
pipeline: vi.fn().mockResolvedValue(mockEmbedder),
mockEmbedder
}
}
// Simple string hash for deterministic mock vectors
function hashString(str) {
let hash = 0
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i)
hash = ((hash << 5) - hash) + char
hash = hash & hash
}
return hash
}
/**
* Creates a mock file system for email testing
*/
export function createEmailFileSystemMock(emails = []) {
const emailMap = new Map(emails.map(e => [e.path, e.content]))
const metaData = {}
return {
existsSync: vi.fn().mockImplementation(p => {
if (emailMap.has(p)) return true
if (p.includes('.apple-tools-mcp')) return true
if (p.includes('index-meta.json')) return true
return false
}),
readFileSync: vi.fn().mockImplementation((p, encoding) => {
if (emailMap.has(p)) return emailMap.get(p)
if (p.includes('index-meta.json')) return JSON.stringify(metaData)
throw new Error('ENOENT: no such file or directory')
}),
writeFileSync: vi.fn().mockImplementation((p, content) => {
if (p.includes('index-meta.json')) {
Object.assign(metaData, JSON.parse(content))
}
}),
mkdirSync: vi.fn(),
unlinkSync: vi.fn(),
statSync: vi.fn().mockReturnValue({
isFile: () => true,
isDirectory: () => false,
mtime: new Date()
}),
readdirSync: vi.fn().mockReturnValue([]),
// Expose internal state for assertions
_emailMap: emailMap,
_metaData: metaData
}
}
/**
* Creates a mock for shell commands (exec, sqlite3, mdfind)
*/
export function createShellMock(options = {}) {
const {
mdfindResults = '',
findResults = '',
messages = [],
events = []
} = options
return {
execAsync: vi.fn().mockImplementation((cmd) => {
if (cmd.includes('mdfind')) {
return Promise.resolve({ stdout: mdfindResults, stderr: '' })
}
if (cmd.includes('find')) {
return Promise.resolve({ stdout: findResults, stderr: '' })
}
return Promise.resolve({ stdout: '', stderr: '' })
}),
safeSqlite3Json: vi.fn().mockImplementation((db, query) => {
if (db.includes('chat.db')) {
return messages
}
if (db.includes('Calendar')) {
return events
}
return []
}),
safeOsascript: vi.fn().mockReturnValue('')
}
}
/**
* Creates a mock for the index metadata file operations
*/
export function createIndexMetaMock(initialMeta = {}) {
let meta = { ...initialMeta }
return {
load: vi.fn().mockImplementation(() => ({ ...meta })),
save: vi.fn().mockImplementation((newMeta) => {
meta = { ...meta, ...newMeta }
}),
getMeta: () => meta,
reset: () => { meta = { ...initialMeta } }
}
}
/**
* Creates a mock lock file manager
*/
export function createLockFileMock() {
let lockHolder = null
let lockContent = null
return {
acquire: vi.fn().mockImplementation((pid = process.pid) => {
if (lockHolder === null || lockHolder === pid) {
lockHolder = pid
lockContent = String(pid)
return true
}
return false
}),
release: vi.fn().mockImplementation((pid = process.pid) => {
if (lockHolder === pid) {
lockHolder = null
lockContent = null
return true
}
return false
}),
isLocked: () => lockHolder !== null,
getHolder: () => lockHolder,
reset: () => {
lockHolder = null
lockContent = null
}
}
}
/**
* Creates a comprehensive mock setup for indexer tests
*/
export function createIndexerMocks(options = {}) {
const lancedb = createLanceDBMock()
const embedding = createEmbeddingMock()
const fs = createEmailFileSystemMock(options.emails || [])
const shell = createShellMock(options)
const meta = createIndexMetaMock(options.initialMeta || {})
const lock = createLockFileMock()
return {
lancedb,
embedding,
fs,
shell,
meta,
lock,
// Convenience method to reset all mocks
resetAll: () => {
vi.clearAllMocks()
lancedb.tables.clear()
meta.reset()
lock.reset()
}
}
}