import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { discoverProject, checkPhaseRequirements, } from '../../lib/parsers/project-discovery.mjs'; function setupProject(structure) { const root = mkdtempSync(join(tmpdir(), 'ultraplan-disc-')); for (const [path, content] of Object.entries(structure)) { const full = join(root, path); mkdirSync(join(full, '..'), { recursive: true }); writeFileSync(full, content); } return root; } test('discoverProject — finds brief, plan, progress at root', () => { const root = setupProject({ 'brief.md': 'b', 'plan.md': 'p', 'progress.json': '{}', }); try { const a = discoverProject(root); assert.equal(a.brief, join(root, 'brief.md')); assert.equal(a.plan, join(root, 'plan.md')); assert.equal(a.progress, join(root, 'progress.json')); } finally { rmSync(root, { recursive: true, force: true }); } }); test('discoverProject — research files sorted by name', () => { const root = setupProject({ 'brief.md': 'b', 'research/03-third.md': 't', 'research/01-first.md': 'f', 'research/02-second.md': 's', }); try { const a = discoverProject(root); assert.equal(a.research.length, 3); assert.match(a.research[0], /01-first/); assert.match(a.research[1], /02-second/); assert.match(a.research[2], /03-third/); } finally { rmSync(root, { recursive: true, force: true }); } }); test('discoverProject — architecture overview + gaps detected', () => { const root = setupProject({ 'brief.md': 'b', 'architecture/overview.md': 'o', 'architecture/gaps.md': 'g', }); try { const a = discoverProject(root); assert.match(a.architecture.overview, /architecture\/overview\.md$/); assert.match(a.architecture.gaps, /architecture\/gaps\.md$/); assert.equal(a.architecture.looseFiles.length, 0); } finally { rmSync(root, { recursive: true, force: true }); } }); test('discoverProject — loose architecture files surfaced for drift detection', () => { const root = setupProject({ 'architecture/overview.md': 'o', 'architecture/random-note.md': 'x', }); try { const a = discoverProject(root); assert.equal(a.architecture.looseFiles.length, 1); assert.match(a.architecture.looseFiles[0], /random-note/); } finally { rmSync(root, { recursive: true, force: true }); } }); test('discoverProject — missing project dir returns empty artifacts', () => { const a = discoverProject('/nonexistent/path/unlikely'); assert.equal(a.brief, null); assert.equal(a.research.length, 0); }); test('checkPhaseRequirements — research needs brief', () => { const r = checkPhaseRequirements({ brief: null }, 'research'); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'PROJECT_NO_BRIEF')); }); test('checkPhaseRequirements — execute needs plan', () => { const r = checkPhaseRequirements({ brief: 'x', plan: null }, 'execute'); assert.equal(r.valid, false); assert.ok(r.errors.find(e => e.code === 'PROJECT_NO_PLAN')); }); test('checkPhaseRequirements — happy path', () => { const r = checkPhaseRequirements({ brief: 'x', plan: 'y' }, 'plan'); assert.equal(r.valid, true); }); test('discoverProject — finds review.md when present', () => { const root = setupProject({ 'brief.md': 'b', 'review.md': 'r', }); try { const a = discoverProject(root); assert.equal(a.review, join(root, 'review.md')); } finally { rmSync(root, { recursive: true, force: true }); } }); test('discoverProject — review null when absent', () => { const root = setupProject({ 'brief.md': 'b', }); try { const a = discoverProject(root); assert.equal(a.review, null); } finally { rmSync(root, { recursive: true, force: true }); } }); test('checkPhaseRequirements — review phase needs brief (error) and tolerates missing progress (warning)', () => { // Missing brief → error const r1 = checkPhaseRequirements({ brief: null, progress: null }, 'review'); assert.equal(r1.valid, false); assert.ok(r1.errors.find(e => e.code === 'PROJECT_NO_BRIEF')); // Has brief, no progress → valid (with warning) const r2 = checkPhaseRequirements({ brief: 'x', progress: null }, 'review'); assert.equal(r2.valid, true, JSON.stringify(r2)); assert.ok(r2.warnings.find(w => w.code === 'PROJECT_NO_PROGRESS')); // Has both → valid, no warning const r3 = checkPhaseRequirements({ brief: 'x', progress: 'p' }, 'review'); assert.equal(r3.valid, true); assert.equal(r3.warnings.length, 0); });