mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-06 07:21:33 -07:00
204 lines
5.6 KiB
TypeScript
204 lines
5.6 KiB
TypeScript
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.
|
|
|
|
<details>
|
|
<summary>Internal changes</summary>
|
|
|
|
### Internal
|
|
- Patched \`undici\`.
|
|
|
|
</details>
|
|
|
|
## 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'],
|
|
);
|
|
});
|