diff --git a/sdk/src/query/phase-lifecycle.test.ts b/sdk/src/query/phase-lifecycle.test.ts index 9969149cd..1c40f7c55 100644 --- a/sdk/src/query/phase-lifecycle.test.ts +++ b/sdk/src/query/phase-lifecycle.test.ts @@ -226,6 +226,44 @@ describe('replaceInCurrentMilestone', () => { // v1.18 old plans line should remain untouched expect(result).toContain('**Plans:** 1/1 plans'); }); + + it('replaces inside active
even when footer text exists after
', async () => { + const { replaceInCurrentMilestone } = await import('./phase-lifecycle.js'); + // Scenario: active milestone is the last
block, but a footer + // (e.g. "---\n*Last updated*") follows it. The fast-path sees after.trim() + // non-empty and replaces in the footer instead of inside the active block. + const content = [ + '# Roadmap', + '', + '
', + 'v1.0 (Archived)', + '', + '**Plans:** 1/1 plans', + '', + '
', + '', + '
', + 'v2.0 (Active)', + '', + '**Plans:** 1/2 plans', + '', + '
', + '', + '---', + '*Last updated: 2026-01-01*', + ].join('\n'); + + const pattern = /\*\*Plans:\*\* [^\n]+/g; + const result = replaceInCurrentMilestone(content, pattern, '**Plans:** 2/2 plans complete'); + + // Active milestone inside last
should be updated + expect(result).toContain('**Plans:** 2/2 plans complete'); + // Archived milestone should remain untouched + expect(result).toContain('**Plans:** 1/1 plans'); + // Footer should be preserved verbatim + expect(result).toContain('---'); + expect(result).toContain('*Last updated: 2026-01-01*'); + }); }); // ─── readModifyWriteRoadmapMd ───────────────────────────────────────────