import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { accessSync, chmodSync, constants, mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync, renameSync, symlinkSync, } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { FileConfigAdapter } from '../config/file-adapter.js'; import { composeContract } from './launch.js'; /** * Composer unit test (R7/R8/R9): asserts the launcher-composed runtime contract * * - includes the per-tier anchors (CONSTITUTION / AGENTS / USER / runtime), * - keeps the CONSTITUTION block byte-equal to the on-disk file (Tier-3 * byte-equality — the bare-launch fallback read must match what is injected), * - merges `*.local.md` operator overlays as deltas-by-value, and omits them * entirely when absent (base-only), * - selects the correct per-harness RUNTIME.md. * * `composeContract` takes `mosaicHome` as a param, so each test runs against an * isolated fixture home. We also chdir to an empty temp cwd so the cwd-relative * mission/PRD blocks contribute nothing (deterministic output). */ const CONSTITUTION = '# CONSTITUTION\n\nGATE-1: the non-negotiable law.\n'; const AGENTS = '# Mosaic Agent Dispatcher\n\nLoad order + guide router.\n'; const USER = '# operator\n\nName: Test Operator\n'; const TOOLS = '# tools index\n\n\n'; const FRAMEWORK_SOURCE = fileURLToPath(new URL('../../framework', import.meta.url)); const SOURCE_TOOLS_PATH = join(FRAMEWORK_SOURCE, 'defaults', 'TOOLS.md'); function makeHome(): { home: string; root: string } { const root = mkdtempSync(join(tmpdir(), 'mosaic-compose-')); const home = join(root, 'mosaic-home'); for (const h of ['claude', 'codex', 'opencode', 'pi']) { mkdirSync(join(home, 'runtime', h), { recursive: true }); writeFileSync(join(home, 'runtime', h, 'RUNTIME.md'), `# ${h} runtime contract\n`); } mkdirSync(join(home, 'defaults'), { recursive: true }); mkdirSync(join(home, 'tools', 'tmux'), { recursive: true }); writeFileSync(join(home, 'defaults', 'TOOLS.md'), TOOLS); const helper = join(home, 'tools', 'tmux', 'agent-send.sh'); writeFileSync(helper, '#!/bin/sh\n'); chmodSync(helper, 0o755); writeFileSync(join(home, 'CONSTITUTION.md'), CONSTITUTION); writeFileSync(join(home, 'AGENTS.md'), AGENTS); writeFileSync(join(home, 'USER.md'), USER); writeFileSync(join(home, 'TOOLS.md'), TOOLS); return { home, root }; } describe('composeContract — overlay composer', () => { let fixture: ReturnType; let prevCwd: string; let cwdDir: string; let prevAgentName: string | undefined; let prevAgentClass: string | undefined; let prevAgentToolPolicy: string | undefined; beforeEach(() => { fixture = makeHome(); prevCwd = process.cwd(); prevAgentName = process.env['MOSAIC_AGENT_NAME']; prevAgentClass = process.env['MOSAIC_AGENT_CLASS']; prevAgentToolPolicy = process.env['MOSAIC_AGENT_TOOL_POLICY']; delete process.env['MOSAIC_AGENT_NAME']; delete process.env['MOSAIC_AGENT_CLASS']; delete process.env['MOSAIC_AGENT_TOOL_POLICY']; cwdDir = mkdtempSync(join(tmpdir(), 'mosaic-cwd-')); process.chdir(cwdDir); // neutralize cwd-relative mission/PRD blocks }); afterEach(() => { process.chdir(prevCwd); if (prevAgentName === undefined) delete process.env['MOSAIC_AGENT_NAME']; else process.env['MOSAIC_AGENT_NAME'] = prevAgentName; if (prevAgentClass === undefined) delete process.env['MOSAIC_AGENT_CLASS']; else process.env['MOSAIC_AGENT_CLASS'] = prevAgentClass; if (prevAgentToolPolicy === undefined) delete process.env['MOSAIC_AGENT_TOOL_POLICY']; else process.env['MOSAIC_AGENT_TOOL_POLICY'] = prevAgentToolPolicy; rmSync(fixture.root, { recursive: true, force: true }); rmSync(cwdDir, { recursive: true, force: true }); }); it('injects the fleet comms cheat-sheet for a spawned fleet agent (situational)', () => { // A spawned agent has MOSAIC_AGENT_NAME set + is a member of the roster. mkdirSync(join(fixture.home, 'fleet'), { recursive: true }); writeFileSync( join(fixture.home, 'fleet', 'roster.yaml'), [ 'version: 1', 'transport: tmux', 'tmux:', ' socket_name: mosaic-fleet', 'agents:', ' - name: orchestrator', ' runtime: claude', ' class: orchestrator', ' host: w-jarvis', ' - name: enhancer', ' runtime: claude', ' class: enhancer', ' host: w-jarvis', ' - name: coder0-0', ' runtime: claude', ' class: implementer', ' host: 10.1.10.37', ' ssh: jwoltje@10.1.10.37', '', ].join('\n'), ); const prev = process.env['MOSAIC_AGENT_NAME']; try { process.env['MOSAIC_AGENT_NAME'] = 'enhancer'; const outputs = (['claude', 'codex', 'opencode', 'pi'] as const).map((runtime) => composeContract(runtime, fixture.home), ); for (const out of outputs) { expect(out).toContain('# Fleet Comms'); expect(out).toContain('Host: `w-jarvis`'); expect(out).toContain('Agent/session: `enhancer`'); expect(out).toContain('tmux socket: `mosaic-fleet`'); expect(out).toContain( `Helper: \`${join(fixture.home, 'tools', 'tmux', 'agent-send.sh')}\``, ); expect(out).toContain('-L mosaic-fleet -s orchestrator -m "…"'); expect(out).toContain('-L mosaic-fleet -H jwoltje@10.1.10.37 -s coder0-0 -m "…"'); expect(out).not.toContain('-H jwoltje@10.1.10.37 -s orchestrator'); } const commsSection = (out: string): string => out.slice(out.indexOf('# Fleet Comms')); const authoritative = commsSection(outputs[0]!); expect(outputs.map(commsSection)).toEqual([ authoritative, authoritative, authoritative, authoritative, ]); } finally { if (prev === undefined) delete process.env['MOSAIC_AGENT_NAME']; else process.env['MOSAIC_AGENT_NAME'] = prev; } }); it.each(['claude', 'codex', 'opencode', 'pi'] as const)( 'derives canonical class, persona, tool policy, and comms from one roster member for %s', (runtime) => { mkdirSync(join(fixture.home, 'fleet', 'roles'), { recursive: true }); writeFileSync( join(fixture.home, 'fleet', 'roles', 'code.md'), '# Code\n\n(`class: code`)\n\nCANONICAL-CODE-MANDATE.\n', ); writeFileSync( join(fixture.home, 'fleet', 'roster.yaml'), [ 'version: 1', 'transport: tmux', 'agents:', ' - name: exact-coder', ` runtime: ${runtime}`, ' class: implementer', ' tool_policy: operator-interaction', '', ].join('\n'), ); process.env['MOSAIC_AGENT_NAME'] = 'exact-coder'; process.env['MOSAIC_AGENT_CLASS'] = 'implementer'; process.env['MOSAIC_AGENT_TOOL_POLICY'] = 'ambient-policy-must-not-win'; const out = composeContract(runtime, fixture.home); expect(out).toContain('# Persona Contract (code)'); expect(out).toContain('CANONICAL-CODE-MANDATE'); expect(out).toContain('Role/class: `code`'); expect(out).toContain('# Fleet Tool Policy (operator-interaction)'); expect(out).not.toContain('ambient-policy-must-not-win'); expect(out.indexOf('# Persona Contract')).toBeLessThan(out.indexOf('# Fleet Comms')); }, ); it.each(['claude', 'codex', 'opencode', 'pi'] as const)( 'does not inherit ambient tool policy when canonical member omits tool_policy for %s', (runtime) => { mkdirSync(join(fixture.home, 'fleet', 'roles'), { recursive: true }); writeFileSync( join(fixture.home, 'fleet', 'roles', 'code.md'), '# Code\n\n(`class: code`)\n\nCANONICAL-CODE-MANDATE.\n', ); writeFileSync( join(fixture.home, 'fleet', 'roster.yaml'), [ 'version: 1', 'transport: tmux', 'agents:', ' - name: exact-coder', ` runtime: ${runtime}`, ' class: implementer', '', ].join('\n'), ); process.env['MOSAIC_AGENT_NAME'] = 'exact-coder'; process.env['MOSAIC_AGENT_CLASS'] = 'implementer'; process.env['MOSAIC_AGENT_TOOL_POLICY'] = 'operator-interaction'; const out = composeContract(runtime, fixture.home); expect(out).toContain('# Persona Contract (code)'); expect(out).toContain('Role/class: `code`'); expect(out).toContain('# Fleet Comms'); expect(out).not.toContain('# Fleet Tool Policy (operator-interaction)'); }, ); it.each(['claude', 'codex', 'opencode', 'pi'] as const)( 'rejects an ambient class that mismatches the canonical roster member for %s', (runtime) => { mkdirSync(join(fixture.home, 'fleet'), { recursive: true }); writeFileSync( join(fixture.home, 'fleet', 'roster.yaml'), [ 'version: 1', 'transport: tmux', 'agents:', ' - name: exact-coder', ` runtime: ${runtime}`, ' class: implementer', '', ].join('\n'), ); process.env['MOSAIC_AGENT_NAME'] = 'exact-coder'; process.env['MOSAIC_AGENT_CLASS'] = 'reviewer'; expect(() => composeContract(runtime, fixture.home)).toThrow( /ambient MOSAIC_AGENT_CLASS.*review.*canonical roster.*code/i, ); }, ); it('composes solo role mandate and boundaries before explicit no-peer authority', () => { mkdirSync(join(fixture.home, 'fleet', 'roles'), { recursive: true }); writeFileSync( join(fixture.home, 'fleet', 'roles', 'orchestrator.md'), '# Orchestrator\n\n## Mandate\n\nCoordinate exact work.\n\n## Boundaries\n\nDo not infer authority.\n', ); writeFileSync( join(fixture.home, 'fleet', 'roster.yaml'), [ 'version: 1', 'transport: tmux', 'agents:', ' - name: solo', ' runtime: claude', ' class: orchestrator', '', ].join('\n'), ); process.env['MOSAIC_AGENT_NAME'] = 'solo'; process.env['MOSAIC_AGENT_CLASS'] = 'orchestrator'; const out = composeContract('claude', fixture.home); expect(out).toContain('## Mandate'); expect(out).toContain('## Boundaries'); expect(out).toContain('Role/class: `orchestrator`'); expect(out).toContain('## Solo authority boundaries'); expect(out.indexOf('## Mandate')).toBeLessThan(out.indexOf('# Fleet Comms')); expect(out.indexOf('## Boundaries')).toBeLessThan(out.indexOf('# Fleet Comms')); expect(out).toContain('no peer, orchestrator, or remote communication authority'); }); it('proves real source TOOLS.md through fresh install, executable helper, and final composition', async () => { const installRoot = mkdtempSync(join(tmpdir(), 'mosaic-real-contract-')); const installedHome = join(installRoot, 'mosaic-home'); mkdirSync(installedHome, { recursive: true }); const previous = process.env['MOSAIC_AGENT_NAME']; try { const adapter = new FileConfigAdapter(installedHome, FRAMEWORK_SOURCE); await adapter.syncFramework('fresh'); const sourceTools = readFileSync(SOURCE_TOOLS_PATH, 'utf8'); const installedToolsPath = join(installedHome, 'TOOLS.md'); expect(readFileSync(installedToolsPath, 'utf8')).toBe(sourceTools); expect(sourceTools).toContain('fleet-comms-contract: 1'); expect(sourceTools).not.toMatch( /<(?:user@host|src_host|src_session|dst_host|dst_session|target-session)>/, ); const helper = join(installedHome, 'tools', 'tmux', 'agent-send.sh'); expect(() => accessSync(helper, constants.X_OK)).not.toThrow(); mkdirSync(join(installedHome, 'fleet'), { recursive: true }); writeFileSync( join(installedHome, 'fleet', 'roster.yaml'), [ 'version: 1', 'transport: tmux', 'tmux:', ' socket_name: exact-socket', 'agents:', ' - name: exact-self', ' runtime: pi', ' class: orchestrator', ' host: local-host', ' - name: exact-peer', ' runtime: claude', ' class: implementer', ' host: local-host', '', ].join('\n'), ); process.env['MOSAIC_AGENT_NAME'] = 'exact-self'; const composed = composeContract('pi', installedHome); expect(composed).toContain(sourceTools); expect(composed).toContain(`Helper: \`${helper}\``); expect(composed).toContain(`${helper} -L exact-socket -s exact-peer -m "…"`); expect(composed).not.toContain('# Fleet Comms Installation Status'); } finally { if (previous === undefined) delete process.env['MOSAIC_AGENT_NAME']; else process.env['MOSAIC_AGENT_NAME'] = previous; rmSync(installRoot, { recursive: true, force: true }); } }); it.each(['claude', 'codex', 'opencode', 'pi'] as const)( 'never injects installed TOOLS.md through a target symlink for %s', (runtime) => { const toolsPath = join(fixture.home, 'TOOLS.md'); const external = join(fixture.root, 'attacker-tools.md'); writeFileSync(external, 'UNSAFE-TARGET-SYMLINK-CONTENT\n'); rmSync(toolsPath); symlinkSync(external, toolsPath); const out = composeContract(runtime, fixture.home); expect(out).not.toContain('UNSAFE-TARGET-SYMLINK-CONTENT'); expect(out).toContain('# Fleet Comms Installation Status'); expect(out).toContain('unavailable'); expect(readFileSync(external, 'utf8')).toBe('UNSAFE-TARGET-SYMLINK-CONTENT\n'); }, ); it.each(['claude', 'codex', 'opencode', 'pi'] as const)( 'never injects installed TOOLS.md through an ancestor symlink for %s', (runtime) => { const realHome = join(fixture.root, 'real-mosaic-home'); renameSync(fixture.home, realHome); symlinkSync(realHome, fixture.home, 'dir'); writeFileSync(join(realHome, 'TOOLS.md'), 'UNSAFE-ANCESTOR-SYMLINK-CONTENT\n'); const out = composeContract(runtime, fixture.home); expect(out).not.toContain('UNSAFE-ANCESTOR-SYMLINK-CONTENT'); expect(out).toContain('# Fleet Comms Installation Status'); expect(out).toContain('unavailable'); expect(readFileSync(join(realHome, 'TOOLS.md'), 'utf8')).toBe( 'UNSAFE-ANCESTOR-SYMLINK-CONTENT\n', ); }, ); it('reports stale preserved TOOLS.md without rewriting it', () => { const toolsPath = join(fixture.home, 'TOOLS.md'); const stale = '# user-customized tools without fleet contract marker\n'; writeFileSync(toolsPath, stale); const out = composeContract('pi', fixture.home); expect(out).toContain('# Fleet Comms Installation Status'); expect(out).toContain('does not byte-match'); expect(out).toContain('active context was not rewritten'); expect(readFileSync(toolsPath, 'utf8')).toBe(stale); }); it('does NOT inject fleet comms when MOSAIC_AGENT_NAME is unset (non-fleet launch)', () => { const prev = process.env['MOSAIC_AGENT_NAME']; try { delete process.env['MOSAIC_AGENT_NAME']; expect(composeContract('claude', fixture.home)).not.toContain('# Fleet Comms'); } finally { if (prev !== undefined) process.env['MOSAIC_AGENT_NAME'] = prev; } }); it('fails closed when an explicitly requested fleet identity is unknown', () => { mkdirSync(join(fixture.home, 'fleet'), { recursive: true }); writeFileSync( join(fixture.home, 'fleet', 'roster.yaml'), ['version: 1', 'transport: tmux', 'agents:', ' - name: exact-agent', ' runtime: pi'].join( '\n', ), ); const previous = process.env['MOSAIC_AGENT_NAME']; try { process.env['MOSAIC_AGENT_NAME'] = 'invented-agent'; expect(() => composeContract('pi', fixture.home)).toThrow(/known exact names: exact-agent/i); } finally { if (previous === undefined) delete process.env['MOSAIC_AGENT_NAME']; else process.env['MOSAIC_AGENT_NAME'] = previous; } }); it('includes the per-tier anchors and the selected harness runtime', () => { const out = composeContract('claude', fixture.home); expect(out).toContain('GATE-1: the non-negotiable law.'); // L0 expect(out).toContain('Mosaic Agent Dispatcher'); // AGENTS expect(out).toContain('# User Profile'); // USER header expect(out).toContain('Name: Test Operator'); // USER body expect(out).toContain('# Runtime-Specific Contract'); expect(out).toContain('# claude runtime contract'); }); it('keeps the CONSTITUTION block byte-equal to the on-disk file (Tier-3)', () => { const out = composeContract('pi', fixture.home); const onDisk = readFileSync(join(fixture.home, 'CONSTITUTION.md'), 'utf-8'); // The injected L0 must be a byte-equal substring of the composed blob, so a // bare-launch fallback read of CONSTITUTION.md matches what was injected. expect(out.includes(onDisk)).toBe(true); }); it('is base-only when no *.local overlays exist', () => { const out = composeContract('claude', fixture.home); expect(out).not.toContain('# Operator Overlays'); expect(out).not.toContain('Operator Overlay (USER.local.md)'); expect(out).not.toContain('Persona Overlay'); expect(out).not.toContain('Standards Overlay'); }); it('merges USER.local.md directly under the operator profile', () => { writeFileSync(join(fixture.home, 'USER.local.md'), 'Prefer terse status updates.\n'); const out = composeContract('claude', fixture.home); expect(out).toContain('## Operator Overlay (USER.local.md)'); expect(out).toContain('Prefer terse status updates.'); // Overlay appears AFTER its base profile. expect(out.indexOf('# User Profile')).toBeLessThan( out.indexOf('## Operator Overlay (USER.local.md)'), ); }); it('merges SOUL.local.md + STANDARDS.local.md as deltas in the Operator Overlays block', () => { writeFileSync(join(fixture.home, 'SOUL.local.md'), 'Tone: dry and direct.\n'); writeFileSync(join(fixture.home, 'STANDARDS.local.md'), 'Require 90% coverage on auth code.\n'); const out = composeContract('claude', fixture.home); expect(out).toContain('# Operator Overlays'); expect(out).toContain('## Persona Overlay (SOUL.local.md)'); expect(out).toContain('Tone: dry and direct.'); expect(out).toContain('## Standards Overlay (STANDARDS.local.md)'); expect(out).toContain('Require 90% coverage on auth code.'); }); it('ignores whitespace-only *.local overlays (no empty overlay section)', () => { writeFileSync(join(fixture.home, 'SOUL.local.md'), ' \n\n'); const out = composeContract('claude', fixture.home); expect(out).not.toContain('# Operator Overlays'); }); it('selects a different RUNTIME.md per harness', () => { expect(composeContract('codex', fixture.home)).toContain('# codex runtime contract'); expect(composeContract('pi', fixture.home)).toContain('# pi runtime contract'); expect(composeContract('codex', fixture.home)).not.toContain('# pi runtime contract'); }); it('injects the configured operator-interaction tool policy into the runtime contract', () => { const previous = process.env['MOSAIC_AGENT_TOOL_POLICY']; try { process.env['MOSAIC_AGENT_TOOL_POLICY'] = 'operator-interaction'; const out = composeContract('pi', fixture.home); expect(out).toContain('# Fleet Tool Policy (operator-interaction)'); expect(out).toContain('Denied by default: coding/general orchestration claims'); } finally { if (previous === undefined) delete process.env['MOSAIC_AGENT_TOOL_POLICY']; else process.env['MOSAIC_AGENT_TOOL_POLICY'] = previous; } }); // ── Persona contract injection (A3b) ────────────────────────────────────── // composeContract reads MOSAIC_AGENT_CLASS and injects the resolved persona // (override-aware). Save/restore the env so these tests don't leak state. describe('persona contract (A3b)', () => { let prevClass: string | undefined; beforeEach(() => { prevClass = process.env['MOSAIC_AGENT_CLASS']; }); afterEach(() => { if (prevClass === undefined) delete process.env['MOSAIC_AGENT_CLASS']; else process.env['MOSAIC_AGENT_CLASS'] = prevClass; }); const seedBaseline = (klass: string, body: string): void => { mkdirSync(join(fixture.home, 'fleet', 'roles'), { recursive: true }); writeFileSync(join(fixture.home, 'fleet', 'roles', `${klass}.md`), body); }; const seedOverride = (klass: string, body: string): void => { mkdirSync(join(fixture.home, 'fleet', 'roles.local'), { recursive: true }); writeFileSync(join(fixture.home, 'fleet', 'roles.local', `${klass}.md`), body); }; it('injects the baseline persona when MOSAIC_AGENT_CLASS is set and a role file exists', () => { seedBaseline('coder', '# Coder\n\n(`class: coder`)\n\nBASELINE-MANDATE: ship the lane.\n'); process.env['MOSAIC_AGENT_CLASS'] = 'coder'; const out = composeContract('claude', fixture.home); expect(out).toContain('# Persona Contract (coder)'); expect(out).toContain('BASELINE-MANDATE'); }); it('OVERRIDE WINS at launch: roles.local persona is injected over baseline (AC-NS-7)', () => { seedBaseline('coder', '# Coder\n\n(`class: coder`)\n\nBASELINE-MANDATE.\n'); seedOverride('coder', '# Coder (override)\n\n(`class: coder`)\n\nOVERRIDE-MANDATE.\n'); process.env['MOSAIC_AGENT_CLASS'] = 'coder'; const out = composeContract('claude', fixture.home); expect(out).toContain('# Persona Contract (coder)'); expect(out).toContain('OVERRIDE-MANDATE'); expect(out).not.toContain('BASELINE-MANDATE'); }); it('does NOT inject a persona when MOSAIC_AGENT_CLASS is unset', () => { seedBaseline('coder', '# Coder\n\n(`class: coder`)\n\nBASELINE-MANDATE.\n'); delete process.env['MOSAIC_AGENT_CLASS']; const out = composeContract('claude', fixture.home); expect(out).not.toContain('# Persona Contract'); }); it('does NOT inject (no throw) when MOSAIC_AGENT_CLASS names an unknown class', () => { seedBaseline('coder', '# Coder\n\n(`class: coder`)\n\nBASELINE-MANDATE.\n'); process.env['MOSAIC_AGENT_CLASS'] = 'nonexistent'; expect(() => composeContract('claude', fixture.home)).not.toThrow(); expect(composeContract('claude', fixture.home)).not.toContain('# Persona Contract'); }); it('places the persona contract BEFORE the fleet comms block (identity, then peers)', () => { seedBaseline('enhancer', '# Enhancer\n\n(`class: enhancer`)\n\nIMPROVE.\n'); mkdirSync(join(fixture.home, 'fleet'), { recursive: true }); writeFileSync( join(fixture.home, 'fleet', 'roster.yaml'), [ 'version: 1', 'transport: tmux', 'agents:', ' - name: orchestrator', ' runtime: claude', ' class: orchestrator', ' - name: enhancer', ' runtime: claude', ' class: enhancer', '', ].join('\n'), ); const prevName = process.env['MOSAIC_AGENT_NAME']; try { process.env['MOSAIC_AGENT_CLASS'] = 'enhancer'; process.env['MOSAIC_AGENT_NAME'] = 'enhancer'; const out = composeContract('claude', fixture.home); expect(out).toContain('# Persona Contract (enhancer)'); expect(out).toContain('# Fleet Comms'); expect(out.indexOf('# Persona Contract')).toBeLessThan(out.indexOf('# Fleet Comms')); } finally { if (prevName === undefined) delete process.env['MOSAIC_AGENT_NAME']; else process.env['MOSAIC_AGENT_NAME'] = prevName; } }); }); });