import { describe, expect, it } from 'bun:test'; import { type BumpType, type ModuleVersionPlan, applyBump, maxBump, parseChangeset, planModuleVersion, renderChangelogSection, } from './changeset-version'; describe('parseChangeset', () => { it('parses quoted id + bump and the summary body', () => { const r = parseChangeset('---\n"lunacycle": minor\n---\n\nAdded a thing.\n'); expect(r.bumps).toEqual({ lunacycle: 'minor' }); expect(r.summary).toBe('Added a thing.'); }); it('parses unquoted ids and multiple modules', () => { const r = parseChangeset('---\nlunacycle: patch\nnigredo: major\n---\nfix\n'); expect(r.bumps).toEqual({ lunacycle: 'patch', nigredo: 'major' }); }); it('throws on a missing frontmatter fence', () => { expect(() => parseChangeset('just a summary, no frontmatter')).toThrow(/frontmatter/); }); it('throws on an invalid bump keyword', () => { expect(() => parseChangeset('---\nlunacycle: huge\n---\n')).toThrow(/major\|minor\|patch/); }); it('throws on a frontmatter line without a colon', () => { expect(() => parseChangeset('---\nlunacycle minor\n---\n')).toThrow(/not "id: bump"/); }); }); describe('maxBump', () => { it('ranks major > minor > patch', () => { expect(maxBump(['patch', 'major', 'minor'])).toBe('major'); expect(maxBump(['patch', 'minor'])).toBe('minor'); expect(maxBump(['patch'])).toBe('patch'); }); it('returns null for an empty list', () => { expect(maxBump([])).toBeNull(); }); }); describe('applyBump', () => { it('bumps each level and zeroes lower segments', () => { expect(applyBump('1.2.3', 'major')).toBe('2.0.0'); expect(applyBump('1.2.3', 'minor')).toBe('1.3.0'); expect(applyBump('1.2.3', 'patch')).toBe('1.2.4'); }); it('throws on a non x.y.z version', () => { expect(() => applyBump('1.2', 'patch')).toThrow(/x\.y\.z/); expect(() => applyBump('1.0.0+4', 'patch')).toThrow(/x\.y\.z/); }); }); describe('planModuleVersion', () => { const cs = (id: string, bump: BumpType, summary = '') => parseChangeset(`---\n"${id}": ${bump}\n---\n${summary}\n`); it('returns null when no changeset targets the module', () => { expect(planModuleVersion('1.0.0', 'lunacycle', [])).toBeNull(); expect(planModuleVersion('1.0.0', 'lunacycle', [cs('nigredo', 'minor')])).toBeNull(); }); it('takes the max bump across relevant changesets', () => { const plan = planModuleVersion('1.2.3', 'lunacycle', [ cs('lunacycle', 'patch', 'a'), cs('lunacycle', 'minor', 'b'), cs('nigredo', 'major', 'unrelated'), ]); expect(plan).not.toBeNull(); expect(plan?.bump).toBe('minor'); expect(plan?.next).toBe('1.3.0'); expect(plan?.entries).toEqual(['a', 'b']); }); it('omits empty summaries from entries', () => { const plan = planModuleVersion('1.0.0', 'lunacycle', [ cs('lunacycle', 'patch', ''), cs('lunacycle', 'patch', 'real'), ]); expect(plan?.entries).toEqual(['real']); }); }); describe('renderChangelogSection', () => { it('renders a newest-on-top section with the bump heading', () => { const plan = planModuleVersion('1.0.0', 'lunacycle', [ parseChangeset('---\nlunacycle: minor\n---\nAdded tasks view.\n'), ]); expect(plan).not.toBeNull(); const md = renderChangelogSection(plan as ModuleVersionPlan); expect(md).toContain('## 1.1.0'); expect(md).toContain('### Minor Changes'); expect(md).toContain('- Added tasks view.'); }); it('falls back to a generic bullet when there are no summaries', () => { const plan = planModuleVersion('1.0.0', 'lunacycle', [ parseChangeset('---\nlunacycle: patch\n---\n'), ]); expect(renderChangelogSection(plan as ModuleVersionPlan)).toContain('- Version bump.'); }); });