Work packages completed: - WP1: packages/forge — pipeline runner, stage adapter, board tasks, brief classifier, persona loader with project-level overrides. 89 tests, 95.62% coverage. - WP2: packages/macp — credential resolver, gate runner, event emitter, protocol types. 65 tests, 96.24% coverage. Full Python-to-TS port preserving all behavior. - WP3: plugins/mosaic-framework — OC rails injection plugin (before_agent_start + subagent_spawning hooks for Mosaic contract enforcement). - WP4: profiles/ (domains, tech-stacks, workflows), guides/ (17 docs), skills/ (5 universal skills), forge pipeline assets (48 markdown files). Board deliberation: docs/reviews/consolidation-board-memo.md Brief: briefs/monorepo-consolidation.md Consolidates mosaic/stack (forge, MACP, bootstrap framework) into mosaic/mosaic-stack. 154 new tests total. Zero Python — all TypeScript/ESM.
142 lines
4.2 KiB
TypeScript
142 lines
4.2 KiB
TypeScript
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);
|
|
});
|
|
});
|