ktg-plugin-marketplace/plugins/ultraplan-local/scripts/profile-loader.test.mjs
Kjell Tore Guttormsen 0b28f008ae feat(ultraplan-local): M0 — profile foundation, no behaviour change
Introduces a profile-loader infrastructure for runtime-instantiable
ultraplan variants (depth × domain × goal axes). M0 ships only the
`default` profile, which mirrors the current hardcoded Phase 5/9 agent
set — so existing flows are unaffected.

What lands:
- profiles/default.yaml — schema v1, lists current 8 exploration agents
  + 2 review agents, captures today's adversarial regime
- scripts/profile-loader.mjs — null-deps Node loader with limited-subset
  YAML parser, listProfiles(), loadProfile(), validateProfile() that
  cross-checks every referenced agent exists in agents/
- scripts/profile-loader.test.mjs — 26 node:test cases (parser, validation,
  loader, integration with built-in default.yaml)
- commands/ultraplan-local.md — Phase 1 gains a "Resolve the profile"
  step (--profile flag → brief.recommended_profile → default fallback)
  and prints profile + source in the mode report. Phase 5/9 unchanged.
- README.md, CLAUDE.md, marketplace README — documentation of the M0
  foundation, the universal-brief design principle, and the M1/M2/M3
  milestones to come.

M1 (next) wires profile recommendation into ultrabrief Phase 4. M2
ships the additional built-in profiles (quick, bugfix, feature, refactor,
security-deep, research-heavy) and replaces the hardcoded Phase 5 agent
table with profile-driven selection. M3 adds user-extensible profiles.

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

379 lines
12 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,
} 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(', ')}`);
});