test: add coverage for model-profiles, template, profile-pipeline, profile-output (#1170)
New test files for 4 previously untested modules: model-profiles.test.cjs (15 tests): - MODEL_PROFILES data integrity (all agents, all profiles, valid aliases) - VALID_PROFILES list validation - getAgentToModelMapForProfile (balanced, budget, quality, agent count) - formatAgentToModelMapAsTable (header, separator, column alignment) template.test.cjs (11 tests): - template select: minimal/standard/complex heuristics, fallback - template fill: summary/plan/verification generation, --plan option - Error paths: existing file, unknown type, missing phase profile-pipeline.test.cjs (7 tests): - scan-sessions: empty dir, synthetic project, multi-session - extract-messages: user message extraction, meta/internal filtering - profile-questionnaire: structure validation profile-output.test.cjs (13 tests): - PROFILING_QUESTIONS data (fields, options, uniqueness) - CLAUDE_INSTRUCTIONS coverage (dimensions, instruction mapping) - write-profile: analysis JSON → USER-PROFILE.md - generate-claude-md: --auto generation, --force protection - generate-dev-preferences: analysis → preferences command Test count: 744 → 798 (+54 new tests, 0 failures)
This commit is contained in:
134
tests/model-profiles.test.cjs
Normal file
134
tests/model-profiles.test.cjs
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Model Profiles Tests
|
||||
*
|
||||
* Tests for MODEL_PROFILES data structure, VALID_PROFILES list,
|
||||
* formatAgentToModelMapAsTable, and getAgentToModelMapForProfile.
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
|
||||
const {
|
||||
MODEL_PROFILES,
|
||||
VALID_PROFILES,
|
||||
formatAgentToModelMapAsTable,
|
||||
getAgentToModelMapForProfile,
|
||||
} = require('../get-shit-done/bin/lib/model-profiles.cjs');
|
||||
|
||||
// ─── MODEL_PROFILES data integrity ────────────────────────────────────────────
|
||||
|
||||
describe('MODEL_PROFILES', () => {
|
||||
test('contains all expected GSD agents', () => {
|
||||
const expectedAgents = [
|
||||
'gsd-planner', 'gsd-roadmapper', 'gsd-executor',
|
||||
'gsd-phase-researcher', 'gsd-project-researcher', 'gsd-research-synthesizer',
|
||||
'gsd-debugger', 'gsd-codebase-mapper', 'gsd-verifier',
|
||||
'gsd-plan-checker', 'gsd-integration-checker', 'gsd-nyquist-auditor',
|
||||
'gsd-ui-researcher', 'gsd-ui-checker', 'gsd-ui-auditor',
|
||||
];
|
||||
for (const agent of expectedAgents) {
|
||||
assert.ok(MODEL_PROFILES[agent], `Missing agent: ${agent}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('every agent has quality, balanced, and budget profiles', () => {
|
||||
for (const [agent, profiles] of Object.entries(MODEL_PROFILES)) {
|
||||
assert.ok(profiles.quality, `${agent} missing quality profile`);
|
||||
assert.ok(profiles.balanced, `${agent} missing balanced profile`);
|
||||
assert.ok(profiles.budget, `${agent} missing budget profile`);
|
||||
}
|
||||
});
|
||||
|
||||
test('all profile values are valid model aliases', () => {
|
||||
const validModels = ['opus', 'sonnet', 'haiku'];
|
||||
for (const [agent, profiles] of Object.entries(MODEL_PROFILES)) {
|
||||
for (const [profile, model] of Object.entries(profiles)) {
|
||||
assert.ok(
|
||||
validModels.includes(model),
|
||||
`${agent}.${profile} has invalid model "${model}" — expected one of ${validModels.join(', ')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('quality profile never uses haiku', () => {
|
||||
for (const [agent, profiles] of Object.entries(MODEL_PROFILES)) {
|
||||
assert.notStrictEqual(
|
||||
profiles.quality, 'haiku',
|
||||
`${agent} quality profile should not use haiku`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── VALID_PROFILES ───────────────────────────────────────────────────────────
|
||||
|
||||
describe('VALID_PROFILES', () => {
|
||||
test('contains quality, balanced, and budget', () => {
|
||||
assert.deepStrictEqual(VALID_PROFILES.sort(), ['balanced', 'budget', 'quality']);
|
||||
});
|
||||
|
||||
test('is derived from MODEL_PROFILES keys', () => {
|
||||
const fromData = Object.keys(MODEL_PROFILES['gsd-planner']);
|
||||
assert.deepStrictEqual(VALID_PROFILES.sort(), fromData.sort());
|
||||
});
|
||||
});
|
||||
|
||||
// ─── getAgentToModelMapForProfile ─────────────────────────────────────────────
|
||||
|
||||
describe('getAgentToModelMapForProfile', () => {
|
||||
test('returns correct models for balanced profile', () => {
|
||||
const map = getAgentToModelMapForProfile('balanced');
|
||||
assert.strictEqual(map['gsd-planner'], 'opus');
|
||||
assert.strictEqual(map['gsd-codebase-mapper'], 'haiku');
|
||||
assert.strictEqual(map['gsd-verifier'], 'sonnet');
|
||||
});
|
||||
|
||||
test('returns correct models for budget profile', () => {
|
||||
const map = getAgentToModelMapForProfile('budget');
|
||||
assert.strictEqual(map['gsd-planner'], 'sonnet');
|
||||
assert.strictEqual(map['gsd-phase-researcher'], 'haiku');
|
||||
});
|
||||
|
||||
test('returns correct models for quality profile', () => {
|
||||
const map = getAgentToModelMapForProfile('quality');
|
||||
assert.strictEqual(map['gsd-planner'], 'opus');
|
||||
assert.strictEqual(map['gsd-executor'], 'opus');
|
||||
});
|
||||
|
||||
test('returns all agents in the map', () => {
|
||||
const map = getAgentToModelMapForProfile('balanced');
|
||||
const agentCount = Object.keys(MODEL_PROFILES).length;
|
||||
assert.strictEqual(Object.keys(map).length, agentCount);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── formatAgentToModelMapAsTable ─────────────────────────────────────────────
|
||||
|
||||
describe('formatAgentToModelMapAsTable', () => {
|
||||
test('produces a table with header and separator', () => {
|
||||
const map = { 'gsd-planner': 'opus', 'gsd-executor': 'sonnet' };
|
||||
const table = formatAgentToModelMapAsTable(map);
|
||||
assert.ok(table.includes('Agent'), 'should have Agent header');
|
||||
assert.ok(table.includes('Model'), 'should have Model header');
|
||||
assert.ok(table.includes('─'), 'should have separator line');
|
||||
assert.ok(table.includes('gsd-planner'), 'should list agent');
|
||||
assert.ok(table.includes('opus'), 'should list model');
|
||||
});
|
||||
|
||||
test('pads columns correctly', () => {
|
||||
const map = { 'a': 'opus', 'very-long-agent-name': 'haiku' };
|
||||
const table = formatAgentToModelMapAsTable(map);
|
||||
const lines = table.split('\n').filter(l => l.trim());
|
||||
// Separator line uses ┼, data/header lines use │
|
||||
const dataLines = lines.filter(l => l.includes('│'));
|
||||
const pipePositions = dataLines.map(l => l.indexOf('│'));
|
||||
const unique = [...new Set(pipePositions)];
|
||||
assert.strictEqual(unique.length, 1, 'all data lines should align on │');
|
||||
});
|
||||
|
||||
test('handles empty map', () => {
|
||||
const table = formatAgentToModelMapAsTable({});
|
||||
assert.ok(table.includes('Agent'), 'should still have header');
|
||||
});
|
||||
});
|
||||
197
tests/profile-output.test.cjs
Normal file
197
tests/profile-output.test.cjs
Normal file
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* Profile Output Tests
|
||||
*
|
||||
* Tests for profile rendering commands and PROFILING_QUESTIONS data.
|
||||
*/
|
||||
|
||||
const { test, describe, beforeEach, afterEach } = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { runGsdTools, createTempProject, createTempGitProject, cleanup } = require('./helpers.cjs');
|
||||
|
||||
const {
|
||||
PROFILING_QUESTIONS,
|
||||
CLAUDE_INSTRUCTIONS,
|
||||
} = require('../get-shit-done/bin/lib/profile-output.cjs');
|
||||
|
||||
// ─── PROFILING_QUESTIONS data ─────────────────────────────────────────────────
|
||||
|
||||
describe('PROFILING_QUESTIONS', () => {
|
||||
test('is a non-empty array', () => {
|
||||
assert.ok(Array.isArray(PROFILING_QUESTIONS));
|
||||
assert.ok(PROFILING_QUESTIONS.length > 0);
|
||||
});
|
||||
|
||||
test('each question has required fields', () => {
|
||||
for (const q of PROFILING_QUESTIONS) {
|
||||
assert.ok(q.dimension, `question missing dimension`);
|
||||
assert.ok(q.header, `${q.dimension} missing header`);
|
||||
assert.ok(q.question, `${q.dimension} missing question`);
|
||||
assert.ok(Array.isArray(q.options), `${q.dimension} options should be array`);
|
||||
assert.ok(q.options.length >= 2, `${q.dimension} should have at least 2 options`);
|
||||
}
|
||||
});
|
||||
|
||||
test('each option has label, value, and rating', () => {
|
||||
for (const q of PROFILING_QUESTIONS) {
|
||||
for (const opt of q.options) {
|
||||
assert.ok(opt.label, `${q.dimension} option missing label`);
|
||||
assert.ok(opt.value, `${q.dimension} option missing value`);
|
||||
assert.ok(opt.rating, `${q.dimension} option missing rating`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('all dimension keys are unique', () => {
|
||||
const dims = PROFILING_QUESTIONS.map(q => q.dimension);
|
||||
const unique = [...new Set(dims)];
|
||||
assert.strictEqual(dims.length, unique.length);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── CLAUDE_INSTRUCTIONS ──────────────────────────────────────────────────────
|
||||
|
||||
describe('CLAUDE_INSTRUCTIONS', () => {
|
||||
test('is a non-empty object', () => {
|
||||
assert.ok(typeof CLAUDE_INSTRUCTIONS === 'object');
|
||||
assert.ok(Object.keys(CLAUDE_INSTRUCTIONS).length > 0);
|
||||
});
|
||||
|
||||
test('each dimension has at least one instruction', () => {
|
||||
for (const [dim, instructions] of Object.entries(CLAUDE_INSTRUCTIONS)) {
|
||||
assert.ok(typeof instructions === 'object', `${dim} should be an object`);
|
||||
assert.ok(Object.keys(instructions).length > 0, `${dim} should have instructions`);
|
||||
}
|
||||
});
|
||||
|
||||
test('every PROFILING_QUESTIONS dimension has CLAUDE_INSTRUCTIONS', () => {
|
||||
for (const q of PROFILING_QUESTIONS) {
|
||||
assert.ok(
|
||||
CLAUDE_INSTRUCTIONS[q.dimension],
|
||||
`${q.dimension} has questions but no CLAUDE_INSTRUCTIONS`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── write-profile command ────────────────────────────────────────────────────
|
||||
|
||||
describe('write-profile command', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('writes USER-PROFILE.md from analysis JSON', () => {
|
||||
const analysis = {
|
||||
profile_version: '1.0',
|
||||
dimensions: {
|
||||
communication_style: { rating: 'terse-direct', confidence: 'HIGH' },
|
||||
decision_speed: { rating: 'fast-intuitive', confidence: 'MEDIUM' },
|
||||
explanation_depth: { rating: 'concise', confidence: 'HIGH' },
|
||||
debugging_approach: { rating: 'fix-first', confidence: 'LOW' },
|
||||
ux_philosophy: { rating: 'function-first', confidence: 'MEDIUM' },
|
||||
vendor_philosophy: { rating: 'pragmatic', confidence: 'HIGH' },
|
||||
frustration_triggers: { rating: 'over-explanation', confidence: 'LOW' },
|
||||
learning_style: { rating: 'hands-on', confidence: 'MEDIUM' },
|
||||
},
|
||||
};
|
||||
|
||||
const analysisPath = path.join(tmpDir, 'analysis.json');
|
||||
fs.writeFileSync(analysisPath, JSON.stringify(analysis));
|
||||
|
||||
const result = runGsdTools(['write-profile', '--input', analysisPath, '--raw'], tmpDir);
|
||||
assert.ok(result.success, `Failed: ${result.error}`);
|
||||
const out = JSON.parse(result.output);
|
||||
assert.ok(out.profile_path, 'should return profile_path');
|
||||
assert.ok(out.dimensions_scored > 0, 'should have scored dimensions');
|
||||
});
|
||||
|
||||
test('errors when --input is missing', () => {
|
||||
const result = runGsdTools('write-profile --raw', tmpDir);
|
||||
assert.ok(!result.success, 'should fail without --input');
|
||||
assert.ok(result.error.includes('--input'), 'should mention --input');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── generate-claude-md command ───────────────────────────────────────────────
|
||||
|
||||
describe('generate-claude-md command', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempGitProject();
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'PROJECT.md'),
|
||||
'# My Project\n\nA test project.\n\n## Tech Stack\n\n- Node.js\n- TypeScript\n'
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('generates CLAUDE.md with --auto flag', () => {
|
||||
const outputPath = path.join(tmpDir, 'CLAUDE.md');
|
||||
const result = runGsdTools(['generate-claude-md', '--output', outputPath, '--auto', '--raw'], tmpDir);
|
||||
assert.ok(result.success, `Failed: ${result.error}`);
|
||||
|
||||
if (fs.existsSync(outputPath)) {
|
||||
const content = fs.readFileSync(outputPath, 'utf-8');
|
||||
assert.ok(content.length > 0, 'should have content');
|
||||
}
|
||||
});
|
||||
|
||||
test('does not overwrite existing CLAUDE.md without --force', () => {
|
||||
const outputPath = path.join(tmpDir, 'CLAUDE.md');
|
||||
fs.writeFileSync(outputPath, '# Custom CLAUDE.md\n\nUser content.\n');
|
||||
|
||||
const result = runGsdTools(['generate-claude-md', '--output', outputPath, '--auto', '--raw'], tmpDir);
|
||||
// Should merge, not overwrite
|
||||
const content = fs.readFileSync(outputPath, 'utf-8');
|
||||
assert.ok(content.length > 0, 'should still have content');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── generate-dev-preferences ─────────────────────────────────────────────────
|
||||
|
||||
describe('generate-dev-preferences command', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('errors when --analysis is missing', () => {
|
||||
const result = runGsdTools('generate-dev-preferences --raw', tmpDir);
|
||||
assert.ok(!result.success, 'should fail without --analysis');
|
||||
assert.ok(result.error.includes('--analysis'), 'should mention --analysis');
|
||||
});
|
||||
|
||||
test('generates preferences from analysis file', () => {
|
||||
const analysis = {
|
||||
profile_version: '1.0',
|
||||
dimensions: {
|
||||
communication_style: { rating: 'terse-direct', confidence: 'HIGH' },
|
||||
decision_speed: { rating: 'fast-intuitive', confidence: 'MEDIUM' },
|
||||
},
|
||||
};
|
||||
const analysisPath = path.join(tmpDir, 'analysis.json');
|
||||
fs.writeFileSync(analysisPath, JSON.stringify(analysis));
|
||||
|
||||
const result = runGsdTools(['generate-dev-preferences', '--analysis', analysisPath, '--raw'], tmpDir);
|
||||
assert.ok(result.success, `Failed: ${result.error}`);
|
||||
const out = JSON.parse(result.output);
|
||||
assert.ok(out.command_path || out.command_name, 'should return command output');
|
||||
});
|
||||
});
|
||||
160
tests/profile-pipeline.test.cjs
Normal file
160
tests/profile-pipeline.test.cjs
Normal file
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Profile Pipeline Tests
|
||||
*
|
||||
* Tests for session scanning, message extraction, and profile sampling.
|
||||
* Uses synthetic session data in temp directories via --path override.
|
||||
*/
|
||||
|
||||
const { test, describe, beforeEach, afterEach } = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
|
||||
|
||||
// ─── scan-sessions ────────────────────────────────────────────────────────────
|
||||
|
||||
describe('scan-sessions command', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-profile-test-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('returns empty array for empty sessions directory', () => {
|
||||
const sessionsDir = path.join(tmpDir, 'projects');
|
||||
fs.mkdirSync(sessionsDir, { recursive: true });
|
||||
const result = runGsdTools(`scan-sessions --path ${sessionsDir} --raw`, tmpDir);
|
||||
assert.ok(result.success, `Failed: ${result.error}`);
|
||||
const out = JSON.parse(result.output);
|
||||
assert.ok(Array.isArray(out), 'should return an array');
|
||||
assert.strictEqual(out.length, 0, 'should be empty');
|
||||
});
|
||||
|
||||
test('scans synthetic project directory', () => {
|
||||
const sessionsDir = path.join(tmpDir, 'projects');
|
||||
const projectDir = path.join(sessionsDir, 'test-project-abc123');
|
||||
fs.mkdirSync(projectDir, { recursive: true });
|
||||
|
||||
// Create a synthetic session file
|
||||
const sessionData = [
|
||||
JSON.stringify({ type: 'user', userType: 'external', message: { content: 'hello' }, timestamp: Date.now() }),
|
||||
JSON.stringify({ type: 'assistant', message: { content: 'hi' }, timestamp: Date.now() }),
|
||||
].join('\n');
|
||||
fs.writeFileSync(path.join(projectDir, 'session-001.jsonl'), sessionData);
|
||||
|
||||
const result = runGsdTools(`scan-sessions --path ${sessionsDir} --raw`, tmpDir);
|
||||
assert.ok(result.success, `Failed: ${result.error}`);
|
||||
const out = JSON.parse(result.output);
|
||||
assert.ok(Array.isArray(out), 'should return array');
|
||||
assert.strictEqual(out.length, 1, 'should find 1 project');
|
||||
assert.strictEqual(out[0].sessionCount, 1, 'should have 1 session');
|
||||
});
|
||||
|
||||
test('reports multiple sessions and sizes', () => {
|
||||
const sessionsDir = path.join(tmpDir, 'projects');
|
||||
const projectDir = path.join(sessionsDir, 'multi-session-project');
|
||||
fs.mkdirSync(projectDir, { recursive: true });
|
||||
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const data = JSON.stringify({ type: 'user', userType: 'external', message: { content: `msg ${i}` }, timestamp: Date.now() });
|
||||
fs.writeFileSync(path.join(projectDir, `session-${i}.jsonl`), data + '\n');
|
||||
}
|
||||
|
||||
const result = runGsdTools(`scan-sessions --path ${sessionsDir} --raw`, tmpDir);
|
||||
assert.ok(result.success, `Failed: ${result.error}`);
|
||||
const out = JSON.parse(result.output);
|
||||
assert.strictEqual(out[0].sessionCount, 3);
|
||||
assert.ok(out[0].totalSize > 0, 'should have non-zero size');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── extract-messages ─────────────────────────────────────────────────────────
|
||||
|
||||
describe('extract-messages command', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-profile-test-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('extracts user messages from synthetic session', () => {
|
||||
const sessionsDir = path.join(tmpDir, 'projects');
|
||||
const projectDir = path.join(sessionsDir, 'my-project');
|
||||
fs.mkdirSync(projectDir, { recursive: true });
|
||||
|
||||
const messages = [
|
||||
{ type: 'user', userType: 'external', message: { content: 'fix the login bug' }, timestamp: Date.now() },
|
||||
{ type: 'assistant', message: { content: 'I will fix it.' }, timestamp: Date.now() },
|
||||
{ type: 'user', userType: 'external', message: { content: 'add dark mode' }, timestamp: Date.now() },
|
||||
{ type: 'user', userType: 'internal', isMeta: true, message: { content: '<local-command' }, timestamp: Date.now() },
|
||||
];
|
||||
fs.writeFileSync(
|
||||
path.join(projectDir, 'session-001.jsonl'),
|
||||
messages.map(m => JSON.stringify(m)).join('\n')
|
||||
);
|
||||
|
||||
const result = runGsdTools(`extract-messages my-project --path ${sessionsDir} --raw`, tmpDir);
|
||||
assert.ok(result.success, `Failed: ${result.error}`);
|
||||
const out = JSON.parse(result.output);
|
||||
assert.strictEqual(out.messages_extracted, 2, 'should extract 2 genuine user messages');
|
||||
assert.strictEqual(out.project, 'my-project');
|
||||
assert.ok(out.output_file, 'should have output file path');
|
||||
});
|
||||
|
||||
test('filters out meta and internal messages', () => {
|
||||
const sessionsDir = path.join(tmpDir, 'projects');
|
||||
const projectDir = path.join(sessionsDir, 'filter-test');
|
||||
fs.mkdirSync(projectDir, { recursive: true });
|
||||
|
||||
const messages = [
|
||||
{ type: 'user', userType: 'external', message: { content: 'real message' }, timestamp: Date.now() },
|
||||
{ type: 'user', userType: 'internal', message: { content: 'internal msg' }, timestamp: Date.now() },
|
||||
{ type: 'user', userType: 'external', isMeta: true, message: { content: 'meta msg' }, timestamp: Date.now() },
|
||||
{ type: 'user', userType: 'external', message: { content: '<local-command test' }, timestamp: Date.now() },
|
||||
{ type: 'user', userType: 'external', message: { content: '' }, timestamp: Date.now() },
|
||||
{ type: 'user', userType: 'external', message: { content: 'second real' }, timestamp: Date.now() },
|
||||
];
|
||||
fs.writeFileSync(
|
||||
path.join(projectDir, 'session-001.jsonl'),
|
||||
messages.map(m => JSON.stringify(m)).join('\n')
|
||||
);
|
||||
|
||||
const result = runGsdTools(`extract-messages filter-test --path ${sessionsDir} --raw`, tmpDir);
|
||||
assert.ok(result.success, `Failed: ${result.error}`);
|
||||
const out = JSON.parse(result.output);
|
||||
assert.strictEqual(out.messages_extracted, 2, 'should only extract 2 genuine external messages');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── profile-questionnaire ────────────────────────────────────────────────────
|
||||
|
||||
describe('profile-questionnaire command', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('returns questionnaire structure', () => {
|
||||
const result = runGsdTools('profile-questionnaire --raw', tmpDir);
|
||||
assert.ok(result.success, `Failed: ${result.error}`);
|
||||
const out = JSON.parse(result.output);
|
||||
assert.ok(out.questions, 'should have questions array');
|
||||
assert.ok(out.questions.length > 0, 'should have at least one question');
|
||||
assert.ok(out.questions[0].dimension, 'each question should have a dimension');
|
||||
assert.ok(out.questions[0].options, 'each question should have options');
|
||||
});
|
||||
});
|
||||
186
tests/template.test.cjs
Normal file
186
tests/template.test.cjs
Normal file
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* Template Tests
|
||||
*
|
||||
* Tests for cmdTemplateSelect (heuristic template selection) and
|
||||
* cmdTemplateFill (summary, plan, verification template generation).
|
||||
*/
|
||||
|
||||
const { test, describe, beforeEach, afterEach } = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
|
||||
|
||||
// ─── template select ──────────────────────────────────────────────────────────
|
||||
|
||||
describe('template select command', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject();
|
||||
// Create a phase directory with a plan
|
||||
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-setup');
|
||||
fs.mkdirSync(phaseDir, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('selects minimal template for simple plan', () => {
|
||||
const planPath = path.join(tmpDir, '.planning', 'phases', '01-setup', '01-01-PLAN.md');
|
||||
fs.writeFileSync(planPath, [
|
||||
'# Plan',
|
||||
'',
|
||||
'### Task 1',
|
||||
'Do the thing.',
|
||||
'',
|
||||
'File: `src/index.ts`',
|
||||
].join('\n'));
|
||||
|
||||
const result = runGsdTools(`template select .planning/phases/01-setup/01-01-PLAN.md`, tmpDir);
|
||||
assert.ok(result.success, `Failed: ${result.error}`);
|
||||
const out = JSON.parse(result.output);
|
||||
assert.strictEqual(out.type, 'minimal');
|
||||
assert.ok(out.template.includes('summary-minimal'));
|
||||
});
|
||||
|
||||
test('selects standard template for moderate plan', () => {
|
||||
const planPath = path.join(tmpDir, '.planning', 'phases', '01-setup', '01-01-PLAN.md');
|
||||
fs.writeFileSync(planPath, [
|
||||
'# Plan',
|
||||
'',
|
||||
'### Task 1',
|
||||
'Create `src/auth/login.ts`',
|
||||
'',
|
||||
'### Task 2',
|
||||
'Create `src/auth/register.ts`',
|
||||
'',
|
||||
'### Task 3',
|
||||
'Update `src/routes/index.ts`',
|
||||
'',
|
||||
'Files: `src/auth/login.ts`, `src/auth/register.ts`, `src/routes/index.ts`, `src/middleware/auth.ts`',
|
||||
].join('\n'));
|
||||
|
||||
const result = runGsdTools(`template select .planning/phases/01-setup/01-01-PLAN.md`, tmpDir);
|
||||
assert.ok(result.success, `Failed: ${result.error}`);
|
||||
const out = JSON.parse(result.output);
|
||||
assert.strictEqual(out.type, 'standard');
|
||||
});
|
||||
|
||||
test('selects complex template for plan with decisions and many files', () => {
|
||||
const planPath = path.join(tmpDir, '.planning', 'phases', '01-setup', '01-01-PLAN.md');
|
||||
const lines = ['# Plan', ''];
|
||||
for (let i = 1; i <= 6; i++) {
|
||||
lines.push(`### Task ${i}`, `Do task ${i}.`, '');
|
||||
}
|
||||
lines.push('Made a decision about architecture.', 'Another decision here.');
|
||||
for (let i = 1; i <= 8; i++) {
|
||||
lines.push(`File: \`src/module${i}/index.ts\``);
|
||||
}
|
||||
fs.writeFileSync(planPath, lines.join('\n'));
|
||||
|
||||
const result = runGsdTools(`template select .planning/phases/01-setup/01-01-PLAN.md`, tmpDir);
|
||||
assert.ok(result.success, `Failed: ${result.error}`);
|
||||
const out = JSON.parse(result.output);
|
||||
assert.strictEqual(out.type, 'complex');
|
||||
});
|
||||
|
||||
test('returns standard as fallback for nonexistent file', () => {
|
||||
const result = runGsdTools(`template select .planning/phases/01-setup/nonexistent.md`, tmpDir);
|
||||
assert.ok(result.success, `Failed: ${result.error}`);
|
||||
const out = JSON.parse(result.output);
|
||||
assert.strictEqual(out.type, 'standard');
|
||||
assert.ok(out.error, 'should include error message');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── template fill ────────────────────────────────────────────────────────────
|
||||
|
||||
describe('template fill command', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject();
|
||||
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-setup');
|
||||
fs.mkdirSync(phaseDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
'## Roadmap\n\n### Phase 1: Setup\n**Goal:** Initial setup\n'
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('fills summary template', () => {
|
||||
const result = runGsdTools('template fill summary --phase 1', tmpDir);
|
||||
assert.ok(result.success, `Failed: ${result.error}`);
|
||||
const out = JSON.parse(result.output);
|
||||
assert.strictEqual(out.created, true);
|
||||
assert.ok(out.path.includes('01-01-SUMMARY.md'));
|
||||
|
||||
const content = fs.readFileSync(path.join(tmpDir, out.path), 'utf-8');
|
||||
assert.ok(content.includes('---'), 'should have frontmatter');
|
||||
assert.ok(content.includes('Phase 1'), 'should reference phase');
|
||||
assert.ok(content.includes('Accomplishments'), 'should have accomplishments section');
|
||||
});
|
||||
|
||||
test('fills plan template', () => {
|
||||
const result = runGsdTools('template fill plan --phase 1', tmpDir);
|
||||
assert.ok(result.success, `Failed: ${result.error}`);
|
||||
const out = JSON.parse(result.output);
|
||||
assert.strictEqual(out.created, true);
|
||||
assert.ok(out.path.includes('01-01-PLAN.md'));
|
||||
|
||||
const content = fs.readFileSync(path.join(tmpDir, out.path), 'utf-8');
|
||||
assert.ok(content.includes('---'), 'should have frontmatter');
|
||||
assert.ok(content.includes('Objective'), 'should have objective section');
|
||||
assert.ok(content.includes('<task type="code">'), 'should have task XML');
|
||||
});
|
||||
|
||||
test('fills verification template', () => {
|
||||
const result = runGsdTools('template fill verification --phase 1', tmpDir);
|
||||
assert.ok(result.success, `Failed: ${result.error}`);
|
||||
const out = JSON.parse(result.output);
|
||||
assert.strictEqual(out.created, true);
|
||||
assert.ok(out.path.includes('01-VERIFICATION.md'));
|
||||
|
||||
const content = fs.readFileSync(path.join(tmpDir, out.path), 'utf-8');
|
||||
assert.ok(content.includes('Observable Truths'), 'should have truths section');
|
||||
assert.ok(content.includes('Required Artifacts'), 'should have artifacts section');
|
||||
});
|
||||
|
||||
test('rejects existing file', () => {
|
||||
// Create the file first
|
||||
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-setup');
|
||||
fs.writeFileSync(path.join(phaseDir, '01-01-SUMMARY.md'), '# Existing');
|
||||
|
||||
const result = runGsdTools('template fill summary --phase 1', tmpDir);
|
||||
assert.ok(result.success); // outputs JSON, doesn't crash
|
||||
const out = JSON.parse(result.output);
|
||||
assert.ok(out.error, 'should report error for existing file');
|
||||
assert.ok(out.error.includes('already exists'));
|
||||
});
|
||||
|
||||
test('errors on unknown template type', () => {
|
||||
const result = runGsdTools('template fill bogus --phase 1', tmpDir);
|
||||
assert.ok(!result.success, 'should fail for unknown type');
|
||||
assert.ok(result.error.includes('Unknown template type'));
|
||||
});
|
||||
|
||||
test('errors when phase not found', () => {
|
||||
const result = runGsdTools('template fill summary --phase 99', tmpDir);
|
||||
assert.ok(result.success);
|
||||
const out = JSON.parse(result.output);
|
||||
assert.ok(out.error, 'should report phase not found');
|
||||
});
|
||||
|
||||
test('respects --plan option for plan number', () => {
|
||||
const result = runGsdTools('template fill plan --phase 1 --plan 03', tmpDir);
|
||||
assert.ok(result.success, `Failed: ${result.error}`);
|
||||
const out = JSON.parse(result.output);
|
||||
assert.ok(out.path.includes('01-03-PLAN.md'), `Expected plan 03 in path, got ${out.path}`);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user