import { describe, expect, test } from 'bun:test'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { anchor, changelogSection, isTestTag, mergeSums, pruneList, type Release, sectionSummary, shortNotes } from '../tools/forgejo-release'; const CL = `# Changelog ## Unreleased Next things. - a ## 0.2.0 · 2026-10-12 Summary line one continues here. Second sentence. - **Bullet.** detail more detail ## 0.1.0 · 2026-10-10 - First bullet text wraps here: - nested - second `; const rel = (id: number, tag: string, draft = false): Release => ({ id, tag_name: tag, name: tag, draft, prerelease: false, body: '', assets: [] }); describe('forgejo release notes', () => { test('anchor matches Forgejo heading ids', () => { expect(anchor('0.6.0 · 2026-10-12')).toBe('0-6-0-2026-10-12'); expect(anchor('Unreleased')).toBe('unreleased'); }); test('section and summary', () => { expect(changelogSection(CL, '0.2.0')!.heading).toBe('0.2.0 · 2026-10-12'); expect(sectionSummary(changelogSection(CL, '0.2.0')!.body)).toBe('Summary line one continues here. Second sentence.'); expect(sectionSummary(changelogSection(CL, '0.1.0')!.body)).toBe('First bullet text wraps here.'); expect(changelogSection(CL, '0.3.0')).toBeNull(); }); test('short notes: summary, install, links; unknown version falls back to Unreleased', () => { const n = shortNotes(CL, '0.2.0'); expect(n.split('\n')[0]).toBe('Summary line one continues here. Second sentence.'); expect(n).toContain('npx storebound@0.2.0 init'); expect(n).toContain('/src/tag/v0.2.0/CHANGELOG.md#0-2-0-2026-10-12'); expect(shortNotes(CL, '0.3.0')).toContain('CHANGELOG.md#unreleased'); }); test('the real CHANGELOG gives at most 8 short lines for every version', () => { const real = readFileSync(join(import.meta.dir, '..', 'CHANGELOG.md'), 'utf8'); for (const v of [...real.matchAll(/^## (\d+\.\d+\.\d+)/gm)].map(m => m[1]!)) { const lines = shortNotes(real, v).split('\n'); expect(lines.length).toBeLessThanOrEqual(8); expect(lines.every(l => l.length <= 200)).toBe(true); } }); }); describe('forgejo release assets and pruning', () => { test('SHA256SUMS merge: new entries win, sorted', () => { const a = 'a'.repeat(64), b = 'b'.repeat(64), c = 'c'.repeat(64); expect(mergeSums(`${a} x.deb\n${a} y.exe\n`, [{ file: 'y.exe', sha256: b }, { file: 'm.dmg', sha256: c }])) .toBe(`${c} m.dmg\n${a} x.deb\n${b} y.exe\n`); expect(mergeSums('', [])).toBe('\n'); }); test('prune keeps only the given tag, never when it is missing or a draft', () => { const rs = [rel(3, 'v0.8.0'), rel(2, 'v0.7.0'), rel(1, 'test-abcdef1', true)]; expect(pruneList(rs, 'v0.8.0').remove.map(r => r.tag_name)).toEqual(['v0.7.0', 'test-abcdef1']); expect(pruneList(rs, 'v0.9.0').problem).toContain('nothing is deleted'); expect(pruneList([rel(4, 'v1.0.0', true), ...rs], 'v1.0.0').problem).toContain('draft'); }); test('only test- tags may be deleted', () => { expect(isTestTag('test-0a1b2c3')).toBe(true); expect(isTestTag('v0.8.0')).toBe(false); expect(isTestTag('test-latest')).toBe(false); }); });