Workflows were calling init commands to get parsed JSON metadata, then immediately reading the same files again with cat to pass raw content to agents. This wastes context tokens. Changes: - Add --include flag to init execute-phase, plan-phase, and progress - Support includes: state, config, roadmap, requirements, context, research, verification, uat, project - Update plan-phase.md to use --include (removes 6 cat calls) - Update execute-phase.md to use --include (removes 2 cat calls) - Update execute-plan.md to use --include (removes 2 cat calls) - Update progress.md to use --include (removes 4 cat calls) - Add 7 tests for --include functionality Token savings: ~5,000-10,000 tokens per plan-phase execution, ~1,500-3,000 per other workflow executions. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1194 lines
43 KiB
JavaScript
1194 lines
43 KiB
JavaScript
/**
|
|
* GSD Tools Tests — Schema validation for history-digest command
|
|
*/
|
|
|
|
const { test, describe, beforeEach, afterEach } = require('node:test');
|
|
const assert = require('node:assert');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { execSync } = require('child_process');
|
|
|
|
const TOOLS_PATH = path.join(__dirname, 'gsd-tools.js');
|
|
|
|
// 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 });
|
|
}
|
|
|
|
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('phases list command', () => {
|
|
let tmpDir;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = createTempProject();
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('empty phases directory returns empty array', () => {
|
|
const result = runGsdTools('phases list', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.deepStrictEqual(output.directories, [], 'directories should be empty');
|
|
assert.strictEqual(output.count, 0, 'count should be 0');
|
|
});
|
|
|
|
test('lists phase directories sorted numerically', () => {
|
|
// Create out-of-order directories
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '10-final'), { recursive: true });
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-api'), { recursive: true });
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-foundation'), { recursive: true });
|
|
|
|
const result = runGsdTools('phases list', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.count, 3, 'should have 3 directories');
|
|
assert.deepStrictEqual(
|
|
output.directories,
|
|
['01-foundation', '02-api', '10-final'],
|
|
'should be sorted numerically'
|
|
);
|
|
});
|
|
|
|
test('handles decimal phases in sort order', () => {
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-api'), { recursive: true });
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02.1-hotfix'), { recursive: true });
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02.2-patch'), { recursive: true });
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-ui'), { recursive: true });
|
|
|
|
const result = runGsdTools('phases list', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.deepStrictEqual(
|
|
output.directories,
|
|
['02-api', '02.1-hotfix', '02.2-patch', '03-ui'],
|
|
'decimal phases should sort correctly between whole numbers'
|
|
);
|
|
});
|
|
|
|
test('--type plans lists only PLAN.md files', () => {
|
|
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-test');
|
|
fs.mkdirSync(phaseDir, { recursive: true });
|
|
fs.writeFileSync(path.join(phaseDir, '01-01-PLAN.md'), '# Plan 1');
|
|
fs.writeFileSync(path.join(phaseDir, '01-02-PLAN.md'), '# Plan 2');
|
|
fs.writeFileSync(path.join(phaseDir, '01-01-SUMMARY.md'), '# Summary');
|
|
fs.writeFileSync(path.join(phaseDir, 'RESEARCH.md'), '# Research');
|
|
|
|
const result = runGsdTools('phases list --type plans', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.deepStrictEqual(
|
|
output.files.sort(),
|
|
['01-01-PLAN.md', '01-02-PLAN.md'],
|
|
'should list only PLAN files'
|
|
);
|
|
});
|
|
|
|
test('--type summaries lists only SUMMARY.md files', () => {
|
|
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-test');
|
|
fs.mkdirSync(phaseDir, { recursive: true });
|
|
fs.writeFileSync(path.join(phaseDir, '01-01-PLAN.md'), '# Plan');
|
|
fs.writeFileSync(path.join(phaseDir, '01-01-SUMMARY.md'), '# Summary 1');
|
|
fs.writeFileSync(path.join(phaseDir, '01-02-SUMMARY.md'), '# Summary 2');
|
|
|
|
const result = runGsdTools('phases list --type summaries', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.deepStrictEqual(
|
|
output.files.sort(),
|
|
['01-01-SUMMARY.md', '01-02-SUMMARY.md'],
|
|
'should list only SUMMARY files'
|
|
);
|
|
});
|
|
|
|
test('--phase filters to specific phase directory', () => {
|
|
const phase01 = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
|
const phase02 = path.join(tmpDir, '.planning', 'phases', '02-api');
|
|
fs.mkdirSync(phase01, { recursive: true });
|
|
fs.mkdirSync(phase02, { recursive: true });
|
|
fs.writeFileSync(path.join(phase01, '01-01-PLAN.md'), '# Plan');
|
|
fs.writeFileSync(path.join(phase02, '02-01-PLAN.md'), '# Plan');
|
|
|
|
const result = runGsdTools('phases list --type plans --phase 01', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.deepStrictEqual(output.files, ['01-01-PLAN.md'], 'should only list phase 01 plans');
|
|
assert.strictEqual(output.phase_dir, 'foundation', 'should report phase name without number prefix');
|
|
});
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// roadmap get-phase command
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
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');
|
|
});
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// phase next-decimal command
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe('phase next-decimal command', () => {
|
|
let tmpDir;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = createTempProject();
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('returns X.1 when no decimal phases exist', () => {
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06-feature'), { recursive: true });
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '07-next'), { recursive: true });
|
|
|
|
const result = runGsdTools('phase next-decimal 06', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.next, '06.1', 'should return 06.1');
|
|
assert.deepStrictEqual(output.existing, [], 'no existing decimals');
|
|
});
|
|
|
|
test('increments from existing decimal phases', () => {
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06-feature'), { recursive: true });
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06.1-hotfix'), { recursive: true });
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06.2-patch'), { recursive: true });
|
|
|
|
const result = runGsdTools('phase next-decimal 06', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.next, '06.3', 'should return 06.3');
|
|
assert.deepStrictEqual(output.existing, ['06.1', '06.2'], 'lists existing decimals');
|
|
});
|
|
|
|
test('handles gaps in decimal sequence', () => {
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06-feature'), { recursive: true });
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06.1-first'), { recursive: true });
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06.3-third'), { recursive: true });
|
|
|
|
const result = runGsdTools('phase next-decimal 06', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
// Should take next after highest, not fill gap
|
|
assert.strictEqual(output.next, '06.4', 'should return 06.4, not fill gap at 06.2');
|
|
});
|
|
|
|
test('handles single-digit phase input', () => {
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06-feature'), { recursive: true });
|
|
|
|
const result = runGsdTools('phase next-decimal 6', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.next, '06.1', 'should normalize to 06.1');
|
|
assert.strictEqual(output.base_phase, '06', 'base phase should be padded');
|
|
});
|
|
|
|
test('returns error if base phase does not exist', () => {
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-start'), { recursive: true });
|
|
|
|
const result = runGsdTools('phase next-decimal 06', tmpDir);
|
|
assert.ok(result.success, `Command should succeed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.found, false, 'base phase not found');
|
|
assert.strictEqual(output.next, '06.1', 'should still suggest 06.1');
|
|
});
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// phase-plan-index command
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe('phase-plan-index command', () => {
|
|
let tmpDir;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = createTempProject();
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('empty phase directory returns empty plans array', () => {
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-api'), { recursive: true });
|
|
|
|
const result = runGsdTools('phase-plan-index 03', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.phase, '03', 'phase number correct');
|
|
assert.deepStrictEqual(output.plans, [], 'plans should be empty');
|
|
assert.deepStrictEqual(output.waves, {}, 'waves should be empty');
|
|
assert.deepStrictEqual(output.incomplete, [], 'incomplete should be empty');
|
|
assert.strictEqual(output.has_checkpoints, false, 'no checkpoints');
|
|
});
|
|
|
|
test('extracts single plan with frontmatter', () => {
|
|
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
|
|
fs.mkdirSync(phaseDir, { recursive: true });
|
|
|
|
fs.writeFileSync(
|
|
path.join(phaseDir, '03-01-PLAN.md'),
|
|
`---
|
|
wave: 1
|
|
autonomous: true
|
|
objective: Set up database schema
|
|
files-modified: [prisma/schema.prisma, src/lib/db.ts]
|
|
---
|
|
|
|
## Task 1: Create schema
|
|
## Task 2: Generate client
|
|
`
|
|
);
|
|
|
|
const result = runGsdTools('phase-plan-index 03', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.plans.length, 1, 'should have 1 plan');
|
|
assert.strictEqual(output.plans[0].id, '03-01', 'plan id correct');
|
|
assert.strictEqual(output.plans[0].wave, 1, 'wave extracted');
|
|
assert.strictEqual(output.plans[0].autonomous, true, 'autonomous extracted');
|
|
assert.strictEqual(output.plans[0].objective, 'Set up database schema', 'objective extracted');
|
|
assert.deepStrictEqual(output.plans[0].files_modified, ['prisma/schema.prisma', 'src/lib/db.ts'], 'files extracted');
|
|
assert.strictEqual(output.plans[0].task_count, 2, 'task count correct');
|
|
assert.strictEqual(output.plans[0].has_summary, false, 'no summary yet');
|
|
});
|
|
|
|
test('groups multiple plans by wave', () => {
|
|
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
|
|
fs.mkdirSync(phaseDir, { recursive: true });
|
|
|
|
fs.writeFileSync(
|
|
path.join(phaseDir, '03-01-PLAN.md'),
|
|
`---
|
|
wave: 1
|
|
autonomous: true
|
|
objective: Database setup
|
|
---
|
|
|
|
## Task 1: Schema
|
|
`
|
|
);
|
|
|
|
fs.writeFileSync(
|
|
path.join(phaseDir, '03-02-PLAN.md'),
|
|
`---
|
|
wave: 1
|
|
autonomous: true
|
|
objective: Auth setup
|
|
---
|
|
|
|
## Task 1: JWT
|
|
`
|
|
);
|
|
|
|
fs.writeFileSync(
|
|
path.join(phaseDir, '03-03-PLAN.md'),
|
|
`---
|
|
wave: 2
|
|
autonomous: false
|
|
objective: API routes
|
|
---
|
|
|
|
## Task 1: Routes
|
|
`
|
|
);
|
|
|
|
const result = runGsdTools('phase-plan-index 03', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.plans.length, 3, 'should have 3 plans');
|
|
assert.deepStrictEqual(output.waves['1'], ['03-01', '03-02'], 'wave 1 has 2 plans');
|
|
assert.deepStrictEqual(output.waves['2'], ['03-03'], 'wave 2 has 1 plan');
|
|
});
|
|
|
|
test('detects incomplete plans (no matching summary)', () => {
|
|
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
|
|
fs.mkdirSync(phaseDir, { recursive: true });
|
|
|
|
// Plan with summary
|
|
fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), `---\nwave: 1\n---\n## Task 1`);
|
|
fs.writeFileSync(path.join(phaseDir, '03-01-SUMMARY.md'), `# Summary`);
|
|
|
|
// Plan without summary
|
|
fs.writeFileSync(path.join(phaseDir, '03-02-PLAN.md'), `---\nwave: 2\n---\n## Task 1`);
|
|
|
|
const result = runGsdTools('phase-plan-index 03', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.plans[0].has_summary, true, 'first plan has summary');
|
|
assert.strictEqual(output.plans[1].has_summary, false, 'second plan has no summary');
|
|
assert.deepStrictEqual(output.incomplete, ['03-02'], 'incomplete list correct');
|
|
});
|
|
|
|
test('detects checkpoints (autonomous: false)', () => {
|
|
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
|
|
fs.mkdirSync(phaseDir, { recursive: true });
|
|
|
|
fs.writeFileSync(
|
|
path.join(phaseDir, '03-01-PLAN.md'),
|
|
`---
|
|
wave: 1
|
|
autonomous: false
|
|
objective: Manual review needed
|
|
---
|
|
|
|
## Task 1: Review
|
|
`
|
|
);
|
|
|
|
const result = runGsdTools('phase-plan-index 03', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.has_checkpoints, true, 'should detect checkpoint');
|
|
assert.strictEqual(output.plans[0].autonomous, false, 'plan marked non-autonomous');
|
|
});
|
|
|
|
test('phase not found returns error', () => {
|
|
const result = runGsdTools('phase-plan-index 99', tmpDir);
|
|
assert.ok(result.success, `Command should succeed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.error, 'Phase not found', 'should report phase not found');
|
|
});
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// state-snapshot command
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
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
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
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 --include flag tests
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe('init commands with --include flag', () => {
|
|
let tmpDir;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = createTempProject();
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('init execute-phase includes state and config content', () => {
|
|
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
|
|
fs.mkdirSync(phaseDir, { recursive: true });
|
|
fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), '# Plan');
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, '.planning', 'STATE.md'),
|
|
'# State\n\n**Current Phase:** 03\n**Status:** In progress'
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, '.planning', 'config.json'),
|
|
JSON.stringify({ model_profile: 'balanced' })
|
|
);
|
|
|
|
const result = runGsdTools('init execute-phase 03 --include state,config', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.ok(output.state_content, 'state_content should be included');
|
|
assert.ok(output.state_content.includes('Current Phase'), 'state content correct');
|
|
assert.ok(output.config_content, 'config_content should be included');
|
|
assert.ok(output.config_content.includes('model_profile'), 'config content correct');
|
|
});
|
|
|
|
test('init execute-phase without --include omits content', () => {
|
|
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
|
|
fs.mkdirSync(phaseDir, { recursive: true });
|
|
fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), '# Plan');
|
|
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), '# State');
|
|
|
|
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_content, undefined, 'state_content should be omitted');
|
|
assert.strictEqual(output.config_content, undefined, 'config_content should be omitted');
|
|
});
|
|
|
|
test('init plan-phase includes multiple file contents', () => {
|
|
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
|
|
fs.mkdirSync(phaseDir, { recursive: true });
|
|
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), '# Project State');
|
|
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), '# Roadmap v1.0');
|
|
fs.writeFileSync(path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), '# Requirements');
|
|
fs.writeFileSync(path.join(phaseDir, '03-CONTEXT.md'), '# Phase Context');
|
|
fs.writeFileSync(path.join(phaseDir, '03-RESEARCH.md'), '# Research Findings');
|
|
|
|
const result = runGsdTools('init plan-phase 03 --include state,roadmap,requirements,context,research', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.ok(output.state_content, 'state_content included');
|
|
assert.ok(output.state_content.includes('Project State'), 'state content correct');
|
|
assert.ok(output.roadmap_content, 'roadmap_content included');
|
|
assert.ok(output.roadmap_content.includes('Roadmap v1.0'), 'roadmap content correct');
|
|
assert.ok(output.requirements_content, 'requirements_content included');
|
|
assert.ok(output.context_content, 'context_content included');
|
|
assert.ok(output.research_content, 'research_content included');
|
|
});
|
|
|
|
test('init plan-phase includes verification and uat content', () => {
|
|
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
|
|
fs.mkdirSync(phaseDir, { recursive: true });
|
|
fs.writeFileSync(path.join(phaseDir, '03-VERIFICATION.md'), '# Verification Results');
|
|
fs.writeFileSync(path.join(phaseDir, '03-UAT.md'), '# UAT Findings');
|
|
|
|
const result = runGsdTools('init plan-phase 03 --include verification,uat', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.ok(output.verification_content, 'verification_content included');
|
|
assert.ok(output.verification_content.includes('Verification Results'), 'verification content correct');
|
|
assert.ok(output.uat_content, 'uat_content included');
|
|
assert.ok(output.uat_content.includes('UAT Findings'), 'uat content correct');
|
|
});
|
|
|
|
test('init progress includes state, roadmap, project, config', () => {
|
|
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), '# State');
|
|
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), '# Roadmap');
|
|
fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECT.md'), '# Project');
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, '.planning', 'config.json'),
|
|
JSON.stringify({ model_profile: 'quality' })
|
|
);
|
|
|
|
const result = runGsdTools('init progress --include state,roadmap,project,config', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.ok(output.state_content, 'state_content included');
|
|
assert.ok(output.roadmap_content, 'roadmap_content included');
|
|
assert.ok(output.project_content, 'project_content included');
|
|
assert.ok(output.config_content, 'config_content included');
|
|
});
|
|
|
|
test('missing files return null in content fields', () => {
|
|
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 --include state,config', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.state_content, null, 'missing state returns null');
|
|
assert.strictEqual(output.config_content, null, 'missing config returns null');
|
|
});
|
|
|
|
test('partial includes work correctly', () => {
|
|
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
|
|
fs.mkdirSync(phaseDir, { recursive: true });
|
|
fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), '# Plan');
|
|
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), '# State');
|
|
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), '# Roadmap');
|
|
|
|
// Only request state, not roadmap
|
|
const result = runGsdTools('init execute-phase 03 --include state', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.ok(output.state_content, 'state_content included');
|
|
assert.strictEqual(output.roadmap_content, undefined, 'roadmap_content not requested, should be undefined');
|
|
});
|
|
});
|