import { describe, expect, test } from 'bun:test'; import { type ModuleVersionFetcher, auditModuleVersions } from './module-versions'; const installed = [ { id: 'caddy', version: '1.2.0' }, { id: 'iptables', version: '1.0.0' }, { id: 'lunacycle', version: '1.0.0' }, ]; describe('auditModuleVersions', () => { test('no findings when every installed module is at registry latest', async () => { const fetcher: ModuleVersionFetcher = async (id) => ({ latest: installed.find((m) => m.id === id)?.version ?? '0.0.0', }); const result = await auditModuleVersions({ installed, fetcher }); expect(result).toEqual([]); }); test('drift finding per outdated module', async () => { const fetcher: ModuleVersionFetcher = async (id) => { if (id === 'caddy') return { latest: '1.3.0' }; if (id === 'iptables') return { latest: '1.0.0' }; if (id === 'lunacycle') return { latest: '1.1.0' }; throw new Error('unexpected id'); }; const result = await auditModuleVersions({ installed, fetcher }); expect(result).toHaveLength(2); expect(result.map((f) => f.subject).sort()).toEqual(['caddy', 'lunacycle']); expect(result.find((f) => f.subject === 'caddy')?.message).toContain('1.2.0 → 1.3.0'); }); test('appends "intervening releases" when intermediateCount > 1', async () => { const fetcher: ModuleVersionFetcher = async () => ({ latest: '1.5.0', intermediateCount: 3, }); const result = await auditModuleVersions({ installed: [{ id: 'caddy', version: '1.2.0' }], fetcher, }); expect(result[0].message).toContain('3 intervening releases'); }); test('renders release.json messages as details when supplied', async () => { const fetcher: ModuleVersionFetcher = async () => ({ latest: '1.5.0', intermediateCount: 3, intermediateMessages: [ { version: '1.5.0', message: 'Add HTTP/3 support' }, { version: '1.4.0', message: 'Fix DNS race' }, ], }); const result = await auditModuleVersions({ installed: [{ id: 'caddy', version: '1.2.0' }], fetcher, }); expect(result[0].details).toContain('Add HTTP/3 support'); expect(result[0].details).toContain('1.5.0'); expect(result[0].details).toContain('Fix DNS race'); expect(result[0].details).toContain('1.4.0'); }); test('module not in registry produces explanatory finding', async () => { const fetcher: ModuleVersionFetcher = async () => ({ latest: null }); const result = await auditModuleVersions({ installed: [{ id: 'private-module', version: '1.0.0' }], fetcher, }); expect(result).toHaveLength(1); expect(result[0].code).toBe('module_not_in_registry'); }); test('per-module fetcher error becomes a drift finding (does not abort)', async () => { const fetcher: ModuleVersionFetcher = async (id) => { if (id === 'iptables') throw new Error('socket hang up'); return { latest: installed.find((m) => m.id === id)?.version ?? '0.0.0' }; }; const result = await auditModuleVersions({ installed, fetcher }); expect(result).toHaveLength(1); expect(result[0]).toMatchObject({ code: 'module_version_lookup_failed', subject: 'iptables', }); expect(result[0].message).toContain('socket hang up'); }); test('local module ahead of registry produces no finding', async () => { const fetcher: ModuleVersionFetcher = async () => ({ latest: '1.0.0' }); const result = await auditModuleVersions({ installed: [{ id: 'caddy', version: '1.2.0' }], fetcher, }); expect(result).toEqual([]); }); });