107 lines
3.9 KiB
TypeScript
107 lines
3.9 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { readPersonaContractBlock } from './persona-contract.js';
|
|
|
|
/**
|
|
* Persona-contract launch injection (A3b). Asserts the override-aware resolver
|
|
* is wired so a customized persona in roles.local/ wins at launch (AC-NS-7), and
|
|
* that any miss (unset/empty/unknown class, missing file) no-ops silently —
|
|
* never throws — mirroring readFleetCommsBlock's tolerant contract.
|
|
*/
|
|
|
|
const BASELINE_CODER = `# Coder — fleet role definition
|
|
|
|
The **coder** persona (\`class: coder\`, \`domain: engineering\`).
|
|
|
|
## Mandate
|
|
|
|
BASELINE-MANDATE: implement the assigned lane.
|
|
`;
|
|
|
|
const OVERRIDE_CODER = `# Coder — fleet role definition (override)
|
|
|
|
The **coder** persona (\`class: coder\`).
|
|
|
|
## Mandate
|
|
|
|
OVERRIDE-MANDATE: implement the assigned lane, the user's way.
|
|
`;
|
|
|
|
function makeHome(): string {
|
|
const root = mkdtempSync(join(tmpdir(), 'mosaic-persona-'));
|
|
return join(root, 'mosaic-home');
|
|
}
|
|
|
|
function seedBaseline(home: string, klass: string, body: string): void {
|
|
const dir = join(home, 'fleet', 'roles');
|
|
mkdirSync(dir, { recursive: true });
|
|
writeFileSync(join(dir, `${klass}.md`), body);
|
|
}
|
|
|
|
function seedOverride(home: string, klass: string, body: string): void {
|
|
const dir = join(home, 'fleet', 'roles.local');
|
|
mkdirSync(dir, { recursive: true });
|
|
writeFileSync(join(dir, `${klass}.md`), body);
|
|
}
|
|
|
|
describe('readPersonaContractBlock — launch-time persona injection (A3b)', () => {
|
|
let home: string;
|
|
|
|
beforeEach(() => {
|
|
home = makeHome();
|
|
});
|
|
|
|
afterEach(() => {
|
|
// root is the parent of mosaic-home
|
|
rmSync(join(home, '..'), { recursive: true, force: true });
|
|
});
|
|
|
|
it('injects the baseline persona when the class has a fleet/roles/<class>.md', () => {
|
|
seedBaseline(home, 'coder', BASELINE_CODER);
|
|
const block = readPersonaContractBlock(home, 'coder');
|
|
expect(block).toContain('# Persona Contract (coder)');
|
|
expect(block).toContain('BASELINE-MANDATE');
|
|
expect(block).toContain('baseline `fleet/roles/` layer');
|
|
});
|
|
|
|
it('OVERRIDE WINS: roles.local/<class>.md content is injected over the baseline (AC-NS-7)', () => {
|
|
seedBaseline(home, 'coder', BASELINE_CODER);
|
|
seedOverride(home, 'coder', OVERRIDE_CODER);
|
|
const block = readPersonaContractBlock(home, 'coder');
|
|
expect(block).toContain('# Persona Contract (coder)');
|
|
expect(block).toContain('OVERRIDE-MANDATE'); // override body present
|
|
expect(block).not.toContain('BASELINE-MANDATE'); // baseline NOT used
|
|
expect(block).toContain('roles.local'); // layer note names the override layer
|
|
});
|
|
|
|
it('injects an override-only (user-added) persona with no baseline at all', () => {
|
|
seedOverride(home, 'reviewer', '# Reviewer\n\n(`class: reviewer`)\n\nCUSTOM-ROLE.\n');
|
|
const block = readPersonaContractBlock(home, 'reviewer');
|
|
expect(block).toContain('# Persona Contract (reviewer)');
|
|
expect(block).toContain('CUSTOM-ROLE');
|
|
});
|
|
|
|
it('no-ops (empty string) when the class is undefined', () => {
|
|
seedBaseline(home, 'coder', BASELINE_CODER);
|
|
expect(readPersonaContractBlock(home, undefined)).toBe('');
|
|
});
|
|
|
|
it('no-ops (empty string) when the class is empty/whitespace', () => {
|
|
seedBaseline(home, 'coder', BASELINE_CODER);
|
|
expect(readPersonaContractBlock(home, '')).toBe('');
|
|
expect(readPersonaContractBlock(home, ' ')).toBe('');
|
|
});
|
|
|
|
it('no-ops (empty string) for an unknown class with no role file', () => {
|
|
seedBaseline(home, 'coder', BASELINE_CODER);
|
|
expect(readPersonaContractBlock(home, 'nonexistent')).toBe('');
|
|
});
|
|
|
|
it('no-ops (empty string, no throw) when no roles directories exist at all', () => {
|
|
expect(() => readPersonaContractBlock(home, 'coder')).not.toThrow();
|
|
expect(readPersonaContractBlock(home, 'coder')).toBe('');
|
|
});
|
|
});
|