From 606ae919f29f54a5a75076c6d8706fc27c11465c Mon Sep 17 00:00:00 2001 From: huntharo Date: Fri, 10 Apr 2026 17:06:07 -0400 Subject: [PATCH] test: add external dogfood harness for configure-nodejs --- .github/workflows/ci.yml | 145 +++++++++++++++++++++++++++ .gitignore | 7 ++ README.md | 5 + fixtures/npm-basic/check.mjs | 7 ++ fixtures/npm-basic/package-lock.json | 19 ++++ fixtures/npm-basic/package.json | 8 ++ fixtures/pnpm-basic/check.mjs | 7 ++ fixtures/pnpm-basic/package.json | 9 ++ fixtures/pnpm-basic/pnpm-lock.yaml | 22 ++++ fixtures/yarn-basic/.yarnrc.yml | 1 + fixtures/yarn-basic/check.mjs | 7 ++ fixtures/yarn-basic/package.json | 9 ++ fixtures/yarn-basic/yarn.lock | 21 ++++ package.json | 8 ++ test/resolve-cache-paths.test.mjs | 109 ++++++++++++++++++++ test/resolve-manager.test.mjs | 144 ++++++++++++++++++++++++++ 16 files changed, 528 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 fixtures/npm-basic/check.mjs create mode 100644 fixtures/npm-basic/package-lock.json create mode 100644 fixtures/npm-basic/package.json create mode 100644 fixtures/pnpm-basic/check.mjs create mode 100644 fixtures/pnpm-basic/package.json create mode 100644 fixtures/pnpm-basic/pnpm-lock.yaml create mode 100644 fixtures/yarn-basic/.yarnrc.yml create mode 100644 fixtures/yarn-basic/check.mjs create mode 100644 fixtures/yarn-basic/package.json create mode 100644 fixtures/yarn-basic/yarn.lock create mode 100644 package.json create mode 100644 test/resolve-cache-paths.test.mjs create mode 100644 test/resolve-manager.test.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c3c4151 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,145 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + inputs: + configure-nodejs-ref: + description: Ref in pwrdrvr/configure-nodejs to validate + default: main + required: false + type: string + +env: + CONFIGURE_NODEJS_REF: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.configure-nodejs-ref || 'main' }} + +jobs: + unit: + name: Unit Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Checkout configure-nodejs + uses: actions/checkout@v6 + with: + repository: pwrdrvr/configure-nodejs + ref: ${{ env.CONFIGURE_NODEJS_REF }} + path: configure-nodejs + + - uses: actions/setup-node@v6 + with: + node-version: 22.x + + - name: Run unit tests + run: npm test + + fixture-lookup: + name: Fixture Lookup (${{ matrix.package-manager }}) + needs: unit + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - package-manager: npm + fixture: fixtures/npm-basic + install-command: npm ci + lockfile-name: package-lock.json + - package-manager: pnpm + fixture: fixtures/pnpm-basic + install-command: pnpm install --frozen-lockfile + lockfile-name: pnpm-lock.yaml + - package-manager: yarn + fixture: fixtures/yarn-basic + install-command: yarn install --immutable + lockfile-name: yarn.lock + steps: + - uses: actions/checkout@v6 + + - name: Checkout configure-nodejs + uses: actions/checkout@v6 + with: + repository: pwrdrvr/configure-nodejs + ref: ${{ env.CONFIGURE_NODEJS_REF }} + path: configure-nodejs + + - name: Configure Node.js + id: configure-nodejs + uses: ./configure-nodejs + with: + working-directory: ${{ matrix.fixture }} + cache-key-suffix: fixture-tests + lookup-only: "true" + + - name: Assert resolved action outputs + shell: bash + run: | + test "${{ steps.configure-nodejs.outputs.package-manager }}" = "${{ matrix.package-manager }}" + test "${{ steps.configure-nodejs.outputs.install-command }}" = "${{ matrix.install-command }}" + test "${{ steps.configure-nodejs.outputs.working-directory }}" = "${{ matrix.fixture }}" + test "${{ steps.configure-nodejs.outputs.lockfile-name }}" = "${{ matrix.lockfile-name }}" + + - name: Assert lookup-only behavior + working-directory: ${{ matrix.fixture }} + shell: bash + run: | + if [ "${{ steps.configure-nodejs.outputs.cache-hit }}" = "true" ]; then + test ! -d node_modules + else + test -d node_modules + node check.mjs + fi + + fixture-validate: + name: Fixture Validate (${{ matrix.package-manager }}) + needs: fixture-lookup + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - package-manager: npm + fixture: fixtures/npm-basic + install-command: npm ci + lockfile-name: package-lock.json + - package-manager: pnpm + fixture: fixtures/pnpm-basic + install-command: pnpm install --frozen-lockfile + lockfile-name: pnpm-lock.yaml + - package-manager: yarn + fixture: fixtures/yarn-basic + install-command: yarn install --immutable + lockfile-name: yarn.lock + steps: + - uses: actions/checkout@v6 + + - name: Checkout configure-nodejs + uses: actions/checkout@v6 + with: + repository: pwrdrvr/configure-nodejs + ref: ${{ env.CONFIGURE_NODEJS_REF }} + path: configure-nodejs + + - name: Configure Node.js + id: configure-nodejs + uses: ./configure-nodejs + with: + working-directory: ${{ matrix.fixture }} + cache-key-suffix: fixture-tests + + - name: Assert restore behavior + shell: bash + run: | + test "${{ steps.configure-nodejs.outputs.package-manager }}" = "${{ matrix.package-manager }}" + test "${{ steps.configure-nodejs.outputs.install-command }}" = "${{ matrix.install-command }}" + test "${{ steps.configure-nodejs.outputs.working-directory }}" = "${{ matrix.fixture }}" + test "${{ steps.configure-nodejs.outputs.lockfile-name }}" = "${{ matrix.lockfile-name }}" + test "${{ steps.configure-nodejs.outputs.cache-hit }}" = "true" + + - name: Validate installed dependency + working-directory: ${{ matrix.fixture }} + run: node check.mjs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d4f6654 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +.DS_Store + +fixtures/*/node_modules/ +fixtures/*/.pnp.* +fixtures/*/.yarn/install-state.gz +fixtures/*/.yarn/cache/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..edd8329 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# configure-nodejs-test + +Dogfood and validation harness for [`pwrdrvr/configure-nodejs`](https://github.com/pwrdrvr/configure-nodejs). + +The workflow in this repository checks out `pwrdrvr/configure-nodejs` at a configurable ref, runs unit tests against its helper scripts, and exercises npm, pnpm, and Yarn fixtures through the action entrypoint. diff --git a/fixtures/npm-basic/check.mjs b/fixtures/npm-basic/check.mjs new file mode 100644 index 0000000..129ef92 --- /dev/null +++ b/fixtures/npm-basic/check.mjs @@ -0,0 +1,7 @@ +import pc from 'picocolors'; + +if (typeof pc.green !== 'function') { + throw new Error('Expected picocolors.green to be available after npm install.'); +} + +console.log(pc.green('npm fixture dependency loaded')); diff --git a/fixtures/npm-basic/package-lock.json b/fixtures/npm-basic/package-lock.json new file mode 100644 index 0000000..475bb1e --- /dev/null +++ b/fixtures/npm-basic/package-lock.json @@ -0,0 +1,19 @@ +{ + "name": "npm-basic-fixture", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "npm-basic-fixture", + "dependencies": { + "picocolors": "1.1.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + } + } +} diff --git a/fixtures/npm-basic/package.json b/fixtures/npm-basic/package.json new file mode 100644 index 0000000..b6d1cd8 --- /dev/null +++ b/fixtures/npm-basic/package.json @@ -0,0 +1,8 @@ +{ + "name": "npm-basic-fixture", + "private": true, + "type": "module", + "dependencies": { + "picocolors": "1.1.1" + } +} diff --git a/fixtures/pnpm-basic/check.mjs b/fixtures/pnpm-basic/check.mjs new file mode 100644 index 0000000..5e0fd88 --- /dev/null +++ b/fixtures/pnpm-basic/check.mjs @@ -0,0 +1,7 @@ +import pc from 'picocolors'; + +if (typeof pc.green !== 'function') { + throw new Error('Expected picocolors.green to be available after pnpm install.'); +} + +console.log(pc.green('pnpm fixture dependency loaded')); diff --git a/fixtures/pnpm-basic/package.json b/fixtures/pnpm-basic/package.json new file mode 100644 index 0000000..7e4d060 --- /dev/null +++ b/fixtures/pnpm-basic/package.json @@ -0,0 +1,9 @@ +{ + "name": "pnpm-basic-fixture", + "private": true, + "type": "module", + "packageManager": "pnpm@10.12.1", + "dependencies": { + "picocolors": "1.1.1" + } +} diff --git a/fixtures/pnpm-basic/pnpm-lock.yaml b/fixtures/pnpm-basic/pnpm-lock.yaml new file mode 100644 index 0000000..8a296ba --- /dev/null +++ b/fixtures/pnpm-basic/pnpm-lock.yaml @@ -0,0 +1,22 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + picocolors: + specifier: 1.1.1 + version: 1.1.1 + +packages: + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + +snapshots: + + picocolors@1.1.1: {} diff --git a/fixtures/yarn-basic/.yarnrc.yml b/fixtures/yarn-basic/.yarnrc.yml new file mode 100644 index 0000000..3186f3f --- /dev/null +++ b/fixtures/yarn-basic/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/fixtures/yarn-basic/check.mjs b/fixtures/yarn-basic/check.mjs new file mode 100644 index 0000000..663fc86 --- /dev/null +++ b/fixtures/yarn-basic/check.mjs @@ -0,0 +1,7 @@ +import pc from 'picocolors'; + +if (typeof pc.green !== 'function') { + throw new Error('Expected picocolors.green to be available after yarn install.'); +} + +console.log(pc.green('yarn fixture dependency loaded')); diff --git a/fixtures/yarn-basic/package.json b/fixtures/yarn-basic/package.json new file mode 100644 index 0000000..94bd3bc --- /dev/null +++ b/fixtures/yarn-basic/package.json @@ -0,0 +1,9 @@ +{ + "name": "yarn-basic-fixture", + "private": true, + "type": "module", + "packageManager": "yarn@4.6.0", + "dependencies": { + "picocolors": "1.1.1" + } +} diff --git a/fixtures/yarn-basic/yarn.lock b/fixtures/yarn-basic/yarn.lock new file mode 100644 index 0000000..b5df1ae --- /dev/null +++ b/fixtures/yarn-basic/yarn.lock @@ -0,0 +1,21 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10c0 + +"picocolors@npm:1.1.1": + version: 1.1.1 + resolution: "picocolors@npm:1.1.1" + checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58 + languageName: node + linkType: hard + +"yarn-basic-fixture@workspace:.": + version: 0.0.0-use.local + resolution: "yarn-basic-fixture@workspace:." + dependencies: + picocolors: "npm:1.1.1" + languageName: unknown + linkType: soft diff --git a/package.json b/package.json new file mode 100644 index 0000000..a86111f --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "name": "@pwrdrvr/configure-nodejs-test", + "private": true, + "type": "module", + "scripts": { + "test": "node --test" + } +} diff --git a/test/resolve-cache-paths.test.mjs b/test/resolve-cache-paths.test.mjs new file mode 100644 index 0000000..f3486b5 --- /dev/null +++ b/test/resolve-cache-paths.test.mjs @@ -0,0 +1,109 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; + +import { + buildCachePaths, + buildResult, + buildWorkingDirectoryKey, + normalizeCacheKeySuffix, + resolveWorkingDirectory, +} from '../configure-nodejs/scripts/resolve-cache-paths.mjs'; + +function withTempDir(callback) { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'configure-nodejs-paths-')); + + try { + callback(tempDir); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } +} + +test('resolveWorkingDirectory preserves root-relative usage', () => { + withTempDir((tempDir) => { + const result = resolveWorkingDirectory(tempDir, '.'); + + assert.equal(result.workingDirectory, '.'); + assert.equal(result.absoluteWorkingDirectory, tempDir); + }); +}); + +test('resolveWorkingDirectory resolves nested directories relative to the repository root', () => { + withTempDir((tempDir) => { + fs.mkdirSync(path.join(tempDir, 'fixtures', 'npm-basic'), { recursive: true }); + + const result = resolveWorkingDirectory(tempDir, 'fixtures/npm-basic'); + + assert.equal(result.workingDirectory, 'fixtures/npm-basic'); + assert.match(result.absoluteWorkingDirectory, /fixtures\/npm-basic$/); + }); +}); + +test('resolveWorkingDirectory rejects paths that escape the repository root', () => { + withTempDir((tempDir) => { + assert.throws( + () => resolveWorkingDirectory(tempDir, '../outside'), + /resolves outside the repository root/, + ); + }); +}); + +test('resolveWorkingDirectory rejects missing paths', () => { + withTempDir((tempDir) => { + assert.throws( + () => resolveWorkingDirectory(tempDir, 'missing'), + /does not exist/, + ); + }); +}); + +test('buildWorkingDirectoryKey generates a stable root key and nested slug', () => { + assert.equal(buildWorkingDirectoryKey('.'), 'root'); + assert.match( + buildWorkingDirectoryKey('fixtures/yarn-basic'), + /^fixtures__yarn-basic-[a-f0-9]{8}$/, + ); +}); + +test('normalizeCacheKeySuffix preserves safe values and normalizes separators', () => { + assert.equal(normalizeCacheKeySuffix('fixture-tests'), 'fixture-tests'); + assert.equal(normalizeCacheKeySuffix(' fixture tests / ci '), 'fixture-tests-ci'); + assert.equal(normalizeCacheKeySuffix(' '), ''); +}); + +test('buildCachePaths scopes cache globs to the resolved working directory', () => { + assert.deepEqual(buildCachePaths('.'), [ + 'node_modules', + '**/node_modules', + '!node_modules/.cache', + '!**/node_modules/.cache', + ]); + + assert.deepEqual(buildCachePaths('fixtures/pnpm-basic'), [ + 'fixtures/pnpm-basic/node_modules', + 'fixtures/pnpm-basic/**/node_modules', + '!fixtures/pnpm-basic/node_modules/.cache', + '!fixtures/pnpm-basic/**/node_modules/.cache', + ]); +}); + +test('buildResult combines normalized working-directory and cache metadata', () => { + withTempDir((tempDir) => { + fs.mkdirSync(path.join(tempDir, 'fixtures', 'npm-basic'), { recursive: true }); + + const result = buildResult({ + cwd: tempDir, + workingDirectory: 'fixtures/npm-basic', + cacheKeySuffix: 'fixture tests', + }); + + assert.equal(result.workingDirectory, 'fixtures/npm-basic'); + assert.match(result.workingDirectoryKey, /^fixtures__npm-basic-[a-f0-9]{8}$/); + assert.equal(result.cachePaths[0], 'fixtures/npm-basic/node_modules'); + assert.equal(result.cacheKeySuffix, 'fixture-tests'); + assert.equal(result.cacheKeySuffixSegment, '-fixture-tests'); + }); +}); diff --git a/test/resolve-manager.test.mjs b/test/resolve-manager.test.mjs new file mode 100644 index 0000000..dbb629b --- /dev/null +++ b/test/resolve-manager.test.mjs @@ -0,0 +1,144 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; + +import { buildResult, normalizeManager } from '../configure-nodejs/scripts/resolve-manager.mjs'; + +function withTempDir(callback) { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'configure-nodejs-manager-')); + + try { + callback(tempDir); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } +} + +test('normalizeManager validates the supported values', () => { + assert.equal(normalizeManager(' PNPM '), 'pnpm'); + assert.throws( + () => normalizeManager('bun'), + /Unsupported package manager "bun"/, + ); +}); + +test('buildResult resolves npm from an explicit package-manager override', () => { + withTempDir((tempDir) => { + fs.writeFileSync( + path.join(tempDir, 'package.json'), + JSON.stringify({ name: 'fixture', version: '1.0.0' }), + ); + fs.writeFileSync(path.join(tempDir, 'package-lock.json'), '{}\n'); + + const result = buildResult({ cwd: tempDir, explicitManager: 'npm' }); + + assert.equal(result.packageManager, 'npm'); + assert.equal(result.installCommand, 'npm ci'); + assert.equal(result.lockfileName, 'package-lock.json'); + }); +}); + +test('buildResult resolves pnpm from packageManager metadata', () => { + withTempDir((tempDir) => { + fs.writeFileSync( + path.join(tempDir, 'package.json'), + JSON.stringify({ + name: 'fixture', + version: '1.0.0', + packageManager: 'pnpm@10.12.1', + }), + ); + fs.writeFileSync(path.join(tempDir, 'pnpm-lock.yaml'), 'lockfileVersion: 9.0\n'); + + const result = buildResult({ cwd: tempDir, explicitManager: '' }); + + assert.equal(result.packageManager, 'pnpm'); + assert.equal(result.packageManagerVersion, '10.12.1'); + assert.equal(result.installCommand, 'pnpm install --frozen-lockfile'); + assert.equal(result.managerCacheKey, 'pnpm-10.12.1'); + }); +}); + +test('buildResult resolves modern Yarn installs with --immutable', () => { + withTempDir((tempDir) => { + fs.writeFileSync( + path.join(tempDir, 'package.json'), + JSON.stringify({ + name: 'fixture', + version: '1.0.0', + packageManager: 'yarn@4.6.0', + }), + ); + fs.writeFileSync(path.join(tempDir, 'yarn.lock'), '# yarn lockfile\n'); + fs.writeFileSync(path.join(tempDir, '.yarnrc.yml'), 'nodeLinker: node-modules\n'); + + const result = buildResult({ cwd: tempDir, explicitManager: '' }); + + assert.equal(result.packageManager, 'yarn'); + assert.equal(result.installCommand, 'yarn install --immutable'); + }); +}); + +test('buildResult falls back to classic Yarn install flags when no Berry signal exists', () => { + withTempDir((tempDir) => { + fs.writeFileSync( + path.join(tempDir, 'package.json'), + JSON.stringify({ name: 'fixture', version: '1.0.0' }), + ); + fs.writeFileSync(path.join(tempDir, 'yarn.lock'), '# yarn classic lockfile\n'); + + const result = buildResult({ cwd: tempDir, explicitManager: 'yarn' }); + + assert.equal(result.packageManager, 'yarn'); + assert.equal(result.installCommand, 'yarn install --frozen-lockfile'); + }); +}); + +test('buildResult rejects multiple lockfiles in one working directory', () => { + withTempDir((tempDir) => { + fs.writeFileSync( + path.join(tempDir, 'package.json'), + JSON.stringify({ name: 'fixture', version: '1.0.0' }), + ); + fs.writeFileSync(path.join(tempDir, 'package-lock.json'), '{}\n'); + fs.writeFileSync(path.join(tempDir, 'pnpm-lock.yaml'), 'lockfileVersion: 9.0\n'); + + assert.throws( + () => buildResult({ cwd: tempDir, explicitManager: '' }), + /Found multiple lockfiles/, + ); + }); +}); + +test('buildResult honors an explicit manager when multiple lockfiles are present', () => { + withTempDir((tempDir) => { + fs.writeFileSync( + path.join(tempDir, 'package.json'), + JSON.stringify({ name: 'fixture', version: '1.0.0' }), + ); + fs.writeFileSync(path.join(tempDir, 'package-lock.json'), '{}\n'); + fs.writeFileSync(path.join(tempDir, 'pnpm-lock.yaml'), 'lockfileVersion: 9.0\n'); + + const result = buildResult({ cwd: tempDir, explicitManager: 'npm' }); + + assert.equal(result.packageManager, 'npm'); + assert.equal(result.lockfileName, 'package-lock.json'); + assert.equal(result.installCommand, 'npm ci'); + }); +}); + +test('buildResult rejects missing lockfiles when the manager cannot be resolved', () => { + withTempDir((tempDir) => { + fs.writeFileSync( + path.join(tempDir, 'package.json'), + JSON.stringify({ name: 'fixture', version: '1.0.0' }), + ); + + assert.throws( + () => buildResult({ cwd: tempDir, explicitManager: '' }), + /Could not determine package manager/, + ); + }); +});