// node:test suite for scripts/profile-loader.mjs // // Run: node --test scripts/profile-loader.test.mjs // // Covers: YAML parser subset, profile validation, agent cross-checks, // listProfiles, loadProfile happy path, error paths. import { test } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtemp, writeFile, mkdir, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { parseYaml, parseScalar, parseInlineList, stripLineComment, validateProfile, loadProfile, listProfiles, selectRecommendation, RECOMMENDATION_THRESHOLD, } from './profile-loader.mjs'; // ===================================================================== // YAML parser tests // ===================================================================== test('parseScalar: numbers, bools, null, strings', () => { assert.equal(parseScalar('42'), 42); assert.equal(parseScalar('-7'), -7); assert.equal(parseScalar('3.14'), 3.14); assert.equal(parseScalar('true'), true); assert.equal(parseScalar('false'), false); assert.equal(parseScalar('null'), null); assert.equal(parseScalar('~'), null); assert.equal(parseScalar('hello'), 'hello'); assert.equal(parseScalar('"quoted with spaces"'), 'quoted with spaces'); assert.equal(parseScalar("'single quoted'"), 'single quoted'); }); test('parseScalar: empty string returns null', () => { assert.equal(parseScalar(''), null); assert.equal(parseScalar(' '), null); }); test('parseInlineList: basic', () => { assert.deepEqual(parseInlineList('[]'), []); assert.deepEqual(parseInlineList('[a, b, c]'), ['a', 'b', 'c']); assert.deepEqual(parseInlineList('[1, 2, 3]'), [1, 2, 3]); assert.deepEqual(parseInlineList('["x y", "z"]'), ['x y', 'z']); }); test('parseInlineList: commas inside quoted strings are preserved', () => { assert.deepEqual(parseInlineList('["a, b", "c"]'), ['a, b', 'c']); }); test('stripLineComment: line-leading hash', () => { assert.equal(stripLineComment('# comment'), ''); assert.equal(stripLineComment(' # indented'), ''); }); test('stripLineComment: trailing comment after value', () => { assert.equal(stripLineComment('key: value # explanation'), 'key: value'); assert.equal(stripLineComment('key: 42 # number'), 'key: 42'); }); test('stripLineComment: hash inside quoted string is preserved', () => { assert.equal(stripLineComment('key: "value # not a comment"'), 'key: "value # not a comment"'); }); test('parseYaml: simple flat mapping', () => { const result = parseYaml('name: foo\nversion: 1\nactive: true'); assert.deepEqual(result, { name: 'foo', version: 1, active: true }); }); test('parseYaml: nested mapping', () => { const text = ` axes: depth: deep domain: security `; const result = parseYaml(text); assert.deepEqual(result, { axes: { depth: 'deep', domain: 'security' } }); }); test('parseYaml: block-style list', () => { const text = ` agents: - architecture-mapper - risk-assessor - task-finder `; const result = parseYaml(text); assert.deepEqual(result, { agents: ['architecture-mapper', 'risk-assessor', 'task-finder'] }); }); test('parseYaml: inline list as scalar value', () => { const text = 'keywords: [a, b, c]'; const result = parseYaml(text); assert.deepEqual(result, { keywords: ['a', 'b', 'c'] }); }); test('parseYaml: full profile-shaped structure', () => { const text = ` name: test description: "A test profile" version: 1 axes: depth: standard domain: general triggers: keywords: ["foo", "bar"] nfr_signals: [] agents: exploration: - architecture-mapper - task-finder review: - plan-critic adversarial: depth: deep iterations: 2 blockers_only: false `; const result = parseYaml(text); assert.equal(result.name, 'test'); assert.equal(result.description, 'A test profile'); assert.equal(result.version, 1); assert.deepEqual(result.axes, { depth: 'standard', domain: 'general' }); assert.deepEqual(result.triggers.keywords, ['foo', 'bar']); assert.deepEqual(result.triggers.nfr_signals, []); assert.deepEqual(result.agents.exploration, ['architecture-mapper', 'task-finder']); assert.deepEqual(result.agents.review, ['plan-critic']); assert.equal(result.adversarial.depth, 'deep'); assert.equal(result.adversarial.iterations, 2); assert.equal(result.adversarial.blockers_only, false); }); test('parseYaml: ignores blank lines and comments', () => { const text = ` # Header comment name: foo # Mid comment version: 1 `; const result = parseYaml(text); assert.deepEqual(result, { name: 'foo', version: 1 }); }); test('parseYaml: throws on missing colon', () => { assert.throws(() => parseYaml('name foo'), /Expected 'key: value'/); }); // ===================================================================== // Helpers: build a minimal plugin tree under tmpdir // ===================================================================== async function makeTempPluginTree(profiles, agentNames) { const root = await mkdtemp(join(tmpdir(), 'profile-loader-test-')); const profilesDir = join(root, 'profiles'); const agentsDir = join(root, 'agents'); await mkdir(profilesDir, { recursive: true }); await mkdir(agentsDir, { recursive: true }); for (const [name, content] of Object.entries(profiles)) { await writeFile(join(profilesDir, `${name}.yaml`), content, 'utf8'); } for (const a of agentNames) { await writeFile(join(agentsDir, `${a}.md`), '# stub', 'utf8'); } return { root, profilesDir, agentsDir }; } // ===================================================================== // validateProfile tests // ===================================================================== test('validateProfile: passes on minimal valid profile', async () => { const { profilesDir, agentsDir, root } = await makeTempPluginTree( {}, ['agent-a', 'agent-b', 'agent-c'] ); try { const profile = { name: 'minimal', description: 'Minimal valid profile', version: 1, agents: { exploration: ['agent-a', 'agent-b'], review: ['agent-c'], }, }; await validateProfile(profile, { agentsDir }); } finally { await rm(root, { recursive: true, force: true }); } }); test('validateProfile: rejects missing required fields', async () => { const { agentsDir, root } = await makeTempPluginTree({}, []); try { await assert.rejects( validateProfile({ name: 'x' }, { agentsDir }), /Missing required field: description/ ); } finally { await rm(root, { recursive: true, force: true }); } }); test('validateProfile: rejects unknown agent name', async () => { const { agentsDir, root } = await makeTempPluginTree({}, ['real-agent']); try { const profile = { name: 'bad', description: 'has ghost agent', version: 1, agents: { exploration: ['real-agent', 'ghost-agent'], review: ['real-agent'], }, }; await assert.rejects( validateProfile(profile, { agentsDir }), /Unknown agent referenced: ghost-agent/ ); } finally { await rm(root, { recursive: true, force: true }); } }); test('validateProfile: rejects unsupported version', async () => { const { agentsDir, root } = await makeTempPluginTree({}, ['a']); try { const profile = { name: 'x', description: 'old', version: 99, agents: { exploration: ['a'], review: ['a'] }, }; await assert.rejects( validateProfile(profile, { agentsDir }), /Unsupported profile version 99/ ); } finally { await rm(root, { recursive: true, force: true }); } }); test('validateProfile: rejects adversarial.depth not in enum', async () => { const { agentsDir, root } = await makeTempPluginTree({}, ['a']); try { const profile = { name: 'x', description: 'bad depth', version: 1, agents: { exploration: ['a'], review: ['a'] }, adversarial: { depth: 'extreme', iterations: 1, blockers_only: false }, }; await assert.rejects( validateProfile(profile, { agentsDir }), /adversarial\.depth must be one of/ ); } finally { await rm(root, { recursive: true, force: true }); } }); test('validateProfile: rejects when agents key is array not mapping', async () => { const { agentsDir, root } = await makeTempPluginTree({}, []); try { const profile = { name: 'x', description: 'wrong shape', version: 1, agents: ['a', 'b'], }; await assert.rejects( validateProfile(profile, { agentsDir }), /agents must be a mapping/ ); } finally { await rm(root, { recursive: true, force: true }); } }); // ===================================================================== // listProfiles + loadProfile // ===================================================================== test('listProfiles: returns sorted basenames without extension', async () => { const { profilesDir, root } = await makeTempPluginTree( { 'zebra': 'name: z\ndescription: z\nversion: 1\nagents:\n exploration: []\n review: []', 'alpha': 'name: a\ndescription: a\nversion: 1\nagents:\n exploration: []\n review: []', 'middle': 'name: m\ndescription: m\nversion: 1\nagents:\n exploration: []\n review: []', }, [] ); try { const names = await listProfiles({ profilesDir }); assert.deepEqual(names, ['alpha', 'middle', 'zebra']); } finally { await rm(root, { recursive: true, force: true }); } }); test('listProfiles: empty when directory missing', async () => { const names = await listProfiles({ profilesDir: '/tmp/does-not-exist-xyz' }); assert.deepEqual(names, []); }); test('loadProfile: parses + validates an actual file', async () => { const yaml = ` name: t description: "Test" version: 1 axes: depth: standard domain: general agents: exploration: - agent-a review: - agent-b `; const { profilesDir, agentsDir, root } = await makeTempPluginTree( { 't': yaml }, ['agent-a', 'agent-b'] ); try { const p = await loadProfile('t', { profilesDir, agentsDir }); assert.equal(p.name, 't'); assert.deepEqual(p.agents.exploration, ['agent-a']); assert.deepEqual(p.agents.review, ['agent-b']); } finally { await rm(root, { recursive: true, force: true }); } }); test('loadProfile: throws helpful message when name missing', async () => { const { profilesDir, agentsDir, root } = await makeTempPluginTree( { 'real': 'name: r\ndescription: r\nversion: 1\nagents:\n exploration: []\n review: []', }, [] ); try { await assert.rejects( loadProfile('not-real', { profilesDir, agentsDir }), /Profile 'not-real' not found.*Available: real/ ); } finally { await rm(root, { recursive: true, force: true }); } }); // ===================================================================== // Integration: built-in default.yaml // ===================================================================== test('built-in default.yaml: parses and validates', async () => { // No path overrides — use the actual plugin profiles/ + agents/ dirs. const profile = await loadProfile('default'); assert.equal(profile.name, 'default'); assert.equal(profile.version, 1); assert.ok(Array.isArray(profile.agents.exploration)); assert.ok(Array.isArray(profile.agents.review)); // Specific agents should be the current Phase 5/9 set. assert.ok(profile.agents.exploration.includes('architecture-mapper')); assert.ok(profile.agents.exploration.includes('task-finder')); assert.ok(profile.agents.review.includes('plan-critic')); assert.ok(profile.agents.review.includes('scope-guardian')); }); test('listProfiles: includes default', async () => { const names = await listProfiles(); assert.ok(names.includes('default'), `Expected default in ${names.join(', ')}`); }); // ===================================================================== // selectRecommendation tests (M1) // ===================================================================== test('selectRecommendation: only-default short-circuit', () => { const result = selectRecommendation([], { availableProfiles: ['default'] }); assert.equal(result.profile, 'default'); assert.equal(result.match, 'default-only'); assert.equal(result.source, 'default-only'); }); test('selectRecommendation: empty ranked input falls back', () => { const result = selectRecommendation([]); assert.equal(result.profile, 'default'); assert.equal(result.match, 'fallback'); assert.equal(result.source, 'fallback'); assert.match(result.rationale, /no ranked profiles/); }); test('selectRecommendation: malformed ranked input falls back', () => { const result = selectRecommendation([null, { not_a_profile: true }]); assert.equal(result.profile, 'default'); assert.equal(result.source, 'fallback'); }); test('selectRecommendation: top score above threshold returns recommendation', () => { const ranked = [ { name: 'security-deep', score: 0.91, match_quality: 'exact', rationale: 'OWASP + JWT in Intent.' }, { name: 'default', score: 0.30, match_quality: 'fallback', rationale: 'No triggers.' }, ]; const result = selectRecommendation(ranked); assert.equal(result.profile, 'security-deep'); assert.equal(result.match, 'exact'); assert.equal(result.source, 'recommended'); assert.equal(result.rationale, 'OWASP + JWT in Intent.'); }); test('selectRecommendation: top score below threshold falls back to default', () => { const ranked = [ { name: 'feature', score: 0.55, match_quality: 'partial', rationale: 'Some keyword hits.' }, { name: 'default', score: 0.30, match_quality: 'fallback', rationale: 'Baseline.' }, ]; const result = selectRecommendation(ranked); assert.equal(result.profile, 'default'); assert.equal(result.match, 'fallback'); assert.equal(result.source, 'fallback'); // Rationale should reference both the score and the top entry's rationale assert.match(result.rationale, /0\.55/); assert.match(result.rationale, /Some keyword hits/); }); test('selectRecommendation: respects custom threshold', () => { const ranked = [ { name: 'feature', score: 0.55, match_quality: 'partial', rationale: 'Match.' }, ]; // With low threshold the same entry IS the recommendation const result = selectRecommendation(ranked, { threshold: 0.5 }); assert.equal(result.profile, 'feature'); assert.equal(result.source, 'recommended'); }); test('selectRecommendation: highest-score wins regardless of input order', () => { const ranked = [ { name: 'a', score: 0.40, match_quality: 'partial', rationale: 'Low.' }, { name: 'b', score: 0.95, match_quality: 'exact', rationale: 'High.' }, { name: 'c', score: 0.72, match_quality: 'partial', rationale: 'Mid.' }, ]; const result = selectRecommendation(ranked); assert.equal(result.profile, 'b'); assert.equal(result.source, 'recommended'); }); test('selectRecommendation: missing score treated as 0', () => { const ranked = [ { name: 'a', match_quality: 'fallback', rationale: 'No score.' }, ]; const result = selectRecommendation(ranked); // Top entry has effective score 0 → falls back assert.equal(result.profile, 'default'); assert.equal(result.source, 'fallback'); }); test('selectRecommendation: missing rationale gets a synthetic one', () => { const ranked = [ { name: 'security-deep', score: 0.85, match_quality: 'exact' }, ]; const result = selectRecommendation(ranked); assert.equal(result.profile, 'security-deep'); assert.match(result.rationale, /Top-ranked/); }); test('RECOMMENDATION_THRESHOLD: matches plan default', () => { // Sanity check that the export agrees with the documented threshold. assert.equal(RECOMMENDATION_THRESHOLD, 0.7); });