* fix: resolve init phase details fallback * chore: add changeset for phase details fallback
This commit is contained in:
@@ -325,6 +325,142 @@ describe('init commands', () => {
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.phase_req_ids, null);
|
||||
});
|
||||
|
||||
test('init plan-phase resolves phase_req_ids from flat Phase Details after active milestone heading', () => {
|
||||
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '11-second-active-phase'), { recursive: true });
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), [
|
||||
'---',
|
||||
'milestone: v0.4.0',
|
||||
'current_phase: 11',
|
||||
'---',
|
||||
'',
|
||||
].join('\n'));
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), [
|
||||
'# Roadmap: Example',
|
||||
'',
|
||||
'## Milestones',
|
||||
'',
|
||||
'- ✅ **v0.3.0 Foundations** - Phases 1-9 (shipped 2026-01-01)',
|
||||
'- 🚧 **v0.4.0 Feature Work** - Phases 10-11 (in progress)',
|
||||
'',
|
||||
'## Phases',
|
||||
'',
|
||||
'<details>',
|
||||
'<summary>✅ v0.3.0 Foundations (Phases 1-9) - SHIPPED 2026-01-01</summary>',
|
||||
'',
|
||||
'- [x] **Phase 1: Bootstrap**',
|
||||
'',
|
||||
'</details>',
|
||||
'',
|
||||
'### 🚧 v0.4.0 Feature Work (Active)',
|
||||
'',
|
||||
'**Milestone Goal:** Deliver the feature set.',
|
||||
'',
|
||||
'- [ ] **Phase 10: First Active Phase**',
|
||||
'- [ ] **Phase 11: Second Active Phase**',
|
||||
'',
|
||||
'### 📋 v0.5+ (Planned)',
|
||||
'',
|
||||
'## Phase Details',
|
||||
'',
|
||||
'### Phase 10: First Active Phase',
|
||||
'**Goal**: Build the first piece.',
|
||||
'**Requirements**: REQ-01',
|
||||
'',
|
||||
'### Phase 11: Second Active Phase',
|
||||
'**Goal**: Build the second piece.',
|
||||
'**Requirements**: REQ-02, REQ-03',
|
||||
'',
|
||||
'## Progress',
|
||||
'',
|
||||
].join('\n'));
|
||||
|
||||
const result = runGsdTools('init plan-phase 11', tmpDir);
|
||||
assert.ok(result.success, `init plan-phase failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.phase_found, true);
|
||||
assert.strictEqual(output.phase_req_ids, 'REQ-02, REQ-03');
|
||||
});
|
||||
|
||||
test('init execute-phase resolves phase_req_ids from flat Phase Details after active milestone heading', () => {
|
||||
seedPhase(tmpDir, '11-second-active-phase', {
|
||||
'11-01-PLAN.md': '# Plan',
|
||||
});
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), [
|
||||
'---',
|
||||
'milestone: v0.4.0',
|
||||
'current_phase: 11',
|
||||
'---',
|
||||
'',
|
||||
].join('\n'));
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), [
|
||||
'# Roadmap: Example',
|
||||
'',
|
||||
'## Phases',
|
||||
'',
|
||||
'### 🚧 v0.4.0 Feature Work (Active)',
|
||||
'',
|
||||
'- [ ] **Phase 10: First Active Phase**',
|
||||
'- [ ] **Phase 11: Second Active Phase**',
|
||||
'',
|
||||
'### 📋 v0.5+ (Planned)',
|
||||
'',
|
||||
'## Phase Details',
|
||||
'',
|
||||
'### Phase 10: First Active Phase',
|
||||
'**Goal**: Build the first piece.',
|
||||
'**Requirements**: REQ-01',
|
||||
'',
|
||||
'### Phase 11: Second Active Phase',
|
||||
'**Goal**: Build the second piece.',
|
||||
'**Requirements**: REQ-02, REQ-03',
|
||||
'',
|
||||
].join('\n'));
|
||||
|
||||
const result = runGsdTools('init execute-phase 11', tmpDir);
|
||||
assert.ok(result.success, `init execute-phase failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.phase_found, true);
|
||||
assert.strictEqual(output.phase_req_ids, 'REQ-02, REQ-03');
|
||||
});
|
||||
|
||||
test('init phase-op resolves a details-summary milestone phase from later flat Phase Details', () => {
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), [
|
||||
'milestone: v1.11',
|
||||
'current_phase: 86',
|
||||
'',
|
||||
].join('\n'));
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), [
|
||||
'# Roadmap',
|
||||
'',
|
||||
'## Phases',
|
||||
'<details open>',
|
||||
'<summary>🔄 v1.11 A06 (Phases 86-91) — IN PROGRESS</summary>',
|
||||
'',
|
||||
'- [ ] **Phase 86: Details Block Regression** — Parser should resolve this (DATA-01)',
|
||||
'- [ ] **Phase 87: Other Work** — Later phase',
|
||||
'</details>',
|
||||
'',
|
||||
'## Phase Details',
|
||||
'',
|
||||
'### Phase 86: Details Block Regression',
|
||||
'**Goal**: Resolve phase details after collapsed milestone block',
|
||||
'**Requirements**: DATA-01',
|
||||
'',
|
||||
'### Phase 87: Other Work',
|
||||
'**Goal**: Not relevant',
|
||||
'',
|
||||
].join('\n'));
|
||||
|
||||
const result = runGsdTools('init phase-op 86', tmpDir);
|
||||
assert.ok(result.success, `init phase-op failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.phase_found, true);
|
||||
assert.strictEqual(output.phase_name, 'Details Block Regression');
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -24,6 +24,7 @@ const ROOT = path.resolve(__dirname, '..');
|
||||
const identity = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'package-identity.cjs'));
|
||||
const pkg = require(path.join(ROOT, 'package.json'));
|
||||
const { MANAGED_HOOKS } = require(path.join(ROOT, 'hooks', 'managed-hooks-registry.cjs'));
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const PLUGIN_JSON_PATH = path.join(ROOT, '.claude-plugin', 'plugin.json');
|
||||
const HOOKS_JSON_PATH = path.join(ROOT, 'hooks', 'hooks.json');
|
||||
@@ -235,9 +236,9 @@ describe('B: hooks/hooks.json', () => {
|
||||
// or changes `name` to an invalid form goes red immediately.
|
||||
//
|
||||
// C2 (OPPORTUNISTIC) — When the `claude` binary IS on PATH, also run
|
||||
// `claude plugin validate . --strict` as an end-to-end smoke test.
|
||||
// This tier provides defence-in-depth for schema changes Claude Code
|
||||
// may introduce that the fixture hasn't yet captured.
|
||||
// `claude plugin validate <temp-plugin-root> --strict` as an end-to-end
|
||||
// smoke test. This tier provides defence-in-depth for schema changes
|
||||
// Claude Code may introduce that the fixture hasn't yet captured.
|
||||
//
|
||||
describe('C: plugin.json schema validation', () => {
|
||||
|
||||
@@ -366,19 +367,29 @@ describe('C: plugin.json schema validation', () => {
|
||||
})();
|
||||
|
||||
test(
|
||||
'C2: claude plugin validate . --strict exits 0 (opportunistic — skip when claude not on PATH)',
|
||||
'C2: claude plugin validate --strict exits 0 (opportunistic — skip when claude not on PATH)',
|
||||
{ skip: !claudeAvailable ? 'claude binary not on PATH' : false },
|
||||
() => {
|
||||
const result = spawnSync('claude', ['plugin', 'validate', '.', '--strict'], {
|
||||
cwd: ROOT,
|
||||
encoding: 'utf-8',
|
||||
timeout: 15000,
|
||||
});
|
||||
assert.equal(
|
||||
result.status,
|
||||
0,
|
||||
`claude plugin validate . --strict exited with ${result.status}.\nstdout: ${result.stdout}\nstderr: ${result.stderr}`
|
||||
);
|
||||
const pluginRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-plugin-validate-'));
|
||||
try {
|
||||
fs.mkdirSync(path.join(pluginRoot, '.claude-plugin'), { recursive: true });
|
||||
fs.copyFileSync(PLUGIN_JSON_PATH, path.join(pluginRoot, '.claude-plugin', 'plugin.json'));
|
||||
fs.symlinkSync(path.join(ROOT, 'commands'), path.join(pluginRoot, 'commands'), 'dir');
|
||||
fs.symlinkSync(path.join(ROOT, 'hooks'), path.join(pluginRoot, 'hooks'), 'dir');
|
||||
|
||||
const result = spawnSync('claude', ['plugin', 'validate', pluginRoot, '--strict'], {
|
||||
cwd: ROOT,
|
||||
encoding: 'utf-8',
|
||||
timeout: 15000,
|
||||
});
|
||||
assert.equal(
|
||||
result.status,
|
||||
0,
|
||||
`claude plugin validate ${pluginRoot} --strict exited with ${result.status}.\nstdout: ${result.stdout}\nstderr: ${result.stderr}`
|
||||
);
|
||||
} finally {
|
||||
cleanup(pluginRoot);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user