Add requirements_completed field to summary-extract output, mapping from SUMMARY frontmatter requirements-completed key. Enables /gsd:audit-milestone cross-check to receive data from SUMMARY source. Re-applied from #631 against refactored codebase (commands.cjs + split tests). Co-authored-by: Colin Johnson <Solvely-Colin@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
662 lines
23 KiB
JavaScript
662 lines
23 KiB
JavaScript
/**
|
|
* GSD Tools Tests - Commands
|
|
*/
|
|
|
|
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');
|
|
|
|
describe('history-digest command', () => {
|
|
let tmpDir;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = createTempProject();
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('empty phases directory returns valid schema', () => {
|
|
const result = runGsdTools('history-digest', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const digest = JSON.parse(result.output);
|
|
|
|
assert.deepStrictEqual(digest.phases, {}, 'phases should be empty object');
|
|
assert.deepStrictEqual(digest.decisions, [], 'decisions should be empty array');
|
|
assert.deepStrictEqual(digest.tech_stack, [], 'tech_stack should be empty array');
|
|
});
|
|
|
|
test('nested frontmatter fields extracted correctly', () => {
|
|
// Create phase directory with SUMMARY containing nested frontmatter
|
|
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
|
fs.mkdirSync(phaseDir, { recursive: true });
|
|
|
|
const summaryContent = `---
|
|
phase: "01"
|
|
name: "Foundation Setup"
|
|
dependency-graph:
|
|
provides:
|
|
- "Database schema"
|
|
- "Auth system"
|
|
affects:
|
|
- "API layer"
|
|
tech-stack:
|
|
added:
|
|
- "prisma"
|
|
- "jose"
|
|
patterns-established:
|
|
- "Repository pattern"
|
|
- "JWT auth flow"
|
|
key-decisions:
|
|
- "Use Prisma over Drizzle"
|
|
- "JWT in httpOnly cookies"
|
|
---
|
|
|
|
# Summary content here
|
|
`;
|
|
|
|
fs.writeFileSync(path.join(phaseDir, '01-01-SUMMARY.md'), summaryContent);
|
|
|
|
const result = runGsdTools('history-digest', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const digest = JSON.parse(result.output);
|
|
|
|
// Check nested dependency-graph.provides
|
|
assert.ok(digest.phases['01'], 'Phase 01 should exist');
|
|
assert.deepStrictEqual(
|
|
digest.phases['01'].provides.sort(),
|
|
['Auth system', 'Database schema'],
|
|
'provides should contain nested values'
|
|
);
|
|
|
|
// Check nested dependency-graph.affects
|
|
assert.deepStrictEqual(
|
|
digest.phases['01'].affects,
|
|
['API layer'],
|
|
'affects should contain nested values'
|
|
);
|
|
|
|
// Check nested tech-stack.added
|
|
assert.deepStrictEqual(
|
|
digest.tech_stack.sort(),
|
|
['jose', 'prisma'],
|
|
'tech_stack should contain nested values'
|
|
);
|
|
|
|
// Check patterns-established (flat array)
|
|
assert.deepStrictEqual(
|
|
digest.phases['01'].patterns.sort(),
|
|
['JWT auth flow', 'Repository pattern'],
|
|
'patterns should be extracted'
|
|
);
|
|
|
|
// Check key-decisions
|
|
assert.strictEqual(digest.decisions.length, 2, 'Should have 2 decisions');
|
|
assert.ok(
|
|
digest.decisions.some(d => d.decision === 'Use Prisma over Drizzle'),
|
|
'Should contain first decision'
|
|
);
|
|
});
|
|
|
|
test('multiple phases merged into single digest', () => {
|
|
// Create phase 01
|
|
const phase01Dir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
|
fs.mkdirSync(phase01Dir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(phase01Dir, '01-01-SUMMARY.md'),
|
|
`---
|
|
phase: "01"
|
|
name: "Foundation"
|
|
provides:
|
|
- "Database"
|
|
patterns-established:
|
|
- "Pattern A"
|
|
key-decisions:
|
|
- "Decision 1"
|
|
---
|
|
`
|
|
);
|
|
|
|
// Create phase 02
|
|
const phase02Dir = path.join(tmpDir, '.planning', 'phases', '02-api');
|
|
fs.mkdirSync(phase02Dir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(phase02Dir, '02-01-SUMMARY.md'),
|
|
`---
|
|
phase: "02"
|
|
name: "API"
|
|
provides:
|
|
- "REST endpoints"
|
|
patterns-established:
|
|
- "Pattern B"
|
|
key-decisions:
|
|
- "Decision 2"
|
|
tech-stack:
|
|
added:
|
|
- "zod"
|
|
---
|
|
`
|
|
);
|
|
|
|
const result = runGsdTools('history-digest', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const digest = JSON.parse(result.output);
|
|
|
|
// Both phases present
|
|
assert.ok(digest.phases['01'], 'Phase 01 should exist');
|
|
assert.ok(digest.phases['02'], 'Phase 02 should exist');
|
|
|
|
// Decisions merged
|
|
assert.strictEqual(digest.decisions.length, 2, 'Should have 2 decisions total');
|
|
|
|
// Tech stack merged
|
|
assert.deepStrictEqual(digest.tech_stack, ['zod'], 'tech_stack should have zod');
|
|
});
|
|
|
|
test('malformed SUMMARY.md skipped gracefully', () => {
|
|
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-test');
|
|
fs.mkdirSync(phaseDir, { recursive: true });
|
|
|
|
// Valid summary
|
|
fs.writeFileSync(
|
|
path.join(phaseDir, '01-01-SUMMARY.md'),
|
|
`---
|
|
phase: "01"
|
|
provides:
|
|
- "Valid feature"
|
|
---
|
|
`
|
|
);
|
|
|
|
// Malformed summary (no frontmatter)
|
|
fs.writeFileSync(
|
|
path.join(phaseDir, '01-02-SUMMARY.md'),
|
|
`# Just a heading
|
|
No frontmatter here
|
|
`
|
|
);
|
|
|
|
// Another malformed summary (broken YAML)
|
|
fs.writeFileSync(
|
|
path.join(phaseDir, '01-03-SUMMARY.md'),
|
|
`---
|
|
broken: [unclosed
|
|
---
|
|
`
|
|
);
|
|
|
|
const result = runGsdTools('history-digest', tmpDir);
|
|
assert.ok(result.success, `Command should succeed despite malformed files: ${result.error}`);
|
|
|
|
const digest = JSON.parse(result.output);
|
|
assert.ok(digest.phases['01'], 'Phase 01 should exist');
|
|
assert.ok(
|
|
digest.phases['01'].provides.includes('Valid feature'),
|
|
'Valid feature should be extracted'
|
|
);
|
|
});
|
|
|
|
test('flat provides field still works (backward compatibility)', () => {
|
|
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-test');
|
|
fs.mkdirSync(phaseDir, { recursive: true });
|
|
|
|
fs.writeFileSync(
|
|
path.join(phaseDir, '01-01-SUMMARY.md'),
|
|
`---
|
|
phase: "01"
|
|
provides:
|
|
- "Direct provides"
|
|
---
|
|
`
|
|
);
|
|
|
|
const result = runGsdTools('history-digest', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const digest = JSON.parse(result.output);
|
|
assert.deepStrictEqual(
|
|
digest.phases['01'].provides,
|
|
['Direct provides'],
|
|
'Direct provides should work'
|
|
);
|
|
});
|
|
|
|
test('inline array syntax supported', () => {
|
|
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-test');
|
|
fs.mkdirSync(phaseDir, { recursive: true });
|
|
|
|
fs.writeFileSync(
|
|
path.join(phaseDir, '01-01-SUMMARY.md'),
|
|
`---
|
|
phase: "01"
|
|
provides: [Feature A, Feature B]
|
|
patterns-established: ["Pattern X", "Pattern Y"]
|
|
---
|
|
`
|
|
);
|
|
|
|
const result = runGsdTools('history-digest', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const digest = JSON.parse(result.output);
|
|
assert.deepStrictEqual(
|
|
digest.phases['01'].provides.sort(),
|
|
['Feature A', 'Feature B'],
|
|
'Inline array should work'
|
|
);
|
|
assert.deepStrictEqual(
|
|
digest.phases['01'].patterns.sort(),
|
|
['Pattern X', 'Pattern Y'],
|
|
'Inline quoted array should work'
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// phases list command
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
describe('summary-extract command', () => {
|
|
let tmpDir;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = createTempProject();
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('missing file returns error', () => {
|
|
const result = runGsdTools('summary-extract .planning/phases/01-test/01-01-SUMMARY.md', tmpDir);
|
|
assert.ok(result.success, `Command should succeed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.error, 'File not found', 'should report missing file');
|
|
});
|
|
|
|
test('extracts all fields from SUMMARY.md', () => {
|
|
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
|
fs.mkdirSync(phaseDir, { recursive: true });
|
|
|
|
fs.writeFileSync(
|
|
path.join(phaseDir, '01-01-SUMMARY.md'),
|
|
`---
|
|
one-liner: Set up Prisma with User and Project models
|
|
key-files:
|
|
- prisma/schema.prisma
|
|
- src/lib/db.ts
|
|
tech-stack:
|
|
added:
|
|
- prisma
|
|
- zod
|
|
patterns-established:
|
|
- Repository pattern
|
|
- Dependency injection
|
|
key-decisions:
|
|
- Use Prisma over Drizzle: Better DX and ecosystem
|
|
- Single database: Start simple, shard later
|
|
requirements-completed:
|
|
- AUTH-01
|
|
- AUTH-02
|
|
---
|
|
|
|
# Summary
|
|
|
|
Full summary content here.
|
|
`
|
|
);
|
|
|
|
const result = runGsdTools('summary-extract .planning/phases/01-foundation/01-01-SUMMARY.md', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.path, '.planning/phases/01-foundation/01-01-SUMMARY.md', 'path correct');
|
|
assert.strictEqual(output.one_liner, 'Set up Prisma with User and Project models', 'one-liner extracted');
|
|
assert.deepStrictEqual(output.key_files, ['prisma/schema.prisma', 'src/lib/db.ts'], 'key files extracted');
|
|
assert.deepStrictEqual(output.tech_added, ['prisma', 'zod'], 'tech added extracted');
|
|
assert.deepStrictEqual(output.patterns, ['Repository pattern', 'Dependency injection'], 'patterns extracted');
|
|
assert.strictEqual(output.decisions.length, 2, 'decisions extracted');
|
|
assert.deepStrictEqual(output.requirements_completed, ['AUTH-01', 'AUTH-02'], 'requirements completed extracted');
|
|
});
|
|
|
|
test('selective extraction with --fields', () => {
|
|
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
|
fs.mkdirSync(phaseDir, { recursive: true });
|
|
|
|
fs.writeFileSync(
|
|
path.join(phaseDir, '01-01-SUMMARY.md'),
|
|
`---
|
|
one-liner: Set up database
|
|
key-files:
|
|
- prisma/schema.prisma
|
|
tech-stack:
|
|
added:
|
|
- prisma
|
|
patterns-established:
|
|
- Repository pattern
|
|
key-decisions:
|
|
- Use Prisma: Better DX
|
|
requirements-completed:
|
|
- AUTH-01
|
|
---
|
|
`
|
|
);
|
|
|
|
const result = runGsdTools('summary-extract .planning/phases/01-foundation/01-01-SUMMARY.md --fields one_liner,key_files,requirements_completed', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.one_liner, 'Set up database', 'one_liner included');
|
|
assert.deepStrictEqual(output.key_files, ['prisma/schema.prisma'], 'key_files included');
|
|
assert.deepStrictEqual(output.requirements_completed, ['AUTH-01'], 'requirements_completed included');
|
|
assert.strictEqual(output.tech_added, undefined, 'tech_added excluded');
|
|
assert.strictEqual(output.patterns, undefined, 'patterns excluded');
|
|
assert.strictEqual(output.decisions, undefined, 'decisions excluded');
|
|
});
|
|
|
|
test('handles missing frontmatter fields gracefully', () => {
|
|
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
|
fs.mkdirSync(phaseDir, { recursive: true });
|
|
|
|
fs.writeFileSync(
|
|
path.join(phaseDir, '01-01-SUMMARY.md'),
|
|
`---
|
|
one-liner: Minimal summary
|
|
---
|
|
|
|
# Summary
|
|
`
|
|
);
|
|
|
|
const result = runGsdTools('summary-extract .planning/phases/01-foundation/01-01-SUMMARY.md', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.one_liner, 'Minimal summary', 'one-liner extracted');
|
|
assert.deepStrictEqual(output.key_files, [], 'key_files defaults to empty');
|
|
assert.deepStrictEqual(output.tech_added, [], 'tech_added defaults to empty');
|
|
assert.deepStrictEqual(output.patterns, [], 'patterns defaults to empty');
|
|
assert.deepStrictEqual(output.decisions, [], 'decisions defaults to empty');
|
|
assert.deepStrictEqual(output.requirements_completed, [], 'requirements_completed defaults to empty');
|
|
});
|
|
|
|
test('parses key-decisions with rationale', () => {
|
|
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
|
fs.mkdirSync(phaseDir, { recursive: true });
|
|
|
|
fs.writeFileSync(
|
|
path.join(phaseDir, '01-01-SUMMARY.md'),
|
|
`---
|
|
key-decisions:
|
|
- Use Prisma: Better DX than alternatives
|
|
- JWT tokens: Stateless auth for scalability
|
|
---
|
|
`
|
|
);
|
|
|
|
const result = runGsdTools('summary-extract .planning/phases/01-foundation/01-01-SUMMARY.md', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.decisions[0].summary, 'Use Prisma', 'decision summary parsed');
|
|
assert.strictEqual(output.decisions[0].rationale, 'Better DX than alternatives', 'decision rationale parsed');
|
|
assert.strictEqual(output.decisions[1].summary, 'JWT tokens', 'second decision summary');
|
|
assert.strictEqual(output.decisions[1].rationale, 'Stateless auth for scalability', 'second decision rationale');
|
|
});
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// init commands tests
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
describe('progress command', () => {
|
|
let tmpDir;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = createTempProject();
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('renders JSON progress', () => {
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
`# Roadmap v1.0 MVP\n`
|
|
);
|
|
const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
|
fs.mkdirSync(p1, { recursive: true });
|
|
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
|
|
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Done');
|
|
fs.writeFileSync(path.join(p1, '01-02-PLAN.md'), '# Plan 2');
|
|
|
|
const result = runGsdTools('progress json', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.total_plans, 2, '2 total plans');
|
|
assert.strictEqual(output.total_summaries, 1, '1 summary');
|
|
assert.strictEqual(output.percent, 50, '50%');
|
|
assert.strictEqual(output.phases.length, 1, '1 phase');
|
|
assert.strictEqual(output.phases[0].status, 'In Progress', 'phase in progress');
|
|
});
|
|
|
|
test('renders bar format', () => {
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
`# Roadmap v1.0\n`
|
|
);
|
|
const p1 = path.join(tmpDir, '.planning', 'phases', '01-test');
|
|
fs.mkdirSync(p1, { recursive: true });
|
|
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
|
|
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Done');
|
|
|
|
const result = runGsdTools('progress bar --raw', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
assert.ok(result.output.includes('1/1'), 'should include count');
|
|
assert.ok(result.output.includes('100%'), 'should include 100%');
|
|
});
|
|
|
|
test('renders table format', () => {
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
`# Roadmap v1.0 MVP\n`
|
|
);
|
|
const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
|
fs.mkdirSync(p1, { recursive: true });
|
|
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
|
|
|
|
const result = runGsdTools('progress table --raw', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
assert.ok(result.output.includes('Phase'), 'should have table header');
|
|
assert.ok(result.output.includes('foundation'), 'should include phase name');
|
|
});
|
|
|
|
test('does not crash when summaries exceed plans (orphaned SUMMARY.md)', () => {
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
`# Roadmap v1.0 MVP\n`
|
|
);
|
|
const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
|
fs.mkdirSync(p1, { recursive: true });
|
|
// 1 plan but 2 summaries (orphaned SUMMARY.md after PLAN.md deletion)
|
|
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
|
|
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Done');
|
|
fs.writeFileSync(path.join(p1, '01-02-SUMMARY.md'), '# Orphaned summary');
|
|
|
|
// bar format - should not crash with RangeError
|
|
const barResult = runGsdTools('progress bar --raw', tmpDir);
|
|
assert.ok(barResult.success, `Bar format crashed: ${barResult.error}`);
|
|
assert.ok(barResult.output.includes('100%'), 'percent should be clamped to 100%');
|
|
|
|
// table format - should not crash with RangeError
|
|
const tableResult = runGsdTools('progress table --raw', tmpDir);
|
|
assert.ok(tableResult.success, `Table format crashed: ${tableResult.error}`);
|
|
|
|
// json format - percent should be clamped
|
|
const jsonResult = runGsdTools('progress json', tmpDir);
|
|
assert.ok(jsonResult.success, `JSON format crashed: ${jsonResult.error}`);
|
|
const output = JSON.parse(jsonResult.output);
|
|
assert.ok(output.percent <= 100, `percent should be <= 100 but got ${output.percent}`);
|
|
});
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// todo complete command
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
describe('todo complete command', () => {
|
|
let tmpDir;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = createTempProject();
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('moves todo from pending to completed', () => {
|
|
const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending');
|
|
fs.mkdirSync(pendingDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(pendingDir, 'add-dark-mode.md'),
|
|
`title: Add dark mode\narea: ui\ncreated: 2025-01-01\n`
|
|
);
|
|
|
|
const result = runGsdTools('todo complete add-dark-mode.md', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.completed, true);
|
|
|
|
// Verify moved
|
|
assert.ok(
|
|
!fs.existsSync(path.join(tmpDir, '.planning', 'todos', 'pending', 'add-dark-mode.md')),
|
|
'should be removed from pending'
|
|
);
|
|
assert.ok(
|
|
fs.existsSync(path.join(tmpDir, '.planning', 'todos', 'completed', 'add-dark-mode.md')),
|
|
'should be in completed'
|
|
);
|
|
|
|
// Verify completion timestamp added
|
|
const content = fs.readFileSync(
|
|
path.join(tmpDir, '.planning', 'todos', 'completed', 'add-dark-mode.md'),
|
|
'utf-8'
|
|
);
|
|
assert.ok(content.startsWith('completed:'), 'should have completed timestamp');
|
|
});
|
|
|
|
test('fails for nonexistent todo', () => {
|
|
const result = runGsdTools('todo complete nonexistent.md', tmpDir);
|
|
assert.ok(!result.success, 'should fail');
|
|
assert.ok(result.error.includes('not found'), 'error mentions not found');
|
|
});
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// scaffold command
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
describe('scaffold command', () => {
|
|
let tmpDir;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = createTempProject();
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('scaffolds context file', () => {
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-api'), { recursive: true });
|
|
|
|
const result = runGsdTools('scaffold context --phase 3', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.created, true);
|
|
|
|
// Verify file content
|
|
const content = fs.readFileSync(
|
|
path.join(tmpDir, '.planning', 'phases', '03-api', '03-CONTEXT.md'),
|
|
'utf-8'
|
|
);
|
|
assert.ok(content.includes('Phase 3'), 'should reference phase number');
|
|
assert.ok(content.includes('Decisions'), 'should have decisions section');
|
|
assert.ok(content.includes('Discretion Areas'), 'should have discretion section');
|
|
});
|
|
|
|
test('scaffolds UAT file', () => {
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-api'), { recursive: true });
|
|
|
|
const result = runGsdTools('scaffold uat --phase 3', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.created, true);
|
|
|
|
const content = fs.readFileSync(
|
|
path.join(tmpDir, '.planning', 'phases', '03-api', '03-UAT.md'),
|
|
'utf-8'
|
|
);
|
|
assert.ok(content.includes('User Acceptance Testing'), 'should have UAT heading');
|
|
assert.ok(content.includes('Test Results'), 'should have test results section');
|
|
});
|
|
|
|
test('scaffolds verification file', () => {
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-api'), { recursive: true });
|
|
|
|
const result = runGsdTools('scaffold verification --phase 3', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.created, true);
|
|
|
|
const content = fs.readFileSync(
|
|
path.join(tmpDir, '.planning', 'phases', '03-api', '03-VERIFICATION.md'),
|
|
'utf-8'
|
|
);
|
|
assert.ok(content.includes('Goal-Backward Verification'), 'should have verification heading');
|
|
});
|
|
|
|
test('scaffolds phase directory', () => {
|
|
const result = runGsdTools('scaffold phase-dir --phase 5 --name User Dashboard', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.created, true);
|
|
assert.ok(
|
|
fs.existsSync(path.join(tmpDir, '.planning', 'phases', '05-user-dashboard')),
|
|
'directory should be created'
|
|
);
|
|
});
|
|
|
|
test('does not overwrite existing files', () => {
|
|
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
|
|
fs.mkdirSync(phaseDir, { recursive: true });
|
|
fs.writeFileSync(path.join(phaseDir, '03-CONTEXT.md'), '# Existing content');
|
|
|
|
const result = runGsdTools('scaffold context --phase 3', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.created, false, 'should not overwrite');
|
|
assert.strictEqual(output.reason, 'already_exists');
|
|
});
|
|
});
|