import { describe, expect, it } from 'bun:test'; import { IncompatibleManifestError, MANIFEST_SCHEMA_VERSION, ManifestParseError, assertCompatibleSchema, buildManifest, parseManifest, } from './backup-manifest'; describe('backup-manifest', () => { describe('buildManifest', () => { it('produces a system manifest with the current schemaVersion', () => { const m = buildManifest({ kind: 'system' }); expect(m.kind).toBe('system'); expect(m.schemaVersion).toBe(MANIFEST_SCHEMA_VERSION); expect(m.moduleId).toBeUndefined(); expect(m.createdAt).toMatch(/^\d{4}-\d{2}-\d{2}T/); }); it('produces a module manifest with moduleId/version', () => { const m = buildManifest({ kind: 'module', moduleId: 'homebridge', moduleVersion: '1.2.3', dataSchemaVersion: '2.0', }); expect(m.kind).toBe('module'); expect(m.moduleId).toBe('homebridge'); expect(m.moduleVersion).toBe('1.2.3'); expect(m.dataSchemaVersion).toBe('2.0'); }); it('honors overrides for testability', () => { const m = buildManifest({ kind: 'system', now: new Date('2020-01-01T00:00:00Z'), hostnameOverride: 'test-box', celiloVersion: '9.9.9', }); expect(m.createdAt).toBe('2020-01-01T00:00:00.000Z'); expect(m.hostname).toBe('test-box'); expect(m.celiloVersion).toBe('9.9.9'); }); }); describe('parseManifest', () => { it('round-trips a built manifest', () => { const built = buildManifest({ kind: 'system' }); const parsed = parseManifest(JSON.stringify(built)); expect(parsed).toEqual(built); }); it('throws ManifestParseError on malformed JSON', () => { expect(() => parseManifest('{not json')).toThrow(ManifestParseError); }); it('throws ManifestParseError on missing required fields', () => { expect(() => parseManifest(JSON.stringify({ kind: 'system' }))).toThrow(ManifestParseError); }); it('throws ManifestParseError on bad schemaVersion format', () => { expect(() => parseManifest( JSON.stringify({ schemaVersion: 'not-a-version', createdAt: '2020-01-01T00:00:00Z', celiloVersion: '0.0.0', hostname: 'h', kind: 'system', }), ), ).toThrow(ManifestParseError); }); it('error message names the cause for operator triage', () => { try { parseManifest('not json'); } catch (err) { expect((err as Error).message).toContain('not valid JSON'); return; } throw new Error('expected throw'); }); }); describe('assertCompatibleSchema', () => { it('accepts the current schemaVersion', () => { const m = buildManifest({ kind: 'system' }); expect(() => assertCompatibleSchema(m)).not.toThrow(); }); it('accepts the same major, different minor', () => { const m = buildManifest({ kind: 'system' }); // Fudge the version to a higher minor of the same major. m.schemaVersion = `${MANIFEST_SCHEMA_VERSION.split('.')[0]}.99`; expect(() => assertCompatibleSchema(m)).not.toThrow(); }); it('rejects a different major with an actionable error', () => { const m = buildManifest({ kind: 'system' }); const otherMajor = String(Number(MANIFEST_SCHEMA_VERSION.split('.')[0]) + 1); m.schemaVersion = `${otherMajor}.0`; try { assertCompatibleSchema(m); } catch (err) { expect(err).toBeInstanceOf(IncompatibleManifestError); expect((err as Error).message).toContain('envelope schema'); expect((err as Error).message).toContain('this celilo supports'); return; } throw new Error('expected throw'); }); }); });