import { access, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { constants } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { loadFleetRoster } from './fleet.js'; import { generateRoster, runProvision } from './fleet-provision.js'; import { loadProfile } from './fleet-profiles.js'; // These are INTEGRATION tests: each exercises real filesystem I/O — scanning the // committed framework/fleet persona library, rendering YAML, writing to a temp // mosaicHome, and round-tripping through the real roster parser. On a heavily // contended CI runner (the whole monorepo's suites run in parallel) that genuine // I/O can exceed vitest's 5s default even though it completes in ~400ms locally. // Give the legitimately I/O-bound work generous headroom so CI is deterministic. vi.setConfig({ testTimeout: 30_000 }); // 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'); /** A fresh temp mosaicHome whose fleet/ dir is empty (for write-path tests). */ async function freshMosaicHome(): Promise { const home = await mkdtemp(join(tmpdir(), 'mosaic-provision-')); await mkdir(join(home, 'fleet'), { recursive: true }); return home; } async function fileExists(path: string): Promise { try { await access(path, constants.F_OK); return true; } catch { return false; } } describe('provision software-delivery (floor, default)', () => { it('materializes only the floor seats with correct flags + valid scaffold', async () => { const profile = await loadProfile('software-delivery', { profilesDir, rolesDir }); const { seats, yaml } = await generateRoster(profile, { profilesDir, rolesDir }); // Floor is orchestrator + enhancer. expect(seats.map((s) => s.name).sort()).toEqual(['enhancer', 'orchestrator']); const orch = seats.find((s) => s.name === 'orchestrator'); const enh = seats.find((s) => s.name === 'enhancer'); // RULE 2: floor + lead get persistent_persona. expect(orch?.persistentPersona).toBe(true); expect(enh?.persistentPersona).toBe(true); // RULE 3: floor/lead do NOT reset between tasks. expect(orch?.resetBetweenTasks).toBeUndefined(); expect(enh?.resetBetweenTasks).toBeUndefined(); // RULE 4: default runtime claude. expect(orch?.runtime).toBe('claude'); // Scaffold round-trips through the real parser. expect(yaml).toContain('version: 1'); expect(yaml).toContain('transport: tmux'); expect(yaml).toContain('socket_name: mosaic-fleet'); }); }); describe('provision --full', () => { it('expands the entire roster, including multiplicity, deterministically', async () => { const profile = await loadProfile('software-delivery', { profilesDir, rolesDir }); const { seats } = await generateRoster(profile, { full: true, profilesDir, rolesDir }); const names = seats.map((s) => s.name); // code multiplicity 2 -> code0/code1 (RULE 1). expect(names).toContain('code0'); expect(names).toContain('code1'); expect(names).not.toContain('code'); // Singleton seats keep the bare class name. expect(names).toContain('planner'); expect(names).toContain('merge-gate'); // Deterministic ordering: profile roster order, multiplicity expanded inline. const codeIdx0 = names.indexOf('code0'); expect(names[codeIdx0 + 1]).toBe('code1'); // RULE 3: a non-floor, non-lead execution seat resets between tasks. const code0 = seats.find((s) => s.name === 'code0'); expect(code0?.resetBetweenTasks).toBe(true); expect(code0?.persistentPersona).toBeUndefined(); // Seat count == sum of multiplicities. const expected = profile.roster.reduce((n, e) => n + e.multiplicity, 0); expect(seats.length).toBe(expected); }); }); describe('generated roster round-trips through the real parser', () => { it('feeds generated YAML back through loadFleetRoster (key correctness proof)', async () => { const home = await freshMosaicHome(); try { const result = await runProvision('software-delivery', { mosaicHome: home, profilesDir, rolesDir, full: true, write: true, }); expect(result.wrote).toBe(join(home, 'fleet', 'roster.yaml')); const parsed = await loadFleetRoster(result.wrote!); // It parses with no error and carries every seat. const profile = await loadProfile('software-delivery', { profilesDir, rolesDir }); const expected = profile.roster.reduce((n, e) => n + e.multiplicity, 0); expect(parsed.agents.length).toBe(expected); // Classes survive the round-trip. expect(parsed.agents.some((a) => a.className === 'orchestrator')).toBe(true); expect(parsed.agents.filter((a) => a.className === 'code').length).toBe(2); // reports_to must NOT have been emitted (parser rejects unknown keys). expect(result.yaml).not.toContain('reports_to'); } finally { await rm(home, { recursive: true, force: true }); } }); }); describe('override-aware persona validation', () => { let dir: string; beforeEach(async () => { dir = await mkdtemp(join(tmpdir(), 'mosaic-provision-ov-')); }); afterEach(async () => { await rm(dir, { recursive: true, force: true }); }); it('resolves a user-added (roles.local-only) persona without a false unresolved error', async () => { const overrideDir = join(dir, 'roles.local'); const customProfilesDir = join(dir, 'profiles'); await mkdir(overrideDir, { recursive: true }); await mkdir(customProfilesDir, { recursive: true }); // A brand-new class that exists ONLY in roles.local. await writeFile( join(overrideDir, 'widget-maker.md'), '# widget-maker\n\nThe widget-maker persona (`class: widget-maker`).\n', ); await writeFile( join(customProfilesDir, 'custom.yaml'), [ 'id: custom', 'title: Custom', 'description: a custom system', 'lead: widget-maker', 'floor: [widget-maker]', 'roster:', ' - class: widget-maker', '', ].join('\n'), ); const result = await runProvision('custom', { mosaicHome: dir, profilesDir: customProfilesDir, // Point baseline rolesDir at a missing dir so resolution depends on override. rolesDir: join(dir, 'no-baseline'), overrideDir, }); expect(result.yaml).toContain('class: widget-maker'); // It resolved from the override layer. // (generateRoster records personaLayer; the seat is present.) expect(result.summary).toContain('persona=override'); }); it('FAILS with a clear message when a profile references a bogus class', async () => { const customProfilesDir = join(dir, 'profiles'); await mkdir(customProfilesDir, { recursive: true }); await writeFile( join(customProfilesDir, 'bogus.yaml'), [ 'id: bogus', 'title: Bogus', 'description: bad system', 'lead: orchestrator', 'floor: [orchestrator]', 'roster:', ' - class: orchestrator', ' - class: not-a-real-persona-xyz', ' reports_to: orchestrator', '', ].join('\n'), ); await expect( runProvision('bogus', { mosaicHome: dir, profilesDir: customProfilesDir, rolesDir, overrideDir: join(dir, 'roles.local'), }), ).rejects.toThrow(/not-a-real-persona-xyz|not a known persona class/); }); }); describe('--write protection', () => { it('refuses to clobber an existing roster.yaml without --force', async () => { const home = await freshMosaicHome(); try { const rosterPath = join(home, 'fleet', 'roster.yaml'); await writeFile(rosterPath, 'version: 1\n# operator customizations\n'); await expect( runProvision('software-delivery', { mosaicHome: home, profilesDir, rolesDir, write: true }), ).rejects.toThrow(/Refusing to overwrite/); // The original file is untouched. const { readFile } = await import('node:fs/promises'); expect(await readFile(rosterPath, 'utf8')).toContain('operator customizations'); } finally { await rm(home, { recursive: true, force: true }); } }); it('--write --force overwrites an existing roster', async () => { const home = await freshMosaicHome(); try { const rosterPath = join(home, 'fleet', 'roster.yaml'); await writeFile(rosterPath, 'version: 1\n# old\n'); const result = await runProvision('software-delivery', { mosaicHome: home, profilesDir, rolesDir, write: true, force: true, }); expect(result.wrote).toBe(rosterPath); const parsed = await loadFleetRoster(rosterPath); expect(parsed.agents.length).toBeGreaterThan(0); } finally { await rm(home, { recursive: true, force: true }); } }); it('--write to a fresh mosaicHome creates the roster file', async () => { const home = await freshMosaicHome(); try { const result = await runProvision('software-delivery', { mosaicHome: home, profilesDir, rolesDir, write: true, }); expect(await fileExists(result.wrote!)).toBe(true); } finally { await rm(home, { recursive: true, force: true }); } }); it('dry run (no --write) writes nothing', async () => { const home = await freshMosaicHome(); try { const result = await runProvision('software-delivery', { mosaicHome: home, profilesDir, rolesDir, }); expect(result.wrote).toBeUndefined(); expect(await fileExists(join(home, 'fleet', 'roster.yaml'))).toBe(false); } finally { await rm(home, { recursive: true, force: true }); } }); });