import { mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { listPersonaClasses, loadProfile, loadProfiles, parseProfile, validateProfile, type FleetProfile, } from './fleet-profiles.js'; // The real, committed library: packages/mosaic/src/commands -> framework/fleet. const frameworkFleet = resolve( dirname(fileURLToPath(import.meta.url)), '..', '..', 'framework', 'fleet', ); const rolesDir = join(frameworkFleet, 'roles'); const profilesDir = join(frameworkFleet, 'profiles'); const realLib = { rolesDir, profilesDir }; const EXPECTED_IDS = [ 'business', 'marketing', 'personal-assistant', 'research', 'software-delivery', ]; describe('listPersonaClasses (real role library)', () => { it('extracts inline `class:` markers from the role contracts', async () => { const classes = await listPersonaClasses(rolesDir); // Personas that carry an inline `class: X` marker. expect(classes.has('code')).toBe(true); expect(classes.has('marketing-lead')).toBe(true); expect(classes.has('ceo')).toBe(true); // support-agent's marker wraps across a newline — must still resolve. expect(classes.has('support-agent')).toBe(true); }); it('covers marker-less engineering personas via filename + LIBRARY index', async () => { const classes = await listPersonaClasses(rolesDir); // planner/decomposition have a role file but no inline marker — they resolve // from the filename + LIBRARY.md row. expect(classes.has('planner')).toBe(true); expect(classes.has('decomposition')).toBe(true); // The dedicated orchestrator persona resolves (inline marker + filename + row). expect(classes.has('orchestrator')).toBe(true); }); it('returns an empty set for a missing roles dir (graceful)', async () => { const classes = await listPersonaClasses(join(tmpdir(), 'definitely-missing-roles-xyz')); expect(classes.size).toBe(0); }); }); describe('baseline profiles (real library)', () => { it('loads exactly the five baseline profiles, sorted by id', async () => { const profiles = await loadProfiles(realLib); expect(profiles.map((p) => p.id)).toEqual(EXPECTED_IDS); }); it('every referenced class resolves against the real role library (drift guard)', async () => { // This is the key test: it fails if a profile drifts from the persona library. const profiles = await loadProfiles(realLib); const validClasses = await listPersonaClasses(rolesDir); for (const profile of profiles) { expect(validateProfile(profile, validClasses)).toEqual([]); } }); it('software-delivery has the expected lead, floor, and roster shape', async () => { const profile = await loadProfile('software-delivery', realLib); expect(profile.lead).toBe('orchestrator'); expect(profile.floor).toEqual(['orchestrator', 'enhancer']); const code = profile.roster.find((r) => r.class === 'code'); expect(code?.multiplicity).toBe(2); expect(code?.reportsTo).toBe('decomposition'); // The dedicated orchestrator is the lead seat (no reports_to); the planner is // now a distinct seat that reports to it. const orchestrator = profile.roster.find((r) => r.class === 'orchestrator'); expect(orchestrator?.reportsTo).toBeUndefined(); const planner = profile.roster.find((r) => r.class === 'planner'); expect(planner?.reportsTo).toBe('orchestrator'); }); it('loadProfile throws on an unknown id', async () => { await expect(loadProfile('does-not-exist', realLib)).rejects.toThrow(/Unknown profile/); }); }); describe('parseProfile', () => { it('defaults multiplicity to 1 and omits reports_to for the lead', () => { const yaml = [ 'id: x', 'title: X', 'description: a system', 'lead: ceo', 'floor: [ceo]', 'roster:', ' - class: ceo', ' - class: code', ' reports_to: ceo', ' multiplicity: 3', '', ].join('\n'); const profile = parseProfile(yaml); expect(profile.roster[0]).toEqual({ class: 'ceo', multiplicity: 1 }); expect(profile.roster[1]).toEqual({ class: 'code', reportsTo: 'ceo', multiplicity: 3 }); }); it('rejects a profile whose id mismatches its filename', () => { expect(() => parseProfile( 'id: other\ntitle: T\ndescription: d\nlead: ceo\nroster: [{class: ceo}]\n', 'expected', ), ).toThrow(/does not match its filename/); }); it('rejects a non-integer multiplicity', () => { const yaml = 'id: x\ntitle: T\ndescription: d\nlead: ceo\nroster:\n - class: ceo\n multiplicity: 1.5\n'; expect(() => parseProfile(yaml)).toThrow(/multiplicity/); }); }); describe('validateProfile', () => { const valid = new Set(['ceo', 'coo', 'code']); it('passes a well-formed profile', () => { const profile: FleetProfile = { id: 'x', title: 'X', description: 'd', lead: 'ceo', floor: ['ceo'], roster: [ { class: 'ceo', multiplicity: 1 }, { class: 'coo', reportsTo: 'ceo', multiplicity: 1 }, ], }; expect(validateProfile(profile, valid)).toEqual([]); }); it('rejects an unknown roster class', () => { const profile: FleetProfile = { id: 'x', title: 'X', description: 'd', lead: 'ceo', floor: [], roster: [{ class: 'not-a-real-persona', multiplicity: 1 }], }; const problems = validateProfile(profile, valid); expect(problems.some((p) => /not-a-real-persona.*not a known persona class/.test(p))).toBe( true, ); }); it('rejects a reports_to that names a class absent from the roster', () => { const profile: FleetProfile = { id: 'x', title: 'X', description: 'd', lead: 'ceo', floor: [], roster: [{ class: 'code', reportsTo: 'coo', multiplicity: 1 }], // coo valid but not in roster }; const problems = validateProfile(profile, valid); expect(problems.some((p) => /reports_to.*not present in this roster/.test(p))).toBe(true); }); it('rejects a reports_to that is not a known persona class at all', () => { const profile: FleetProfile = { id: 'x', title: 'X', description: 'd', lead: 'ceo', floor: [], roster: [ { class: 'ceo', multiplicity: 1 }, { class: 'code', reportsTo: 'ghost', multiplicity: 1 }, ], }; const problems = validateProfile(profile, valid); expect(problems.some((p) => /ghost.*not a known persona class/.test(p))).toBe(true); }); }); describe('loadProfiles with a temp override dir', () => { let dir: string; beforeEach(async () => { dir = await mkdtemp(join(tmpdir(), 'mosaic-profiles-')); }); afterEach(async () => { await rm(dir, { recursive: true, force: true }); }); it('throws when a profile references an unknown class (validated against real roles)', async () => { await writeFile( join(dir, 'bad.yaml'), 'id: bad\ntitle: Bad\ndescription: d\nlead: nope-not-real\nroster:\n - class: nope-not-real\n', ); await expect(loadProfiles({ profilesDir: dir, rolesDir })).rejects.toThrow( /is invalid|not a known persona class/, ); }); it('throws on duplicate profile ids across files', async () => { const body = 'title: Dup\ndescription: d\nlead: ceo\nroster:\n - class: ceo\n'; // Same declared id in two differently-named files -> id mismatches filename // first; use matching filenames+id to force the duplicate-id path instead. await writeFile(join(dir, 'dup.yaml'), `id: dup\n${body}`); await writeFile(join(dir, 'dup.yml'), `id: dup\n${body}`); await expect(loadProfiles({ profilesDir: dir, rolesDir })).rejects.toThrow( /Duplicate profile id/, ); }); });