import { expect, tap } from '@git.zone/tstest/tapbundle'; import type * as smartnpm from '@push.rocks/smartnpm'; import * as smartupdate from '../ts/index.js'; const createMockedSmartUpdate = (versionsArg: Record) => { const checkedPackages: string[] = []; const testSmartUpdate = new smartupdate.SmartUpdate({ logLevel: 'SILENT', noColor: true, cacheDuration: { minutes: 30 }, cacheStore: { storeType: 'ephemeral' }, }); testSmartUpdate.npmRegistry.getPackageInfo = async (packageNameArg: string) => { checkedPackages.push(packageNameArg); const version = versionsArg[packageNameArg]; if (!version) { throw new Error(`Package not found: ${packageNameArg}`); } return { name: packageNameArg, version, } as unknown as smartnpm.NpmPackage; }; return { testSmartUpdate, checkedPackages }; }; tap.test('backward compatibility: should create an instance of SmartUpdate', async () => { const { testSmartUpdate } = createMockedSmartUpdate({ lodash: '4.17.21' }); expect(testSmartUpdate).toBeInstanceOf(smartupdate.SmartUpdate); }); tap.test('backward compatibility: should check for a npm module using old API', async () => { const { testSmartUpdate } = createMockedSmartUpdate({ lodash: '4.17.21' }); const result = await testSmartUpdate.check('lodash', '1.0.5'); expect(result).toBeTrue(); }); tap.test('modern API: checkForUpdate should return rich result object', async () => { const { testSmartUpdate } = createMockedSmartUpdate({ lodash: '4.17.21' }); const result = await testSmartUpdate.checkForUpdate({ packageName: 'lodash', currentVersion: '1.0.0', cacheStrategy: 'never', }); expect(result).toBeTypeOf('object'); expect(result.status).toEqual('update-available'); expect(result.packageName).toEqual('lodash'); expect(result.currentVersion).toEqual('1.0.0'); expect(result.latestVersion).toEqual('4.17.21'); expect(result.checkTime).toBeInstanceOf(Date); expect(result.cacheHit).toBeFalse(); }); tap.test('modern API: checkForUpdate with up-to-date version', async () => { const { testSmartUpdate } = createMockedSmartUpdate({ '@push.rocks/smartversion': '3.1.0' }); const result = await testSmartUpdate.checkForUpdate({ packageName: '@push.rocks/smartversion', currentVersion: '999.999.999', cacheStrategy: 'never', }); expect(result.status).toEqual('up-to-date'); expect(result.latestVersion).toEqual('3.1.0'); }); tap.test('modern API: getLatestVersion utility method', async () => { const { testSmartUpdate } = createMockedSmartUpdate({ lodash: '4.17.21' }); const latestVersion = await testSmartUpdate.getLatestVersion('lodash'); expect(latestVersion).toEqual('4.17.21'); }); tap.test('modern API: error handling for non-existent package', async () => { const { testSmartUpdate } = createMockedSmartUpdate({}); const result = await testSmartUpdate.checkForUpdate({ packageName: 'this-package-definitely-does-not-exist-12345', currentVersion: '1.0.0', cacheStrategy: 'never', }); expect(result.status).toEqual('error'); expect(result.error).toBeInstanceOf(smartupdate.PackageNotFoundError); }); tap.test('modern API: invalid versions are reported before registry access', async () => { const { testSmartUpdate, checkedPackages } = createMockedSmartUpdate({ lodash: '4.17.21' }); const result = await testSmartUpdate.checkForUpdate({ packageName: 'lodash', currentVersion: 'not-a-version', cacheStrategy: 'never', }); expect(result.status).toEqual('error'); expect(result.error).toBeInstanceOf(smartupdate.InvalidVersionError); expect(checkedPackages).toHaveLength(0); }); tap.test('modern API: invalid package names are reported before registry access', async () => { const { testSmartUpdate, checkedPackages } = createMockedSmartUpdate({ lodash: '4.17.21' }); const result = await testSmartUpdate.checkForUpdate({ packageName: 'invalid package name', currentVersion: '1.0.0', cacheStrategy: 'never', }); expect(result.status).toEqual('error'); expect(result.error).toBeInstanceOf(smartupdate.InvalidPackageNameError); expect(checkedPackages).toHaveLength(0); }); tap.test('modern API: caches update-available results for time-based checks', async () => { const { testSmartUpdate, checkedPackages } = createMockedSmartUpdate({ express: '5.0.0' }); const result1 = await testSmartUpdate.checkForUpdate({ packageName: 'express', currentVersion: '1.0.0', cacheStrategy: 'time-based', }); const result2 = await testSmartUpdate.checkForUpdate({ packageName: 'express', currentVersion: '1.0.0', cacheStrategy: 'time-based', }); expect(result1.status).toEqual('update-available'); expect(result2.status).toEqual('check-skipped'); expect(result2.cacheHit).toBeTrue(); expect(result2.latestVersion).toEqual('5.0.0'); expect(checkedPackages).toHaveLength(1); }); tap.test('modern API: caches up-to-date results for time-based checks', async () => { const { testSmartUpdate, checkedPackages } = createMockedSmartUpdate({ express: '5.0.0' }); const result1 = await testSmartUpdate.checkForUpdate({ packageName: 'express', currentVersion: '5.0.0', cacheStrategy: 'time-based', }); const result2 = await testSmartUpdate.checkForUpdate({ packageName: 'express', currentVersion: '5.0.0', cacheStrategy: 'time-based', }); expect(result1.status).toEqual('up-to-date'); expect(result2.status).toEqual('check-skipped'); expect(result2.cacheHit).toBeTrue(); expect(result2.latestVersion).toEqual('5.0.0'); expect(checkedPackages).toHaveLength(1); }); tap.test('modern API: cache entries are scoped to the checked current version', async () => { const { testSmartUpdate, checkedPackages } = createMockedSmartUpdate({ express: '5.0.0' }); const result1 = await testSmartUpdate.checkForUpdate({ packageName: 'express', currentVersion: '1.0.0', cacheStrategy: 'time-based', }); const result2 = await testSmartUpdate.checkForUpdate({ packageName: 'express', currentVersion: '5.0.0', cacheStrategy: 'time-based', }); expect(result1.status).toEqual('update-available'); expect(result2.status).toEqual('up-to-date'); expect(result2.cacheHit).toBeFalse(); expect(checkedPackages).toHaveLength(2); }); tap.test('modern API: checkForCli returns true for cached known updates', async () => { const { testSmartUpdate, checkedPackages } = createMockedSmartUpdate({ 'my-cli': '2.0.0' }); const result1 = await testSmartUpdate.checkForCli('my-cli', '1.0.0'); const result2 = await testSmartUpdate.checkForCli('my-cli', '1.0.0'); expect(result1).toBeTrue(); expect(result2).toBeTrue(); expect(checkedPackages).toHaveLength(1); }); tap.test('cache manager: clears package-specific and complete cache data', async () => { const cacheManager = new smartupdate.UpdateCacheManager({ durationMs: 60_000, storeType: 'ephemeral', }); await cacheManager.setCached('one', cacheManager.createCacheStatus('1.0.0')); await cacheManager.setCached('two', cacheManager.createCacheStatus('2.0.0')); await cacheManager.clearCache('one'); expect(await cacheManager.getCached('one')).toBeNull(); expect(await cacheManager.getCached('two')).toBeTypeOf('object'); await cacheManager.clearCache(); expect(await cacheManager.getCached('two')).toBeNull(); }); tap.test('modern API: exports all runtime classes and constants', async () => { expect(smartupdate.SmartUpdate).toBeTypeOf('function'); expect(smartupdate.UpdateCacheManager).toBeTypeOf('function'); expect(smartupdate.UpdateNotifier).toBeTypeOf('function'); expect(smartupdate.RegistryUnavailableError).toBeTypeOf('function'); expect(smartupdate.PackageNotFoundError).toBeTypeOf('function'); expect(smartupdate.InvalidVersionError).toBeTypeOf('function'); expect(smartupdate.InvalidPackageNameError).toBeTypeOf('function'); expect(smartupdate.LOG_LEVELS).toBeTypeOf('object'); }); export default tap.start();