ktg-plugin-marketplace/plugins/ultraplan-local/scripts/profile-loader.test.mjs
Kjell Tore Guttormsen 7e2d9e151e feat(ultraplan-local): M1 — profile recommendation flow in ultrabrief
Adds the profile recommendation step to /ultrabrief-local Phase 4. The
brief stays universal (same questions, same template); the new step is
purely a processing-decision layer that records which profile downstream
commands should apply.

What lands:
- agents/profile-recommender.md — new sonnet agent that scores available
  profiles against the finalized brief (keyword + NFR-signal matching,
  axis bumps, hallucination gate that forbids inventing profile names).
  Emits a fenced JSON block with ranked entries.
- templates/ultrabrief-template.md — frontmatter gains
  recommended_profile, profile_match, profile_rationale (default values
  applied when only `default` is available — true at M1).
- commands/ultrabrief-local.md — Phase 4 gains Step 4h with explicit
  branches: short-circuit when only `default` exists; AskUserQuestion
  confirmation when top score ≥ 0.7; explicit fallback message when below
  threshold; manual selection sub-question on user override. Persists the
  three frontmatter fields to brief.md after user confirmation. JSON
  parser failure falls back to `default` with `profile_match: fallback`
  rather than blocking — silent fallback is the worst outcome, but a
  *visible* fallback is acceptable.
- scripts/profile-loader.mjs — adds selectRecommendation(ranked, opts) +
  RECOMMENDATION_THRESHOLD=0.7 export. Single source of truth for the
  threshold logic so the command spec and the helper agree.
- scripts/profile-loader.test.mjs — 10 new tests for selectRecommendation
  (default-only, empty/malformed input, above/below threshold, custom
  threshold, max-by-score, missing fields). Total now 36/36.
- README.md / CLAUDE.md / marketplace landing — docs reflect M0 + M1
  shipped, M2 + M3 still pending.

In practice nothing changes for users at M1 because only `default` is
available — Step 4h takes the short-circuit path and writes
`profile_match: default-only`. M2 ships the additional profiles that
make the recommender meaningful.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 14:21:54 +02:00

477 lines
16 KiB
JavaScript

// 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);
});