From 584fe2d378241b907e0c35f82ba4a3f302819b99 Mon Sep 17 00:00:00 2001 From: Kim T Date: Tue, 21 Jul 2026 17:09:42 -0700 Subject: [PATCH 1/2] refactor: remove Node.js setup steps from release workflow and streamline test steps --- .github/workflows/release.yml | 9 --------- .github/workflows/test.yml | 15 +++++---------- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2ebfdfe..008f0fc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,15 +16,6 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Setup NodeJs - uses: actions/setup-node@v4 - with: - node-version: 'lts/*' - registry-url: 'https://registry.npmjs.org' - - - name: Update npm - run: npm install -g npm@latest - - name: Install dependencies run: npm ci diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 97f468a..522b7d6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,21 +23,16 @@ jobs: uses: actions/checkout@v4 - name: Install - run: | - npm ci + run: npm ci - name: Audit - run: | - npm audit + run: npm audit - name: Lint - run: | - npm run lint + run: npm run lint - name: Test - run: | - npm test + run: npm test - name: Build - run: | - npm run build + run: npm run build From 2a0998d354b22c8a85977d3762b08100691ef92a Mon Sep 17 00:00:00 2001 From: Kim T Date: Tue, 21 Jul 2026 17:26:44 -0700 Subject: [PATCH 2/2] test: exclude live download counts from fixture comparisons Manager/ManagerLocal tests sync against the real registry, whose `downloads` field changes continuously and is only set when > 0. Compare fixtures against results with `downloads` omitted on both sides rather than asserting a point-in-time snapshot of it. Co-Authored-By: Claude Sonnet 5 --- tests/classes/Manager.test.ts | 3 ++- tests/classes/ManagerLocal.test.ts | 29 +++++++++++++++-------------- tests/testUtils.ts | 25 +++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 15 deletions(-) create mode 100644 tests/testUtils.ts diff --git a/tests/classes/Manager.test.ts b/tests/classes/Manager.test.ts index 9c6f078..bef0792 100644 --- a/tests/classes/Manager.test.ts +++ b/tests/classes/Manager.test.ts @@ -14,6 +14,7 @@ import { License } from '../../src/types/License'; import { SystemType } from '../../src/types/SystemType'; import { Architecture } from '../../src/types/Architecture'; import { packageCompatibleFiles } from '../../src/helpers/package'; +import { omitDownloads } from '../testUtils'; test('Manager add multiple package versions', () => { const manager = new Manager(RegistryType.Plugins); @@ -187,7 +188,7 @@ test('Manager sync from registries', async () => { const manager = new Manager(RegistryType.Plugins); await manager.sync(); const pkg = manager.getPackage(PLUGIN_PACKAGE.slug); - expect(pkg?.getVersion(PLUGIN_PACKAGE.version)).toEqual(PLUGIN); + expect(omitDownloads(pkg?.getVersion(PLUGIN_PACKAGE.version))).toEqual(omitDownloads(PLUGIN)); }); test('Manager sync with existing package', async () => { diff --git a/tests/classes/ManagerLocal.test.ts b/tests/classes/ManagerLocal.test.ts index 51ad611..f3c1013 100644 --- a/tests/classes/ManagerLocal.test.ts +++ b/tests/classes/ManagerLocal.test.ts @@ -15,6 +15,7 @@ import { dirDelete, fileReadJson } from '../../src/helpers/file'; import { RegistryType } from '../../src/types/Registry'; import { ConfigInterface } from '../../src/types/Config'; import { PackageVersion } from '../../src/types/Package'; +import { omitDownloads } from '../testUtils'; const APP_DIR: string = 'test'; const CONFIG: ConfigInterface = { @@ -52,15 +53,15 @@ test('Plugin sync, install, rescan, uninstall', async () => { const pkgReturned: PackageVersion | void = await manager.install(PLUGIN_PACKAGE.slug, PLUGIN_PACKAGE.version); const pkgGet = manager.getPackage(PLUGIN_PACKAGE.slug); - expect(pkgReturned).toEqual(PLUGIN_INSTALLED); - expect(pkgGet?.getVersion(PLUGIN_PACKAGE.version)).toEqual(PLUGIN_INSTALLED); + expect(omitDownloads(pkgReturned)).toEqual(omitDownloads(PLUGIN_INSTALLED)); + expect(omitDownloads(pkgGet?.getVersion(PLUGIN_PACKAGE.version))).toEqual(omitDownloads(PLUGIN_INSTALLED)); manager.scan(); const pkgGet2 = manager.getPackage(PLUGIN_PACKAGE.slug); - expect(pkgGet2?.getVersion(PLUGIN_PACKAGE.version)).toEqual(PLUGIN_INSTALLED); + expect(omitDownloads(pkgGet2?.getVersion(PLUGIN_PACKAGE.version))).toEqual(omitDownloads(PLUGIN_INSTALLED)); const pkgReturned2: PackageVersion | void = await manager.uninstall(PLUGIN_PACKAGE.slug, PLUGIN_PACKAGE.version); - expect(pkgReturned2).toEqual(PLUGIN); + expect(omitDownloads(pkgReturned2)).toEqual(omitDownloads(PLUGIN)); }); test('Preset sync, install, rescan, uninstall', async () => { @@ -86,15 +87,15 @@ test('Project sync, install, rescan, uninstall', async () => { const pkgReturned: PackageVersion | void = await manager.install(PROJECT_PACKAGE.slug, PROJECT_PACKAGE.version); const pkgGet = manager.getPackage(PROJECT_PACKAGE.slug); - expect(pkgReturned).toEqual(PROJECT_INSTALLED); - expect(pkgGet?.getVersion(PROJECT_PACKAGE.version)).toEqual(PROJECT_INSTALLED); + expect(omitDownloads(pkgReturned)).toEqual(omitDownloads(PROJECT_INSTALLED)); + expect(omitDownloads(pkgGet?.getVersion(PROJECT_PACKAGE.version))).toEqual(omitDownloads(PROJECT_INSTALLED)); manager.scan(); const pkgGet2 = manager.getPackage(PROJECT_PACKAGE.slug); - expect(pkgGet2?.getVersion(PROJECT_PACKAGE.version)).toEqual(PROJECT_INSTALLED); + expect(omitDownloads(pkgGet2?.getVersion(PROJECT_PACKAGE.version))).toEqual(omitDownloads(PROJECT_INSTALLED)); const pkgReturned2: PackageVersion | void = await manager.uninstall(PROJECT_PACKAGE.slug, PROJECT_PACKAGE.version); - expect(pkgReturned2).toEqual(PROJECT); + expect(omitDownloads(pkgReturned2)).toEqual(omitDownloads(PROJECT)); }); test('Project sync, install project, install dependencies, uninstall dependencies', async () => { @@ -106,14 +107,14 @@ test('Project sync, install project, install dependencies, uninstall dependencie await manager.installDependencies(PROJECT_PATH); await pluginManager.scan(); - expect(pluginManager.toJSON()).toEqual({ - [PLUGIN_PACKAGE.slug]: PLUGIN_PACKAGE_INSTALLED, + expect(omitDownloads(pluginManager.toJSON())).toEqual({ + [PLUGIN_PACKAGE.slug]: omitDownloads(PLUGIN_PACKAGE_INSTALLED), }); await manager.uninstallDependencies(PROJECT_PATH); await pluginManager.scan(); // TODO update when headless installation is working. - expect(pluginManager.toJSON()).toEqual({ - [PLUGIN_PACKAGE.slug]: PLUGIN_PACKAGE_INSTALLED, + expect(omitDownloads(pluginManager.toJSON())).toEqual({ + [PLUGIN_PACKAGE.slug]: omitDownloads(PLUGIN_PACKAGE_INSTALLED), }); }); @@ -122,8 +123,8 @@ test('Project sync, install project, add new dependency, remove new dependency', await manager.sync(); await manager.install(PROJECT_PACKAGE.slug, PROJECT_PACKAGE.version); const pkgDeps = await manager.installDependency(PLUGIN_PACKAGE.slug, '1.3.4', PROJECT_PATH); - expect(pkgDeps).toEqual(PROJECT_DEPS); + expect(omitDownloads(pkgDeps)).toEqual(omitDownloads(PROJECT_DEPS)); const pkgNoDeps = await manager.uninstallDependency(PLUGIN_PACKAGE.slug, '1.3.4', PROJECT_PATH); - expect(pkgNoDeps).toEqual(PROJECT_NO_DEPS); + expect(omitDownloads(pkgNoDeps)).toEqual(omitDownloads(PROJECT_NO_DEPS)); }); diff --git a/tests/testUtils.ts b/tests/testUtils.ts new file mode 100644 index 0000000..70ff4a4 --- /dev/null +++ b/tests/testUtils.ts @@ -0,0 +1,25 @@ +// Deep-clones `value`, dropping any key in `keys` at any depth. Used to compare fixtures against +// real results while excluding fields that are inherently non-reproducible - see `omitDownloads`. +export function omitKeysDeep(value: T, keys: string[]): T { + if (Array.isArray(value)) return value.map(item => omitKeysDeep(item, keys)) as T; + if (value && typeof value === 'object') { + const result: any = {}; + for (const [key, val] of Object.entries(value)) { + if (keys.includes(key)) continue; + result[key] = omitKeysDeep(val, keys); + } + return result; + } + return value; +} + +// `downloads` is computed fresh at build time from live GitHub release download counts (see +// registry's enrichDownloads()) - unlike every other field on these fixtures (hash, size, date), +// it changes continuously in the real, live registry these tests sync against, and is only ever +// set when > 0 (omitted otherwise). Because it can be entirely absent, an asymmetric matcher like +// `expect.any(Number)` can't stand in for it - vitest's toEqual still requires the key to exist on +// both sides before consulting a matcher. So exclude it from both the actual result and the +// expected fixture before comparing, rather than asserting a point-in-time snapshot of it. +export function omitDownloads(value: T): T { + return omitKeysDeep(value, ['downloads']); +}