import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { rm } from 'node:fs/promises'; import { FileEventStore } from './file-store.js'; import type { StoredEvent } from './types.js'; function makeEvent(overrides?: Partial): StoredEvent { return { id: crypto.randomUUID(), type: 'test.event', userId: 'u1', productId: 'testprod', timestamp: new Date().toISOString(), payload: {}, ...overrides, }; } describe('FileEventStore', () => { let store: FileEventStore; let filePath: string; beforeEach(() => { filePath = join( tmpdir(), `event-store-test-${Date.now()}-${Math.random().toString(36).slice(2)}.jsonl` ); store = new FileEventStore({ filePath }); }); afterEach(async () => { try { await rm(filePath); } catch { /* may not exist */ } }); it('appends and retrieves events', async () => { await store.append(makeEvent()); expect(await store.count()).toBe(1); const recent = await store.recent(); expect(recent).toHaveLength(1); }); it('persists multiple events across reads', async () => { await store.append(makeEvent({ id: 'e1' })); await store.append(makeEvent({ id: 'e2' })); await store.append(makeEvent({ id: 'e3' })); expect(await store.count()).toBe(3); const recent = await store.recent(2); expect(recent).toHaveLength(2); expect(recent[0].id).toBe('e2'); }); it('queries by userId', async () => { await store.append(makeEvent({ userId: 'u1' })); await store.append(makeEvent({ userId: 'u2' })); await store.append(makeEvent({ userId: 'u1' })); const results = await store.query({ userId: 'u1' }); expect(results).toHaveLength(2); }); it('queries by type', async () => { await store.append(makeEvent({ type: 'task.created' })); await store.append(makeEvent({ type: 'schedule.generated' })); const results = await store.query({ type: 'task.created' }); expect(results).toHaveLength(1); }); it('queries with time range', async () => { await store.append(makeEvent({ timestamp: '2026-01-01T00:00:00Z' })); await store.append(makeEvent({ timestamp: '2026-03-01T00:00:00Z' })); await store.append(makeEvent({ timestamp: '2026-06-01T00:00:00Z' })); const results = await store.query({ after: '2026-02-01T00:00:00Z', before: '2026-04-01T00:00:00Z', }); expect(results).toHaveLength(1); }); it('queries with limit', async () => { for (let i = 0; i < 10; i++) { await store.append(makeEvent()); } const results = await store.query({ limit: 3 }); expect(results).toHaveLength(3); }); it('clears all events', async () => { await store.append(makeEvent()); await store.append(makeEvent()); await store.clear(); expect(await store.count()).toBe(0); }); it('returns empty for non-existent file', async () => { const fresh = new FileEventStore({ filePath: join(tmpdir(), 'nonexistent-' + Date.now() + '.jsonl'), }); expect(await fresh.count()).toBe(0); expect(await fresh.recent()).toEqual([]); }); it('creates parent directory if needed', async () => { const nested = join(tmpdir(), `nested-${Date.now()}`, 'sub', 'events.jsonl'); const nestedStore = new FileEventStore({ filePath: nested }); await nestedStore.append(makeEvent()); expect(await nestedStore.count()).toBe(1); await rm(join(tmpdir(), `nested-${Date.now()}`), { recursive: true }).catch(() => {}); }); });