* test(#2692): add behavioral --wave N test, annotate source-text assertions Adds two behavioral tests for wave filtering via phase-plan-index: - Verifies plans with wave frontmatter are correctly grouped by wave number - Verifies plans with no wave field default to wave 1 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(#2684,#2676): milestone.complete version validation + parallel milestone phase routing #2684: Confirms milestone.complete correctly validates and uses its version argument end-to-end. The inline archive path in milestoneComplete already forwarded version correctly; regression tests lock in that contract. #2676: phase.complete applied getMilestonePhaseFilter unconditionally, using STATE.md's primary milestone to scope the candidate set. When the completed phase belongs to a parallel (secondary) milestone, the filter excluded all phases from that milestone, leaving an empty candidate set and incorrectly returning is_last_phase: true / next_phase: null. Fix: before applying the milestone filter in Step E, check whether the completed phase itself appears in the filtered set. If not, skip the filter for both the directory scan and the ROADMAP.md fallback so phases from the secondary milestone remain visible for next-phase detection. Closes #2684 Closes #2676 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
126 lines
4.6 KiB
JavaScript
126 lines
4.6 KiB
JavaScript
/**
|
|
* Regression tests for bug #2684:
|
|
* `gsd-sdk query milestone.complete <version>` always fails with
|
|
* GSDError: version required for phases archive.
|
|
*
|
|
* Root cause: milestoneComplete extracted version from args[0] but passed
|
|
* [] instead of args (or [version]) to phasesArchive, so phasesArchive
|
|
* never received the version string and threw immediately.
|
|
*
|
|
* Fix: pass args (or [version]) when delegating to phasesArchive.
|
|
*/
|
|
|
|
const { describe, test, beforeEach, afterEach } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
|
|
|
|
const SDK_CLI = path.join(__dirname, '..', 'sdk', 'dist', 'cli.js');
|
|
const { execFileSync } = require('child_process');
|
|
|
|
function runSdkQuery(args, cwd) {
|
|
try {
|
|
const result = execFileSync(process.execPath, [SDK_CLI, 'query', ...args], {
|
|
cwd,
|
|
encoding: 'utf-8',
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
});
|
|
const parsed = JSON.parse(result.trim());
|
|
return { success: true, data: parsed };
|
|
} catch (err) {
|
|
const stderr = err.stderr?.toString().trim() || '';
|
|
const stdout = err.stdout?.toString().trim() || '';
|
|
// If the output is JSON despite non-zero exit, parse it
|
|
try {
|
|
const parsed = JSON.parse(stdout);
|
|
return { success: true, data: parsed };
|
|
} catch {
|
|
/* not JSON */
|
|
}
|
|
return { success: false, error: stderr || err.message };
|
|
}
|
|
}
|
|
|
|
describe('bug #2684: milestone.complete forwards version to phases.archive', () => {
|
|
let tmpDir;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = createTempProject();
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('milestone.complete v1.0 does not throw version required error', () => {
|
|
// Minimal project: ROADMAP.md so milestone filter can run, one phase dir
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
`# Roadmap\n\n### Phase 1: Foundation\n**Goal:** Setup\n`
|
|
);
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-foundation'), { recursive: true });
|
|
|
|
const result = runSdkQuery(['milestone.complete', 'v1.0'], tmpDir);
|
|
|
|
assert.ok(
|
|
result.success,
|
|
`milestone.complete should succeed, got error: ${result.error}`
|
|
);
|
|
assert.ok(
|
|
!result.error || !result.error.includes('version required'),
|
|
`should not throw "version required" — got: ${result.error}`
|
|
);
|
|
});
|
|
|
|
test('milestone.complete returns version in response data', () => {
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
`# Roadmap\n\n### Phase 1: Foundation\n**Goal:** Setup\n`
|
|
);
|
|
|
|
const result = runSdkQuery(['milestone.complete', 'v2.5'], tmpDir);
|
|
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
assert.strictEqual(result.data.version, 'v2.5', 'version should be echoed in response');
|
|
});
|
|
|
|
test('milestone.complete with --archive-phases forwards version correctly', () => {
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
`# Roadmap\n\n### Phase 1: Foundation\n**Goal:** Setup\n`
|
|
);
|
|
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation');
|
|
fs.mkdirSync(phaseDir, { recursive: true });
|
|
fs.writeFileSync(path.join(phaseDir, '01-01-PLAN.md'), '# Plan');
|
|
fs.writeFileSync(path.join(phaseDir, '01-01-SUMMARY.md'), '# Summary');
|
|
|
|
// With --archive-phases, the version must reach the archive logic
|
|
// Without the fix this would throw "version required for phases archive"
|
|
const result = runSdkQuery(['milestone.complete', 'v1.0', '--archive-phases'], tmpDir);
|
|
|
|
assert.ok(result.success, `milestone.complete --archive-phases failed: ${result.error}`);
|
|
assert.strictEqual(result.data.version, 'v1.0');
|
|
// The archive flag should have moved the phase dir
|
|
assert.ok(
|
|
result.data.archived.phases === true,
|
|
'phases should be archived when --archive-phases is passed'
|
|
);
|
|
const archiveDir = path.join(tmpDir, '.planning', 'milestones', 'v1.0-phases');
|
|
assert.ok(fs.existsSync(archiveDir), 'archive directory should exist');
|
|
});
|
|
|
|
test('phases.archive v1.0 (direct call, workaround) also works', () => {
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
|
`# Roadmap\n\n### Phase 1: Foundation\n**Goal:** Setup\n`
|
|
);
|
|
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-foundation'), { recursive: true });
|
|
|
|
const result = runSdkQuery(['phases.archive', 'v1.0'], tmpDir);
|
|
|
|
assert.ok(result.success, `phases.archive failed: ${result.error}`);
|
|
assert.strictEqual(result.data.version, 'v1.0');
|
|
});
|
|
});
|