import { readFile } from 'node:fs/promises'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it, vi } from 'vitest'; import { parseNorthStar, renderNorthStarMarkdown, resolveNorthStarPaths, type NorthStar, } from './fleet.js'; // Repo root resolved from this spec file: packages/mosaic/src/commands → up 4. const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..'); const yamlPath = join(repoRoot, 'docs', 'fleet', 'NORTH_STAR.yaml'); async function loadYamlText(): Promise { return readFile(yamlPath, 'utf8'); } async function loadParsed(): Promise { return parseNorthStar(await loadYamlText()); } describe('NORTH_STAR.yaml', () => { it('parses to a typed object with the required top-level keys', async () => { const ns = await loadParsed(); expect(ns.version).toBeTypeOf('number'); expect(ns.mission).toContain('self-driving Mosaic delivery fleet'); expect(ns.substrate.note).toBeTruthy(); expect(ns.standing_objectives.length).toBeGreaterThan(0); expect(ns.success_criteria.length).toBeGreaterThan(0); expect(ns.workstreams.length).toBeGreaterThan(0); expect(ns.goals.length).toBeGreaterThan(0); expect(ns.assumptions.length).toBeGreaterThan(0); expect(ns.spend.advisory).toBe(true); }); it('names the native Postgres storage layer and declares no Hermes runtime dependency', async () => { const rawText = await loadYamlText(); const lower = rawText.toLowerCase(); expect(rawText).toContain('@mosaicstack/db'); expect(lower).toContain('postgres'); expect(lower).toContain('pglite'); // The doctrine explicitly disowns Hermes ("NOT Hermes"); the only mentions // are negations. Assert there is no Hermes RUNTIME dependency: no hermes // CLI/kanban invocation and no ~/.hermes storage reference. expect(lower).not.toContain('hermes kanban'); expect(lower).not.toContain('~/.hermes'); expect(lower).not.toContain('hermes mcp'); }); it('declares all NS-1..NS-8 standing objectives', async () => { const ns = await loadParsed(); const ids = ns.standing_objectives.map((o) => o.id); for (let n = 1; n <= 8; n += 1) { expect(ids).toContain(`NS-${n}`); } }); it('declares all AC-NS-1..AC-NS-5 success criteria', async () => { const ns = await loadParsed(); const ids = ns.success_criteria.map((c) => c.id); for (let n = 1; n <= 5; n += 1) { expect(ids).toContain(`AC-NS-${n}`); } }); it('seeds the expected backlog goal ids', async () => { const ns = await loadParsed(); const ids = ns.goals.map((g) => g.id); expect(ids).toEqual( expect.arrayContaining(['A1', 'A2', 'A3a', 'A3b', 'A4', 'B1', 'B2', 'B3a', 'B3b', 'G1']), ); }); it('has a coherent depends_on DAG (every dependency references a known goal)', async () => { const ns = await loadParsed(); const ids = new Set(ns.goals.map((g) => g.id)); for (const goal of ns.goals) { for (const dep of goal.depends_on) { expect(ids.has(dep)).toBe(true); } // No goal may depend on itself. expect(goal.depends_on).not.toContain(goal.id); } // A1 is the root: no dependencies. const a1 = ns.goals.find((g) => g.id === 'A1'); expect(a1?.depends_on).toEqual([]); }); it('marks spend as advisory with a degrade-to-TTL note', async () => { const ns = await loadParsed(); expect(ns.spend.advisory).toBe(true); expect(ns.spend.note.toLowerCase()).toContain('ttl'); }); }); describe('renderNorthStarMarkdown', () => { it('is a pure deterministic projection (round-trip stable)', async () => { const ns = await loadParsed(); const first = renderNorthStarMarkdown(ns); const second = renderNorthStarMarkdown(ns); expect(first).toBe(second); // Re-parsing the same YAML and re-rendering yields identical bytes. const reparsed = parseNorthStar(await loadYamlText()); expect(renderNorthStarMarkdown(reparsed)).toBe(first); }); it('matches the committed NORTH_STAR.md projection (regenerate if this fails)', async () => { const ns = await loadParsed(); const rendered = `${renderNorthStarMarkdown(ns)}\n`; const committed = await readFile(join(repoRoot, 'docs', 'fleet', 'NORTH_STAR.md'), 'utf8'); expect(rendered).toBe(committed); }); it('projects mission, objectives, criteria, goals, assumptions, and spend', async () => { const ns = await loadParsed(); const md = renderNorthStarMarkdown(ns); expect(md).toContain('# Mosaic Fleet — NORTH STAR'); expect(md).toContain('## Mission'); expect(md).toContain('## Standing objectives'); expect(md).toContain('**NS-1**'); expect(md).toContain('**AC-NS-5**'); expect(md).toContain('## Goals (backlog projection)'); // Tables are column-padded (prettier-style); match the row id, not exact spacing. expect(md).toMatch(/\| A1\s+\|/); expect(md).toContain('## Assumptions (vetoable)'); expect(md).toContain('**advisory:** true'); // The banner disowns Hermes; the projection carries no Hermes runtime hook. expect(md.toLowerCase()).not.toContain('hermes kanban'); expect(md.toLowerCase()).not.toContain('~/.hermes'); }); it('does no network or CLI work (pure functions; only the writer touches IO)', () => { // parseNorthStar + renderNorthStarMarkdown take strings and return strings. // Guard against accidental IO by asserting fetch/spawn are never invoked. const fetchSpy = vi.spyOn(globalThis, 'fetch' as never).mockImplementation((() => { throw new Error('network access is forbidden in the NORTH_STAR generator'); }) as never); try { const yaml = [ 'version: 1', 'mission: m', 'substrate:', ' note: n', 'standing_objectives:', ' - { id: NS-1, text: t }', 'success_criteria:', ' - { id: AC-NS-1, text: t }', 'workstreams:', ' - { id: A, title: t }', 'goals:', ' - { id: A1, title: t, phase: 1, priority: must-have, depends_on: [] }', 'assumptions:', ' - { id: ASM-1, vetoable: true, text: t }', 'spend:', ' advisory: true', ' note: TTL', '', ].join('\n'); const ns = parseNorthStar(yaml); const md = renderNorthStarMarkdown(ns); expect(md).toContain('# Mosaic Fleet — NORTH STAR'); expect(fetchSpy).not.toHaveBeenCalled(); } finally { fetchSpy.mockRestore(); } }); }); describe('parseNorthStar validation', () => { it('throws on a missing required key', () => { expect(() => parseNorthStar('version: 1\n')).toThrow(); }); it('throws when spend.advisory is not a boolean', () => { const yaml = [ 'version: 1', 'mission: m', 'substrate: { note: n }', 'standing_objectives: [{ id: NS-1, text: t }]', 'success_criteria: [{ id: AC-NS-1, text: t }]', 'workstreams: [{ id: A, title: t }]', 'goals: [{ id: A1, title: t, phase: 1, priority: must-have, depends_on: [] }]', 'assumptions: [{ id: ASM-1, vetoable: true, text: t }]', 'spend: { advisory: maybe, note: TTL }', '', ].join('\n'); expect(() => parseNorthStar(yaml)).toThrow(/spend\.advisory/); }); }); describe('resolveNorthStarPaths', () => { it('resolves YAML + Markdown under docs/fleet from a given repo root', () => { const paths = resolveNorthStarPaths('/repo'); expect(paths.yamlPath).toBe('/repo/docs/fleet/NORTH_STAR.yaml'); expect(paths.markdownPath).toBe('/repo/docs/fleet/NORTH_STAR.md'); }); });