import { tap, expect } from '@git.zone/tstest/tapbundle'; import { VersionResolver, SmartMigrationError } from '../ts/index.js'; import type { IMigrationStepDefinition } from '../ts/index.js'; const noopHandler = async () => {}; function makeStep(id: string, from: string, to: string): IMigrationStepDefinition { return { id, fromVersion: from, toVersion: to, isResumable: false, handler: noopHandler }; } tap.test('equals / lessThan / greaterThan', async () => { expect(VersionResolver.equals('1.0.0', '1.0.0')).toBeTrue(); expect(VersionResolver.equals('1.0.0', '1.0.1')).toBeFalse(); expect(VersionResolver.lessThan('1.0.0', '1.0.1')).toBeTrue(); expect(VersionResolver.greaterThan('2.0.0', '1.99.99')).toBeTrue(); }); tap.test('assertValid: throws on garbage', async () => { let caught: SmartMigrationError | undefined; try { VersionResolver.assertValid('not-a-version', 'test'); } catch (err) { caught = err as SmartMigrationError; } expect(caught).toBeInstanceOf(SmartMigrationError); expect(caught!.code).toEqual('INVALID_VERSION'); }); tap.test('validateChain: empty chain is valid', async () => { VersionResolver.validateChain([]); }); tap.test('validateChain: catches duplicate ids', async () => { let caught: SmartMigrationError | undefined; try { VersionResolver.validateChain([ makeStep('a', '1.0.0', '1.1.0'), makeStep('a', '1.1.0', '1.2.0'), ]); } catch (err) { caught = err as SmartMigrationError; } expect(caught!.code).toEqual('DUPLICATE_STEP_ID'); }); tap.test('validateChain: catches non-increasing step', async () => { let caught: SmartMigrationError | undefined; try { VersionResolver.validateChain([makeStep('a', '1.5.0', '1.5.0')]); } catch (err) { caught = err as SmartMigrationError; } expect(caught!.code).toEqual('NON_INCREASING_STEP'); }); tap.test('validateChain: catches gap in chain', async () => { let caught: SmartMigrationError | undefined; try { VersionResolver.validateChain([ makeStep('a', '1.0.0', '1.1.0'), makeStep('b', '1.2.0', '1.3.0'), // gap! a.to=1.1.0, b.from=1.2.0 ]); } catch (err) { caught = err as SmartMigrationError; } expect(caught!.code).toEqual('CHAIN_GAP'); }); tap.test('validateChain: contiguous chain passes', async () => { VersionResolver.validateChain([ makeStep('a', '1.0.0', '1.1.0'), makeStep('b', '1.1.0', '1.5.0'), makeStep('c', '1.5.0', '2.0.0'), ]); }); tap.test('computePlan: returns empty when current === target', async () => { const steps = [makeStep('a', '1.0.0', '1.1.0')]; expect(VersionResolver.computePlan(steps, '1.0.0', '1.0.0')).toEqual([]); }); tap.test('computePlan: returns full chain when starting from beginning', async () => { const steps = [ makeStep('a', '1.0.0', '1.1.0'), makeStep('b', '1.1.0', '1.5.0'), makeStep('c', '1.5.0', '2.0.0'), ]; const plan = VersionResolver.computePlan(steps, '1.0.0', '2.0.0'); expect(plan.map((s) => s.id)).toEqual(['a', 'b', 'c']); }); tap.test('computePlan: returns partial chain when starting in the middle', async () => { const steps = [ makeStep('a', '1.0.0', '1.1.0'), makeStep('b', '1.1.0', '1.5.0'), makeStep('c', '1.5.0', '2.0.0'), ]; const plan = VersionResolver.computePlan(steps, '1.1.0', '2.0.0'); expect(plan.map((s) => s.id)).toEqual(['b', 'c']); }); tap.test('computePlan: returns sub-slice when target is mid-chain', async () => { const steps = [ makeStep('a', '1.0.0', '1.1.0'), makeStep('b', '1.1.0', '1.5.0'), makeStep('c', '1.5.0', '2.0.0'), ]; const plan = VersionResolver.computePlan(steps, '1.0.0', '1.5.0'); expect(plan.map((s) => s.id)).toEqual(['a', 'b']); }); tap.test('computePlan: throws when current does not match any step from', async () => { const steps = [ makeStep('a', '1.0.0', '1.1.0'), makeStep('b', '1.1.0', '2.0.0'), ]; let caught: SmartMigrationError | undefined; try { VersionResolver.computePlan(steps, '1.0.5', '2.0.0'); } catch (err) { caught = err as SmartMigrationError; } expect(caught!.code).toEqual('CHAIN_NOT_AT_CURRENT'); }); tap.test('computePlan: throws when target overshoots', async () => { const steps = [ makeStep('a', '1.0.0', '1.1.0'), makeStep('b', '1.1.0', '2.0.0'), ]; let caught: SmartMigrationError | undefined; try { VersionResolver.computePlan(steps, '1.0.0', '1.5.0'); } catch (err) { caught = err as SmartMigrationError; } expect(caught!.code).toEqual('TARGET_NOT_REACHABLE'); }); tap.test('computePlan: rejects downgrade', async () => { const steps = [makeStep('a', '1.0.0', '2.0.0')]; let caught: SmartMigrationError | undefined; try { VersionResolver.computePlan(steps, '2.0.0', '1.0.0'); } catch (err) { caught = err as SmartMigrationError; } expect(caught!.code).toEqual('DOWNGRADE_NOT_SUPPORTED'); }); export default tap.start();