refactor(tests): consolidate Init Command Module — 7 files → 5 (#3756)
* fix(3687): update insert-phase docs and roadmapper to use --insert flag Updates stale references in insert-phase.md workflow and gsd-roadmapper.md agent to use the consolidated /gsd:phase --insert command syntax instead of the retired /gsd-insert-phase and /gsd:phase insert forms. Closes #3687 * refactor(tests): consolidate Init Command Module — 7 files → 5 Closes #3755 Merges `tests/init-manager-deps.test.cjs` (#2267 regression) into `tests/init-manager.test.cjs` (718 LOC), and `sdk/src/query/init-progress-precedence.test.ts` (#2674 regression) into `sdk/src/query/init-complex.test.ts` (788 LOC). The 800 LOC ceiling prevents further consolidation: - `tests/init.test.cjs` is pre-existing at 1630 LOC - `sdk/src/query/init.test.ts` is at 791 LOC - `sdk/src/query/init-workstream-milestone-op.test.ts` is a distinct seam testing initMilestoneOp, roadmapAnalyze, and resolveQueryRuntimeContext workstream resolution. Also adds Init Command Module Glossary entry to CONTEXT.md. Allowlist update deferred to rebase after #3738 merges (allowlist file does not exist on origin/main). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(init): initExecutePhase preserves same-milestone archived phase dir (#3469) When `phases clear` archives current-milestone phases into `.planning/milestones/<version>-phases/` but the workflow is still on that same milestone, `shouldDropArchivedPhaseMatch` was unconditionally dropping the archived dir match. This caused `phase_dir: null` when the phase was still executing in the current milestone. Fix: detect when `phaseInfo.archived === currentMilestone` (read from STATE.md) and skip the drop. The #2391 regression guard is safe because that scenario involves archived.version != current milestone. Also corrects two tests in `initRemoveWorkspace` to expect thrown GSDError instead of `{ data: { error } }` — the production code was intentionally changed to throw for CLI non-zero exit propagation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: arya rizky <aryarizkyardhipratama@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -614,3 +614,175 @@ describe('initManager workstream (#2731)', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// initProgress + initManager precedence (#2674)
|
||||
//
|
||||
// Both handlers must agree on phase status given the same inputs. Specifically,
|
||||
// a ROADMAP `- [x] Phase N` checkbox wins over disk state: a stub phase dir
|
||||
// with no SUMMARY.md that is checked in ROADMAP reports as `complete` from
|
||||
// both handlers.
|
||||
//
|
||||
// Pre-fix: initManager reported `complete` (explicit override), initProgress
|
||||
// reported `pending` (disk-only policy). This mismatch meant /gsd-manager and
|
||||
// /gsd-progress disagreed on the same data. Post-fix: both apply the
|
||||
// ROADMAP-[x]-wins policy.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const PRECEDENCE_CONFIG = JSON.stringify({
|
||||
model_profile: 'balanced',
|
||||
commit_docs: false,
|
||||
git: {
|
||||
branching_strategy: 'none',
|
||||
phase_branch_template: 'gsd/phase-{phase}-{slug}',
|
||||
milestone_branch_template: 'gsd/{milestone}-{slug}',
|
||||
quick_branch_template: null,
|
||||
},
|
||||
workflow: { research: true, plan_check: true, verifier: true, nyquist_validation: true },
|
||||
});
|
||||
|
||||
const PRECEDENCE_STATE = [
|
||||
'---',
|
||||
'milestone: v1.0',
|
||||
'---',
|
||||
].join('\n');
|
||||
|
||||
/** Find a phase by numeric value regardless of zero-padding ('3' vs '03'). */
|
||||
function findPhaseByNum(
|
||||
phases: Record<string, unknown>[],
|
||||
num: number,
|
||||
): Record<string, unknown> | undefined {
|
||||
return phases.find(p => parseInt(p.number as string, 10) === num);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a ROADMAP.md with the given phase list. Each entry is
|
||||
* `{num, name, checked}`. Emits both the checkbox summary lines AND the
|
||||
* `### Phase N:` heading sections (so initManager picks them up).
|
||||
*/
|
||||
async function writePrecedenceRoadmap(
|
||||
dir: string,
|
||||
phases: Array<{ num: string; name: string; checked: boolean }>,
|
||||
): Promise<void> {
|
||||
const checkboxes = phases
|
||||
.map(p => `- [${p.checked ? 'x' : ' '}] Phase ${p.num}: ${p.name}`)
|
||||
.join('\n');
|
||||
const sections = phases
|
||||
.map(p => `### Phase ${p.num}: ${p.name}\n\n**Goal:** ${p.name} goal\n\n**Depends on:** None\n`)
|
||||
.join('\n');
|
||||
await writeFile(join(dir, '.planning', 'ROADMAP.md'), [
|
||||
'# Roadmap',
|
||||
'',
|
||||
'## v1.0: Test',
|
||||
'',
|
||||
checkboxes,
|
||||
'',
|
||||
sections,
|
||||
].join('\n'));
|
||||
}
|
||||
|
||||
describe('initProgress + initManager precedence (#2674)', () => {
|
||||
let precedenceDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
precedenceDir = await mkdtemp(join(tmpdir(), 'gsd-2674-'));
|
||||
await mkdir(join(precedenceDir, '.planning', 'phases'), { recursive: true });
|
||||
await writeFile(join(precedenceDir, '.planning', 'config.json'), PRECEDENCE_CONFIG);
|
||||
await writeFile(join(precedenceDir, '.planning', 'STATE.md'), PRECEDENCE_STATE);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(precedenceDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('case 1: ROADMAP [x] + stub phase dir + no SUMMARY → both report complete', async () => {
|
||||
await writePrecedenceRoadmap(precedenceDir, [{ num: '3', name: 'Stubbed', checked: true }]);
|
||||
await mkdir(join(precedenceDir, '.planning', 'phases', '03-stubbed'), { recursive: true });
|
||||
// stub dir, no PLAN/SUMMARY/RESEARCH/CONTEXT files
|
||||
|
||||
const progress = (await initProgress([], precedenceDir)).data as Record<string, unknown>;
|
||||
const manager = (await initManager([], precedenceDir)).data as Record<string, unknown>;
|
||||
|
||||
const pPhase = findPhaseByNum(progress.phases as Record<string, unknown>[], 3);
|
||||
const mPhase = findPhaseByNum(manager.phases as Record<string, unknown>[], 3);
|
||||
|
||||
expect(pPhase?.status).toBe('complete');
|
||||
expect(mPhase?.disk_status).toBe('complete');
|
||||
});
|
||||
|
||||
it('case 2: ROADMAP [x] + phase dir + SUMMARY present → both complete (sanity)', async () => {
|
||||
await writePrecedenceRoadmap(precedenceDir, [{ num: '3', name: 'Done', checked: true }]);
|
||||
await mkdir(join(precedenceDir, '.planning', 'phases', '03-done'), { recursive: true });
|
||||
await writeFile(join(precedenceDir, '.planning', 'phases', '03-done', '03-01-PLAN.md'), '# plan');
|
||||
await writeFile(join(precedenceDir, '.planning', 'phases', '03-done', '03-01-SUMMARY.md'), '# done');
|
||||
|
||||
const progress = (await initProgress([], precedenceDir)).data as Record<string, unknown>;
|
||||
const manager = (await initManager([], precedenceDir)).data as Record<string, unknown>;
|
||||
|
||||
const pPhase = findPhaseByNum(progress.phases as Record<string, unknown>[], 3);
|
||||
const mPhase = findPhaseByNum(manager.phases as Record<string, unknown>[], 3);
|
||||
|
||||
expect(pPhase?.status).toBe('complete');
|
||||
expect(mPhase?.disk_status).toBe('complete');
|
||||
});
|
||||
|
||||
it('case 3: ROADMAP [ ] + phase dir + SUMMARY present → disk authoritative (complete)', async () => {
|
||||
await writePrecedenceRoadmap(precedenceDir, [{ num: '3', name: 'Disk', checked: false }]);
|
||||
await mkdir(join(precedenceDir, '.planning', 'phases', '03-disk'), { recursive: true });
|
||||
await writeFile(join(precedenceDir, '.planning', 'phases', '03-disk', '03-01-PLAN.md'), '# plan');
|
||||
await writeFile(join(precedenceDir, '.planning', 'phases', '03-disk', '03-01-SUMMARY.md'), '# done');
|
||||
|
||||
const progress = (await initProgress([], precedenceDir)).data as Record<string, unknown>;
|
||||
const manager = (await initManager([], precedenceDir)).data as Record<string, unknown>;
|
||||
|
||||
const pPhase = findPhaseByNum(progress.phases as Record<string, unknown>[], 3);
|
||||
const mPhase = findPhaseByNum(manager.phases as Record<string, unknown>[], 3);
|
||||
|
||||
expect(pPhase?.status).toBe('complete');
|
||||
expect(mPhase?.disk_status).toBe('complete');
|
||||
});
|
||||
|
||||
it('case 4: ROADMAP [ ] + stub phase dir + no SUMMARY → not complete', async () => {
|
||||
await writePrecedenceRoadmap(precedenceDir, [{ num: '3', name: 'Empty', checked: false }]);
|
||||
await mkdir(join(precedenceDir, '.planning', 'phases', '03-empty'), { recursive: true });
|
||||
|
||||
const progress = (await initProgress([], precedenceDir)).data as Record<string, unknown>;
|
||||
const manager = (await initManager([], precedenceDir)).data as Record<string, unknown>;
|
||||
|
||||
const pPhase = findPhaseByNum(progress.phases as Record<string, unknown>[], 3);
|
||||
const mPhase = findPhaseByNum(manager.phases as Record<string, unknown>[], 3);
|
||||
|
||||
// Neither should be 'complete' — preserves pre-existing classification.
|
||||
expect(pPhase?.status).not.toBe('complete');
|
||||
expect(mPhase?.disk_status).not.toBe('complete');
|
||||
});
|
||||
|
||||
it('case 5: ROADMAP [x] + no phase dir → both complete (ROADMAP-only branch preserved)', async () => {
|
||||
await writePrecedenceRoadmap(precedenceDir, [{ num: '3', name: 'Paper', checked: true }]);
|
||||
// no directory for phase 3
|
||||
|
||||
const progress = (await initProgress([], precedenceDir)).data as Record<string, unknown>;
|
||||
const manager = (await initManager([], precedenceDir)).data as Record<string, unknown>;
|
||||
|
||||
const pPhase = findPhaseByNum(progress.phases as Record<string, unknown>[], 3);
|
||||
const mPhase = findPhaseByNum(manager.phases as Record<string, unknown>[], 3);
|
||||
|
||||
expect(pPhase?.status).toBe('complete');
|
||||
expect(mPhase?.disk_status).toBe('complete');
|
||||
});
|
||||
|
||||
it('case 6: completed_count agrees across handlers for the stub-dir [x] case', async () => {
|
||||
await writePrecedenceRoadmap(precedenceDir, [
|
||||
{ num: '3', name: 'Stub', checked: true },
|
||||
{ num: '4', name: 'Todo', checked: false },
|
||||
]);
|
||||
await mkdir(join(precedenceDir, '.planning', 'phases', '03-stub'), { recursive: true });
|
||||
await mkdir(join(precedenceDir, '.planning', 'phases', '04-todo'), { recursive: true });
|
||||
|
||||
const progress = (await initProgress([], precedenceDir)).data as Record<string, unknown>;
|
||||
const manager = (await initManager([], precedenceDir)).data as Record<string, unknown>;
|
||||
|
||||
expect(progress.completed_count).toBe(1);
|
||||
expect(manager.completed_count).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user