import { mkdirSync, readFileSync, rmSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { randomUUID } from 'node:crypto'; import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { nowISO, appendEvent, emitEvent } from '../src/event-emitter.js'; import type { MACPEvent } from '../src/types.js'; function makeTmpDir(): string { const dir = join(tmpdir(), `macp-event-${randomUUID()}`); mkdirSync(dir, { recursive: true }); return dir; } describe('nowISO', () => { it('returns a valid ISO timestamp', () => { const ts = nowISO(); expect(() => new Date(ts)).not.toThrow(); expect(new Date(ts).toISOString()).toBe(ts); }); }); describe('appendEvent', () => { let tmp: string; beforeEach(() => { tmp = makeTmpDir(); }); afterEach(() => { rmSync(tmp, { recursive: true, force: true }); }); it('appends event as ndjson line', () => { const eventsPath = join(tmp, 'events.ndjson'); const event: MACPEvent = { event_id: 'evt-1', event_type: 'task.started', task_id: 'task-1', status: 'running', timestamp: nowISO(), source: 'test', message: 'Test event', metadata: {}, }; appendEvent(eventsPath, event); const content = readFileSync(eventsPath, 'utf-8'); const lines = content.trim().split('\n'); expect(lines).toHaveLength(1); const parsed = JSON.parse(lines[0]!); expect(parsed.event_id).toBe('evt-1'); expect(parsed.event_type).toBe('task.started'); expect(parsed.task_id).toBe('task-1'); }); it('appends multiple events', () => { const eventsPath = join(tmp, 'events.ndjson'); const base: MACPEvent = { event_id: '', event_type: 'task.started', task_id: 'task-1', status: 'running', timestamp: nowISO(), source: 'test', message: '', metadata: {}, }; appendEvent(eventsPath, { ...base, event_id: 'evt-1', message: 'first' }); appendEvent(eventsPath, { ...base, event_id: 'evt-2', message: 'second' }); const lines = readFileSync(eventsPath, 'utf-8').trim().split('\n'); expect(lines).toHaveLength(2); }); it('creates parent directories', () => { const eventsPath = join(tmp, 'nested', 'deep', 'events.ndjson'); const event: MACPEvent = { event_id: 'evt-1', event_type: 'task.started', task_id: 'task-1', status: 'running', timestamp: nowISO(), source: 'test', message: 'nested', metadata: {}, }; appendEvent(eventsPath, event); expect(readFileSync(eventsPath, 'utf-8')).toContain('nested'); }); }); describe('emitEvent', () => { let tmp: string; beforeEach(() => { tmp = makeTmpDir(); }); afterEach(() => { rmSync(tmp, { recursive: true, force: true }); }); it('creates event with all required fields', () => { const eventsPath = join(tmp, 'events.ndjson'); emitEvent(eventsPath, 'task.completed', 'task-42', 'completed', 'controller', 'Task done'); const content = readFileSync(eventsPath, 'utf-8'); const event = JSON.parse(content.trim()); expect(event.event_id).toBeTruthy(); expect(event.event_type).toBe('task.completed'); expect(event.task_id).toBe('task-42'); expect(event.status).toBe('completed'); expect(event.source).toBe('controller'); expect(event.message).toBe('Task done'); expect(event.timestamp).toBeTruthy(); expect(event.metadata).toEqual({}); }); it('includes metadata when provided', () => { const eventsPath = join(tmp, 'events.ndjson'); emitEvent(eventsPath, 'task.failed', 'task-1', 'failed', 'worker', 'err', { exit_code: 1, }); const event = JSON.parse(readFileSync(eventsPath, 'utf-8').trim()); expect(event.metadata).toEqual({ exit_code: 1 }); }); it('generates unique event_ids', () => { const eventsPath = join(tmp, 'events.ndjson'); emitEvent(eventsPath, 'task.started', 'task-1', 'running', 'test', 'a'); emitEvent(eventsPath, 'task.started', 'task-1', 'running', 'test', 'b'); const events = readFileSync(eventsPath, 'utf-8') .trim() .split('\n') .map((l) => JSON.parse(l)); expect(events[0].event_id).not.toBe(events[1].event_id); }); });