gsd-codex-cli 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex/prompts/gsd-add-phase.md +44 -0
- package/.codex/prompts/gsd-add-todo.md +43 -0
- package/.codex/prompts/gsd-audit-milestone.md +43 -0
- package/.codex/prompts/gsd-check-todos.md +43 -0
- package/.codex/prompts/gsd-complete-milestone.md +43 -0
- package/.codex/prompts/gsd-debug.md +46 -0
- package/.codex/prompts/gsd-discuss-phase.md +43 -0
- package/.codex/prompts/gsd-execute-phase.md +43 -0
- package/.codex/prompts/gsd-help.md +43 -0
- package/.codex/prompts/gsd-insert-phase.md +43 -0
- package/.codex/prompts/gsd-list-phase-assumptions.md +43 -0
- package/.codex/prompts/gsd-map-codebase.md +43 -0
- package/.codex/prompts/gsd-new-milestone.md +43 -0
- package/.codex/prompts/gsd-new-project.md +43 -0
- package/.codex/prompts/gsd-pause-work.md +43 -0
- package/.codex/prompts/gsd-plan-milestone-gaps.md +43 -0
- package/.codex/prompts/gsd-plan-phase.md +43 -0
- package/.codex/prompts/gsd-progress.md +43 -0
- package/.codex/prompts/gsd-quick.md +43 -0
- package/.codex/prompts/gsd-remove-phase.md +43 -0
- package/.codex/prompts/gsd-research-phase.md +43 -0
- package/.codex/prompts/gsd-resume-work.md +43 -0
- package/.codex/prompts/gsd-set-profile.md +43 -0
- package/.codex/prompts/gsd-settings.md +43 -0
- package/.codex/prompts/gsd-update.md +43 -0
- package/.codex/prompts/gsd-verify-work.md +43 -0
- package/.codex/skills/get-shit-done-codex/SKILL.md +65 -0
- package/.codex/skills/get-shit-done-codex/references/compat.md +32 -0
- package/.codex/skills/get-shit-done-codex/references/windows.md +23 -0
- package/CHANGELOG.md +1434 -0
- package/LICENSE +21 -0
- package/README.md +690 -0
- package/agents/gsd-codebase-mapper.md +761 -0
- package/agents/gsd-debugger.md +1198 -0
- package/agents/gsd-executor.md +419 -0
- package/agents/gsd-integration-checker.md +423 -0
- package/agents/gsd-phase-researcher.md +469 -0
- package/agents/gsd-plan-checker.md +622 -0
- package/agents/gsd-planner.md +1159 -0
- package/agents/gsd-project-researcher.md +618 -0
- package/agents/gsd-research-synthesizer.md +236 -0
- package/agents/gsd-roadmapper.md +639 -0
- package/agents/gsd-verifier.md +541 -0
- package/bin/install-codex.js +100 -0
- package/bin/install.js +1806 -0
- package/commands/gsd/add-phase.md +39 -0
- package/commands/gsd/add-todo.md +42 -0
- package/commands/gsd/audit-milestone.md +42 -0
- package/commands/gsd/check-todos.md +41 -0
- package/commands/gsd/cleanup.md +18 -0
- package/commands/gsd/complete-milestone.md +136 -0
- package/commands/gsd/debug.md +162 -0
- package/commands/gsd/discuss-phase.md +87 -0
- package/commands/gsd/execute-phase.md +42 -0
- package/commands/gsd/health.md +22 -0
- package/commands/gsd/help.md +22 -0
- package/commands/gsd/insert-phase.md +33 -0
- package/commands/gsd/join-discord.md +18 -0
- package/commands/gsd/list-phase-assumptions.md +50 -0
- package/commands/gsd/map-codebase.md +71 -0
- package/commands/gsd/new-milestone.md +51 -0
- package/commands/gsd/new-project.md +42 -0
- package/commands/gsd/pause-work.md +35 -0
- package/commands/gsd/plan-milestone-gaps.md +40 -0
- package/commands/gsd/plan-phase.md +44 -0
- package/commands/gsd/progress.md +24 -0
- package/commands/gsd/quick.md +40 -0
- package/commands/gsd/reapply-patches.md +110 -0
- package/commands/gsd/remove-phase.md +32 -0
- package/commands/gsd/research-phase.md +187 -0
- package/commands/gsd/resume-work.md +40 -0
- package/commands/gsd/set-profile.md +34 -0
- package/commands/gsd/settings.md +36 -0
- package/commands/gsd/update.md +37 -0
- package/commands/gsd/verify-work.md +39 -0
- package/get-shit-done/bin/gsd-tools.cjs +5243 -0
- package/get-shit-done/bin/gsd-tools.test.cjs +2273 -0
- package/get-shit-done/references/checkpoints.md +775 -0
- package/get-shit-done/references/continuation-format.md +249 -0
- package/get-shit-done/references/decimal-phase-calculation.md +65 -0
- package/get-shit-done/references/git-integration.md +248 -0
- package/get-shit-done/references/git-planning-commit.md +38 -0
- package/get-shit-done/references/model-profile-resolution.md +34 -0
- package/get-shit-done/references/model-profiles.md +92 -0
- package/get-shit-done/references/phase-argument-parsing.md +61 -0
- package/get-shit-done/references/planning-config.md +196 -0
- package/get-shit-done/references/questioning.md +145 -0
- package/get-shit-done/references/tdd.md +263 -0
- package/get-shit-done/references/ui-brand.md +160 -0
- package/get-shit-done/references/verification-patterns.md +612 -0
- package/get-shit-done/templates/DEBUG.md +159 -0
- package/get-shit-done/templates/UAT.md +247 -0
- package/get-shit-done/templates/codebase/architecture.md +255 -0
- package/get-shit-done/templates/codebase/concerns.md +310 -0
- package/get-shit-done/templates/codebase/conventions.md +307 -0
- package/get-shit-done/templates/codebase/integrations.md +280 -0
- package/get-shit-done/templates/codebase/stack.md +186 -0
- package/get-shit-done/templates/codebase/structure.md +285 -0
- package/get-shit-done/templates/codebase/testing.md +480 -0
- package/get-shit-done/templates/config.json +36 -0
- package/get-shit-done/templates/context.md +283 -0
- package/get-shit-done/templates/continue-here.md +78 -0
- package/get-shit-done/templates/debug-subagent-prompt.md +91 -0
- package/get-shit-done/templates/discovery.md +146 -0
- package/get-shit-done/templates/milestone-archive.md +123 -0
- package/get-shit-done/templates/milestone.md +115 -0
- package/get-shit-done/templates/phase-prompt.md +567 -0
- package/get-shit-done/templates/planner-subagent-prompt.md +117 -0
- package/get-shit-done/templates/project.md +184 -0
- package/get-shit-done/templates/requirements.md +231 -0
- package/get-shit-done/templates/research-project/ARCHITECTURE.md +204 -0
- package/get-shit-done/templates/research-project/FEATURES.md +147 -0
- package/get-shit-done/templates/research-project/PITFALLS.md +200 -0
- package/get-shit-done/templates/research-project/STACK.md +120 -0
- package/get-shit-done/templates/research-project/SUMMARY.md +170 -0
- package/get-shit-done/templates/research.md +552 -0
- package/get-shit-done/templates/roadmap.md +202 -0
- package/get-shit-done/templates/state.md +176 -0
- package/get-shit-done/templates/summary-complex.md +59 -0
- package/get-shit-done/templates/summary-minimal.md +41 -0
- package/get-shit-done/templates/summary-standard.md +48 -0
- package/get-shit-done/templates/summary.md +246 -0
- package/get-shit-done/templates/user-setup.md +311 -0
- package/get-shit-done/templates/verification-report.md +322 -0
- package/get-shit-done/workflows/add-phase.md +111 -0
- package/get-shit-done/workflows/add-todo.md +157 -0
- package/get-shit-done/workflows/audit-milestone.md +242 -0
- package/get-shit-done/workflows/check-todos.md +176 -0
- package/get-shit-done/workflows/cleanup.md +152 -0
- package/get-shit-done/workflows/complete-milestone.md +674 -0
- package/get-shit-done/workflows/diagnose-issues.md +219 -0
- package/get-shit-done/workflows/discovery-phase.md +289 -0
- package/get-shit-done/workflows/discuss-phase.md +485 -0
- package/get-shit-done/workflows/execute-phase.md +408 -0
- package/get-shit-done/workflows/execute-plan.md +441 -0
- package/get-shit-done/workflows/health.md +156 -0
- package/get-shit-done/workflows/help.md +486 -0
- package/get-shit-done/workflows/insert-phase.md +129 -0
- package/get-shit-done/workflows/list-phase-assumptions.md +178 -0
- package/get-shit-done/workflows/map-codebase.md +327 -0
- package/get-shit-done/workflows/new-milestone.md +373 -0
- package/get-shit-done/workflows/new-project.md +1113 -0
- package/get-shit-done/workflows/pause-work.md +122 -0
- package/get-shit-done/workflows/plan-milestone-gaps.md +256 -0
- package/get-shit-done/workflows/plan-phase.md +448 -0
- package/get-shit-done/workflows/progress.md +393 -0
- package/get-shit-done/workflows/quick.md +444 -0
- package/get-shit-done/workflows/remove-phase.md +154 -0
- package/get-shit-done/workflows/research-phase.md +74 -0
- package/get-shit-done/workflows/resume-project.md +306 -0
- package/get-shit-done/workflows/set-profile.md +80 -0
- package/get-shit-done/workflows/settings.md +200 -0
- package/get-shit-done/workflows/transition.md +539 -0
- package/get-shit-done/workflows/update.md +214 -0
- package/get-shit-done/workflows/verify-phase.md +242 -0
- package/get-shit-done/workflows/verify-work.md +570 -0
- package/hooks/dist/gsd-check-update.js +62 -0
- package/hooks/dist/gsd-statusline.js +91 -0
- package/package.json +54 -0
- package/scripts/build-hooks.js +42 -0
|
@@ -0,0 +1,2273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSD Tools Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { test, describe, beforeEach, afterEach } = require('node:test');
|
|
6
|
+
const assert = require('node:assert');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { execSync } = require('child_process');
|
|
10
|
+
|
|
11
|
+
const TOOLS_PATH = path.join(__dirname, 'gsd-tools.cjs');
|
|
12
|
+
|
|
13
|
+
// Helper to run gsd-tools command
|
|
14
|
+
function runGsdTools(args, cwd = process.cwd()) {
|
|
15
|
+
try {
|
|
16
|
+
const result = execSync(`node "${TOOLS_PATH}" ${args}`, {
|
|
17
|
+
cwd,
|
|
18
|
+
encoding: 'utf-8',
|
|
19
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
20
|
+
});
|
|
21
|
+
return { success: true, output: result.trim() };
|
|
22
|
+
} catch (err) {
|
|
23
|
+
return {
|
|
24
|
+
success: false,
|
|
25
|
+
output: err.stdout?.toString().trim() || '',
|
|
26
|
+
error: err.stderr?.toString().trim() || err.message,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Create temp directory structure
|
|
32
|
+
function createTempProject() {
|
|
33
|
+
const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'gsd-test-'));
|
|
34
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases'), { recursive: true });
|
|
35
|
+
return tmpDir;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function cleanup(tmpDir) {
|
|
39
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('history-digest command', () => {
|
|
43
|
+
let tmpDir;
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
tmpDir = createTempProject();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
cleanup(tmpDir);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('empty phases directory returns valid schema', () => {
|
|
54
|
+
const result = runGsdTools('history-digest', tmpDir);
|
|
55
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
56
|
+
|
|
57
|
+
const digest = JSON.parse(result.output);
|
|
58
|
+
|
|
59
|
+
assert.deepStrictEqual(digest.phases, {}, 'phases should be empty object');
|
|
60
|
+
assert.deepStrictEqual(digest.decisions, [], 'decisions should be empty array');
|
|
61
|
+
assert.deepStrictEqual(digest.tech_stack, [], 'tech_stack should be empty array');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('nested frontmatter fields extracted correctly', () => {
|
|
65
|
+
// Create phase directory with SUMMARY containing nested frontmatter
|
|
66
|
+
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
|
67
|
+
fs.mkdirSync(phaseDir, { recursive: true });
|
|
68
|
+
|
|
69
|
+
const summaryContent = `---
|
|
70
|
+
phase: "01"
|
|
71
|
+
name: "Foundation Setup"
|
|
72
|
+
dependency-graph:
|
|
73
|
+
provides:
|
|
74
|
+
- "Database schema"
|
|
75
|
+
- "Auth system"
|
|
76
|
+
affects:
|
|
77
|
+
- "API layer"
|
|
78
|
+
tech-stack:
|
|
79
|
+
added:
|
|
80
|
+
- "prisma"
|
|
81
|
+
- "jose"
|
|
82
|
+
patterns-established:
|
|
83
|
+
- "Repository pattern"
|
|
84
|
+
- "JWT auth flow"
|
|
85
|
+
key-decisions:
|
|
86
|
+
- "Use Prisma over Drizzle"
|
|
87
|
+
- "JWT in httpOnly cookies"
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
# Summary content here
|
|
91
|
+
`;
|
|
92
|
+
|
|
93
|
+
fs.writeFileSync(path.join(phaseDir, '01-01-SUMMARY.md'), summaryContent);
|
|
94
|
+
|
|
95
|
+
const result = runGsdTools('history-digest', tmpDir);
|
|
96
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
97
|
+
|
|
98
|
+
const digest = JSON.parse(result.output);
|
|
99
|
+
|
|
100
|
+
// Check nested dependency-graph.provides
|
|
101
|
+
assert.ok(digest.phases['01'], 'Phase 01 should exist');
|
|
102
|
+
assert.deepStrictEqual(
|
|
103
|
+
digest.phases['01'].provides.sort(),
|
|
104
|
+
['Auth system', 'Database schema'],
|
|
105
|
+
'provides should contain nested values'
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Check nested dependency-graph.affects
|
|
109
|
+
assert.deepStrictEqual(
|
|
110
|
+
digest.phases['01'].affects,
|
|
111
|
+
['API layer'],
|
|
112
|
+
'affects should contain nested values'
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Check nested tech-stack.added
|
|
116
|
+
assert.deepStrictEqual(
|
|
117
|
+
digest.tech_stack.sort(),
|
|
118
|
+
['jose', 'prisma'],
|
|
119
|
+
'tech_stack should contain nested values'
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// Check patterns-established (flat array)
|
|
123
|
+
assert.deepStrictEqual(
|
|
124
|
+
digest.phases['01'].patterns.sort(),
|
|
125
|
+
['JWT auth flow', 'Repository pattern'],
|
|
126
|
+
'patterns should be extracted'
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// Check key-decisions
|
|
130
|
+
assert.strictEqual(digest.decisions.length, 2, 'Should have 2 decisions');
|
|
131
|
+
assert.ok(
|
|
132
|
+
digest.decisions.some(d => d.decision === 'Use Prisma over Drizzle'),
|
|
133
|
+
'Should contain first decision'
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('multiple phases merged into single digest', () => {
|
|
138
|
+
// Create phase 01
|
|
139
|
+
const phase01Dir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
|
140
|
+
fs.mkdirSync(phase01Dir, { recursive: true });
|
|
141
|
+
fs.writeFileSync(
|
|
142
|
+
path.join(phase01Dir, '01-01-SUMMARY.md'),
|
|
143
|
+
`---
|
|
144
|
+
phase: "01"
|
|
145
|
+
name: "Foundation"
|
|
146
|
+
provides:
|
|
147
|
+
- "Database"
|
|
148
|
+
patterns-established:
|
|
149
|
+
- "Pattern A"
|
|
150
|
+
key-decisions:
|
|
151
|
+
- "Decision 1"
|
|
152
|
+
---
|
|
153
|
+
`
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Create phase 02
|
|
157
|
+
const phase02Dir = path.join(tmpDir, '.planning', 'phases', '02-api');
|
|
158
|
+
fs.mkdirSync(phase02Dir, { recursive: true });
|
|
159
|
+
fs.writeFileSync(
|
|
160
|
+
path.join(phase02Dir, '02-01-SUMMARY.md'),
|
|
161
|
+
`---
|
|
162
|
+
phase: "02"
|
|
163
|
+
name: "API"
|
|
164
|
+
provides:
|
|
165
|
+
- "REST endpoints"
|
|
166
|
+
patterns-established:
|
|
167
|
+
- "Pattern B"
|
|
168
|
+
key-decisions:
|
|
169
|
+
- "Decision 2"
|
|
170
|
+
tech-stack:
|
|
171
|
+
added:
|
|
172
|
+
- "zod"
|
|
173
|
+
---
|
|
174
|
+
`
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const result = runGsdTools('history-digest', tmpDir);
|
|
178
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
179
|
+
|
|
180
|
+
const digest = JSON.parse(result.output);
|
|
181
|
+
|
|
182
|
+
// Both phases present
|
|
183
|
+
assert.ok(digest.phases['01'], 'Phase 01 should exist');
|
|
184
|
+
assert.ok(digest.phases['02'], 'Phase 02 should exist');
|
|
185
|
+
|
|
186
|
+
// Decisions merged
|
|
187
|
+
assert.strictEqual(digest.decisions.length, 2, 'Should have 2 decisions total');
|
|
188
|
+
|
|
189
|
+
// Tech stack merged
|
|
190
|
+
assert.deepStrictEqual(digest.tech_stack, ['zod'], 'tech_stack should have zod');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('malformed SUMMARY.md skipped gracefully', () => {
|
|
194
|
+
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-test');
|
|
195
|
+
fs.mkdirSync(phaseDir, { recursive: true });
|
|
196
|
+
|
|
197
|
+
// Valid summary
|
|
198
|
+
fs.writeFileSync(
|
|
199
|
+
path.join(phaseDir, '01-01-SUMMARY.md'),
|
|
200
|
+
`---
|
|
201
|
+
phase: "01"
|
|
202
|
+
provides:
|
|
203
|
+
- "Valid feature"
|
|
204
|
+
---
|
|
205
|
+
`
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// Malformed summary (no frontmatter)
|
|
209
|
+
fs.writeFileSync(
|
|
210
|
+
path.join(phaseDir, '01-02-SUMMARY.md'),
|
|
211
|
+
`# Just a heading
|
|
212
|
+
No frontmatter here
|
|
213
|
+
`
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// Another malformed summary (broken YAML)
|
|
217
|
+
fs.writeFileSync(
|
|
218
|
+
path.join(phaseDir, '01-03-SUMMARY.md'),
|
|
219
|
+
`---
|
|
220
|
+
broken: [unclosed
|
|
221
|
+
---
|
|
222
|
+
`
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const result = runGsdTools('history-digest', tmpDir);
|
|
226
|
+
assert.ok(result.success, `Command should succeed despite malformed files: ${result.error}`);
|
|
227
|
+
|
|
228
|
+
const digest = JSON.parse(result.output);
|
|
229
|
+
assert.ok(digest.phases['01'], 'Phase 01 should exist');
|
|
230
|
+
assert.ok(
|
|
231
|
+
digest.phases['01'].provides.includes('Valid feature'),
|
|
232
|
+
'Valid feature should be extracted'
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('flat provides field still works (backward compatibility)', () => {
|
|
237
|
+
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-test');
|
|
238
|
+
fs.mkdirSync(phaseDir, { recursive: true });
|
|
239
|
+
|
|
240
|
+
fs.writeFileSync(
|
|
241
|
+
path.join(phaseDir, '01-01-SUMMARY.md'),
|
|
242
|
+
`---
|
|
243
|
+
phase: "01"
|
|
244
|
+
provides:
|
|
245
|
+
- "Direct provides"
|
|
246
|
+
---
|
|
247
|
+
`
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const result = runGsdTools('history-digest', tmpDir);
|
|
251
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
252
|
+
|
|
253
|
+
const digest = JSON.parse(result.output);
|
|
254
|
+
assert.deepStrictEqual(
|
|
255
|
+
digest.phases['01'].provides,
|
|
256
|
+
['Direct provides'],
|
|
257
|
+
'Direct provides should work'
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('inline array syntax supported', () => {
|
|
262
|
+
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-test');
|
|
263
|
+
fs.mkdirSync(phaseDir, { recursive: true });
|
|
264
|
+
|
|
265
|
+
fs.writeFileSync(
|
|
266
|
+
path.join(phaseDir, '01-01-SUMMARY.md'),
|
|
267
|
+
`---
|
|
268
|
+
phase: "01"
|
|
269
|
+
provides: [Feature A, Feature B]
|
|
270
|
+
patterns-established: ["Pattern X", "Pattern Y"]
|
|
271
|
+
---
|
|
272
|
+
`
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const result = runGsdTools('history-digest', tmpDir);
|
|
276
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
277
|
+
|
|
278
|
+
const digest = JSON.parse(result.output);
|
|
279
|
+
assert.deepStrictEqual(
|
|
280
|
+
digest.phases['01'].provides.sort(),
|
|
281
|
+
['Feature A', 'Feature B'],
|
|
282
|
+
'Inline array should work'
|
|
283
|
+
);
|
|
284
|
+
assert.deepStrictEqual(
|
|
285
|
+
digest.phases['01'].patterns.sort(),
|
|
286
|
+
['Pattern X', 'Pattern Y'],
|
|
287
|
+
'Inline quoted array should work'
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
293
|
+
// phases list command
|
|
294
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
describe('phases list command', () => {
|
|
297
|
+
let tmpDir;
|
|
298
|
+
|
|
299
|
+
beforeEach(() => {
|
|
300
|
+
tmpDir = createTempProject();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
afterEach(() => {
|
|
304
|
+
cleanup(tmpDir);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test('empty phases directory returns empty array', () => {
|
|
308
|
+
const result = runGsdTools('phases list', tmpDir);
|
|
309
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
310
|
+
|
|
311
|
+
const output = JSON.parse(result.output);
|
|
312
|
+
assert.deepStrictEqual(output.directories, [], 'directories should be empty');
|
|
313
|
+
assert.strictEqual(output.count, 0, 'count should be 0');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test('lists phase directories sorted numerically', () => {
|
|
317
|
+
// Create out-of-order directories
|
|
318
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '10-final'), { recursive: true });
|
|
319
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-api'), { recursive: true });
|
|
320
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-foundation'), { recursive: true });
|
|
321
|
+
|
|
322
|
+
const result = runGsdTools('phases list', tmpDir);
|
|
323
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
324
|
+
|
|
325
|
+
const output = JSON.parse(result.output);
|
|
326
|
+
assert.strictEqual(output.count, 3, 'should have 3 directories');
|
|
327
|
+
assert.deepStrictEqual(
|
|
328
|
+
output.directories,
|
|
329
|
+
['01-foundation', '02-api', '10-final'],
|
|
330
|
+
'should be sorted numerically'
|
|
331
|
+
);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test('handles decimal phases in sort order', () => {
|
|
335
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-api'), { recursive: true });
|
|
336
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02.1-hotfix'), { recursive: true });
|
|
337
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02.2-patch'), { recursive: true });
|
|
338
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-ui'), { recursive: true });
|
|
339
|
+
|
|
340
|
+
const result = runGsdTools('phases list', tmpDir);
|
|
341
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
342
|
+
|
|
343
|
+
const output = JSON.parse(result.output);
|
|
344
|
+
assert.deepStrictEqual(
|
|
345
|
+
output.directories,
|
|
346
|
+
['02-api', '02.1-hotfix', '02.2-patch', '03-ui'],
|
|
347
|
+
'decimal phases should sort correctly between whole numbers'
|
|
348
|
+
);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test('--type plans lists only PLAN.md files', () => {
|
|
352
|
+
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-test');
|
|
353
|
+
fs.mkdirSync(phaseDir, { recursive: true });
|
|
354
|
+
fs.writeFileSync(path.join(phaseDir, '01-01-PLAN.md'), '# Plan 1');
|
|
355
|
+
fs.writeFileSync(path.join(phaseDir, '01-02-PLAN.md'), '# Plan 2');
|
|
356
|
+
fs.writeFileSync(path.join(phaseDir, '01-01-SUMMARY.md'), '# Summary');
|
|
357
|
+
fs.writeFileSync(path.join(phaseDir, 'RESEARCH.md'), '# Research');
|
|
358
|
+
|
|
359
|
+
const result = runGsdTools('phases list --type plans', tmpDir);
|
|
360
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
361
|
+
|
|
362
|
+
const output = JSON.parse(result.output);
|
|
363
|
+
assert.deepStrictEqual(
|
|
364
|
+
output.files.sort(),
|
|
365
|
+
['01-01-PLAN.md', '01-02-PLAN.md'],
|
|
366
|
+
'should list only PLAN files'
|
|
367
|
+
);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test('--type summaries lists only SUMMARY.md files', () => {
|
|
371
|
+
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-test');
|
|
372
|
+
fs.mkdirSync(phaseDir, { recursive: true });
|
|
373
|
+
fs.writeFileSync(path.join(phaseDir, '01-01-PLAN.md'), '# Plan');
|
|
374
|
+
fs.writeFileSync(path.join(phaseDir, '01-01-SUMMARY.md'), '# Summary 1');
|
|
375
|
+
fs.writeFileSync(path.join(phaseDir, '01-02-SUMMARY.md'), '# Summary 2');
|
|
376
|
+
|
|
377
|
+
const result = runGsdTools('phases list --type summaries', tmpDir);
|
|
378
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
379
|
+
|
|
380
|
+
const output = JSON.parse(result.output);
|
|
381
|
+
assert.deepStrictEqual(
|
|
382
|
+
output.files.sort(),
|
|
383
|
+
['01-01-SUMMARY.md', '01-02-SUMMARY.md'],
|
|
384
|
+
'should list only SUMMARY files'
|
|
385
|
+
);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test('--phase filters to specific phase directory', () => {
|
|
389
|
+
const phase01 = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
|
390
|
+
const phase02 = path.join(tmpDir, '.planning', 'phases', '02-api');
|
|
391
|
+
fs.mkdirSync(phase01, { recursive: true });
|
|
392
|
+
fs.mkdirSync(phase02, { recursive: true });
|
|
393
|
+
fs.writeFileSync(path.join(phase01, '01-01-PLAN.md'), '# Plan');
|
|
394
|
+
fs.writeFileSync(path.join(phase02, '02-01-PLAN.md'), '# Plan');
|
|
395
|
+
|
|
396
|
+
const result = runGsdTools('phases list --type plans --phase 01', tmpDir);
|
|
397
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
398
|
+
|
|
399
|
+
const output = JSON.parse(result.output);
|
|
400
|
+
assert.deepStrictEqual(output.files, ['01-01-PLAN.md'], 'should only list phase 01 plans');
|
|
401
|
+
assert.strictEqual(output.phase_dir, 'foundation', 'should report phase name without number prefix');
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
406
|
+
// roadmap get-phase command
|
|
407
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
408
|
+
|
|
409
|
+
describe('roadmap get-phase command', () => {
|
|
410
|
+
let tmpDir;
|
|
411
|
+
|
|
412
|
+
beforeEach(() => {
|
|
413
|
+
tmpDir = createTempProject();
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
afterEach(() => {
|
|
417
|
+
cleanup(tmpDir);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test('extracts phase section from ROADMAP.md', () => {
|
|
421
|
+
fs.writeFileSync(
|
|
422
|
+
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
423
|
+
`# Roadmap v1.0
|
|
424
|
+
|
|
425
|
+
## Phases
|
|
426
|
+
|
|
427
|
+
### Phase 1: Foundation
|
|
428
|
+
**Goal:** Set up project infrastructure
|
|
429
|
+
**Plans:** 2 plans
|
|
430
|
+
|
|
431
|
+
Some description here.
|
|
432
|
+
|
|
433
|
+
### Phase 2: API
|
|
434
|
+
**Goal:** Build REST API
|
|
435
|
+
**Plans:** 3 plans
|
|
436
|
+
`
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
const result = runGsdTools('roadmap get-phase 1', tmpDir);
|
|
440
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
441
|
+
|
|
442
|
+
const output = JSON.parse(result.output);
|
|
443
|
+
assert.strictEqual(output.found, true, 'phase should be found');
|
|
444
|
+
assert.strictEqual(output.phase_number, '1', 'phase number correct');
|
|
445
|
+
assert.strictEqual(output.phase_name, 'Foundation', 'phase name extracted');
|
|
446
|
+
assert.strictEqual(output.goal, 'Set up project infrastructure', 'goal extracted');
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test('returns not found for missing phase', () => {
|
|
450
|
+
fs.writeFileSync(
|
|
451
|
+
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
452
|
+
`# Roadmap v1.0
|
|
453
|
+
|
|
454
|
+
### Phase 1: Foundation
|
|
455
|
+
**Goal:** Set up project
|
|
456
|
+
`
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
const result = runGsdTools('roadmap get-phase 5', tmpDir);
|
|
460
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
461
|
+
|
|
462
|
+
const output = JSON.parse(result.output);
|
|
463
|
+
assert.strictEqual(output.found, false, 'phase should not be found');
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test('handles decimal phase numbers', () => {
|
|
467
|
+
fs.writeFileSync(
|
|
468
|
+
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
469
|
+
`# Roadmap
|
|
470
|
+
|
|
471
|
+
### Phase 2: Main
|
|
472
|
+
**Goal:** Main work
|
|
473
|
+
|
|
474
|
+
### Phase 2.1: Hotfix
|
|
475
|
+
**Goal:** Emergency fix
|
|
476
|
+
`
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
const result = runGsdTools('roadmap get-phase 2.1', tmpDir);
|
|
480
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
481
|
+
|
|
482
|
+
const output = JSON.parse(result.output);
|
|
483
|
+
assert.strictEqual(output.found, true, 'decimal phase should be found');
|
|
484
|
+
assert.strictEqual(output.phase_name, 'Hotfix', 'phase name correct');
|
|
485
|
+
assert.strictEqual(output.goal, 'Emergency fix', 'goal extracted');
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
test('extracts full section content', () => {
|
|
489
|
+
fs.writeFileSync(
|
|
490
|
+
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
491
|
+
`# Roadmap
|
|
492
|
+
|
|
493
|
+
### Phase 1: Setup
|
|
494
|
+
**Goal:** Initialize everything
|
|
495
|
+
|
|
496
|
+
This phase covers:
|
|
497
|
+
- Database setup
|
|
498
|
+
- Auth configuration
|
|
499
|
+
- CI/CD pipeline
|
|
500
|
+
|
|
501
|
+
### Phase 2: Build
|
|
502
|
+
**Goal:** Build features
|
|
503
|
+
`
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
const result = runGsdTools('roadmap get-phase 1', tmpDir);
|
|
507
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
508
|
+
|
|
509
|
+
const output = JSON.parse(result.output);
|
|
510
|
+
assert.ok(output.section.includes('Database setup'), 'section includes description');
|
|
511
|
+
assert.ok(output.section.includes('CI/CD pipeline'), 'section includes all bullets');
|
|
512
|
+
assert.ok(!output.section.includes('Phase 2'), 'section does not include next phase');
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
test('handles missing ROADMAP.md gracefully', () => {
|
|
516
|
+
const result = runGsdTools('roadmap get-phase 1', tmpDir);
|
|
517
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
518
|
+
|
|
519
|
+
const output = JSON.parse(result.output);
|
|
520
|
+
assert.strictEqual(output.found, false, 'should return not found');
|
|
521
|
+
assert.strictEqual(output.error, 'ROADMAP.md not found', 'should explain why');
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test('accepts ## phase headers (two hashes)', () => {
|
|
525
|
+
fs.writeFileSync(
|
|
526
|
+
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
527
|
+
`# Roadmap v1.0
|
|
528
|
+
|
|
529
|
+
## Phase 1: Foundation
|
|
530
|
+
**Goal:** Set up project infrastructure
|
|
531
|
+
**Plans:** 2 plans
|
|
532
|
+
|
|
533
|
+
## Phase 2: API
|
|
534
|
+
**Goal:** Build REST API
|
|
535
|
+
`
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
const result = runGsdTools('roadmap get-phase 1', tmpDir);
|
|
539
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
540
|
+
|
|
541
|
+
const output = JSON.parse(result.output);
|
|
542
|
+
assert.strictEqual(output.found, true, 'phase with ## header should be found');
|
|
543
|
+
assert.strictEqual(output.phase_name, 'Foundation', 'phase name extracted');
|
|
544
|
+
assert.strictEqual(output.goal, 'Set up project infrastructure', 'goal extracted');
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test('detects malformed ROADMAP with summary list but no detail sections', () => {
|
|
548
|
+
fs.writeFileSync(
|
|
549
|
+
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
550
|
+
`# Roadmap v1.0
|
|
551
|
+
|
|
552
|
+
## Phases
|
|
553
|
+
|
|
554
|
+
- [ ] **Phase 1: Foundation** - Set up project
|
|
555
|
+
- [ ] **Phase 2: API** - Build REST API
|
|
556
|
+
`
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
const result = runGsdTools('roadmap get-phase 1', tmpDir);
|
|
560
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
561
|
+
|
|
562
|
+
const output = JSON.parse(result.output);
|
|
563
|
+
assert.strictEqual(output.found, false, 'phase should not be found');
|
|
564
|
+
assert.strictEqual(output.error, 'malformed_roadmap', 'should identify malformed roadmap');
|
|
565
|
+
assert.ok(output.message.includes('missing'), 'should explain the issue');
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
570
|
+
// phase next-decimal command
|
|
571
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
572
|
+
|
|
573
|
+
describe('phase next-decimal command', () => {
|
|
574
|
+
let tmpDir;
|
|
575
|
+
|
|
576
|
+
beforeEach(() => {
|
|
577
|
+
tmpDir = createTempProject();
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
afterEach(() => {
|
|
581
|
+
cleanup(tmpDir);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
test('returns X.1 when no decimal phases exist', () => {
|
|
585
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06-feature'), { recursive: true });
|
|
586
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '07-next'), { recursive: true });
|
|
587
|
+
|
|
588
|
+
const result = runGsdTools('phase next-decimal 06', tmpDir);
|
|
589
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
590
|
+
|
|
591
|
+
const output = JSON.parse(result.output);
|
|
592
|
+
assert.strictEqual(output.next, '06.1', 'should return 06.1');
|
|
593
|
+
assert.deepStrictEqual(output.existing, [], 'no existing decimals');
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
test('increments from existing decimal phases', () => {
|
|
597
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06-feature'), { recursive: true });
|
|
598
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06.1-hotfix'), { recursive: true });
|
|
599
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06.2-patch'), { recursive: true });
|
|
600
|
+
|
|
601
|
+
const result = runGsdTools('phase next-decimal 06', tmpDir);
|
|
602
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
603
|
+
|
|
604
|
+
const output = JSON.parse(result.output);
|
|
605
|
+
assert.strictEqual(output.next, '06.3', 'should return 06.3');
|
|
606
|
+
assert.deepStrictEqual(output.existing, ['06.1', '06.2'], 'lists existing decimals');
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
test('handles gaps in decimal sequence', () => {
|
|
610
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06-feature'), { recursive: true });
|
|
611
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06.1-first'), { recursive: true });
|
|
612
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06.3-third'), { recursive: true });
|
|
613
|
+
|
|
614
|
+
const result = runGsdTools('phase next-decimal 06', tmpDir);
|
|
615
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
616
|
+
|
|
617
|
+
const output = JSON.parse(result.output);
|
|
618
|
+
// Should take next after highest, not fill gap
|
|
619
|
+
assert.strictEqual(output.next, '06.4', 'should return 06.4, not fill gap at 06.2');
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
test('handles single-digit phase input', () => {
|
|
623
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06-feature'), { recursive: true });
|
|
624
|
+
|
|
625
|
+
const result = runGsdTools('phase next-decimal 6', tmpDir);
|
|
626
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
627
|
+
|
|
628
|
+
const output = JSON.parse(result.output);
|
|
629
|
+
assert.strictEqual(output.next, '06.1', 'should normalize to 06.1');
|
|
630
|
+
assert.strictEqual(output.base_phase, '06', 'base phase should be padded');
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
test('returns error if base phase does not exist', () => {
|
|
634
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-start'), { recursive: true });
|
|
635
|
+
|
|
636
|
+
const result = runGsdTools('phase next-decimal 06', tmpDir);
|
|
637
|
+
assert.ok(result.success, `Command should succeed: ${result.error}`);
|
|
638
|
+
|
|
639
|
+
const output = JSON.parse(result.output);
|
|
640
|
+
assert.strictEqual(output.found, false, 'base phase not found');
|
|
641
|
+
assert.strictEqual(output.next, '06.1', 'should still suggest 06.1');
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
646
|
+
// phase-plan-index command
|
|
647
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
648
|
+
|
|
649
|
+
describe('phase-plan-index command', () => {
|
|
650
|
+
let tmpDir;
|
|
651
|
+
|
|
652
|
+
beforeEach(() => {
|
|
653
|
+
tmpDir = createTempProject();
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
afterEach(() => {
|
|
657
|
+
cleanup(tmpDir);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
test('empty phase directory returns empty plans array', () => {
|
|
661
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-api'), { recursive: true });
|
|
662
|
+
|
|
663
|
+
const result = runGsdTools('phase-plan-index 03', tmpDir);
|
|
664
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
665
|
+
|
|
666
|
+
const output = JSON.parse(result.output);
|
|
667
|
+
assert.strictEqual(output.phase, '03', 'phase number correct');
|
|
668
|
+
assert.deepStrictEqual(output.plans, [], 'plans should be empty');
|
|
669
|
+
assert.deepStrictEqual(output.waves, {}, 'waves should be empty');
|
|
670
|
+
assert.deepStrictEqual(output.incomplete, [], 'incomplete should be empty');
|
|
671
|
+
assert.strictEqual(output.has_checkpoints, false, 'no checkpoints');
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
test('extracts single plan with frontmatter', () => {
|
|
675
|
+
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
|
|
676
|
+
fs.mkdirSync(phaseDir, { recursive: true });
|
|
677
|
+
|
|
678
|
+
fs.writeFileSync(
|
|
679
|
+
path.join(phaseDir, '03-01-PLAN.md'),
|
|
680
|
+
`---
|
|
681
|
+
wave: 1
|
|
682
|
+
autonomous: true
|
|
683
|
+
objective: Set up database schema
|
|
684
|
+
files-modified: [prisma/schema.prisma, src/lib/db.ts]
|
|
685
|
+
---
|
|
686
|
+
|
|
687
|
+
## Task 1: Create schema
|
|
688
|
+
## Task 2: Generate client
|
|
689
|
+
`
|
|
690
|
+
);
|
|
691
|
+
|
|
692
|
+
const result = runGsdTools('phase-plan-index 03', tmpDir);
|
|
693
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
694
|
+
|
|
695
|
+
const output = JSON.parse(result.output);
|
|
696
|
+
assert.strictEqual(output.plans.length, 1, 'should have 1 plan');
|
|
697
|
+
assert.strictEqual(output.plans[0].id, '03-01', 'plan id correct');
|
|
698
|
+
assert.strictEqual(output.plans[0].wave, 1, 'wave extracted');
|
|
699
|
+
assert.strictEqual(output.plans[0].autonomous, true, 'autonomous extracted');
|
|
700
|
+
assert.strictEqual(output.plans[0].objective, 'Set up database schema', 'objective extracted');
|
|
701
|
+
assert.deepStrictEqual(output.plans[0].files_modified, ['prisma/schema.prisma', 'src/lib/db.ts'], 'files extracted');
|
|
702
|
+
assert.strictEqual(output.plans[0].task_count, 2, 'task count correct');
|
|
703
|
+
assert.strictEqual(output.plans[0].has_summary, false, 'no summary yet');
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
test('groups multiple plans by wave', () => {
|
|
707
|
+
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
|
|
708
|
+
fs.mkdirSync(phaseDir, { recursive: true });
|
|
709
|
+
|
|
710
|
+
fs.writeFileSync(
|
|
711
|
+
path.join(phaseDir, '03-01-PLAN.md'),
|
|
712
|
+
`---
|
|
713
|
+
wave: 1
|
|
714
|
+
autonomous: true
|
|
715
|
+
objective: Database setup
|
|
716
|
+
---
|
|
717
|
+
|
|
718
|
+
## Task 1: Schema
|
|
719
|
+
`
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
fs.writeFileSync(
|
|
723
|
+
path.join(phaseDir, '03-02-PLAN.md'),
|
|
724
|
+
`---
|
|
725
|
+
wave: 1
|
|
726
|
+
autonomous: true
|
|
727
|
+
objective: Auth setup
|
|
728
|
+
---
|
|
729
|
+
|
|
730
|
+
## Task 1: JWT
|
|
731
|
+
`
|
|
732
|
+
);
|
|
733
|
+
|
|
734
|
+
fs.writeFileSync(
|
|
735
|
+
path.join(phaseDir, '03-03-PLAN.md'),
|
|
736
|
+
`---
|
|
737
|
+
wave: 2
|
|
738
|
+
autonomous: false
|
|
739
|
+
objective: API routes
|
|
740
|
+
---
|
|
741
|
+
|
|
742
|
+
## Task 1: Routes
|
|
743
|
+
`
|
|
744
|
+
);
|
|
745
|
+
|
|
746
|
+
const result = runGsdTools('phase-plan-index 03', tmpDir);
|
|
747
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
748
|
+
|
|
749
|
+
const output = JSON.parse(result.output);
|
|
750
|
+
assert.strictEqual(output.plans.length, 3, 'should have 3 plans');
|
|
751
|
+
assert.deepStrictEqual(output.waves['1'], ['03-01', '03-02'], 'wave 1 has 2 plans');
|
|
752
|
+
assert.deepStrictEqual(output.waves['2'], ['03-03'], 'wave 2 has 1 plan');
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
test('detects incomplete plans (no matching summary)', () => {
|
|
756
|
+
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
|
|
757
|
+
fs.mkdirSync(phaseDir, { recursive: true });
|
|
758
|
+
|
|
759
|
+
// Plan with summary
|
|
760
|
+
fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), `---\nwave: 1\n---\n## Task 1`);
|
|
761
|
+
fs.writeFileSync(path.join(phaseDir, '03-01-SUMMARY.md'), `# Summary`);
|
|
762
|
+
|
|
763
|
+
// Plan without summary
|
|
764
|
+
fs.writeFileSync(path.join(phaseDir, '03-02-PLAN.md'), `---\nwave: 2\n---\n## Task 1`);
|
|
765
|
+
|
|
766
|
+
const result = runGsdTools('phase-plan-index 03', tmpDir);
|
|
767
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
768
|
+
|
|
769
|
+
const output = JSON.parse(result.output);
|
|
770
|
+
assert.strictEqual(output.plans[0].has_summary, true, 'first plan has summary');
|
|
771
|
+
assert.strictEqual(output.plans[1].has_summary, false, 'second plan has no summary');
|
|
772
|
+
assert.deepStrictEqual(output.incomplete, ['03-02'], 'incomplete list correct');
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
test('detects checkpoints (autonomous: false)', () => {
|
|
776
|
+
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
|
|
777
|
+
fs.mkdirSync(phaseDir, { recursive: true });
|
|
778
|
+
|
|
779
|
+
fs.writeFileSync(
|
|
780
|
+
path.join(phaseDir, '03-01-PLAN.md'),
|
|
781
|
+
`---
|
|
782
|
+
wave: 1
|
|
783
|
+
autonomous: false
|
|
784
|
+
objective: Manual review needed
|
|
785
|
+
---
|
|
786
|
+
|
|
787
|
+
## Task 1: Review
|
|
788
|
+
`
|
|
789
|
+
);
|
|
790
|
+
|
|
791
|
+
const result = runGsdTools('phase-plan-index 03', tmpDir);
|
|
792
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
793
|
+
|
|
794
|
+
const output = JSON.parse(result.output);
|
|
795
|
+
assert.strictEqual(output.has_checkpoints, true, 'should detect checkpoint');
|
|
796
|
+
assert.strictEqual(output.plans[0].autonomous, false, 'plan marked non-autonomous');
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
test('phase not found returns error', () => {
|
|
800
|
+
const result = runGsdTools('phase-plan-index 99', tmpDir);
|
|
801
|
+
assert.ok(result.success, `Command should succeed: ${result.error}`);
|
|
802
|
+
|
|
803
|
+
const output = JSON.parse(result.output);
|
|
804
|
+
assert.strictEqual(output.error, 'Phase not found', 'should report phase not found');
|
|
805
|
+
});
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
809
|
+
// state-snapshot command
|
|
810
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
811
|
+
|
|
812
|
+
describe('state-snapshot command', () => {
|
|
813
|
+
let tmpDir;
|
|
814
|
+
|
|
815
|
+
beforeEach(() => {
|
|
816
|
+
tmpDir = createTempProject();
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
afterEach(() => {
|
|
820
|
+
cleanup(tmpDir);
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
test('missing STATE.md returns error', () => {
|
|
824
|
+
const result = runGsdTools('state-snapshot', tmpDir);
|
|
825
|
+
assert.ok(result.success, `Command should succeed: ${result.error}`);
|
|
826
|
+
|
|
827
|
+
const output = JSON.parse(result.output);
|
|
828
|
+
assert.strictEqual(output.error, 'STATE.md not found', 'should report missing file');
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
test('extracts basic fields from STATE.md', () => {
|
|
832
|
+
fs.writeFileSync(
|
|
833
|
+
path.join(tmpDir, '.planning', 'STATE.md'),
|
|
834
|
+
`# Project State
|
|
835
|
+
|
|
836
|
+
**Current Phase:** 03
|
|
837
|
+
**Current Phase Name:** API Layer
|
|
838
|
+
**Total Phases:** 6
|
|
839
|
+
**Current Plan:** 03-02
|
|
840
|
+
**Total Plans in Phase:** 3
|
|
841
|
+
**Status:** In progress
|
|
842
|
+
**Progress:** 45%
|
|
843
|
+
**Last Activity:** 2024-01-15
|
|
844
|
+
**Last Activity Description:** Completed 03-01-PLAN.md
|
|
845
|
+
`
|
|
846
|
+
);
|
|
847
|
+
|
|
848
|
+
const result = runGsdTools('state-snapshot', tmpDir);
|
|
849
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
850
|
+
|
|
851
|
+
const output = JSON.parse(result.output);
|
|
852
|
+
assert.strictEqual(output.current_phase, '03', 'current phase extracted');
|
|
853
|
+
assert.strictEqual(output.current_phase_name, 'API Layer', 'phase name extracted');
|
|
854
|
+
assert.strictEqual(output.total_phases, 6, 'total phases extracted');
|
|
855
|
+
assert.strictEqual(output.current_plan, '03-02', 'current plan extracted');
|
|
856
|
+
assert.strictEqual(output.total_plans_in_phase, 3, 'total plans extracted');
|
|
857
|
+
assert.strictEqual(output.status, 'In progress', 'status extracted');
|
|
858
|
+
assert.strictEqual(output.progress_percent, 45, 'progress extracted');
|
|
859
|
+
assert.strictEqual(output.last_activity, '2024-01-15', 'last activity date extracted');
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
test('extracts decisions table', () => {
|
|
863
|
+
fs.writeFileSync(
|
|
864
|
+
path.join(tmpDir, '.planning', 'STATE.md'),
|
|
865
|
+
`# Project State
|
|
866
|
+
|
|
867
|
+
**Current Phase:** 01
|
|
868
|
+
|
|
869
|
+
## Decisions Made
|
|
870
|
+
|
|
871
|
+
| Phase | Decision | Rationale |
|
|
872
|
+
|-------|----------|-----------|
|
|
873
|
+
| 01 | Use Prisma | Better DX than raw SQL |
|
|
874
|
+
| 02 | JWT auth | Stateless authentication |
|
|
875
|
+
`
|
|
876
|
+
);
|
|
877
|
+
|
|
878
|
+
const result = runGsdTools('state-snapshot', tmpDir);
|
|
879
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
880
|
+
|
|
881
|
+
const output = JSON.parse(result.output);
|
|
882
|
+
assert.strictEqual(output.decisions.length, 2, 'should have 2 decisions');
|
|
883
|
+
assert.strictEqual(output.decisions[0].phase, '01', 'first decision phase');
|
|
884
|
+
assert.strictEqual(output.decisions[0].summary, 'Use Prisma', 'first decision summary');
|
|
885
|
+
assert.strictEqual(output.decisions[0].rationale, 'Better DX than raw SQL', 'first decision rationale');
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
test('extracts blockers list', () => {
|
|
889
|
+
fs.writeFileSync(
|
|
890
|
+
path.join(tmpDir, '.planning', 'STATE.md'),
|
|
891
|
+
`# Project State
|
|
892
|
+
|
|
893
|
+
**Current Phase:** 03
|
|
894
|
+
|
|
895
|
+
## Blockers
|
|
896
|
+
|
|
897
|
+
- Waiting for API credentials
|
|
898
|
+
- Need design review for dashboard
|
|
899
|
+
`
|
|
900
|
+
);
|
|
901
|
+
|
|
902
|
+
const result = runGsdTools('state-snapshot', tmpDir);
|
|
903
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
904
|
+
|
|
905
|
+
const output = JSON.parse(result.output);
|
|
906
|
+
assert.deepStrictEqual(output.blockers, [
|
|
907
|
+
'Waiting for API credentials',
|
|
908
|
+
'Need design review for dashboard',
|
|
909
|
+
], 'blockers extracted');
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
test('extracts session continuity info', () => {
|
|
913
|
+
fs.writeFileSync(
|
|
914
|
+
path.join(tmpDir, '.planning', 'STATE.md'),
|
|
915
|
+
`# Project State
|
|
916
|
+
|
|
917
|
+
**Current Phase:** 03
|
|
918
|
+
|
|
919
|
+
## Session
|
|
920
|
+
|
|
921
|
+
**Last Date:** 2024-01-15
|
|
922
|
+
**Stopped At:** Phase 3, Plan 2, Task 1
|
|
923
|
+
**Resume File:** .planning/phases/03-api/03-02-PLAN.md
|
|
924
|
+
`
|
|
925
|
+
);
|
|
926
|
+
|
|
927
|
+
const result = runGsdTools('state-snapshot', tmpDir);
|
|
928
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
929
|
+
|
|
930
|
+
const output = JSON.parse(result.output);
|
|
931
|
+
assert.strictEqual(output.session.last_date, '2024-01-15', 'session date extracted');
|
|
932
|
+
assert.strictEqual(output.session.stopped_at, 'Phase 3, Plan 2, Task 1', 'stopped at extracted');
|
|
933
|
+
assert.strictEqual(output.session.resume_file, '.planning/phases/03-api/03-02-PLAN.md', 'resume file extracted');
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
test('handles paused_at field', () => {
|
|
937
|
+
fs.writeFileSync(
|
|
938
|
+
path.join(tmpDir, '.planning', 'STATE.md'),
|
|
939
|
+
`# Project State
|
|
940
|
+
|
|
941
|
+
**Current Phase:** 03
|
|
942
|
+
**Paused At:** Phase 3, Plan 1, Task 2 - mid-implementation
|
|
943
|
+
`
|
|
944
|
+
);
|
|
945
|
+
|
|
946
|
+
const result = runGsdTools('state-snapshot', tmpDir);
|
|
947
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
948
|
+
|
|
949
|
+
const output = JSON.parse(result.output);
|
|
950
|
+
assert.strictEqual(output.paused_at, 'Phase 3, Plan 1, Task 2 - mid-implementation', 'paused_at extracted');
|
|
951
|
+
});
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
955
|
+
// summary-extract command
|
|
956
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
957
|
+
|
|
958
|
+
describe('summary-extract command', () => {
|
|
959
|
+
let tmpDir;
|
|
960
|
+
|
|
961
|
+
beforeEach(() => {
|
|
962
|
+
tmpDir = createTempProject();
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
afterEach(() => {
|
|
966
|
+
cleanup(tmpDir);
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
test('missing file returns error', () => {
|
|
970
|
+
const result = runGsdTools('summary-extract .planning/phases/01-test/01-01-SUMMARY.md', tmpDir);
|
|
971
|
+
assert.ok(result.success, `Command should succeed: ${result.error}`);
|
|
972
|
+
|
|
973
|
+
const output = JSON.parse(result.output);
|
|
974
|
+
assert.strictEqual(output.error, 'File not found', 'should report missing file');
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
test('extracts all fields from SUMMARY.md', () => {
|
|
978
|
+
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
|
979
|
+
fs.mkdirSync(phaseDir, { recursive: true });
|
|
980
|
+
|
|
981
|
+
fs.writeFileSync(
|
|
982
|
+
path.join(phaseDir, '01-01-SUMMARY.md'),
|
|
983
|
+
`---
|
|
984
|
+
one-liner: Set up Prisma with User and Project models
|
|
985
|
+
key-files:
|
|
986
|
+
- prisma/schema.prisma
|
|
987
|
+
- src/lib/db.ts
|
|
988
|
+
tech-stack:
|
|
989
|
+
added:
|
|
990
|
+
- prisma
|
|
991
|
+
- zod
|
|
992
|
+
patterns-established:
|
|
993
|
+
- Repository pattern
|
|
994
|
+
- Dependency injection
|
|
995
|
+
key-decisions:
|
|
996
|
+
- Use Prisma over Drizzle: Better DX and ecosystem
|
|
997
|
+
- Single database: Start simple, shard later
|
|
998
|
+
---
|
|
999
|
+
|
|
1000
|
+
# Summary
|
|
1001
|
+
|
|
1002
|
+
Full summary content here.
|
|
1003
|
+
`
|
|
1004
|
+
);
|
|
1005
|
+
|
|
1006
|
+
const result = runGsdTools('summary-extract .planning/phases/01-foundation/01-01-SUMMARY.md', tmpDir);
|
|
1007
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
1008
|
+
|
|
1009
|
+
const output = JSON.parse(result.output);
|
|
1010
|
+
assert.strictEqual(output.path, '.planning/phases/01-foundation/01-01-SUMMARY.md', 'path correct');
|
|
1011
|
+
assert.strictEqual(output.one_liner, 'Set up Prisma with User and Project models', 'one-liner extracted');
|
|
1012
|
+
assert.deepStrictEqual(output.key_files, ['prisma/schema.prisma', 'src/lib/db.ts'], 'key files extracted');
|
|
1013
|
+
assert.deepStrictEqual(output.tech_added, ['prisma', 'zod'], 'tech added extracted');
|
|
1014
|
+
assert.deepStrictEqual(output.patterns, ['Repository pattern', 'Dependency injection'], 'patterns extracted');
|
|
1015
|
+
assert.strictEqual(output.decisions.length, 2, 'decisions extracted');
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
test('selective extraction with --fields', () => {
|
|
1019
|
+
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
|
1020
|
+
fs.mkdirSync(phaseDir, { recursive: true });
|
|
1021
|
+
|
|
1022
|
+
fs.writeFileSync(
|
|
1023
|
+
path.join(phaseDir, '01-01-SUMMARY.md'),
|
|
1024
|
+
`---
|
|
1025
|
+
one-liner: Set up database
|
|
1026
|
+
key-files:
|
|
1027
|
+
- prisma/schema.prisma
|
|
1028
|
+
tech-stack:
|
|
1029
|
+
added:
|
|
1030
|
+
- prisma
|
|
1031
|
+
patterns-established:
|
|
1032
|
+
- Repository pattern
|
|
1033
|
+
key-decisions:
|
|
1034
|
+
- Use Prisma: Better DX
|
|
1035
|
+
---
|
|
1036
|
+
`
|
|
1037
|
+
);
|
|
1038
|
+
|
|
1039
|
+
const result = runGsdTools('summary-extract .planning/phases/01-foundation/01-01-SUMMARY.md --fields one_liner,key_files', tmpDir);
|
|
1040
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
1041
|
+
|
|
1042
|
+
const output = JSON.parse(result.output);
|
|
1043
|
+
assert.strictEqual(output.one_liner, 'Set up database', 'one_liner included');
|
|
1044
|
+
assert.deepStrictEqual(output.key_files, ['prisma/schema.prisma'], 'key_files included');
|
|
1045
|
+
assert.strictEqual(output.tech_added, undefined, 'tech_added excluded');
|
|
1046
|
+
assert.strictEqual(output.patterns, undefined, 'patterns excluded');
|
|
1047
|
+
assert.strictEqual(output.decisions, undefined, 'decisions excluded');
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
test('handles missing frontmatter fields gracefully', () => {
|
|
1051
|
+
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
|
1052
|
+
fs.mkdirSync(phaseDir, { recursive: true });
|
|
1053
|
+
|
|
1054
|
+
fs.writeFileSync(
|
|
1055
|
+
path.join(phaseDir, '01-01-SUMMARY.md'),
|
|
1056
|
+
`---
|
|
1057
|
+
one-liner: Minimal summary
|
|
1058
|
+
---
|
|
1059
|
+
|
|
1060
|
+
# Summary
|
|
1061
|
+
`
|
|
1062
|
+
);
|
|
1063
|
+
|
|
1064
|
+
const result = runGsdTools('summary-extract .planning/phases/01-foundation/01-01-SUMMARY.md', tmpDir);
|
|
1065
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
1066
|
+
|
|
1067
|
+
const output = JSON.parse(result.output);
|
|
1068
|
+
assert.strictEqual(output.one_liner, 'Minimal summary', 'one-liner extracted');
|
|
1069
|
+
assert.deepStrictEqual(output.key_files, [], 'key_files defaults to empty');
|
|
1070
|
+
assert.deepStrictEqual(output.tech_added, [], 'tech_added defaults to empty');
|
|
1071
|
+
assert.deepStrictEqual(output.patterns, [], 'patterns defaults to empty');
|
|
1072
|
+
assert.deepStrictEqual(output.decisions, [], 'decisions defaults to empty');
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
test('parses key-decisions with rationale', () => {
|
|
1076
|
+
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
|
1077
|
+
fs.mkdirSync(phaseDir, { recursive: true });
|
|
1078
|
+
|
|
1079
|
+
fs.writeFileSync(
|
|
1080
|
+
path.join(phaseDir, '01-01-SUMMARY.md'),
|
|
1081
|
+
`---
|
|
1082
|
+
key-decisions:
|
|
1083
|
+
- Use Prisma: Better DX than alternatives
|
|
1084
|
+
- JWT tokens: Stateless auth for scalability
|
|
1085
|
+
---
|
|
1086
|
+
`
|
|
1087
|
+
);
|
|
1088
|
+
|
|
1089
|
+
const result = runGsdTools('summary-extract .planning/phases/01-foundation/01-01-SUMMARY.md', tmpDir);
|
|
1090
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
1091
|
+
|
|
1092
|
+
const output = JSON.parse(result.output);
|
|
1093
|
+
assert.strictEqual(output.decisions[0].summary, 'Use Prisma', 'decision summary parsed');
|
|
1094
|
+
assert.strictEqual(output.decisions[0].rationale, 'Better DX than alternatives', 'decision rationale parsed');
|
|
1095
|
+
assert.strictEqual(output.decisions[1].summary, 'JWT tokens', 'second decision summary');
|
|
1096
|
+
assert.strictEqual(output.decisions[1].rationale, 'Stateless auth for scalability', 'second decision rationale');
|
|
1097
|
+
});
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1101
|
+
// init --include flag tests
|
|
1102
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1103
|
+
|
|
1104
|
+
describe('init commands with --include flag', () => {
|
|
1105
|
+
let tmpDir;
|
|
1106
|
+
|
|
1107
|
+
beforeEach(() => {
|
|
1108
|
+
tmpDir = createTempProject();
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
afterEach(() => {
|
|
1112
|
+
cleanup(tmpDir);
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
test('init execute-phase includes state and config content', () => {
|
|
1116
|
+
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
|
|
1117
|
+
fs.mkdirSync(phaseDir, { recursive: true });
|
|
1118
|
+
fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), '# Plan');
|
|
1119
|
+
fs.writeFileSync(
|
|
1120
|
+
path.join(tmpDir, '.planning', 'STATE.md'),
|
|
1121
|
+
'# State\n\n**Current Phase:** 03\n**Status:** In progress'
|
|
1122
|
+
);
|
|
1123
|
+
fs.writeFileSync(
|
|
1124
|
+
path.join(tmpDir, '.planning', 'config.json'),
|
|
1125
|
+
JSON.stringify({ model_profile: 'balanced' })
|
|
1126
|
+
);
|
|
1127
|
+
|
|
1128
|
+
const result = runGsdTools('init execute-phase 03 --include state,config', tmpDir);
|
|
1129
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
1130
|
+
|
|
1131
|
+
const output = JSON.parse(result.output);
|
|
1132
|
+
assert.ok(output.state_content, 'state_content should be included');
|
|
1133
|
+
assert.ok(output.state_content.includes('Current Phase'), 'state content correct');
|
|
1134
|
+
assert.ok(output.config_content, 'config_content should be included');
|
|
1135
|
+
assert.ok(output.config_content.includes('model_profile'), 'config content correct');
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
test('init execute-phase without --include omits content', () => {
|
|
1139
|
+
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
|
|
1140
|
+
fs.mkdirSync(phaseDir, { recursive: true });
|
|
1141
|
+
fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), '# Plan');
|
|
1142
|
+
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), '# State');
|
|
1143
|
+
|
|
1144
|
+
const result = runGsdTools('init execute-phase 03', tmpDir);
|
|
1145
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
1146
|
+
|
|
1147
|
+
const output = JSON.parse(result.output);
|
|
1148
|
+
assert.strictEqual(output.state_content, undefined, 'state_content should be omitted');
|
|
1149
|
+
assert.strictEqual(output.config_content, undefined, 'config_content should be omitted');
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
test('init plan-phase includes multiple file contents', () => {
|
|
1153
|
+
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
|
|
1154
|
+
fs.mkdirSync(phaseDir, { recursive: true });
|
|
1155
|
+
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), '# Project State');
|
|
1156
|
+
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), '# Roadmap v1.0');
|
|
1157
|
+
fs.writeFileSync(path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), '# Requirements');
|
|
1158
|
+
fs.writeFileSync(path.join(phaseDir, '03-CONTEXT.md'), '# Phase Context');
|
|
1159
|
+
fs.writeFileSync(path.join(phaseDir, '03-RESEARCH.md'), '# Research Findings');
|
|
1160
|
+
|
|
1161
|
+
const result = runGsdTools('init plan-phase 03 --include state,roadmap,requirements,context,research', tmpDir);
|
|
1162
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
1163
|
+
|
|
1164
|
+
const output = JSON.parse(result.output);
|
|
1165
|
+
assert.ok(output.state_content, 'state_content included');
|
|
1166
|
+
assert.ok(output.state_content.includes('Project State'), 'state content correct');
|
|
1167
|
+
assert.ok(output.roadmap_content, 'roadmap_content included');
|
|
1168
|
+
assert.ok(output.roadmap_content.includes('Roadmap v1.0'), 'roadmap content correct');
|
|
1169
|
+
assert.ok(output.requirements_content, 'requirements_content included');
|
|
1170
|
+
assert.ok(output.context_content, 'context_content included');
|
|
1171
|
+
assert.ok(output.research_content, 'research_content included');
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
test('init plan-phase includes verification and uat content', () => {
|
|
1175
|
+
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
|
|
1176
|
+
fs.mkdirSync(phaseDir, { recursive: true });
|
|
1177
|
+
fs.writeFileSync(path.join(phaseDir, '03-VERIFICATION.md'), '# Verification Results');
|
|
1178
|
+
fs.writeFileSync(path.join(phaseDir, '03-UAT.md'), '# UAT Findings');
|
|
1179
|
+
|
|
1180
|
+
const result = runGsdTools('init plan-phase 03 --include verification,uat', tmpDir);
|
|
1181
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
1182
|
+
|
|
1183
|
+
const output = JSON.parse(result.output);
|
|
1184
|
+
assert.ok(output.verification_content, 'verification_content included');
|
|
1185
|
+
assert.ok(output.verification_content.includes('Verification Results'), 'verification content correct');
|
|
1186
|
+
assert.ok(output.uat_content, 'uat_content included');
|
|
1187
|
+
assert.ok(output.uat_content.includes('UAT Findings'), 'uat content correct');
|
|
1188
|
+
});
|
|
1189
|
+
|
|
1190
|
+
test('init progress includes state, roadmap, project, config', () => {
|
|
1191
|
+
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), '# State');
|
|
1192
|
+
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), '# Roadmap');
|
|
1193
|
+
fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECT.md'), '# Project');
|
|
1194
|
+
fs.writeFileSync(
|
|
1195
|
+
path.join(tmpDir, '.planning', 'config.json'),
|
|
1196
|
+
JSON.stringify({ model_profile: 'quality' })
|
|
1197
|
+
);
|
|
1198
|
+
|
|
1199
|
+
const result = runGsdTools('init progress --include state,roadmap,project,config', tmpDir);
|
|
1200
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
1201
|
+
|
|
1202
|
+
const output = JSON.parse(result.output);
|
|
1203
|
+
assert.ok(output.state_content, 'state_content included');
|
|
1204
|
+
assert.ok(output.roadmap_content, 'roadmap_content included');
|
|
1205
|
+
assert.ok(output.project_content, 'project_content included');
|
|
1206
|
+
assert.ok(output.config_content, 'config_content included');
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
test('missing files return null in content fields', () => {
|
|
1210
|
+
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
|
|
1211
|
+
fs.mkdirSync(phaseDir, { recursive: true });
|
|
1212
|
+
fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), '# Plan');
|
|
1213
|
+
|
|
1214
|
+
const result = runGsdTools('init execute-phase 03 --include state,config', tmpDir);
|
|
1215
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
1216
|
+
|
|
1217
|
+
const output = JSON.parse(result.output);
|
|
1218
|
+
assert.strictEqual(output.state_content, null, 'missing state returns null');
|
|
1219
|
+
assert.strictEqual(output.config_content, null, 'missing config returns null');
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
test('partial includes work correctly', () => {
|
|
1223
|
+
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
|
|
1224
|
+
fs.mkdirSync(phaseDir, { recursive: true });
|
|
1225
|
+
fs.writeFileSync(path.join(phaseDir, '03-01-PLAN.md'), '# Plan');
|
|
1226
|
+
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), '# State');
|
|
1227
|
+
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), '# Roadmap');
|
|
1228
|
+
|
|
1229
|
+
// Only request state, not roadmap
|
|
1230
|
+
const result = runGsdTools('init execute-phase 03 --include state', tmpDir);
|
|
1231
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
1232
|
+
|
|
1233
|
+
const output = JSON.parse(result.output);
|
|
1234
|
+
assert.ok(output.state_content, 'state_content included');
|
|
1235
|
+
assert.strictEqual(output.roadmap_content, undefined, 'roadmap_content not requested, should be undefined');
|
|
1236
|
+
});
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1240
|
+
// roadmap analyze command
|
|
1241
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1242
|
+
|
|
1243
|
+
describe('roadmap analyze command', () => {
|
|
1244
|
+
let tmpDir;
|
|
1245
|
+
|
|
1246
|
+
beforeEach(() => {
|
|
1247
|
+
tmpDir = createTempProject();
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
afterEach(() => {
|
|
1251
|
+
cleanup(tmpDir);
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
test('missing ROADMAP.md returns error', () => {
|
|
1255
|
+
const result = runGsdTools('roadmap analyze', tmpDir);
|
|
1256
|
+
assert.ok(result.success, `Command should succeed: ${result.error}`);
|
|
1257
|
+
|
|
1258
|
+
const output = JSON.parse(result.output);
|
|
1259
|
+
assert.strictEqual(output.error, 'ROADMAP.md not found');
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
test('parses phases with goals and disk status', () => {
|
|
1263
|
+
fs.writeFileSync(
|
|
1264
|
+
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
1265
|
+
`# Roadmap v1.0
|
|
1266
|
+
|
|
1267
|
+
### Phase 1: Foundation
|
|
1268
|
+
**Goal:** Set up infrastructure
|
|
1269
|
+
|
|
1270
|
+
### Phase 2: Authentication
|
|
1271
|
+
**Goal:** Add user auth
|
|
1272
|
+
|
|
1273
|
+
### Phase 3: Features
|
|
1274
|
+
**Goal:** Build core features
|
|
1275
|
+
`
|
|
1276
|
+
);
|
|
1277
|
+
|
|
1278
|
+
// Create phase dirs with varying completion
|
|
1279
|
+
const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
|
1280
|
+
fs.mkdirSync(p1, { recursive: true });
|
|
1281
|
+
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
|
|
1282
|
+
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
|
|
1283
|
+
|
|
1284
|
+
const p2 = path.join(tmpDir, '.planning', 'phases', '02-authentication');
|
|
1285
|
+
fs.mkdirSync(p2, { recursive: true });
|
|
1286
|
+
fs.writeFileSync(path.join(p2, '02-01-PLAN.md'), '# Plan');
|
|
1287
|
+
|
|
1288
|
+
const result = runGsdTools('roadmap analyze', tmpDir);
|
|
1289
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
1290
|
+
|
|
1291
|
+
const output = JSON.parse(result.output);
|
|
1292
|
+
assert.strictEqual(output.phase_count, 3, 'should find 3 phases');
|
|
1293
|
+
assert.strictEqual(output.phases[0].disk_status, 'complete', 'phase 1 complete');
|
|
1294
|
+
assert.strictEqual(output.phases[1].disk_status, 'planned', 'phase 2 planned');
|
|
1295
|
+
assert.strictEqual(output.phases[2].disk_status, 'no_directory', 'phase 3 no directory');
|
|
1296
|
+
assert.strictEqual(output.completed_phases, 1, '1 phase complete');
|
|
1297
|
+
assert.strictEqual(output.total_plans, 2, '2 total plans');
|
|
1298
|
+
assert.strictEqual(output.total_summaries, 1, '1 total summary');
|
|
1299
|
+
assert.strictEqual(output.progress_percent, 50, '50% complete');
|
|
1300
|
+
assert.strictEqual(output.current_phase, '2', 'current phase is 2');
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
test('extracts goals and dependencies', () => {
|
|
1304
|
+
fs.writeFileSync(
|
|
1305
|
+
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
1306
|
+
`# Roadmap
|
|
1307
|
+
|
|
1308
|
+
### Phase 1: Setup
|
|
1309
|
+
**Goal:** Initialize project
|
|
1310
|
+
**Depends on:** Nothing
|
|
1311
|
+
|
|
1312
|
+
### Phase 2: Build
|
|
1313
|
+
**Goal:** Build features
|
|
1314
|
+
**Depends on:** Phase 1
|
|
1315
|
+
`
|
|
1316
|
+
);
|
|
1317
|
+
|
|
1318
|
+
const result = runGsdTools('roadmap analyze', tmpDir);
|
|
1319
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
1320
|
+
|
|
1321
|
+
const output = JSON.parse(result.output);
|
|
1322
|
+
assert.strictEqual(output.phases[0].goal, 'Initialize project');
|
|
1323
|
+
assert.strictEqual(output.phases[0].depends_on, 'Nothing');
|
|
1324
|
+
assert.strictEqual(output.phases[1].goal, 'Build features');
|
|
1325
|
+
assert.strictEqual(output.phases[1].depends_on, 'Phase 1');
|
|
1326
|
+
});
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1330
|
+
// phase add command
|
|
1331
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1332
|
+
|
|
1333
|
+
describe('phase add command', () => {
|
|
1334
|
+
let tmpDir;
|
|
1335
|
+
|
|
1336
|
+
beforeEach(() => {
|
|
1337
|
+
tmpDir = createTempProject();
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
afterEach(() => {
|
|
1341
|
+
cleanup(tmpDir);
|
|
1342
|
+
});
|
|
1343
|
+
|
|
1344
|
+
test('adds phase after highest existing', () => {
|
|
1345
|
+
fs.writeFileSync(
|
|
1346
|
+
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
1347
|
+
`# Roadmap v1.0
|
|
1348
|
+
|
|
1349
|
+
### Phase 1: Foundation
|
|
1350
|
+
**Goal:** Setup
|
|
1351
|
+
|
|
1352
|
+
### Phase 2: API
|
|
1353
|
+
**Goal:** Build API
|
|
1354
|
+
|
|
1355
|
+
---
|
|
1356
|
+
`
|
|
1357
|
+
);
|
|
1358
|
+
|
|
1359
|
+
const result = runGsdTools('phase add User Dashboard', tmpDir);
|
|
1360
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
1361
|
+
|
|
1362
|
+
const output = JSON.parse(result.output);
|
|
1363
|
+
assert.strictEqual(output.phase_number, 3, 'should be phase 3');
|
|
1364
|
+
assert.strictEqual(output.slug, 'user-dashboard');
|
|
1365
|
+
|
|
1366
|
+
// Verify directory created
|
|
1367
|
+
assert.ok(
|
|
1368
|
+
fs.existsSync(path.join(tmpDir, '.planning', 'phases', '03-user-dashboard')),
|
|
1369
|
+
'directory should be created'
|
|
1370
|
+
);
|
|
1371
|
+
|
|
1372
|
+
// Verify ROADMAP updated
|
|
1373
|
+
const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
|
|
1374
|
+
assert.ok(roadmap.includes('### Phase 3: User Dashboard'), 'roadmap should include new phase');
|
|
1375
|
+
assert.ok(roadmap.includes('**Depends on:** Phase 2'), 'should depend on previous');
|
|
1376
|
+
});
|
|
1377
|
+
|
|
1378
|
+
test('handles empty roadmap', () => {
|
|
1379
|
+
fs.writeFileSync(
|
|
1380
|
+
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
1381
|
+
`# Roadmap v1.0\n`
|
|
1382
|
+
);
|
|
1383
|
+
|
|
1384
|
+
const result = runGsdTools('phase add Initial Setup', tmpDir);
|
|
1385
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
1386
|
+
|
|
1387
|
+
const output = JSON.parse(result.output);
|
|
1388
|
+
assert.strictEqual(output.phase_number, 1, 'should be phase 1');
|
|
1389
|
+
});
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1393
|
+
// phase insert command
|
|
1394
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1395
|
+
|
|
1396
|
+
describe('phase insert command', () => {
|
|
1397
|
+
let tmpDir;
|
|
1398
|
+
|
|
1399
|
+
beforeEach(() => {
|
|
1400
|
+
tmpDir = createTempProject();
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
afterEach(() => {
|
|
1404
|
+
cleanup(tmpDir);
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
test('inserts decimal phase after target', () => {
|
|
1408
|
+
fs.writeFileSync(
|
|
1409
|
+
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
1410
|
+
`# Roadmap
|
|
1411
|
+
|
|
1412
|
+
### Phase 1: Foundation
|
|
1413
|
+
**Goal:** Setup
|
|
1414
|
+
|
|
1415
|
+
### Phase 2: API
|
|
1416
|
+
**Goal:** Build API
|
|
1417
|
+
`
|
|
1418
|
+
);
|
|
1419
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-foundation'), { recursive: true });
|
|
1420
|
+
|
|
1421
|
+
const result = runGsdTools('phase insert 1 Fix Critical Bug', tmpDir);
|
|
1422
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
1423
|
+
|
|
1424
|
+
const output = JSON.parse(result.output);
|
|
1425
|
+
assert.strictEqual(output.phase_number, '01.1', 'should be 01.1');
|
|
1426
|
+
assert.strictEqual(output.after_phase, '1');
|
|
1427
|
+
|
|
1428
|
+
// Verify directory
|
|
1429
|
+
assert.ok(
|
|
1430
|
+
fs.existsSync(path.join(tmpDir, '.planning', 'phases', '01.1-fix-critical-bug')),
|
|
1431
|
+
'decimal phase directory should be created'
|
|
1432
|
+
);
|
|
1433
|
+
|
|
1434
|
+
// Verify ROADMAP
|
|
1435
|
+
const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
|
|
1436
|
+
assert.ok(roadmap.includes('Phase 01.1: Fix Critical Bug (INSERTED)'), 'roadmap should include inserted phase');
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
test('increments decimal when siblings exist', () => {
|
|
1440
|
+
fs.writeFileSync(
|
|
1441
|
+
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
1442
|
+
`# Roadmap
|
|
1443
|
+
|
|
1444
|
+
### Phase 1: Foundation
|
|
1445
|
+
**Goal:** Setup
|
|
1446
|
+
|
|
1447
|
+
### Phase 2: API
|
|
1448
|
+
**Goal:** Build API
|
|
1449
|
+
`
|
|
1450
|
+
);
|
|
1451
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-foundation'), { recursive: true });
|
|
1452
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01.1-hotfix'), { recursive: true });
|
|
1453
|
+
|
|
1454
|
+
const result = runGsdTools('phase insert 1 Another Fix', tmpDir);
|
|
1455
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
1456
|
+
|
|
1457
|
+
const output = JSON.parse(result.output);
|
|
1458
|
+
assert.strictEqual(output.phase_number, '01.2', 'should be 01.2');
|
|
1459
|
+
});
|
|
1460
|
+
|
|
1461
|
+
test('rejects missing phase', () => {
|
|
1462
|
+
fs.writeFileSync(
|
|
1463
|
+
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
1464
|
+
`# Roadmap\n### Phase 1: Test\n**Goal:** Test\n`
|
|
1465
|
+
);
|
|
1466
|
+
|
|
1467
|
+
const result = runGsdTools('phase insert 99 Fix Something', tmpDir);
|
|
1468
|
+
assert.ok(!result.success, 'should fail for missing phase');
|
|
1469
|
+
assert.ok(result.error.includes('not found'), 'error mentions not found');
|
|
1470
|
+
});
|
|
1471
|
+
|
|
1472
|
+
test('handles padding mismatch between input and roadmap', () => {
|
|
1473
|
+
fs.writeFileSync(
|
|
1474
|
+
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
1475
|
+
`# Roadmap
|
|
1476
|
+
|
|
1477
|
+
## Phase 09.05: Existing Decimal Phase
|
|
1478
|
+
**Goal:** Test padding
|
|
1479
|
+
|
|
1480
|
+
## Phase 09.1: Next Phase
|
|
1481
|
+
**Goal:** Test
|
|
1482
|
+
`
|
|
1483
|
+
);
|
|
1484
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '09.05-existing'), { recursive: true });
|
|
1485
|
+
|
|
1486
|
+
// Pass unpadded "9.05" but roadmap has "09.05"
|
|
1487
|
+
const result = runGsdTools('phase insert 9.05 Padding Test', tmpDir);
|
|
1488
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
1489
|
+
|
|
1490
|
+
const output = JSON.parse(result.output);
|
|
1491
|
+
assert.strictEqual(output.after_phase, '9.05');
|
|
1492
|
+
|
|
1493
|
+
const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
|
|
1494
|
+
assert.ok(roadmap.includes('(INSERTED)'), 'roadmap should include inserted phase');
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1497
|
+
test('handles #### heading depth from multi-milestone roadmaps', () => {
|
|
1498
|
+
fs.writeFileSync(
|
|
1499
|
+
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
1500
|
+
`# Roadmap
|
|
1501
|
+
|
|
1502
|
+
### v1.1 Milestone
|
|
1503
|
+
|
|
1504
|
+
#### Phase 5: Feature Work
|
|
1505
|
+
**Goal:** Build features
|
|
1506
|
+
|
|
1507
|
+
#### Phase 6: Polish
|
|
1508
|
+
**Goal:** Polish
|
|
1509
|
+
`
|
|
1510
|
+
);
|
|
1511
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '05-feature-work'), { recursive: true });
|
|
1512
|
+
|
|
1513
|
+
const result = runGsdTools('phase insert 5 Hotfix', tmpDir);
|
|
1514
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
1515
|
+
|
|
1516
|
+
const output = JSON.parse(result.output);
|
|
1517
|
+
assert.strictEqual(output.phase_number, '05.1');
|
|
1518
|
+
|
|
1519
|
+
const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
|
|
1520
|
+
assert.ok(roadmap.includes('Phase 05.1: Hotfix (INSERTED)'), 'roadmap should include inserted phase');
|
|
1521
|
+
});
|
|
1522
|
+
});
|
|
1523
|
+
|
|
1524
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1525
|
+
// phase remove command
|
|
1526
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1527
|
+
|
|
1528
|
+
describe('phase remove command', () => {
|
|
1529
|
+
let tmpDir;
|
|
1530
|
+
|
|
1531
|
+
beforeEach(() => {
|
|
1532
|
+
tmpDir = createTempProject();
|
|
1533
|
+
});
|
|
1534
|
+
|
|
1535
|
+
afterEach(() => {
|
|
1536
|
+
cleanup(tmpDir);
|
|
1537
|
+
});
|
|
1538
|
+
|
|
1539
|
+
test('removes phase directory and renumbers subsequent', () => {
|
|
1540
|
+
// Setup 3 phases
|
|
1541
|
+
fs.writeFileSync(
|
|
1542
|
+
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
1543
|
+
`# Roadmap
|
|
1544
|
+
|
|
1545
|
+
### Phase 1: Foundation
|
|
1546
|
+
**Goal:** Setup
|
|
1547
|
+
**Depends on:** Nothing
|
|
1548
|
+
|
|
1549
|
+
### Phase 2: Auth
|
|
1550
|
+
**Goal:** Authentication
|
|
1551
|
+
**Depends on:** Phase 1
|
|
1552
|
+
|
|
1553
|
+
### Phase 3: Features
|
|
1554
|
+
**Goal:** Core features
|
|
1555
|
+
**Depends on:** Phase 2
|
|
1556
|
+
`
|
|
1557
|
+
);
|
|
1558
|
+
|
|
1559
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-foundation'), { recursive: true });
|
|
1560
|
+
const p2 = path.join(tmpDir, '.planning', 'phases', '02-auth');
|
|
1561
|
+
fs.mkdirSync(p2, { recursive: true });
|
|
1562
|
+
fs.writeFileSync(path.join(p2, '02-01-PLAN.md'), '# Plan');
|
|
1563
|
+
const p3 = path.join(tmpDir, '.planning', 'phases', '03-features');
|
|
1564
|
+
fs.mkdirSync(p3, { recursive: true });
|
|
1565
|
+
fs.writeFileSync(path.join(p3, '03-01-PLAN.md'), '# Plan');
|
|
1566
|
+
fs.writeFileSync(path.join(p3, '03-02-PLAN.md'), '# Plan 2');
|
|
1567
|
+
|
|
1568
|
+
// Remove phase 2
|
|
1569
|
+
const result = runGsdTools('phase remove 2', tmpDir);
|
|
1570
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
1571
|
+
|
|
1572
|
+
const output = JSON.parse(result.output);
|
|
1573
|
+
assert.strictEqual(output.removed, '2');
|
|
1574
|
+
assert.strictEqual(output.directory_deleted, '02-auth');
|
|
1575
|
+
|
|
1576
|
+
// Phase 3 should be renumbered to 02
|
|
1577
|
+
assert.ok(
|
|
1578
|
+
fs.existsSync(path.join(tmpDir, '.planning', 'phases', '02-features')),
|
|
1579
|
+
'phase 3 should be renumbered to 02-features'
|
|
1580
|
+
);
|
|
1581
|
+
assert.ok(
|
|
1582
|
+
!fs.existsSync(path.join(tmpDir, '.planning', 'phases', '03-features')),
|
|
1583
|
+
'old 03-features should not exist'
|
|
1584
|
+
);
|
|
1585
|
+
|
|
1586
|
+
// Files inside should be renamed
|
|
1587
|
+
assert.ok(
|
|
1588
|
+
fs.existsSync(path.join(tmpDir, '.planning', 'phases', '02-features', '02-01-PLAN.md')),
|
|
1589
|
+
'plan file should be renumbered to 02-01'
|
|
1590
|
+
);
|
|
1591
|
+
assert.ok(
|
|
1592
|
+
fs.existsSync(path.join(tmpDir, '.planning', 'phases', '02-features', '02-02-PLAN.md')),
|
|
1593
|
+
'plan 2 should be renumbered to 02-02'
|
|
1594
|
+
);
|
|
1595
|
+
|
|
1596
|
+
// ROADMAP should be updated
|
|
1597
|
+
const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
|
|
1598
|
+
assert.ok(!roadmap.includes('Phase 2: Auth'), 'removed phase should not be in roadmap');
|
|
1599
|
+
assert.ok(roadmap.includes('Phase 2: Features'), 'phase 3 should be renumbered to 2');
|
|
1600
|
+
});
|
|
1601
|
+
|
|
1602
|
+
test('rejects removal of phase with summaries unless --force', () => {
|
|
1603
|
+
const p1 = path.join(tmpDir, '.planning', 'phases', '01-test');
|
|
1604
|
+
fs.mkdirSync(p1, { recursive: true });
|
|
1605
|
+
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
|
|
1606
|
+
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
|
|
1607
|
+
fs.writeFileSync(
|
|
1608
|
+
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
1609
|
+
`# Roadmap\n### Phase 1: Test\n**Goal:** Test\n`
|
|
1610
|
+
);
|
|
1611
|
+
|
|
1612
|
+
// Should fail without --force
|
|
1613
|
+
const result = runGsdTools('phase remove 1', tmpDir);
|
|
1614
|
+
assert.ok(!result.success, 'should fail without --force');
|
|
1615
|
+
assert.ok(result.error.includes('executed plan'), 'error mentions executed plans');
|
|
1616
|
+
|
|
1617
|
+
// Should succeed with --force
|
|
1618
|
+
const forceResult = runGsdTools('phase remove 1 --force', tmpDir);
|
|
1619
|
+
assert.ok(forceResult.success, `Force remove failed: ${forceResult.error}`);
|
|
1620
|
+
});
|
|
1621
|
+
|
|
1622
|
+
test('removes decimal phase and renumbers siblings', () => {
|
|
1623
|
+
fs.writeFileSync(
|
|
1624
|
+
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
1625
|
+
`# Roadmap\n### Phase 6: Main\n**Goal:** Main\n### Phase 6.1: Fix A\n**Goal:** Fix A\n### Phase 6.2: Fix B\n**Goal:** Fix B\n### Phase 6.3: Fix C\n**Goal:** Fix C\n`
|
|
1626
|
+
);
|
|
1627
|
+
|
|
1628
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06-main'), { recursive: true });
|
|
1629
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06.1-fix-a'), { recursive: true });
|
|
1630
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06.2-fix-b'), { recursive: true });
|
|
1631
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '06.3-fix-c'), { recursive: true });
|
|
1632
|
+
|
|
1633
|
+
const result = runGsdTools('phase remove 6.2', tmpDir);
|
|
1634
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
1635
|
+
|
|
1636
|
+
// 06.3 should become 06.2
|
|
1637
|
+
assert.ok(
|
|
1638
|
+
fs.existsSync(path.join(tmpDir, '.planning', 'phases', '06.2-fix-c')),
|
|
1639
|
+
'06.3 should be renumbered to 06.2'
|
|
1640
|
+
);
|
|
1641
|
+
assert.ok(
|
|
1642
|
+
!fs.existsSync(path.join(tmpDir, '.planning', 'phases', '06.3-fix-c')),
|
|
1643
|
+
'old 06.3 should not exist'
|
|
1644
|
+
);
|
|
1645
|
+
});
|
|
1646
|
+
|
|
1647
|
+
test('updates STATE.md phase count', () => {
|
|
1648
|
+
fs.writeFileSync(
|
|
1649
|
+
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
1650
|
+
`# Roadmap\n### Phase 1: A\n**Goal:** A\n### Phase 2: B\n**Goal:** B\n`
|
|
1651
|
+
);
|
|
1652
|
+
fs.writeFileSync(
|
|
1653
|
+
path.join(tmpDir, '.planning', 'STATE.md'),
|
|
1654
|
+
`# State\n\n**Current Phase:** 1\n**Total Phases:** 2\n`
|
|
1655
|
+
);
|
|
1656
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true });
|
|
1657
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-b'), { recursive: true });
|
|
1658
|
+
|
|
1659
|
+
runGsdTools('phase remove 2', tmpDir);
|
|
1660
|
+
|
|
1661
|
+
const state = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
|
|
1662
|
+
assert.ok(state.includes('**Total Phases:** 1'), 'total phases should be decremented');
|
|
1663
|
+
});
|
|
1664
|
+
});
|
|
1665
|
+
|
|
1666
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1667
|
+
// phase complete command
|
|
1668
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1669
|
+
|
|
1670
|
+
describe('phase complete command', () => {
|
|
1671
|
+
let tmpDir;
|
|
1672
|
+
|
|
1673
|
+
beforeEach(() => {
|
|
1674
|
+
tmpDir = createTempProject();
|
|
1675
|
+
});
|
|
1676
|
+
|
|
1677
|
+
afterEach(() => {
|
|
1678
|
+
cleanup(tmpDir);
|
|
1679
|
+
});
|
|
1680
|
+
|
|
1681
|
+
test('marks phase complete and transitions to next', () => {
|
|
1682
|
+
fs.writeFileSync(
|
|
1683
|
+
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
1684
|
+
`# Roadmap
|
|
1685
|
+
|
|
1686
|
+
- [ ] Phase 1: Foundation
|
|
1687
|
+
- [ ] Phase 2: API
|
|
1688
|
+
|
|
1689
|
+
### Phase 1: Foundation
|
|
1690
|
+
**Goal:** Setup
|
|
1691
|
+
**Plans:** 1 plans
|
|
1692
|
+
|
|
1693
|
+
### Phase 2: API
|
|
1694
|
+
**Goal:** Build API
|
|
1695
|
+
`
|
|
1696
|
+
);
|
|
1697
|
+
fs.writeFileSync(
|
|
1698
|
+
path.join(tmpDir, '.planning', 'STATE.md'),
|
|
1699
|
+
`# State\n\n**Current Phase:** 01\n**Current Phase Name:** Foundation\n**Status:** In progress\n**Current Plan:** 01-01\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working on phase 1\n`
|
|
1700
|
+
);
|
|
1701
|
+
|
|
1702
|
+
const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
|
1703
|
+
fs.mkdirSync(p1, { recursive: true });
|
|
1704
|
+
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
|
|
1705
|
+
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
|
|
1706
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-api'), { recursive: true });
|
|
1707
|
+
|
|
1708
|
+
const result = runGsdTools('phase complete 1', tmpDir);
|
|
1709
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
1710
|
+
|
|
1711
|
+
const output = JSON.parse(result.output);
|
|
1712
|
+
assert.strictEqual(output.completed_phase, '1');
|
|
1713
|
+
assert.strictEqual(output.plans_executed, '1/1');
|
|
1714
|
+
assert.strictEqual(output.next_phase, '02');
|
|
1715
|
+
assert.strictEqual(output.is_last_phase, false);
|
|
1716
|
+
|
|
1717
|
+
// Verify STATE.md updated
|
|
1718
|
+
const state = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
|
|
1719
|
+
assert.ok(state.includes('**Current Phase:** 02'), 'should advance to phase 02');
|
|
1720
|
+
assert.ok(state.includes('**Status:** Ready to plan'), 'status should be ready to plan');
|
|
1721
|
+
assert.ok(state.includes('**Current Plan:** Not started'), 'plan should be reset');
|
|
1722
|
+
|
|
1723
|
+
// Verify ROADMAP checkbox
|
|
1724
|
+
const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
|
|
1725
|
+
assert.ok(roadmap.includes('[x]'), 'phase should be checked off');
|
|
1726
|
+
assert.ok(roadmap.includes('completed'), 'completion date should be added');
|
|
1727
|
+
});
|
|
1728
|
+
|
|
1729
|
+
test('detects last phase in milestone', () => {
|
|
1730
|
+
fs.writeFileSync(
|
|
1731
|
+
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
1732
|
+
`# Roadmap\n### Phase 1: Only Phase\n**Goal:** Everything\n`
|
|
1733
|
+
);
|
|
1734
|
+
fs.writeFileSync(
|
|
1735
|
+
path.join(tmpDir, '.planning', 'STATE.md'),
|
|
1736
|
+
`# State\n\n**Current Phase:** 01\n**Status:** In progress\n**Current Plan:** 01-01\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n`
|
|
1737
|
+
);
|
|
1738
|
+
|
|
1739
|
+
const p1 = path.join(tmpDir, '.planning', 'phases', '01-only-phase');
|
|
1740
|
+
fs.mkdirSync(p1, { recursive: true });
|
|
1741
|
+
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
|
|
1742
|
+
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
|
|
1743
|
+
|
|
1744
|
+
const result = runGsdTools('phase complete 1', tmpDir);
|
|
1745
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
1746
|
+
|
|
1747
|
+
const output = JSON.parse(result.output);
|
|
1748
|
+
assert.strictEqual(output.is_last_phase, true, 'should detect last phase');
|
|
1749
|
+
assert.strictEqual(output.next_phase, null, 'no next phase');
|
|
1750
|
+
|
|
1751
|
+
const state = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
|
|
1752
|
+
assert.ok(state.includes('Milestone complete'), 'status should be milestone complete');
|
|
1753
|
+
});
|
|
1754
|
+
|
|
1755
|
+
test('updates REQUIREMENTS.md traceability when phase completes', () => {
|
|
1756
|
+
fs.writeFileSync(
|
|
1757
|
+
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
1758
|
+
`# Roadmap
|
|
1759
|
+
|
|
1760
|
+
- [ ] Phase 1: Auth
|
|
1761
|
+
|
|
1762
|
+
### Phase 1: Auth
|
|
1763
|
+
**Goal:** User authentication
|
|
1764
|
+
**Requirements:** AUTH-01, AUTH-02
|
|
1765
|
+
**Plans:** 1 plans
|
|
1766
|
+
|
|
1767
|
+
### Phase 2: API
|
|
1768
|
+
**Goal:** Build API
|
|
1769
|
+
**Requirements:** API-01
|
|
1770
|
+
`
|
|
1771
|
+
);
|
|
1772
|
+
fs.writeFileSync(
|
|
1773
|
+
path.join(tmpDir, '.planning', 'REQUIREMENTS.md'),
|
|
1774
|
+
`# Requirements
|
|
1775
|
+
|
|
1776
|
+
## v1 Requirements
|
|
1777
|
+
|
|
1778
|
+
### Authentication
|
|
1779
|
+
|
|
1780
|
+
- [ ] **AUTH-01**: User can sign up with email
|
|
1781
|
+
- [ ] **AUTH-02**: User can log in
|
|
1782
|
+
- [ ] **AUTH-03**: User can reset password
|
|
1783
|
+
|
|
1784
|
+
### API
|
|
1785
|
+
|
|
1786
|
+
- [ ] **API-01**: REST endpoints
|
|
1787
|
+
|
|
1788
|
+
## Traceability
|
|
1789
|
+
|
|
1790
|
+
| Requirement | Phase | Status |
|
|
1791
|
+
|-------------|-------|--------|
|
|
1792
|
+
| AUTH-01 | Phase 1 | Pending |
|
|
1793
|
+
| AUTH-02 | Phase 1 | Pending |
|
|
1794
|
+
| AUTH-03 | Phase 2 | Pending |
|
|
1795
|
+
| API-01 | Phase 2 | Pending |
|
|
1796
|
+
`
|
|
1797
|
+
);
|
|
1798
|
+
fs.writeFileSync(
|
|
1799
|
+
path.join(tmpDir, '.planning', 'STATE.md'),
|
|
1800
|
+
`# State\n\n**Current Phase:** 01\n**Current Phase Name:** Auth\n**Status:** In progress\n**Current Plan:** 01-01\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n`
|
|
1801
|
+
);
|
|
1802
|
+
|
|
1803
|
+
const p1 = path.join(tmpDir, '.planning', 'phases', '01-auth');
|
|
1804
|
+
fs.mkdirSync(p1, { recursive: true });
|
|
1805
|
+
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
|
|
1806
|
+
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
|
|
1807
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-api'), { recursive: true });
|
|
1808
|
+
|
|
1809
|
+
const result = runGsdTools('phase complete 1', tmpDir);
|
|
1810
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
1811
|
+
|
|
1812
|
+
const req = fs.readFileSync(path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), 'utf-8');
|
|
1813
|
+
|
|
1814
|
+
// Checkboxes updated for phase 1 requirements
|
|
1815
|
+
assert.ok(req.includes('- [x] **AUTH-01**'), 'AUTH-01 checkbox should be checked');
|
|
1816
|
+
assert.ok(req.includes('- [x] **AUTH-02**'), 'AUTH-02 checkbox should be checked');
|
|
1817
|
+
// Other requirements unchanged
|
|
1818
|
+
assert.ok(req.includes('- [ ] **AUTH-03**'), 'AUTH-03 should remain unchecked');
|
|
1819
|
+
assert.ok(req.includes('- [ ] **API-01**'), 'API-01 should remain unchecked');
|
|
1820
|
+
|
|
1821
|
+
// Traceability table updated
|
|
1822
|
+
assert.ok(req.includes('| AUTH-01 | Phase 1 | Complete |'), 'AUTH-01 status should be Complete');
|
|
1823
|
+
assert.ok(req.includes('| AUTH-02 | Phase 1 | Complete |'), 'AUTH-02 status should be Complete');
|
|
1824
|
+
assert.ok(req.includes('| AUTH-03 | Phase 2 | Pending |'), 'AUTH-03 should remain Pending');
|
|
1825
|
+
assert.ok(req.includes('| API-01 | Phase 2 | Pending |'), 'API-01 should remain Pending');
|
|
1826
|
+
});
|
|
1827
|
+
|
|
1828
|
+
test('handles phase with no requirements mapping', () => {
|
|
1829
|
+
fs.writeFileSync(
|
|
1830
|
+
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
1831
|
+
`# Roadmap
|
|
1832
|
+
|
|
1833
|
+
- [ ] Phase 1: Setup
|
|
1834
|
+
|
|
1835
|
+
### Phase 1: Setup
|
|
1836
|
+
**Goal:** Project setup (no requirements)
|
|
1837
|
+
**Plans:** 1 plans
|
|
1838
|
+
`
|
|
1839
|
+
);
|
|
1840
|
+
fs.writeFileSync(
|
|
1841
|
+
path.join(tmpDir, '.planning', 'REQUIREMENTS.md'),
|
|
1842
|
+
`# Requirements
|
|
1843
|
+
|
|
1844
|
+
## v1 Requirements
|
|
1845
|
+
|
|
1846
|
+
- [ ] **REQ-01**: Some requirement
|
|
1847
|
+
|
|
1848
|
+
## Traceability
|
|
1849
|
+
|
|
1850
|
+
| Requirement | Phase | Status |
|
|
1851
|
+
|-------------|-------|--------|
|
|
1852
|
+
| REQ-01 | Phase 2 | Pending |
|
|
1853
|
+
`
|
|
1854
|
+
);
|
|
1855
|
+
fs.writeFileSync(
|
|
1856
|
+
path.join(tmpDir, '.planning', 'STATE.md'),
|
|
1857
|
+
`# State\n\n**Current Phase:** 01\n**Status:** In progress\n**Current Plan:** 01-01\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n`
|
|
1858
|
+
);
|
|
1859
|
+
|
|
1860
|
+
const p1 = path.join(tmpDir, '.planning', 'phases', '01-setup');
|
|
1861
|
+
fs.mkdirSync(p1, { recursive: true });
|
|
1862
|
+
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
|
|
1863
|
+
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
|
|
1864
|
+
|
|
1865
|
+
const result = runGsdTools('phase complete 1', tmpDir);
|
|
1866
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
1867
|
+
|
|
1868
|
+
// REQUIREMENTS.md should be unchanged
|
|
1869
|
+
const req = fs.readFileSync(path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), 'utf-8');
|
|
1870
|
+
assert.ok(req.includes('- [ ] **REQ-01**'), 'REQ-01 should remain unchecked');
|
|
1871
|
+
assert.ok(req.includes('| REQ-01 | Phase 2 | Pending |'), 'REQ-01 should remain Pending');
|
|
1872
|
+
});
|
|
1873
|
+
|
|
1874
|
+
test('handles missing REQUIREMENTS.md gracefully', () => {
|
|
1875
|
+
fs.writeFileSync(
|
|
1876
|
+
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
1877
|
+
`# Roadmap
|
|
1878
|
+
|
|
1879
|
+
- [ ] Phase 1: Foundation
|
|
1880
|
+
**Requirements:** REQ-01
|
|
1881
|
+
|
|
1882
|
+
### Phase 1: Foundation
|
|
1883
|
+
**Goal:** Setup
|
|
1884
|
+
`
|
|
1885
|
+
);
|
|
1886
|
+
fs.writeFileSync(
|
|
1887
|
+
path.join(tmpDir, '.planning', 'STATE.md'),
|
|
1888
|
+
`# State\n\n**Current Phase:** 01\n**Status:** In progress\n**Current Plan:** 01-01\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n`
|
|
1889
|
+
);
|
|
1890
|
+
|
|
1891
|
+
const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
|
1892
|
+
fs.mkdirSync(p1, { recursive: true });
|
|
1893
|
+
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
|
|
1894
|
+
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
|
|
1895
|
+
|
|
1896
|
+
const result = runGsdTools('phase complete 1', tmpDir);
|
|
1897
|
+
assert.ok(result.success, `Command should succeed even without REQUIREMENTS.md: ${result.error}`);
|
|
1898
|
+
});
|
|
1899
|
+
});
|
|
1900
|
+
|
|
1901
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1902
|
+
// milestone complete command
|
|
1903
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1904
|
+
|
|
1905
|
+
describe('milestone complete command', () => {
|
|
1906
|
+
let tmpDir;
|
|
1907
|
+
|
|
1908
|
+
beforeEach(() => {
|
|
1909
|
+
tmpDir = createTempProject();
|
|
1910
|
+
});
|
|
1911
|
+
|
|
1912
|
+
afterEach(() => {
|
|
1913
|
+
cleanup(tmpDir);
|
|
1914
|
+
});
|
|
1915
|
+
|
|
1916
|
+
test('archives roadmap, requirements, creates MILESTONES.md', () => {
|
|
1917
|
+
fs.writeFileSync(
|
|
1918
|
+
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
1919
|
+
`# Roadmap v1.0 MVP\n\n### Phase 1: Foundation\n**Goal:** Setup\n`
|
|
1920
|
+
);
|
|
1921
|
+
fs.writeFileSync(
|
|
1922
|
+
path.join(tmpDir, '.planning', 'REQUIREMENTS.md'),
|
|
1923
|
+
`# Requirements\n\n- [ ] User auth\n- [ ] Dashboard\n`
|
|
1924
|
+
);
|
|
1925
|
+
fs.writeFileSync(
|
|
1926
|
+
path.join(tmpDir, '.planning', 'STATE.md'),
|
|
1927
|
+
`# State\n\n**Status:** In progress\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n`
|
|
1928
|
+
);
|
|
1929
|
+
|
|
1930
|
+
const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
|
1931
|
+
fs.mkdirSync(p1, { recursive: true });
|
|
1932
|
+
fs.writeFileSync(
|
|
1933
|
+
path.join(p1, '01-01-SUMMARY.md'),
|
|
1934
|
+
`---\none-liner: Set up project infrastructure\n---\n# Summary\n`
|
|
1935
|
+
);
|
|
1936
|
+
|
|
1937
|
+
const result = runGsdTools('milestone complete v1.0 --name MVP Foundation', tmpDir);
|
|
1938
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
1939
|
+
|
|
1940
|
+
const output = JSON.parse(result.output);
|
|
1941
|
+
assert.strictEqual(output.version, 'v1.0');
|
|
1942
|
+
assert.strictEqual(output.phases, 1);
|
|
1943
|
+
assert.ok(output.archived.roadmap, 'roadmap should be archived');
|
|
1944
|
+
assert.ok(output.archived.requirements, 'requirements should be archived');
|
|
1945
|
+
|
|
1946
|
+
// Verify archive files exist
|
|
1947
|
+
assert.ok(
|
|
1948
|
+
fs.existsSync(path.join(tmpDir, '.planning', 'milestones', 'v1.0-ROADMAP.md')),
|
|
1949
|
+
'archived roadmap should exist'
|
|
1950
|
+
);
|
|
1951
|
+
assert.ok(
|
|
1952
|
+
fs.existsSync(path.join(tmpDir, '.planning', 'milestones', 'v1.0-REQUIREMENTS.md')),
|
|
1953
|
+
'archived requirements should exist'
|
|
1954
|
+
);
|
|
1955
|
+
|
|
1956
|
+
// Verify MILESTONES.md created
|
|
1957
|
+
assert.ok(
|
|
1958
|
+
fs.existsSync(path.join(tmpDir, '.planning', 'MILESTONES.md')),
|
|
1959
|
+
'MILESTONES.md should be created'
|
|
1960
|
+
);
|
|
1961
|
+
const milestones = fs.readFileSync(path.join(tmpDir, '.planning', 'MILESTONES.md'), 'utf-8');
|
|
1962
|
+
assert.ok(milestones.includes('v1.0 MVP Foundation'), 'milestone entry should contain name');
|
|
1963
|
+
assert.ok(milestones.includes('Set up project infrastructure'), 'accomplishments should be listed');
|
|
1964
|
+
});
|
|
1965
|
+
|
|
1966
|
+
test('appends to existing MILESTONES.md', () => {
|
|
1967
|
+
fs.writeFileSync(
|
|
1968
|
+
path.join(tmpDir, '.planning', 'MILESTONES.md'),
|
|
1969
|
+
`# Milestones\n\n## v0.9 Alpha (Shipped: 2025-01-01)\n\n---\n\n`
|
|
1970
|
+
);
|
|
1971
|
+
fs.writeFileSync(
|
|
1972
|
+
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
1973
|
+
`# Roadmap v1.0\n`
|
|
1974
|
+
);
|
|
1975
|
+
fs.writeFileSync(
|
|
1976
|
+
path.join(tmpDir, '.planning', 'STATE.md'),
|
|
1977
|
+
`# State\n\n**Status:** In progress\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n`
|
|
1978
|
+
);
|
|
1979
|
+
|
|
1980
|
+
const result = runGsdTools('milestone complete v1.0 --name Beta', tmpDir);
|
|
1981
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
1982
|
+
|
|
1983
|
+
const milestones = fs.readFileSync(path.join(tmpDir, '.planning', 'MILESTONES.md'), 'utf-8');
|
|
1984
|
+
assert.ok(milestones.includes('v0.9 Alpha'), 'existing entry should be preserved');
|
|
1985
|
+
assert.ok(milestones.includes('v1.0 Beta'), 'new entry should be appended');
|
|
1986
|
+
});
|
|
1987
|
+
});
|
|
1988
|
+
|
|
1989
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1990
|
+
// validate consistency command
|
|
1991
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1992
|
+
|
|
1993
|
+
describe('validate consistency command', () => {
|
|
1994
|
+
let tmpDir;
|
|
1995
|
+
|
|
1996
|
+
beforeEach(() => {
|
|
1997
|
+
tmpDir = createTempProject();
|
|
1998
|
+
});
|
|
1999
|
+
|
|
2000
|
+
afterEach(() => {
|
|
2001
|
+
cleanup(tmpDir);
|
|
2002
|
+
});
|
|
2003
|
+
|
|
2004
|
+
test('passes for consistent project', () => {
|
|
2005
|
+
fs.writeFileSync(
|
|
2006
|
+
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
2007
|
+
`# Roadmap\n### Phase 1: A\n### Phase 2: B\n### Phase 3: C\n`
|
|
2008
|
+
);
|
|
2009
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true });
|
|
2010
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-b'), { recursive: true });
|
|
2011
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-c'), { recursive: true });
|
|
2012
|
+
|
|
2013
|
+
const result = runGsdTools('validate consistency', tmpDir);
|
|
2014
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
2015
|
+
|
|
2016
|
+
const output = JSON.parse(result.output);
|
|
2017
|
+
assert.strictEqual(output.passed, true, 'should pass');
|
|
2018
|
+
assert.strictEqual(output.warning_count, 0, 'no warnings');
|
|
2019
|
+
});
|
|
2020
|
+
|
|
2021
|
+
test('warns about phase on disk but not in roadmap', () => {
|
|
2022
|
+
fs.writeFileSync(
|
|
2023
|
+
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
2024
|
+
`# Roadmap\n### Phase 1: A\n`
|
|
2025
|
+
);
|
|
2026
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true });
|
|
2027
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-orphan'), { recursive: true });
|
|
2028
|
+
|
|
2029
|
+
const result = runGsdTools('validate consistency', tmpDir);
|
|
2030
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
2031
|
+
|
|
2032
|
+
const output = JSON.parse(result.output);
|
|
2033
|
+
assert.ok(output.warning_count > 0, 'should have warnings');
|
|
2034
|
+
assert.ok(
|
|
2035
|
+
output.warnings.some(w => w.includes('disk but not in ROADMAP')),
|
|
2036
|
+
'should warn about orphan directory'
|
|
2037
|
+
);
|
|
2038
|
+
});
|
|
2039
|
+
|
|
2040
|
+
test('warns about gaps in phase numbering', () => {
|
|
2041
|
+
fs.writeFileSync(
|
|
2042
|
+
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
2043
|
+
`# Roadmap\n### Phase 1: A\n### Phase 3: C\n`
|
|
2044
|
+
);
|
|
2045
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-a'), { recursive: true });
|
|
2046
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-c'), { recursive: true });
|
|
2047
|
+
|
|
2048
|
+
const result = runGsdTools('validate consistency', tmpDir);
|
|
2049
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
2050
|
+
|
|
2051
|
+
const output = JSON.parse(result.output);
|
|
2052
|
+
assert.ok(
|
|
2053
|
+
output.warnings.some(w => w.includes('Gap in phase numbering')),
|
|
2054
|
+
'should warn about gap'
|
|
2055
|
+
);
|
|
2056
|
+
});
|
|
2057
|
+
});
|
|
2058
|
+
|
|
2059
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2060
|
+
// progress command
|
|
2061
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2062
|
+
|
|
2063
|
+
describe('progress command', () => {
|
|
2064
|
+
let tmpDir;
|
|
2065
|
+
|
|
2066
|
+
beforeEach(() => {
|
|
2067
|
+
tmpDir = createTempProject();
|
|
2068
|
+
});
|
|
2069
|
+
|
|
2070
|
+
afterEach(() => {
|
|
2071
|
+
cleanup(tmpDir);
|
|
2072
|
+
});
|
|
2073
|
+
|
|
2074
|
+
test('renders JSON progress', () => {
|
|
2075
|
+
fs.writeFileSync(
|
|
2076
|
+
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
2077
|
+
`# Roadmap v1.0 MVP\n`
|
|
2078
|
+
);
|
|
2079
|
+
const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
|
2080
|
+
fs.mkdirSync(p1, { recursive: true });
|
|
2081
|
+
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
|
|
2082
|
+
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Done');
|
|
2083
|
+
fs.writeFileSync(path.join(p1, '01-02-PLAN.md'), '# Plan 2');
|
|
2084
|
+
|
|
2085
|
+
const result = runGsdTools('progress json', tmpDir);
|
|
2086
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
2087
|
+
|
|
2088
|
+
const output = JSON.parse(result.output);
|
|
2089
|
+
assert.strictEqual(output.total_plans, 2, '2 total plans');
|
|
2090
|
+
assert.strictEqual(output.total_summaries, 1, '1 summary');
|
|
2091
|
+
assert.strictEqual(output.percent, 50, '50%');
|
|
2092
|
+
assert.strictEqual(output.phases.length, 1, '1 phase');
|
|
2093
|
+
assert.strictEqual(output.phases[0].status, 'In Progress', 'phase in progress');
|
|
2094
|
+
});
|
|
2095
|
+
|
|
2096
|
+
test('renders bar format', () => {
|
|
2097
|
+
fs.writeFileSync(
|
|
2098
|
+
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
2099
|
+
`# Roadmap v1.0\n`
|
|
2100
|
+
);
|
|
2101
|
+
const p1 = path.join(tmpDir, '.planning', 'phases', '01-test');
|
|
2102
|
+
fs.mkdirSync(p1, { recursive: true });
|
|
2103
|
+
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
|
|
2104
|
+
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Done');
|
|
2105
|
+
|
|
2106
|
+
const result = runGsdTools('progress bar --raw', tmpDir);
|
|
2107
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
2108
|
+
assert.ok(result.output.includes('1/1'), 'should include count');
|
|
2109
|
+
assert.ok(result.output.includes('100%'), 'should include 100%');
|
|
2110
|
+
});
|
|
2111
|
+
|
|
2112
|
+
test('renders table format', () => {
|
|
2113
|
+
fs.writeFileSync(
|
|
2114
|
+
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
2115
|
+
`# Roadmap v1.0 MVP\n`
|
|
2116
|
+
);
|
|
2117
|
+
const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
|
2118
|
+
fs.mkdirSync(p1, { recursive: true });
|
|
2119
|
+
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
|
|
2120
|
+
|
|
2121
|
+
const result = runGsdTools('progress table --raw', tmpDir);
|
|
2122
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
2123
|
+
assert.ok(result.output.includes('Phase'), 'should have table header');
|
|
2124
|
+
assert.ok(result.output.includes('foundation'), 'should include phase name');
|
|
2125
|
+
});
|
|
2126
|
+
});
|
|
2127
|
+
|
|
2128
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2129
|
+
// todo complete command
|
|
2130
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2131
|
+
|
|
2132
|
+
describe('todo complete command', () => {
|
|
2133
|
+
let tmpDir;
|
|
2134
|
+
|
|
2135
|
+
beforeEach(() => {
|
|
2136
|
+
tmpDir = createTempProject();
|
|
2137
|
+
});
|
|
2138
|
+
|
|
2139
|
+
afterEach(() => {
|
|
2140
|
+
cleanup(tmpDir);
|
|
2141
|
+
});
|
|
2142
|
+
|
|
2143
|
+
test('moves todo from pending to completed', () => {
|
|
2144
|
+
const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending');
|
|
2145
|
+
fs.mkdirSync(pendingDir, { recursive: true });
|
|
2146
|
+
fs.writeFileSync(
|
|
2147
|
+
path.join(pendingDir, 'add-dark-mode.md'),
|
|
2148
|
+
`title: Add dark mode\narea: ui\ncreated: 2025-01-01\n`
|
|
2149
|
+
);
|
|
2150
|
+
|
|
2151
|
+
const result = runGsdTools('todo complete add-dark-mode.md', tmpDir);
|
|
2152
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
2153
|
+
|
|
2154
|
+
const output = JSON.parse(result.output);
|
|
2155
|
+
assert.strictEqual(output.completed, true);
|
|
2156
|
+
|
|
2157
|
+
// Verify moved
|
|
2158
|
+
assert.ok(
|
|
2159
|
+
!fs.existsSync(path.join(tmpDir, '.planning', 'todos', 'pending', 'add-dark-mode.md')),
|
|
2160
|
+
'should be removed from pending'
|
|
2161
|
+
);
|
|
2162
|
+
assert.ok(
|
|
2163
|
+
fs.existsSync(path.join(tmpDir, '.planning', 'todos', 'completed', 'add-dark-mode.md')),
|
|
2164
|
+
'should be in completed'
|
|
2165
|
+
);
|
|
2166
|
+
|
|
2167
|
+
// Verify completion timestamp added
|
|
2168
|
+
const content = fs.readFileSync(
|
|
2169
|
+
path.join(tmpDir, '.planning', 'todos', 'completed', 'add-dark-mode.md'),
|
|
2170
|
+
'utf-8'
|
|
2171
|
+
);
|
|
2172
|
+
assert.ok(content.startsWith('completed:'), 'should have completed timestamp');
|
|
2173
|
+
});
|
|
2174
|
+
|
|
2175
|
+
test('fails for nonexistent todo', () => {
|
|
2176
|
+
const result = runGsdTools('todo complete nonexistent.md', tmpDir);
|
|
2177
|
+
assert.ok(!result.success, 'should fail');
|
|
2178
|
+
assert.ok(result.error.includes('not found'), 'error mentions not found');
|
|
2179
|
+
});
|
|
2180
|
+
});
|
|
2181
|
+
|
|
2182
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2183
|
+
// scaffold command
|
|
2184
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2185
|
+
|
|
2186
|
+
describe('scaffold command', () => {
|
|
2187
|
+
let tmpDir;
|
|
2188
|
+
|
|
2189
|
+
beforeEach(() => {
|
|
2190
|
+
tmpDir = createTempProject();
|
|
2191
|
+
});
|
|
2192
|
+
|
|
2193
|
+
afterEach(() => {
|
|
2194
|
+
cleanup(tmpDir);
|
|
2195
|
+
});
|
|
2196
|
+
|
|
2197
|
+
test('scaffolds context file', () => {
|
|
2198
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-api'), { recursive: true });
|
|
2199
|
+
|
|
2200
|
+
const result = runGsdTools('scaffold context --phase 3', tmpDir);
|
|
2201
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
2202
|
+
|
|
2203
|
+
const output = JSON.parse(result.output);
|
|
2204
|
+
assert.strictEqual(output.created, true);
|
|
2205
|
+
|
|
2206
|
+
// Verify file content
|
|
2207
|
+
const content = fs.readFileSync(
|
|
2208
|
+
path.join(tmpDir, '.planning', 'phases', '03-api', '03-CONTEXT.md'),
|
|
2209
|
+
'utf-8'
|
|
2210
|
+
);
|
|
2211
|
+
assert.ok(content.includes('Phase 3'), 'should reference phase number');
|
|
2212
|
+
assert.ok(content.includes('Decisions'), 'should have decisions section');
|
|
2213
|
+
assert.ok(content.includes('Discretion Areas'), 'should have discretion section');
|
|
2214
|
+
});
|
|
2215
|
+
|
|
2216
|
+
test('scaffolds UAT file', () => {
|
|
2217
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-api'), { recursive: true });
|
|
2218
|
+
|
|
2219
|
+
const result = runGsdTools('scaffold uat --phase 3', tmpDir);
|
|
2220
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
2221
|
+
|
|
2222
|
+
const output = JSON.parse(result.output);
|
|
2223
|
+
assert.strictEqual(output.created, true);
|
|
2224
|
+
|
|
2225
|
+
const content = fs.readFileSync(
|
|
2226
|
+
path.join(tmpDir, '.planning', 'phases', '03-api', '03-UAT.md'),
|
|
2227
|
+
'utf-8'
|
|
2228
|
+
);
|
|
2229
|
+
assert.ok(content.includes('User Acceptance Testing'), 'should have UAT heading');
|
|
2230
|
+
assert.ok(content.includes('Test Results'), 'should have test results section');
|
|
2231
|
+
});
|
|
2232
|
+
|
|
2233
|
+
test('scaffolds verification file', () => {
|
|
2234
|
+
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-api'), { recursive: true });
|
|
2235
|
+
|
|
2236
|
+
const result = runGsdTools('scaffold verification --phase 3', tmpDir);
|
|
2237
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
2238
|
+
|
|
2239
|
+
const output = JSON.parse(result.output);
|
|
2240
|
+
assert.strictEqual(output.created, true);
|
|
2241
|
+
|
|
2242
|
+
const content = fs.readFileSync(
|
|
2243
|
+
path.join(tmpDir, '.planning', 'phases', '03-api', '03-VERIFICATION.md'),
|
|
2244
|
+
'utf-8'
|
|
2245
|
+
);
|
|
2246
|
+
assert.ok(content.includes('Goal-Backward Verification'), 'should have verification heading');
|
|
2247
|
+
});
|
|
2248
|
+
|
|
2249
|
+
test('scaffolds phase directory', () => {
|
|
2250
|
+
const result = runGsdTools('scaffold phase-dir --phase 5 --name User Dashboard', tmpDir);
|
|
2251
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
2252
|
+
|
|
2253
|
+
const output = JSON.parse(result.output);
|
|
2254
|
+
assert.strictEqual(output.created, true);
|
|
2255
|
+
assert.ok(
|
|
2256
|
+
fs.existsSync(path.join(tmpDir, '.planning', 'phases', '05-user-dashboard')),
|
|
2257
|
+
'directory should be created'
|
|
2258
|
+
);
|
|
2259
|
+
});
|
|
2260
|
+
|
|
2261
|
+
test('does not overwrite existing files', () => {
|
|
2262
|
+
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
|
|
2263
|
+
fs.mkdirSync(phaseDir, { recursive: true });
|
|
2264
|
+
fs.writeFileSync(path.join(phaseDir, '03-CONTEXT.md'), '# Existing content');
|
|
2265
|
+
|
|
2266
|
+
const result = runGsdTools('scaffold context --phase 3', tmpDir);
|
|
2267
|
+
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
2268
|
+
|
|
2269
|
+
const output = JSON.parse(result.output);
|
|
2270
|
+
assert.strictEqual(output.created, false, 'should not overwrite');
|
|
2271
|
+
assert.strictEqual(output.reason, 'already_exists');
|
|
2272
|
+
});
|
|
2273
|
+
});
|