import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import test from 'node:test'; import { parseChangelog, resolveChangelogGroupKey } from './changelog-parse'; const SAMPLE = `# Changelog ## v0.19.2 (2026-08-04) ### Changed - Subsync: picks both tracks now. ### Fixed - Overlay: shows the plain line immediately.
Internal changes ### Internal - Patched \`undici\`.
## v0.19.1 (2026-08-01) ### Added - **Word Card Type:** - Adds a setting. - Flags clear each other. ## v0.18.0 (2026-07-01) ### Fixed - Something older. `; test('changelog parser reads versions, dates, and sections in file order', () => { const entries = parseChangelog(SAMPLE); assert.deepEqual( entries.map((entry) => `${entry.version}@${entry.date}`), ['0.19.2@2026-08-04', '0.19.1@2026-08-01', '0.18.0@2026-07-01'], ); assert.deepEqual( entries[0]?.sections.map((section) => section.heading), ['Changed', 'Fixed', 'Internal'], ); assert.deepEqual(entries[0]?.sections[1]?.items, [ { text: 'Overlay: shows the plain line immediately.', children: [] }, ]); }); test('changelog parser flags sections inside the details block as internal', () => { const entries = parseChangelog(SAMPLE); const sections = entries[0]?.sections ?? []; assert.deepEqual( sections.map((section) => section.internal), [false, false, true], ); assert.deepEqual(sections[2]?.items, [{ text: 'Patched `undici`.', children: [] }]); }); test('changelog parser groups entries by major.minor', () => { const entries = parseChangelog(SAMPLE); assert.deepEqual( entries.map((entry) => entry.groupKey), ['0.19', '0.19', '0.18'], ); assert.equal(resolveChangelogGroupKey('1.2.3'), '1.2'); }); test('changelog parser keeps bullets that precede any section heading', () => { const entries = parseChangelog('## v0.1.0 (2025-01-01)\n\n- Initial release.\n'); assert.deepEqual(entries[0]?.sections, [ { heading: 'Changes', items: [{ text: 'Initial release.', children: [] }], internal: false, }, ]); }); test('changelog parser drops empty sections and tolerates missing dates', () => { const entries = parseChangelog('## v0.2.0\n\n### Added\n\n### Fixed\n- One fix.\n'); assert.equal(entries[0]?.date, ''); assert.deepEqual( entries[0]?.sections.map((section) => section.heading), ['Fixed'], ); }); test('changelog parser keeps indented sub-bullets nested under their lead bullet', () => { const entries = parseChangelog(SAMPLE); const added = entries[1]?.sections.find((section) => section.heading === 'Added'); assert.deepEqual(added?.items, [ { text: '**Word Card Type:**', children: [ { text: 'Adds a setting.', children: [] }, { text: 'Flags clear each other.', children: [] }, ], }, ]); }); test('changelog parser nests three bullet levels and rejoins wrapped lines', () => { const entries = parseChangelog( [ '## v0.9.0 (2025-05-05)', '', '### Added', '- Top level', ' - Second level', ' - Third level', ' continued on the next line', ' - Back to second level', '- Another top level', '', ].join('\n'), ); assert.deepEqual(entries[0]?.sections[0]?.items, [ { text: 'Top level', children: [ { text: 'Second level', children: [{ text: 'Third level continued on the next line', children: [] }], }, { text: 'Back to second level', children: [] }, ], }, { text: 'Another top level', children: [] }, ]); }); test('changelog parser reads prerelease and build metadata version headings', () => { const entries = parseChangelog( [ '## v0.16.0 (2026-06-01)', '', '### Added', '- New in 0.16.', '', '## v0.15.0-rc.1+build.2 (2026-05-29)', '', '### Added', '- Release candidate note.', '', ].join('\n'), ); // The prerelease heading has to become its own entry. Asserting the exact // version list is what catches the failure mode: a heading the regex misses // is not skipped, its notes silently fold into the release above it. assert.deepEqual( entries.map((entry) => entry.version), ['0.16.0', '0.15.0-rc.1+build.2'], ); assert.equal(entries[1]?.date, '2026-05-29'); assert.equal(entries[1]?.groupKey, '0.15'); assert.equal(entries[0]?.sections.length, 1); // The prerelease body has to land on its own entry, not fold into 0.16.0. assert.deepEqual(entries[1]?.sections, [ { heading: 'Added', items: [{ text: 'Release candidate note.', children: [] }], internal: false, }, ]); }); test('changelog parser handles the repo CHANGELOG.md', () => { const markdown = fs.readFileSync(path.join(process.cwd(), 'CHANGELOG.md'), 'utf8'); const entries = parseChangelog(markdown); assert.ok(entries.length > 3); for (const entry of entries) { assert.match(entry.version, /^\d+\.\d+\.\d+/); assert.ok(entry.sections.length > 0, `expected sections for v${entry.version}`); for (const section of entry.sections) { for (const item of section.items) { assert.ok(item.text.length > 0, `empty bullet in v${entry.version}`); } } } // Older entries group notes under a bold lead bullet; nesting must survive. const breaking = entries .find((entry) => entry.version === '0.15.0') ?.sections.find((section) => section.heading === 'Breaking Changes'); assert.deepEqual( breaking?.items.map((item) => `${item.text}:${item.children.length}`), ['**Subsync:**:2', '**N+1 Highlighting:**:2'], ); });