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:
196
packages/forge/__tests__/persona-loader.test.ts
Normal file
196
packages/forge/__tests__/persona-loader.test.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
|
||||
import {
|
||||
slugify,
|
||||
personaNameFromMarkdown,
|
||||
loadBoardPersonas,
|
||||
loadPersonaOverrides,
|
||||
loadForgeConfig,
|
||||
getEffectivePersonas,
|
||||
} from '../src/persona-loader.js';
|
||||
|
||||
describe('slugify', () => {
|
||||
it('converts to lowercase and replaces non-alphanumeric with hyphens', () => {
|
||||
expect(slugify('Chief Executive Officer')).toBe('chief-executive-officer');
|
||||
});
|
||||
|
||||
it('strips leading and trailing hyphens', () => {
|
||||
expect(slugify('--hello--')).toBe('hello');
|
||||
});
|
||||
|
||||
it('returns "persona" for empty string', () => {
|
||||
expect(slugify('')).toBe('persona');
|
||||
});
|
||||
|
||||
it('handles special characters', () => {
|
||||
expect(slugify('CTO — Technical')).toBe('cto-technical');
|
||||
});
|
||||
});
|
||||
|
||||
describe('personaNameFromMarkdown', () => {
|
||||
it('extracts name from heading', () => {
|
||||
expect(personaNameFromMarkdown('# CEO — Chief Executive Officer', 'FALLBACK')).toBe('CEO');
|
||||
});
|
||||
|
||||
it('strips markdown heading markers', () => {
|
||||
expect(personaNameFromMarkdown('## CTO - Technical Lead', 'FALLBACK')).toBe('CTO');
|
||||
});
|
||||
|
||||
it('returns fallback for empty content', () => {
|
||||
expect(personaNameFromMarkdown('', 'FALLBACK')).toBe('FALLBACK');
|
||||
});
|
||||
|
||||
it('returns full heading if no separator', () => {
|
||||
expect(personaNameFromMarkdown('# SimpleTitle', 'FALLBACK')).toBe('SimpleTitle');
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadBoardPersonas', () => {
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'forge-personas-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('returns empty array for non-existent directory', () => {
|
||||
expect(loadBoardPersonas('/nonexistent')).toEqual([]);
|
||||
});
|
||||
|
||||
it('loads personas from markdown files', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, 'ceo.md'),
|
||||
'# CEO — Visionary Leader\n\nThe CEO sets direction.',
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, 'cto.md'),
|
||||
'# CTO — Technical Realist\n\nThe CTO evaluates feasibility.',
|
||||
);
|
||||
|
||||
const personas = loadBoardPersonas(tmpDir);
|
||||
expect(personas).toHaveLength(2);
|
||||
expect(personas[0]!.name).toBe('CEO');
|
||||
expect(personas[0]!.slug).toBe('ceo');
|
||||
expect(personas[1]!.name).toBe('CTO');
|
||||
});
|
||||
|
||||
it('sorts by filename', () => {
|
||||
fs.writeFileSync(path.join(tmpDir, 'z-last.md'), '# Z Last');
|
||||
fs.writeFileSync(path.join(tmpDir, 'a-first.md'), '# A First');
|
||||
|
||||
const personas = loadBoardPersonas(tmpDir);
|
||||
expect(personas[0]!.slug).toBe('a-first');
|
||||
expect(personas[1]!.slug).toBe('z-last');
|
||||
});
|
||||
|
||||
it('ignores non-markdown files', () => {
|
||||
fs.writeFileSync(path.join(tmpDir, 'notes.txt'), 'not a persona');
|
||||
fs.writeFileSync(path.join(tmpDir, 'ceo.md'), '# CEO');
|
||||
|
||||
const personas = loadBoardPersonas(tmpDir);
|
||||
expect(personas).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadPersonaOverrides', () => {
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'forge-overrides-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('returns empty object when .forge/personas/ does not exist', () => {
|
||||
expect(loadPersonaOverrides(tmpDir)).toEqual({});
|
||||
});
|
||||
|
||||
it('loads override files', () => {
|
||||
const overridesDir = path.join(tmpDir, '.forge', 'personas');
|
||||
fs.mkdirSync(overridesDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(overridesDir, 'ceo.md'), 'Additional CEO context');
|
||||
|
||||
const overrides = loadPersonaOverrides(tmpDir);
|
||||
expect(overrides['ceo']).toBe('Additional CEO context');
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadForgeConfig', () => {
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'forge-config-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('returns empty config when file does not exist', () => {
|
||||
expect(loadForgeConfig(tmpDir)).toEqual({});
|
||||
});
|
||||
|
||||
it('parses board skipMembers', () => {
|
||||
const configDir = path.join(tmpDir, '.forge');
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(configDir, 'config.yaml'),
|
||||
'board:\n skipMembers:\n - cfo\n - coo\n',
|
||||
);
|
||||
|
||||
const config = loadForgeConfig(tmpDir);
|
||||
expect(config.board?.skipMembers).toEqual(['cfo', 'coo']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEffectivePersonas', () => {
|
||||
let tmpDir: string;
|
||||
let boardDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'forge-effective-'));
|
||||
boardDir = path.join(tmpDir, 'board-agents');
|
||||
fs.mkdirSync(boardDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(boardDir, 'ceo.md'), '# CEO — Visionary');
|
||||
fs.writeFileSync(path.join(boardDir, 'cto.md'), '# CTO — Technical');
|
||||
fs.writeFileSync(path.join(boardDir, 'cfo.md'), '# CFO — Financial');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('returns all personas with no overrides or config', () => {
|
||||
const personas = getEffectivePersonas(tmpDir, boardDir);
|
||||
expect(personas).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('appends project overrides to base description', () => {
|
||||
const overridesDir = path.join(tmpDir, '.forge', 'personas');
|
||||
fs.mkdirSync(overridesDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(overridesDir, 'ceo.md'), 'Focus on AI strategy');
|
||||
|
||||
const personas = getEffectivePersonas(tmpDir, boardDir);
|
||||
const ceo = personas.find((p) => p.slug === 'ceo')!;
|
||||
expect(ceo.description).toContain('# CEO — Visionary');
|
||||
expect(ceo.description).toContain('Focus on AI strategy');
|
||||
});
|
||||
|
||||
it('removes skipped members via config', () => {
|
||||
const configDir = path.join(tmpDir, '.forge');
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(configDir, 'config.yaml'), 'board:\n skipMembers:\n - cfo\n');
|
||||
|
||||
const personas = getEffectivePersonas(tmpDir, boardDir);
|
||||
expect(personas).toHaveLength(2);
|
||||
expect(personas.find((p) => p.slug === 'cfo')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user