Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
168 lines
7.2 KiB
TypeScript
168 lines
7.2 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
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';
|
|
|
|
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`);
|
|
}
|
|
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<typeof makeHome>;
|
|
let prevCwd: string;
|
|
let cwdDir: string;
|
|
|
|
beforeEach(() => {
|
|
fixture = makeHome();
|
|
prevCwd = process.cwd();
|
|
cwdDir = mkdtempSync(join(tmpdir(), 'mosaic-cwd-'));
|
|
process.chdir(cwdDir); // neutralize cwd-relative mission/PRD blocks
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.chdir(prevCwd);
|
|
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',
|
|
'agents:',
|
|
' - name: orchestrator',
|
|
' runtime: claude',
|
|
' class: orchestrator',
|
|
' - name: enhancer',
|
|
' runtime: claude',
|
|
' class: enhancer',
|
|
' - 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 out = composeContract('claude', fixture.home);
|
|
expect(out).toContain('# Fleet Comms');
|
|
expect(out).toMatch(/`\[[^\]]+:enhancer\]`/); // own [host:session] identity (host machine-dependent)
|
|
// local peer → no -H; cross-host peer → -H ssh
|
|
expect(out).toContain('-s orchestrator -m "…"');
|
|
expect(out).toContain('-H jwoltje@10.1.10.37 -s coder0-0 -m "…"');
|
|
expect(out).not.toContain('-H jwoltje@10.1.10.37 -s orchestrator'); // local stays local
|
|
} finally {
|
|
if (prev === undefined) delete process.env['MOSAIC_AGENT_NAME'];
|
|
else process.env['MOSAIC_AGENT_NAME'] = prev;
|
|
}
|
|
});
|
|
|
|
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('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');
|
|
});
|
|
});
|