feat: monorepo consolidation — forge pipeline, MACP protocol, framework plugin, profiles/guides/skills
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.
This commit is contained in:
141
packages/macp/__tests__/event-emitter.test.ts
Normal file
141
packages/macp/__tests__/event-emitter.test.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user