114 lines
3.5 KiB
TypeScript
114 lines
3.5 KiB
TypeScript
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>): 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(() => {});
|
|
});
|
|
});
|