fix(milestone): deduplicate phase filter and handle empty MILESTONES.md

Two hardening changes to cmdMilestoneComplete:

1. Replace 27 lines of inline isDirInMilestone logic (roadmap parsing,
   normalization, and matching) with a single call to the shared
   getMilestonePhaseFilter(cwd) from core.cjs. The inline copy was
   identical to the core version — deduplicating prevents future drift.

2. Handle empty MILESTONES.md files. Previously, an existing but empty
   file would fall into the headerMatch branch and produce malformed
   output. Now an empty file is treated the same as a missing one,
   writing the standard "# Milestones" header before the entry.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Tibsfox
2026-03-03 01:48:47 -08:00
committed by Lex Christopherson
parent 15226feb18
commit 8b8d1074b8
3 changed files with 121 additions and 48 deletions

View File

@@ -365,6 +365,84 @@ describe('mergeCodexConfig', () => {
assert.strictEqual(first, second, 'idempotent merge');
});
test('case 2 after case 3 with existing [features]: no duplicate sections', () => {
const configPath = path.join(tmpDir, 'config.toml');
fs.writeFileSync(configPath, '[features]\nother_feature = true\n\n[model]\nname = "o3"\n');
mergeCodexConfig(configPath, sampleBlock);
mergeCodexConfig(configPath, sampleBlock);
const content = fs.readFileSync(configPath, 'utf8');
const featuresCount = (content.match(/^\[features\]\s*$/gm) || []).length;
const agentsCount = (content.match(/^\[agents\]\s*$/gm) || []).length;
assert.strictEqual(featuresCount, 1, 'exactly one [features] section');
assert.strictEqual(agentsCount, 1, 'exactly one [agents] section');
assert.ok(content.includes('other_feature = true'), 'preserves user feature keys');
assert.ok(content.includes('multi_agent = true'), 'has GSD feature key');
assert.ok(content.includes('[agents.gsd-executor]'), 'has agent');
});
test('case 2 re-injects missing feature keys', () => {
const configPath = path.join(tmpDir, 'config.toml');
const manualContent = '[features]\nother_feature = true\n\n' + GSD_CODEX_MARKER + '\n[agents]\nmax_threads = 4\n';
fs.writeFileSync(configPath, manualContent);
mergeCodexConfig(configPath, sampleBlock);
const content = fs.readFileSync(configPath, 'utf8');
assert.ok(content.includes('multi_agent = true'), 're-injects multi_agent');
assert.ok(content.includes('default_mode_request_user_input = true'), 're-injects request_user_input');
assert.ok(content.includes('other_feature = true'), 'preserves user feature');
});
test('case 2 strips leaked [agents] from before content', () => {
const configPath = path.join(tmpDir, 'config.toml');
const brokenContent = [
'[features]',
'default_mode_request_user_input = true',
'multi_agent = true',
'child_agents_md = false',
'',
'[agents]',
'max_threads = 4',
'max_depth = 2',
'',
'[agents.gsd-executor]',
'description = "old"',
'config_file = "agents/gsd-executor.toml"',
'',
GSD_CODEX_MARKER,
'[agents]',
'max_threads = 4',
'',
].join('\n');
fs.writeFileSync(configPath, brokenContent);
mergeCodexConfig(configPath, sampleBlock);
const content = fs.readFileSync(configPath, 'utf8');
const agentsCount = (content.match(/^\[agents\]\s*$/gm) || []).length;
assert.strictEqual(agentsCount, 1, 'exactly one [agents] section');
assert.ok(content.includes('child_agents_md = false'), 'preserves user feature keys');
assert.ok(content.includes('[agents.gsd-executor]'), 'has agent from fresh block');
});
test('case 2 idempotent after case 3 with existing [features]', () => {
const configPath = path.join(tmpDir, 'config.toml');
fs.writeFileSync(configPath, '[features]\nother_feature = true\n');
mergeCodexConfig(configPath, sampleBlock);
const first = fs.readFileSync(configPath, 'utf8');
mergeCodexConfig(configPath, sampleBlock);
const second = fs.readFileSync(configPath, 'utf8');
mergeCodexConfig(configPath, sampleBlock);
const third = fs.readFileSync(configPath, 'utf8');
assert.strictEqual(first, second, 'idempotent after 2nd merge');
assert.strictEqual(second, third, 'idempotent after 3rd merge');
});
});
// ─── Integration: installCodexConfig ────────────────────────────────────────────