import { describe, test, expect } from 'bun:test'; import { parseSemver, isMinorOrMajorBump, isNewerVersion, extractChangelogBetween, } from '../src/commands/check-update.ts'; describe('parseSemver', () => { test('parses standard version', () => { expect(parseSemver('0.4.0')).toEqual([0, 4, 0, 0]); }); test('strips v prefix', () => { expect(parseSemver('v0.5.0')).toEqual([0, 5, 0, 0]); }); test('returns null for malformed version', () => { expect(parseSemver('0.4')).toBeNull(); expect(parseSemver('abc')).toBeNull(); expect(parseSemver('')).toBeNull(); }); test('preserves the 4th micro segment', () => { expect(parseSemver('0.2.0.1')).toEqual([0, 2, 0, 1]); }); }); describe('isNewerVersion', () => { test('detects the patch segment used by the gbrain release train', () => { expect(isNewerVersion('0.42.10.0', '0.42.66.0')).toBe(true); }); test('detects a micro follow-up release', () => { expect(isNewerVersion('0.42.66.0', '0.42.66.1')).toBe(true); }); test('orders legacy 3-segment against 4-segment: 0.42.67.0 > 0.42.66.1 > 0.42.66', () => { expect(isNewerVersion('0.42.66.1', '0.42.67.0')).toBe(true); expect(isNewerVersion('0.42.66', '0.42.66.1')).toBe(true); expect(isNewerVersion('0.42.66', '0.42.66.0')).toBe(false); // 3-segment == its .0 micro expect(isNewerVersion('0.42.67.0', '0.42.66.1')).toBe(false); }); test('rejects equal, older, and malformed versions', () => { expect(isNewerVersion('0.42.66.0', '0.42.66.0')).toBe(false); expect(isNewerVersion('0.42.66.1', '0.42.66.0')).toBe(false); expect(isNewerVersion('0.42.66.0', 'garbage')).toBe(false); }); }); describe('isMinorOrMajorBump', () => { test('0.4.0 vs 0.5.0 → update available (minor bump)', () => { expect(isMinorOrMajorBump('0.4.0', '0.5.0')).toBe(true); }); test('0.4.0 vs 0.4.1 → NOT available (patch only)', () => { expect(isMinorOrMajorBump('0.4.0', '0.4.1')).toBe(false); }); test('0.4.0 vs 1.0.0 → update available (major bump)', () => { expect(isMinorOrMajorBump('0.4.0', '1.0.0')).toBe(true); }); test('0.4.0 vs 0.4.0 → NOT available (same version)', () => { expect(isMinorOrMajorBump('0.4.0', '0.4.0')).toBe(false); }); test('0.4.0 vs 0.3.0 → NOT available (older)', () => { expect(isMinorOrMajorBump('0.4.0', '0.3.0')).toBe(false); }); test('0.4.1 vs 0.5.0 → update available (minor bump, different patch)', () => { expect(isMinorOrMajorBump('0.4.1', '0.5.0')).toBe(true); }); test('malformed version → returns false', () => { expect(isMinorOrMajorBump('0.4.0', 'abc')).toBe(false); expect(isMinorOrMajorBump('bad', '0.5.0')).toBe(false); }); test('handles v prefix on latest', () => { expect(isMinorOrMajorBump('0.4.0', 'v0.5.0')).toBe(true); }); }); describe('extractChangelogBetween', () => { const changelog = `# Changelog ## [0.5.0] - 2026-05-01 ### Added - Feature X ## [0.4.1] - 2026-04-15 ### Fixed - Bug Y ## [0.4.0] - 2026-04-09 ### Added - Feature Z ## [0.3.0] - 2026-04-08 ### Added - Feature W `; test('extracts entries between 0.4.0 and 0.5.0', () => { const result = extractChangelogBetween(changelog, '0.4.0', '0.5.0'); expect(result).toContain('Feature X'); expect(result).toContain('Bug Y'); expect(result).not.toContain('Feature Z'); expect(result).not.toContain('Feature W'); }); test('extracts only 0.5.0 when upgrading from 0.4.1', () => { const result = extractChangelogBetween(changelog, '0.4.1', '0.5.0'); expect(result).toContain('Feature X'); expect(result).not.toContain('Bug Y'); }); test('returns empty for same version', () => { const result = extractChangelogBetween(changelog, '0.5.0', '0.5.0'); expect(result).toBe(''); }); test('returns empty for malformed from version', () => { const result = extractChangelogBetween(changelog, 'bad', '0.5.0'); expect(result).toBe(''); }); test('does not capture older major versions incorrectly', () => { const crossMajor = `# Changelog ## [2.0.0] - 2026-06-01 ### Added - Major 2 ## [0.5.0] - 2026-05-01 ### Added - Minor 5 `; const result = extractChangelogBetween(crossMajor, '1.2.0', '2.0.0'); expect(result).toContain('Major 2'); expect(result).not.toContain('Minor 5'); }); test('distinguishes micro releases', () => { const micro = `# Changelog ## [0.42.66.1] - 2026-07-26 - Follow-up fix ## [0.42.66.0] - 2026-07-25 - Original release `; const result = extractChangelogBetween(micro, '0.42.66.0', '0.42.66.1'); expect(result).toContain('Follow-up fix'); expect(result).not.toContain('Original release'); }); }); describe('check-update CLI', () => { test('check-update is in CLI_ONLY set', async () => { const source = await Bun.file( new URL('../src/cli.ts', import.meta.url).pathname ).text(); expect(source).toContain("'check-update'"); }); test('--help prints usage and exits 0', async () => { const proc = Bun.spawn(['bun', 'run', 'src/cli.ts', 'check-update', '--help'], { cwd: new URL('..', import.meta.url).pathname, stdout: 'pipe', stderr: 'pipe', }); const stdout = await new Response(proc.stdout).text(); const exitCode = await proc.exited; expect(stdout).toContain('check-update'); expect(exitCode).toBe(0); }); test('--json returns valid JSON with required fields', async () => { const proc = Bun.spawn(['bun', 'run', 'src/cli.ts', 'check-update', '--json'], { cwd: new URL('..', import.meta.url).pathname, stdout: 'pipe', stderr: 'pipe', }); const stdout = await new Response(proc.stdout).text(); const exitCode = await proc.exited; expect(exitCode).toBe(0); const output = JSON.parse(stdout); expect(output).toHaveProperty('current_version'); expect(output).toHaveProperty('update_available'); expect(output).toHaveProperty('upgrade_command'); expect(output).toHaveProperty('current_source', 'package-json'); expect(typeof output.update_available).toBe('boolean'); }); });