refactor: split gsd-tools.test.cjs into domain test files
Move 81 tests (18 describe blocks) from single monolithic test file into 7 domain-specific test files under tests/ with shared helpers. Test parity verified: 81/81 pass before and after split. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
625
tests/commands.test.cjs
Normal file
625
tests/commands.test.cjs
Normal file
@@ -0,0 +1,625 @@
|
||||
/**
|
||||
* 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
|
||||
---
|
||||
|
||||
# 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');
|
||||
});
|
||||
|
||||
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
|
||||
---
|
||||
`
|
||||
);
|
||||
|
||||
const result = runGsdTools('summary-extract .planning/phases/01-foundation/01-01-SUMMARY.md --fields one_liner,key_files', 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.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');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 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');
|
||||
});
|
||||
});
|
||||
40
tests/helpers.cjs
Normal file
40
tests/helpers.cjs
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* GSD Tools Test Helpers
|
||||
*/
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const TOOLS_PATH = path.join(__dirname, '..', 'get-shit-done', 'bin', 'gsd-tools.cjs');
|
||||
|
||||
// Helper to run gsd-tools command
|
||||
function runGsdTools(args, cwd = process.cwd()) {
|
||||
try {
|
||||
const result = execSync(`node "${TOOLS_PATH}" ${args}`, {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
return { success: true, output: result.trim() };
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
output: err.stdout?.toString().trim() || '',
|
||||
error: err.stderr?.toString().trim() || err.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Create temp directory structure
|
||||
function createTempProject() {
|
||||
const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'gsd-test-'));
|
||||
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases'), { recursive: true });
|
||||
return tmpDir;
|
||||
}
|
||||
|
||||
function cleanup(tmpDir) {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
module.exports = { runGsdTools, createTempProject, cleanup, TOOLS_PATH };
|
||||
105
tests/init.test.cjs
Normal file
105
tests/init.test.cjs
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* GSD Tools Tests - Init
|
||||
*/
|
||||
|
||||
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('init commands', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('init execute-phase returns file paths', () => {
|
||||
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
|
||||
fs.mkdirSync(phaseDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), '# Plan');
|
||||
|
||||
const result = runGsdTools('init execute-phase 03', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.state_path, '.planning/STATE.md');
|
||||
assert.strictEqual(output.roadmap_path, '.planning/ROADMAP.md');
|
||||
assert.strictEqual(output.config_path, '.planning/config.json');
|
||||
});
|
||||
|
||||
test('init plan-phase returns file paths', () => {
|
||||
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
|
||||
fs.mkdirSync(phaseDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(phaseDir, '03-CONTEXT.md'), '# Phase Context');
|
||||
fs.writeFileSync(path.join(phaseDir, '03-RESEARCH.md'), '# Research Findings');
|
||||
fs.writeFileSync(path.join(phaseDir, '03-VERIFICATION.md'), '# Verification');
|
||||
fs.writeFileSync(path.join(phaseDir, '03-UAT.md'), '# UAT');
|
||||
|
||||
const result = runGsdTools('init plan-phase 03', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.state_path, '.planning/STATE.md');
|
||||
assert.strictEqual(output.roadmap_path, '.planning/ROADMAP.md');
|
||||
assert.strictEqual(output.requirements_path, '.planning/REQUIREMENTS.md');
|
||||
assert.strictEqual(output.context_path, '.planning/phases/03-api/03-CONTEXT.md');
|
||||
assert.strictEqual(output.research_path, '.planning/phases/03-api/03-RESEARCH.md');
|
||||
assert.strictEqual(output.verification_path, '.planning/phases/03-api/03-VERIFICATION.md');
|
||||
assert.strictEqual(output.uat_path, '.planning/phases/03-api/03-UAT.md');
|
||||
});
|
||||
|
||||
test('init progress returns file paths', () => {
|
||||
const result = runGsdTools('init progress', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.state_path, '.planning/STATE.md');
|
||||
assert.strictEqual(output.roadmap_path, '.planning/ROADMAP.md');
|
||||
assert.strictEqual(output.project_path, '.planning/PROJECT.md');
|
||||
assert.strictEqual(output.config_path, '.planning/config.json');
|
||||
});
|
||||
|
||||
test('init phase-op returns core and optional phase file paths', () => {
|
||||
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
|
||||
fs.mkdirSync(phaseDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(phaseDir, '03-CONTEXT.md'), '# Phase Context');
|
||||
fs.writeFileSync(path.join(phaseDir, '03-RESEARCH.md'), '# Research');
|
||||
fs.writeFileSync(path.join(phaseDir, '03-VERIFICATION.md'), '# Verification');
|
||||
fs.writeFileSync(path.join(phaseDir, '03-UAT.md'), '# UAT');
|
||||
|
||||
const result = runGsdTools('init phase-op 03', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.state_path, '.planning/STATE.md');
|
||||
assert.strictEqual(output.roadmap_path, '.planning/ROADMAP.md');
|
||||
assert.strictEqual(output.requirements_path, '.planning/REQUIREMENTS.md');
|
||||
assert.strictEqual(output.context_path, '.planning/phases/03-api/03-CONTEXT.md');
|
||||
assert.strictEqual(output.research_path, '.planning/phases/03-api/03-RESEARCH.md');
|
||||
assert.strictEqual(output.verification_path, '.planning/phases/03-api/03-VERIFICATION.md');
|
||||
assert.strictEqual(output.uat_path, '.planning/phases/03-api/03-UAT.md');
|
||||
});
|
||||
|
||||
test('init plan-phase omits optional paths if files missing', () => {
|
||||
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
|
||||
fs.mkdirSync(phaseDir, { recursive: true });
|
||||
|
||||
const result = runGsdTools('init plan-phase 03', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.context_path, undefined);
|
||||
assert.strictEqual(output.research_path, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// roadmap analyze command
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
98
tests/milestone.test.cjs
Normal file
98
tests/milestone.test.cjs
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* GSD Tools Tests - Milestone
|
||||
*/
|
||||
|
||||
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('milestone complete command', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('archives roadmap, requirements, creates MILESTONES.md', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
`# Roadmap v1.0 MVP\n\n### Phase 1: Foundation\n**Goal:** Setup\n`
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'REQUIREMENTS.md'),
|
||||
`# Requirements\n\n- [ ] User auth\n- [ ] Dashboard\n`
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'STATE.md'),
|
||||
`# State\n\n**Status:** In progress\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n`
|
||||
);
|
||||
|
||||
const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
||||
fs.mkdirSync(p1, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(p1, '01-01-SUMMARY.md'),
|
||||
`---\none-liner: Set up project infrastructure\n---\n# Summary\n`
|
||||
);
|
||||
|
||||
const result = runGsdTools('milestone complete v1.0 --name MVP Foundation', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.version, 'v1.0');
|
||||
assert.strictEqual(output.phases, 1);
|
||||
assert.ok(output.archived.roadmap, 'roadmap should be archived');
|
||||
assert.ok(output.archived.requirements, 'requirements should be archived');
|
||||
|
||||
// Verify archive files exist
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(tmpDir, '.planning', 'milestones', 'v1.0-ROADMAP.md')),
|
||||
'archived roadmap should exist'
|
||||
);
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(tmpDir, '.planning', 'milestones', 'v1.0-REQUIREMENTS.md')),
|
||||
'archived requirements should exist'
|
||||
);
|
||||
|
||||
// Verify MILESTONES.md created
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(tmpDir, '.planning', 'MILESTONES.md')),
|
||||
'MILESTONES.md should be created'
|
||||
);
|
||||
const milestones = fs.readFileSync(path.join(tmpDir, '.planning', 'MILESTONES.md'), 'utf-8');
|
||||
assert.ok(milestones.includes('v1.0 MVP Foundation'), 'milestone entry should contain name');
|
||||
assert.ok(milestones.includes('Set up project infrastructure'), 'accomplishments should be listed');
|
||||
});
|
||||
|
||||
test('appends to existing MILESTONES.md', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'MILESTONES.md'),
|
||||
`# Milestones\n\n## v0.9 Alpha (Shipped: 2025-01-01)\n\n---\n\n`
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
`# Roadmap v1.0\n`
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'STATE.md'),
|
||||
`# State\n\n**Status:** In progress\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n`
|
||||
);
|
||||
|
||||
const result = runGsdTools('milestone complete v1.0 --name Beta', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const milestones = fs.readFileSync(path.join(tmpDir, '.planning', 'MILESTONES.md'), 'utf-8');
|
||||
assert.ok(milestones.includes('v0.9 Alpha'), 'existing entry should be preserved');
|
||||
assert.ok(milestones.includes('v1.0 Beta'), 'new entry should be appended');
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// validate consistency command
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
1013
tests/phase.test.cjs
Normal file
1013
tests/phase.test.cjs
Normal file
File diff suppressed because it is too large
Load Diff
265
tests/roadmap.test.cjs
Normal file
265
tests/roadmap.test.cjs
Normal file
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* GSD Tools Tests - Roadmap
|
||||
*/
|
||||
|
||||
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('roadmap get-phase command', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('extracts phase section from ROADMAP.md', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
`# Roadmap v1.0
|
||||
|
||||
## Phases
|
||||
|
||||
### Phase 1: Foundation
|
||||
**Goal:** Set up project infrastructure
|
||||
**Plans:** 2 plans
|
||||
|
||||
Some description here.
|
||||
|
||||
### Phase 2: API
|
||||
**Goal:** Build REST API
|
||||
**Plans:** 3 plans
|
||||
`
|
||||
);
|
||||
|
||||
const result = runGsdTools('roadmap get-phase 1', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.found, true, 'phase should be found');
|
||||
assert.strictEqual(output.phase_number, '1', 'phase number correct');
|
||||
assert.strictEqual(output.phase_name, 'Foundation', 'phase name extracted');
|
||||
assert.strictEqual(output.goal, 'Set up project infrastructure', 'goal extracted');
|
||||
});
|
||||
|
||||
test('returns not found for missing phase', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
`# Roadmap v1.0
|
||||
|
||||
### Phase 1: Foundation
|
||||
**Goal:** Set up project
|
||||
`
|
||||
);
|
||||
|
||||
const result = runGsdTools('roadmap get-phase 5', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.found, false, 'phase should not be found');
|
||||
});
|
||||
|
||||
test('handles decimal phase numbers', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
`# Roadmap
|
||||
|
||||
### Phase 2: Main
|
||||
**Goal:** Main work
|
||||
|
||||
### Phase 2.1: Hotfix
|
||||
**Goal:** Emergency fix
|
||||
`
|
||||
);
|
||||
|
||||
const result = runGsdTools('roadmap get-phase 2.1', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.found, true, 'decimal phase should be found');
|
||||
assert.strictEqual(output.phase_name, 'Hotfix', 'phase name correct');
|
||||
assert.strictEqual(output.goal, 'Emergency fix', 'goal extracted');
|
||||
});
|
||||
|
||||
test('extracts full section content', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
`# Roadmap
|
||||
|
||||
### Phase 1: Setup
|
||||
**Goal:** Initialize everything
|
||||
|
||||
This phase covers:
|
||||
- Database setup
|
||||
- Auth configuration
|
||||
- CI/CD pipeline
|
||||
|
||||
### Phase 2: Build
|
||||
**Goal:** Build features
|
||||
`
|
||||
);
|
||||
|
||||
const result = runGsdTools('roadmap get-phase 1', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.ok(output.section.includes('Database setup'), 'section includes description');
|
||||
assert.ok(output.section.includes('CI/CD pipeline'), 'section includes all bullets');
|
||||
assert.ok(!output.section.includes('Phase 2'), 'section does not include next phase');
|
||||
});
|
||||
|
||||
test('handles missing ROADMAP.md gracefully', () => {
|
||||
const result = runGsdTools('roadmap get-phase 1', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.found, false, 'should return not found');
|
||||
assert.strictEqual(output.error, 'ROADMAP.md not found', 'should explain why');
|
||||
});
|
||||
|
||||
test('accepts ## phase headers (two hashes)', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
`# Roadmap v1.0
|
||||
|
||||
## Phase 1: Foundation
|
||||
**Goal:** Set up project infrastructure
|
||||
**Plans:** 2 plans
|
||||
|
||||
## Phase 2: API
|
||||
**Goal:** Build REST API
|
||||
`
|
||||
);
|
||||
|
||||
const result = runGsdTools('roadmap get-phase 1', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.found, true, 'phase with ## header should be found');
|
||||
assert.strictEqual(output.phase_name, 'Foundation', 'phase name extracted');
|
||||
assert.strictEqual(output.goal, 'Set up project infrastructure', 'goal extracted');
|
||||
});
|
||||
|
||||
test('detects malformed ROADMAP with summary list but no detail sections', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
`# Roadmap v1.0
|
||||
|
||||
## Phases
|
||||
|
||||
- [ ] **Phase 1: Foundation** - Set up project
|
||||
- [ ] **Phase 2: API** - Build REST API
|
||||
`
|
||||
);
|
||||
|
||||
const result = runGsdTools('roadmap get-phase 1', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.found, false, 'phase should not be found');
|
||||
assert.strictEqual(output.error, 'malformed_roadmap', 'should identify malformed roadmap');
|
||||
assert.ok(output.message.includes('missing'), 'should explain the issue');
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// phase next-decimal command
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
describe('roadmap analyze command', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('missing ROADMAP.md returns error', () => {
|
||||
const result = runGsdTools('roadmap analyze', tmpDir);
|
||||
assert.ok(result.success, `Command should succeed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.error, 'ROADMAP.md not found');
|
||||
});
|
||||
|
||||
test('parses phases with goals and disk status', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
`# Roadmap v1.0
|
||||
|
||||
### Phase 1: Foundation
|
||||
**Goal:** Set up infrastructure
|
||||
|
||||
### Phase 2: Authentication
|
||||
**Goal:** Add user auth
|
||||
|
||||
### Phase 3: Features
|
||||
**Goal:** Build core features
|
||||
`
|
||||
);
|
||||
|
||||
// Create phase dirs with varying completion
|
||||
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'), '# Summary');
|
||||
|
||||
const p2 = path.join(tmpDir, '.planning', 'phases', '02-authentication');
|
||||
fs.mkdirSync(p2, { recursive: true });
|
||||
fs.writeFileSync(path.join(p2, '02-01-PLAN.md'), '# Plan');
|
||||
|
||||
const result = runGsdTools('roadmap analyze', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.phase_count, 3, 'should find 3 phases');
|
||||
assert.strictEqual(output.phases[0].disk_status, 'complete', 'phase 1 complete');
|
||||
assert.strictEqual(output.phases[1].disk_status, 'planned', 'phase 2 planned');
|
||||
assert.strictEqual(output.phases[2].disk_status, 'no_directory', 'phase 3 no directory');
|
||||
assert.strictEqual(output.completed_phases, 1, '1 phase complete');
|
||||
assert.strictEqual(output.total_plans, 2, '2 total plans');
|
||||
assert.strictEqual(output.total_summaries, 1, '1 total summary');
|
||||
assert.strictEqual(output.progress_percent, 50, '50% complete');
|
||||
assert.strictEqual(output.current_phase, '2', 'current phase is 2');
|
||||
});
|
||||
|
||||
test('extracts goals and dependencies', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
`# Roadmap
|
||||
|
||||
### Phase 1: Setup
|
||||
**Goal:** Initialize project
|
||||
**Depends on:** Nothing
|
||||
|
||||
### Phase 2: Build
|
||||
**Goal:** Build features
|
||||
**Depends on:** Phase 1
|
||||
`
|
||||
);
|
||||
|
||||
const result = runGsdTools('roadmap analyze', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.phases[0].goal, 'Initialize project');
|
||||
assert.strictEqual(output.phases[0].depends_on, 'Nothing');
|
||||
assert.strictEqual(output.phases[1].goal, 'Build features');
|
||||
assert.strictEqual(output.phases[1].depends_on, 'Phase 1');
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// phase add command
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
156
tests/state.test.cjs
Normal file
156
tests/state.test.cjs
Normal file
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* GSD Tools Tests - State
|
||||
*/
|
||||
|
||||
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('state-snapshot command', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('missing STATE.md returns error', () => {
|
||||
const result = runGsdTools('state-snapshot', tmpDir);
|
||||
assert.ok(result.success, `Command should succeed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.error, 'STATE.md not found', 'should report missing file');
|
||||
});
|
||||
|
||||
test('extracts basic fields from STATE.md', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'STATE.md'),
|
||||
`# Project State
|
||||
|
||||
**Current Phase:** 03
|
||||
**Current Phase Name:** API Layer
|
||||
**Total Phases:** 6
|
||||
**Current Plan:** 03-02
|
||||
**Total Plans in Phase:** 3
|
||||
**Status:** In progress
|
||||
**Progress:** 45%
|
||||
**Last Activity:** 2024-01-15
|
||||
**Last Activity Description:** Completed 03-01-PLAN.md
|
||||
`
|
||||
);
|
||||
|
||||
const result = runGsdTools('state-snapshot', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.current_phase, '03', 'current phase extracted');
|
||||
assert.strictEqual(output.current_phase_name, 'API Layer', 'phase name extracted');
|
||||
assert.strictEqual(output.total_phases, 6, 'total phases extracted');
|
||||
assert.strictEqual(output.current_plan, '03-02', 'current plan extracted');
|
||||
assert.strictEqual(output.total_plans_in_phase, 3, 'total plans extracted');
|
||||
assert.strictEqual(output.status, 'In progress', 'status extracted');
|
||||
assert.strictEqual(output.progress_percent, 45, 'progress extracted');
|
||||
assert.strictEqual(output.last_activity, '2024-01-15', 'last activity date extracted');
|
||||
});
|
||||
|
||||
test('extracts decisions table', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'STATE.md'),
|
||||
`# Project State
|
||||
|
||||
**Current Phase:** 01
|
||||
|
||||
## Decisions Made
|
||||
|
||||
| Phase | Decision | Rationale |
|
||||
|-------|----------|-----------|
|
||||
| 01 | Use Prisma | Better DX than raw SQL |
|
||||
| 02 | JWT auth | Stateless authentication |
|
||||
`
|
||||
);
|
||||
|
||||
const result = runGsdTools('state-snapshot', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.decisions.length, 2, 'should have 2 decisions');
|
||||
assert.strictEqual(output.decisions[0].phase, '01', 'first decision phase');
|
||||
assert.strictEqual(output.decisions[0].summary, 'Use Prisma', 'first decision summary');
|
||||
assert.strictEqual(output.decisions[0].rationale, 'Better DX than raw SQL', 'first decision rationale');
|
||||
});
|
||||
|
||||
test('extracts blockers list', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'STATE.md'),
|
||||
`# Project State
|
||||
|
||||
**Current Phase:** 03
|
||||
|
||||
## Blockers
|
||||
|
||||
- Waiting for API credentials
|
||||
- Need design review for dashboard
|
||||
`
|
||||
);
|
||||
|
||||
const result = runGsdTools('state-snapshot', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.deepStrictEqual(output.blockers, [
|
||||
'Waiting for API credentials',
|
||||
'Need design review for dashboard',
|
||||
], 'blockers extracted');
|
||||
});
|
||||
|
||||
test('extracts session continuity info', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'STATE.md'),
|
||||
`# Project State
|
||||
|
||||
**Current Phase:** 03
|
||||
|
||||
## Session
|
||||
|
||||
**Last Date:** 2024-01-15
|
||||
**Stopped At:** Phase 3, Plan 2, Task 1
|
||||
**Resume File:** .planning/phases/03-api/03-02-PLAN.md
|
||||
`
|
||||
);
|
||||
|
||||
const result = runGsdTools('state-snapshot', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.session.last_date, '2024-01-15', 'session date extracted');
|
||||
assert.strictEqual(output.session.stopped_at, 'Phase 3, Plan 2, Task 1', 'stopped at extracted');
|
||||
assert.strictEqual(output.session.resume_file, '.planning/phases/03-api/03-02-PLAN.md', 'resume file extracted');
|
||||
});
|
||||
|
||||
test('handles paused_at field', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'STATE.md'),
|
||||
`# Project State
|
||||
|
||||
**Current Phase:** 03
|
||||
**Paused At:** Phase 3, Plan 1, Task 2 - mid-implementation
|
||||
`
|
||||
);
|
||||
|
||||
const result = runGsdTools('state-snapshot', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.paused_at, 'Phase 3, Plan 1, Task 2 - mid-implementation', 'paused_at extracted');
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// summary-extract command
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
80
tests/verify.test.cjs
Normal file
80
tests/verify.test.cjs
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* GSD Tools Tests - Verify
|
||||
*/
|
||||
|
||||
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('validate consistency command', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('passes for consistent project', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
`# Roadmap\n### Phase 1: A\n### Phase 2: B\n### Phase 3: C\n`
|
||||
);
|
||||
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true });
|
||||
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-b'), { recursive: true });
|
||||
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-c'), { recursive: true });
|
||||
|
||||
const result = runGsdTools('validate consistency', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.passed, true, 'should pass');
|
||||
assert.strictEqual(output.warning_count, 0, 'no warnings');
|
||||
});
|
||||
|
||||
test('warns about phase on disk but not in roadmap', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
`# Roadmap\n### Phase 1: A\n`
|
||||
);
|
||||
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true });
|
||||
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-orphan'), { recursive: true });
|
||||
|
||||
const result = runGsdTools('validate consistency', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.ok(output.warning_count > 0, 'should have warnings');
|
||||
assert.ok(
|
||||
output.warnings.some(w => w.includes('disk but not in ROADMAP')),
|
||||
'should warn about orphan directory'
|
||||
);
|
||||
});
|
||||
|
||||
test('warns about gaps in phase numbering', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
`# Roadmap\n### Phase 1: A\n### Phase 3: C\n`
|
||||
);
|
||||
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true });
|
||||
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-c'), { recursive: true });
|
||||
|
||||
const result = runGsdTools('validate consistency', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.ok(
|
||||
output.warnings.some(w => w.includes('Gap in phase numbering')),
|
||||
'should warn about gap'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// progress command
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user