/**
* Unit tests for index metadata handling
* Tests: loadIndexMeta, saveIndexMeta, metadata file operations
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { createIndexMetaMock, createEmailFileSystemMock } from '../helpers/indexing-mocks.js'
import path from 'path'
describe('loadIndexMeta', () => {
let fsMock
beforeEach(() => {
vi.clearAllMocks()
fsMock = createEmailFileSystemMock()
})
afterEach(() => {
vi.restoreAllMocks()
})
describe('missing file handling', () => {
it('should return empty object when file does not exist', () => {
// Simulate file not existing
fsMock.existsSync.mockReturnValue(false)
// loadIndexMeta should return {} when file doesn't exist
const result = fsMock.existsSync('/nonexistent/path') ? JSON.parse(fsMock.readFileSync('/nonexistent/path')) : {}
expect(result).toEqual({})
})
})
describe('valid JSON parsing', () => {
it('should parse valid JSON from file', () => {
const expectedMeta = {
lastEmailIndexTime: Date.now() - 3600000,
version: '1.0'
}
fsMock.existsSync.mockReturnValue(true)
fsMock.readFileSync.mockReturnValue(JSON.stringify(expectedMeta))
const result = JSON.parse(fsMock.readFileSync('/path/to/meta.json'))
expect(result).toEqual(expectedMeta)
expect(result.lastEmailIndexTime).toBeDefined()
})
it('should handle timestamps correctly', () => {
const timestamp = Date.now()
const meta = { lastEmailIndexTime: timestamp }
fsMock.readFileSync.mockReturnValue(JSON.stringify(meta))
const result = JSON.parse(fsMock.readFileSync('/path/to/meta.json'))
expect(result.lastEmailIndexTime).toBe(timestamp)
expect(typeof result.lastEmailIndexTime).toBe('number')
})
})
describe('error handling', () => {
it('should return empty object on parse error', () => {
fsMock.existsSync.mockReturnValue(true)
fsMock.readFileSync.mockReturnValue('invalid json {{{')
let result = {}
try {
result = JSON.parse(fsMock.readFileSync('/path/to/meta.json'))
} catch {
result = {}
}
expect(result).toEqual({})
})
it('should return empty object on read error', () => {
fsMock.existsSync.mockReturnValue(true)
fsMock.readFileSync.mockImplementation(() => {
throw new Error('EACCES: permission denied')
})
let result = {}
try {
result = JSON.parse(fsMock.readFileSync('/path/to/meta.json'))
} catch {
result = {}
}
expect(result).toEqual({})
})
})
})
describe('saveIndexMeta', () => {
let fsMock
beforeEach(() => {
vi.clearAllMocks()
fsMock = createEmailFileSystemMock()
})
afterEach(() => {
vi.restoreAllMocks()
})
describe('directory creation', () => {
it('should create directory if not exists', () => {
fsMock.existsSync.mockReturnValue(false)
// Simulate saveIndexMeta behavior
const filePath = '/Users/test/.apple-tools-mcp/index-meta.json'
const dir = path.dirname(filePath)
if (!fsMock.existsSync(dir)) {
fsMock.mkdirSync(dir, { recursive: true })
}
expect(fsMock.mkdirSync).toHaveBeenCalledWith(dir, { recursive: true })
})
it('should not recreate existing directory', () => {
fsMock.existsSync.mockReturnValue(true)
const filePath = '/Users/test/.apple-tools-mcp/index-meta.json'
const dir = path.dirname(filePath)
if (!fsMock.existsSync(dir)) {
fsMock.mkdirSync(dir, { recursive: true })
}
expect(fsMock.mkdirSync).not.toHaveBeenCalled()
})
})
describe('JSON formatting', () => {
it('should write formatted JSON', () => {
const meta = { lastEmailIndexTime: 1234567890 }
// Simulate saveIndexMeta behavior
const formatted = JSON.stringify(meta, null, 2)
fsMock.writeFileSync('/path/to/meta.json', formatted)
expect(fsMock.writeFileSync).toHaveBeenCalledWith(
'/path/to/meta.json',
expect.stringContaining('"lastEmailIndexTime"')
)
// Verify it's properly indented
expect(formatted).toContain('\n')
})
it('should handle nested objects', () => {
const meta = {
lastEmailIndexTime: Date.now(),
stats: {
emails: 100,
messages: 50
}
}
const formatted = JSON.stringify(meta, null, 2)
expect(formatted).toContain('"stats"')
expect(formatted).toContain('"emails"')
})
})
describe('field preservation', () => {
it('should preserve existing metadata fields when updating', () => {
const existingMeta = {
lastEmailIndexTime: 1000000,
customField: 'preserved'
}
const newMeta = {
...existingMeta,
lastEmailIndexTime: 2000000
}
expect(newMeta.customField).toBe('preserved')
expect(newMeta.lastEmailIndexTime).toBe(2000000)
})
it('should not overwrite other keys when updating single field', () => {
const metaMock = createIndexMetaMock({
lastEmailIndexTime: 1000000,
version: '1.0',
indexedCount: 500
})
metaMock.save({ lastEmailIndexTime: 2000000 })
const result = metaMock.getMeta()
expect(result.lastEmailIndexTime).toBe(2000000)
expect(result.version).toBe('1.0')
expect(result.indexedCount).toBe(500)
})
})
describe('atomic write considerations', () => {
it('should write complete JSON in single call', () => {
const meta = { lastEmailIndexTime: Date.now() }
fsMock.writeFileSync('/path/to/meta.json', JSON.stringify(meta, null, 2))
// Verify single write call
expect(fsMock.writeFileSync).toHaveBeenCalledTimes(1)
})
})
})
describe('IndexMetaMock', () => {
it('should track metadata changes', () => {
const mock = createIndexMetaMock({ initial: true })
mock.save({ updated: true })
const result = mock.getMeta()
expect(result.initial).toBe(true)
expect(result.updated).toBe(true)
})
it('should reset to initial state', () => {
const mock = createIndexMetaMock({ original: 'value' })
mock.save({ new: 'data' })
mock.reset()
const result = mock.getMeta()
expect(result).toEqual({ original: 'value' })
expect(result.new).toBeUndefined()
})
})
describe('metadata timestamp handling', () => {
it('should save current timestamp before indexing starts', () => {
const indexStartTime = Date.now()
// This ensures any emails arriving during indexing
// will be picked up on the next incremental scan
const meta = { lastEmailIndexTime: indexStartTime }
expect(meta.lastEmailIndexTime).toBe(indexStartTime)
expect(meta.lastEmailIndexTime).toBeLessThanOrEqual(Date.now())
})
it('should apply 1-day buffer for incremental scans', () => {
const ONE_DAY_MS = 24 * 60 * 60 * 1000
const lastIndexTime = Date.now() - (2 * ONE_DAY_MS)
// The implementation applies a 1-day buffer to catch edge cases
const effectiveTime = lastIndexTime - ONE_DAY_MS
expect(effectiveTime).toBeLessThan(lastIndexTime)
expect(lastIndexTime - effectiveTime).toBe(ONE_DAY_MS)
})
})