diff --git a/.github/workflows/update-rocksdb-js.yml b/.github/workflows/update-rocksdb-js.yml index 51d27b5465..e40d4160e8 100644 --- a/.github/workflows/update-rocksdb-js.yml +++ b/.github/workflows/update-rocksdb-js.yml @@ -34,7 +34,7 @@ jobs: package-manager-cache: false - name: Install latest rocksdb-js - run: npm install --save @harperfast/rocksdb-js@latest + run: npm install --save-exact @harperfast/rocksdb-js@latest - name: Stage changes id: stage-changes diff --git a/AGENTS.md b/AGENTS.md index ad6e011e4b..70aaecad49 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -114,7 +114,7 @@ Use this to land in the right folder before grepping. Every top-level folder is - **`bin/`** — covered above (it's source). - **`benchmarks/`** — HNSW vector-search benchmark only (`hnsw-search.js`). Stand-alone; not part of CI. -- **`build-tools/`** — shell scripts for the build pipeline (`build.sh`, `build-studio.sh`, `download-prebuilds.js`). No tests. +- **`build-tools/`** — build-pipeline scripts. Tests: `unitTests/build-tools/`; run `npm run test:unit:main` after changes. - **`dev/`** — single dev utility (`sync-commits.js`) for cross-repo commit syncing. Not runtime. - **`integrationTests/`** — end-to-end tests against a built distribution. Run with `npm run test:integration` / `npm run test:integration:all`. Subdirs mirror source. See `integrationTests/README.md`. - **`unitTests/`** — Mocha unit tests; subdir per source layer. Run with `npm run test:unit:`. diff --git a/build-tools/check-shrinkwrap-pins.mjs b/build-tools/check-shrinkwrap-pins.mjs index fc7107c199..bef7b35f29 100644 --- a/build-tools/check-shrinkwrap-pins.mjs +++ b/build-tools/check-shrinkwrap-pins.mjs @@ -24,13 +24,18 @@ // "latest" dist-tag: if the range excludes a newer major, a broken install could never // reach it either, so comparing to absolute latest would flag a canary as fine when it // has actually gone vacuous within the range that matters. +// Exact manifest specs remain in the installed-version check, but cannot discriminate a +// shrinkwrap install from a fresh resolution, so the discrimination check skips them. // // Usage: node check-shrinkwrap-pins.mjs import { execFileSync } from 'node:child_process'; -import { readFileSync } from 'node:fs'; +import { existsSync, readFileSync } from 'node:fs'; -const CHECKED_DEPS = ['@harperfast/rocksdb-js', 'fastify']; +const CHECKED_DEPS = ['@harperfast/rocksdb-js', 'fastify', '@aws-sdk/client-s3']; +const ROCKSDB_SINGLE_INSTANCE_DEPS = ['@harperfast/extended-iterable', 'msgpackr']; +const REGISTRY_QUERY_ATTEMPTS = 3; +const retryWait = new Int32Array(new SharedArrayBuffer(4)); const pkgRoot = process.argv[2]; if (!pkgRoot) { @@ -85,11 +90,14 @@ for (const dep of CHECKED_DEPS) { continue; } - console.log(`shrinkwrap honored: ${dep}@${installed} matches the packed pin`); + const status = isExactVersion(range) ? 'shrinkwrap pin matches (exact manifest spec)' : 'shrinkwrap honored'; + console.log(`${status}: ${dep}@${installed} matches the packed pin`); } +verifyRocksDbDependencyAlignment(); + // A canary only proves the check works while its pin lags the registry -- if a lock bump -// ever lands both canaries on registry-latest, the pin-match loop above would pass on a +// ever lands every ranged canary on registry-latest, the pin-match loop above would pass on a // reverted, broken Dockerfile just as easily as on this one. Fail loudly rather than let // that happen silently. verifyCanariesDiscriminate(pins); @@ -97,42 +105,149 @@ verifyCanariesDiscriminate(pins); process.exit(failed ? 1 : 0); function verifyCanariesDiscriminate(pins) { + const rangedPins = Object.fromEntries(Object.entries(pins).filter(([, { range }]) => !isExactVersion(range))); + if (Object.keys(pins).length === 0) return; + if (Object.keys(rangedPins).length === 0) { + console.error( + '::error::every checked canary has an exact declared version -- exact dependencies cannot distinguish a shrinkwrap install from a fresh resolution. Add a canary with a ranged manifest spec.' + ); + failed = true; + return; + } + // What a broken/reverted install would actually resolve to: the max version satisfying // the declared range, not the registry's bare "latest" dist-tag (which could be a newer // major the range excludes, and a broken install could never reach that either). const rangeLatest = {}; - for (const [dep, { range }] of Object.entries(pins)) { - try { - // `npm view @ version --json` returns every matching version, not - // just the max, and the order isn't a documented contract (it can track - // publish/insertion order rather than semver order, e.g. a backported patch - // published after a newer minor) -- compare them ourselves rather than trust - // the last array entry. - const out = execFileSync('npm', ['view', `${dep}@${range}`, 'version', '--json'], { encoding: 'utf8' }); - const versions = JSON.parse(out); - rangeLatest[dep] = Array.isArray(versions) - ? versions.reduce((max, v) => (compareVersions(v, max) > 0 ? v : max)) - : versions; - } catch (e) { - console.log( - `::warning::could not check registry-latest-in-range for ${dep}@${range} (${e.message}) -- skipping discrimination check for it` - ); + const failedQueries = []; + for (const [dep, { range }] of Object.entries(rangedPins)) { + for (let attempt = 1; attempt <= REGISTRY_QUERY_ATTEMPTS; attempt++) { + try { + // `npm view @ version --json` returns every matching version, not + // just the max, and the order isn't a documented contract (it can track + // publish/insertion order rather than semver order, e.g. a backported patch + // published after a newer minor) -- compare them ourselves rather than trust + // the last array entry. + const out = execFileSync('npm', ['view', `${dep}@${range}`, 'version', '--json'], { encoding: 'utf8' }); + const versions = JSON.parse(out); + if (Array.isArray(versions) && versions.length === 0) { + reportMissingRange(dep, range); + return; + } + if ( + (Array.isArray(versions) && versions.some((version) => !isExactVersion(version))) || + (!Array.isArray(versions) && !isExactVersion(versions)) + ) { + throw new Error('npm returned an invalid version payload'); + } + rangeLatest[dep] = Array.isArray(versions) + ? versions.reduce((max, v) => (compareVersions(v, max) > 0 ? v : max)) + : versions; + break; + } catch (e) { + if (isNpmNotFoundError(e)) { + reportMissingRange(dep, range); + return; + } + console.log( + `::warning::registry query attempt ${attempt}/${REGISTRY_QUERY_ATTEMPTS} failed for ${dep}@${range} (${e.message})` + ); + if (attempt === REGISTRY_QUERY_ATTEMPTS) failedQueries.push(`${dep}@${range}`); + else Atomics.wait(retryWait, 0, 0, 1000 * attempt); + } } } const checkable = Object.keys(rangeLatest); - if (checkable.length === 0) { - console.log('::warning::registry unreachable -- could not verify the canary set still discriminates'); - return; - } const stillDiscriminates = checkable.some((dep) => rangeLatest[dep] !== pins[dep].pinned); - if (!stillDiscriminates) { + if (stillDiscriminates) return; + if (failedQueries.length > 0) { console.error( - `::error::every checked canary (${checkable.join(', ')}) is now pinned at the latest version its declared range allows -- this check would pass even on a reverted, unpinned install. Pick a new canary whose shrinkwrap pin lags what its range allows.` + `::error title=Retry dependency canary check::Could not verify that the shrinkwrap canaries still discriminate after ${REGISTRY_QUERY_ATTEMPTS} registry query attempts for ${failedQueries.join(', ')}. Retry this job; if the error persists, check npm/registry/runner configuration and confirm the listed package ranges match published versions.` ); failed = true; + return; + } + console.error( + `::error::every checked canary (${checkable.join(', ')}) is now pinned at the latest version its declared range allows -- this check would pass even on a reverted, unpinned install. Pick a new canary whose shrinkwrap pin lags what its range allows.` + ); + failed = true; +} + +function verifyRocksDbDependencyAlignment() { + let rocksdbManifest; + try { + rocksdbManifest = JSON.parse(readFileSync(`${pkgRoot}/node_modules/@harperfast/rocksdb-js/package.json`, 'utf8')); + } catch (e) { + console.error(`::error::could not inspect rocksdb-js dependency alignment: ${e.message}`); + failed = true; + return; + } + + for (const dep of ROCKSDB_SINGLE_INSTANCE_DEPS) { + const rootSpec = manifest.dependencies?.[dep]; + const rocksdbSpec = rocksdbManifest.dependencies?.[dep]; + if (!isExactVersion(rootSpec)) { + console.error( + `::error::the root ${dep} spec must be exact, received ${rootSpec ?? 'missing'} -- update it with rocksdb-js to preserve one module instance` + ); + failed = true; + continue; + } + if (rootSpec !== rocksdbSpec) { + console.error( + `::error::the root ${dep} pin ${rootSpec} does not match rocksdb-js ${rocksdbSpec ?? 'missing'} -- update these pins together to preserve one module instance` + ); + failed = true; + continue; + } + + try { + const installed = JSON.parse(readFileSync(`${pkgRoot}/node_modules/${dep}/package.json`, 'utf8')).version; + if (installed !== rootSpec) { + console.error(`::error::${dep} resolved to ${installed}, expected the aligned exact pin ${rootSpec}`); + failed = true; + } + } catch (e) { + console.error(`::error::could not inspect the root ${dep} instance: ${e.message}`); + failed = true; + } + + const nestedManifest = `${pkgRoot}/node_modules/@harperfast/rocksdb-js/node_modules/${dep}/package.json`; + if (existsSync(nestedManifest)) { + let nestedVersion = 'unknown'; + try { + nestedVersion = JSON.parse(readFileSync(nestedManifest, 'utf8')).version; + } catch {} + console.error( + `::error::rocksdb-js loaded a nested ${dep}@${nestedVersion} -- root and rocksdb-js must share one module instance` + ); + failed = true; + } } } +function isExactVersion(range) { + return ( + typeof range === 'string' && + /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.test(range) + ); +} + +function isNpmNotFoundError(error) { + try { + return JSON.parse(error.stdout).error?.code === 'E404'; + } catch { + return false; + } +} + +function reportMissingRange(dep, range) { + console.error( + `::error::${dep}@${range} matches no published version in the configured registry -- correct the declared range in package.json or confirm the configured registry carries this package` + ); + failed = true; +} + // Numeric major.minor.patch comparison, ignoring any prerelease/build suffix -- sufficient // for the stable releases this check compares (avoids depending on a semver-parsing // package that may not be resolvable from this script's own location). diff --git a/package-lock.json b/package-lock.json index 7e3fb2fc75..f7712f10e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,8 +16,8 @@ "@fastify/compress": "^8.3.1", "@fastify/cors": "^11.2.0", "@fastify/static": "^9.1.3", - "@harperfast/extended-iterable": "^1.0.1", - "@harperfast/rocksdb-js": "^2.7.1", + "@harperfast/extended-iterable": "1.0.3", + "@harperfast/rocksdb-js": "2.7.1", "@harperfast/skills": "^1.10.8", "@turf/area": "6.5.0", "@turf/boolean-contains": "6.5.0", @@ -61,7 +61,7 @@ "minimist": "1.2.8", "moment": "2.30.1", "mqtt-packet": "~9.0.1", - "msgpackr": "^2.0.5", + "msgpackr": "2.0.5", "needle": "3.5.0", "node-forge": "^1.3.1", "node-stream-zip": "1.16.0", @@ -82,7 +82,7 @@ "ses": "^1.15.0", "stream-chain": "2.2.5", "stream-json": "1.9.1", - "structon": "^1.0.7", + "structon": "1.0.7", "systeminformation": "^5.31.4", "tar-fs": "^3.1.2", "tar-stream": "^3.1.8", @@ -141,9 +141,9 @@ "node": "^22.18.0 || >=24.0.0" }, "optionalDependencies": { - "bufferutil": "^4.0.9", - "segfault-handler": "^1.3.0", - "utf-8-validate": "^5.0.10" + "bufferutil": "4.1.0", + "segfault-handler": "1.3.0", + "utf-8-validate": "5.0.10" }, "peerDependencies": { "@aws-sdk/client-bedrock-runtime": "^3.0.0", diff --git a/package.json b/package.json index 32aadf430a..10dc0813e7 100644 --- a/package.json +++ b/package.json @@ -178,8 +178,8 @@ "@fastify/compress": "^8.3.1", "@fastify/cors": "^11.2.0", "@fastify/static": "^9.1.3", - "@harperfast/extended-iterable": "^1.0.1", - "@harperfast/rocksdb-js": "^2.7.1", + "@harperfast/extended-iterable": "1.0.3", + "@harperfast/rocksdb-js": "2.7.1", "@harperfast/skills": "^1.10.8", "@turf/area": "6.5.0", "@turf/boolean-contains": "6.5.0", @@ -223,7 +223,7 @@ "minimist": "1.2.8", "moment": "2.30.1", "mqtt-packet": "~9.0.1", - "msgpackr": "^2.0.5", + "msgpackr": "2.0.5", "needle": "3.5.0", "node-forge": "^1.3.1", "node-stream-zip": "1.16.0", @@ -244,7 +244,7 @@ "ses": "^1.15.0", "stream-chain": "2.2.5", "stream-json": "1.9.1", - "structon": "^1.0.7", + "structon": "1.0.7", "systeminformation": "^5.31.4", "tar-fs": "^3.1.2", "tar-stream": "^3.1.8", @@ -261,9 +261,9 @@ } }, "optionalDependencies": { - "bufferutil": "^4.0.9", - "segfault-handler": "^1.3.0", - "utf-8-validate": "^5.0.10" + "bufferutil": "4.1.0", + "segfault-handler": "1.3.0", + "utf-8-validate": "5.0.10" }, "peerDependencies": { "@aws-sdk/client-bedrock-runtime": "^3.0.0", diff --git a/unitTests/build-tools/checkShrinkwrapPins.test.mjs b/unitTests/build-tools/checkShrinkwrapPins.test.mjs new file mode 100644 index 0000000000..e8298280a0 --- /dev/null +++ b/unitTests/build-tools/checkShrinkwrapPins.test.mjs @@ -0,0 +1,407 @@ +import assert from 'node:assert'; +import { spawnSync } from 'node:child_process'; +import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); +const script = join(root, 'build-tools/check-shrinkwrap-pins.mjs'); +const dependencies = ['@harperfast/rocksdb-js', 'fastify', '@aws-sdk/client-s3']; +const alignedDependencies = { + '@harperfast/extended-iterable': '1.0.3', + 'msgpackr': '2.0.5', +}; + +describe('shrinkwrap pin canaries', function () { + it('keeps the checked canaries present and ranged in the real manifest', async function () { + const manifest = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')); + const fixture = await createFixture(manifest.dependencies); + try { + const result = runCheck(fixture); + assert.strictEqual(result.status, 0, result.stderr); + } finally { + await fixture.cleanup(); + } + }); + + it('fails when a root encoder pin diverges from rocksdb-js', async function () { + const fixture = await createFixture({ + '@harperfast/rocksdb-js': '2.7.1', + 'fastify': '^5.8.2', + '@aws-sdk/client-s3': '^3.1012.0', + }); + try { + await writeFile( + join(fixture.packageRoot, 'node_modules/@harperfast/rocksdb-js/package.json'), + JSON.stringify({ + version: '1.0.0', + dependencies: { ...alignedDependencies, msgpackr: '2.0.6' }, + }) + ); + const result = runCheck(fixture); + assert.strictEqual(result.status, 1); + assert.match(result.stderr, /root msgpackr pin 2\.0\.5 does not match rocksdb-js 2\.0\.6/); + } finally { + await fixture.cleanup(); + } + }); + + it('fails when a root encoder spec is not exact', async function () { + const fixture = await createFixture({ + '@harperfast/rocksdb-js': '2.7.1', + 'fastify': '^5.8.2', + '@aws-sdk/client-s3': '^3.1012.0', + 'msgpackr': '^2.0.5', + }); + try { + const result = runCheck(fixture); + assert.strictEqual(result.status, 1); + assert.match(result.stderr, /root msgpackr spec must be exact, received \^2\.0\.5/); + } finally { + await fixture.cleanup(); + } + }); + + it('fails when rocksdb-js installs a nested encoder instance', async function () { + const fixture = await createFixture({ + '@harperfast/rocksdb-js': '2.7.1', + 'fastify': '^5.8.2', + '@aws-sdk/client-s3': '^3.1012.0', + }); + try { + const nestedDir = join(fixture.packageRoot, 'node_modules/@harperfast/rocksdb-js/node_modules/msgpackr'); + await mkdir(nestedDir, { recursive: true }); + await writeFile(join(nestedDir, 'package.json'), JSON.stringify({ version: '2.0.5' })); + const result = runCheck(fixture); + assert.strictEqual(result.status, 1); + assert.match(result.stderr, /rocksdb-js loaded a nested msgpackr@2\.0\.5/); + } finally { + await fixture.cleanup(); + } + }); + + it('does not misdiagnose an empty checked set as all-exact', async function () { + const fixture = await createFixture({}); + try { + const result = runCheck(fixture); + assert.strictEqual(result.status, 1); + assert.doesNotMatch(result.stderr, /every checked canary has an exact declared version/); + } finally { + await fixture.cleanup(); + } + }); + + it('does not query exact versions while a ranged canary still discriminates', async function () { + const fixture = await createFixture({ + '@harperfast/rocksdb-js': '2.7.1', + 'fastify': '^5.8.2', + '@aws-sdk/client-s3': '^3.1012.0', + }); + try { + const result = runCheck(fixture); + assert.strictEqual(result.status, 0, result.stderr); + const queries = await readFile(fixture.queryLog, 'utf8'); + assert.doesNotMatch(queries, /@harperfast\/rocksdb-js/); + assert.match(queries, /@aws-sdk\/client-s3@\^3\.1012\.0/); + } finally { + await fixture.cleanup(); + } + }); + + it('still compares exact pins with the installed dependency', async function () { + const fixture = await createFixture( + { + '@harperfast/rocksdb-js': '2.7.1', + 'fastify': '^5.8.2', + '@aws-sdk/client-s3': '^3.1012.0', + }, + { '@harperfast/rocksdb-js': '1.0.1' } + ); + try { + const result = runCheck(fixture); + assert.strictEqual(result.status, 1); + assert.match(result.stderr, /@harperfast\/rocksdb-js resolved to 1\.0\.1 but the packed shrinkwrap pins 1\.0\.0/); + assert.doesNotMatch(await readFile(fixture.queryLog, 'utf8'), /@harperfast\/rocksdb-js/); + } finally { + await fixture.cleanup(); + } + }); + + it('fails when every canary has an exact manifest version', async function () { + const fixture = await createFixture(Object.fromEntries(dependencies.map((dependency) => [dependency, '1.0.0']))); + try { + const result = runCheck(fixture); + assert.strictEqual(result.status, 1); + assert.match(result.stderr, /every checked canary has an exact declared version/); + assert.strictEqual(await readFile(fixture.queryLog, 'utf8'), ''); + } finally { + await fixture.cleanup(); + } + }); + + it('fails when every ranged canary is pinned at its max-in-range version', async function () { + const fixture = await createFixture( + { + '@harperfast/rocksdb-js': '2.7.1', + 'fastify': '^5.8.2', + '@aws-sdk/client-s3': '^3.1012.0', + }, + {}, + true + ); + try { + const result = runCheck(fixture); + assert.strictEqual(result.status, 1); + assert.match( + result.stderr, + /every checked canary \(fastify, @aws-sdk\/client-s3\) is now pinned at the latest version/ + ); + } finally { + await fixture.cleanup(); + } + }); + + it('retries a transient registry failure before evaluating the canary set', async function () { + const fixture = await createFixture( + { + '@harperfast/rocksdb-js': '2.7.1', + 'fastify': '^5.8.2', + '@aws-sdk/client-s3': '^3.1012.0', + }, + {}, + false, + '@aws-sdk/client-s3@^3.1012.0', + 1 + ); + try { + const result = runCheck(fixture); + assert.strictEqual(result.status, 0, result.stderr); + assert.match(result.stdout, /registry query attempt 1\/3 failed/); + const queries = (await readFile(fixture.queryLog, 'utf8')).split('\n'); + assert.strictEqual(queries.filter((query) => query === '@aws-sdk/client-s3@^3.1012.0').length, 2); + } finally { + await fixture.cleanup(); + } + }); + + it('accepts a proven canary after another registry query exhausts its retries', async function () { + const fixture = await createFixture( + { + '@harperfast/rocksdb-js': '2.7.1', + 'fastify': '^5.8.2', + '@aws-sdk/client-s3': '^3.1012.0', + }, + {}, + false, + 'fastify@^5.8.2' + ); + try { + const result = runCheck(fixture); + assert.strictEqual(result.status, 0, result.stderr); + const queries = (await readFile(fixture.queryLog, 'utf8')).split('\n'); + assert.strictEqual(queries.filter((query) => query === 'fastify@^5.8.2').length, 3); + } finally { + await fixture.cleanup(); + } + }); + + it('fails with a retry-specific error when registry failures leave no proven canary', async function () { + const fixture = await createFixture( + { + '@harperfast/rocksdb-js': '2.7.1', + 'fastify': '^5.8.2', + '@aws-sdk/client-s3': '^3.1012.0', + }, + {}, + true, + '@aws-sdk/client-s3@^3.1012.0' + ); + try { + const result = runCheck(fixture); + assert.strictEqual(result.status, 1); + assert.match(result.stderr, /::error title=Retry dependency canary check::/); + assert.match(result.stderr, /after 3 registry query attempts/); + assert.match(result.stderr, /Retry this job/); + assert.match(result.stderr, /confirm the listed package ranges match published versions/); + const queries = (await readFile(fixture.queryLog, 'utf8')).split('\n'); + assert.strictEqual(queries.filter((query) => query === '@aws-sdk/client-s3@^3.1012.0').length, 3); + } finally { + await fixture.cleanup(); + } + }); + + it('fails without retries when npm reports no matching published version', async function () { + const fixture = await createFixture( + { + '@harperfast/rocksdb-js': '2.7.1', + 'fastify': '^5.8.2', + '@aws-sdk/client-s3': '^3.1012.0', + }, + {}, + false, + '', + 3, + { missingRange: '@aws-sdk/client-s3@^3.1012.0' } + ); + try { + const result = runCheck(fixture); + assert.strictEqual(result.status, 1); + assert.match(result.stderr, /matches no published version/); + const queries = (await readFile(fixture.queryLog, 'utf8')).split('\n'); + assert.strictEqual(queries.filter((query) => query === '@aws-sdk/client-s3@^3.1012.0').length, 1); + } finally { + await fixture.cleanup(); + } + }); + + it('fails without retries when a registry returns an empty version list', async function () { + const fixture = await createFixture( + { + '@harperfast/rocksdb-js': '2.7.1', + 'fastify': '^5.8.2', + '@aws-sdk/client-s3': '^3.1012.0', + }, + {}, + false, + '', + 3, + { emptyRange: '@aws-sdk/client-s3@^3.1012.0' } + ); + try { + const result = runCheck(fixture); + assert.strictEqual(result.status, 1); + assert.match(result.stderr, /matches no published version in the configured registry/); + const queries = (await readFile(fixture.queryLog, 'utf8')).split('\n'); + assert.strictEqual(queries.filter((query) => query === '@aws-sdk/client-s3@^3.1012.0').length, 1); + } finally { + await fixture.cleanup(); + } + }); + + it('retries an invalid successful registry payload instead of treating it as proof', async function () { + const fixture = await createFixture( + { + '@harperfast/rocksdb-js': '2.7.1', + 'fastify': '^5.8.2', + '@aws-sdk/client-s3': '^3.1012.0', + }, + {}, + true, + '', + 3, + { invalidRange: '@aws-sdk/client-s3@^3.1012.0' } + ); + try { + const result = runCheck(fixture); + assert.strictEqual(result.status, 1); + assert.match(result.stderr, /::error title=Retry dependency canary check::/); + const queries = (await readFile(fixture.queryLog, 'utf8')).split('\n'); + assert.strictEqual(queries.filter((query) => query === '@aws-sdk/client-s3@^3.1012.0').length, 3); + } finally { + await fixture.cleanup(); + } + }); +}); + +async function createFixture( + manifestDependencies, + installedVersions = {}, + allRangeVersionsCurrent = false, + failedRange = '', + failedAttempts = 3, + registryResponses = {} +) { + const tempDir = await mkdtemp(join(tmpdir(), 'harper-shrinkwrap-canary-')); + const packageRoot = join(tempDir, 'package'); + const binDir = join(tempDir, 'bin'); + const queryLog = join(tempDir, 'queries.log'); + await mkdir(packageRoot, { recursive: true }); + await mkdir(binDir, { recursive: true }); + await writeFile(queryLog, ''); + const packageDependencies = { ...alignedDependencies, ...manifestDependencies }; + await writeFile(join(packageRoot, 'package.json'), JSON.stringify({ dependencies: packageDependencies })); + await writeFile( + join(packageRoot, 'npm-shrinkwrap.packed.json'), + JSON.stringify({ + lockfileVersion: 3, + packages: Object.fromEntries( + dependencies.map((dependency) => [`node_modules/${dependency}`, { version: '1.0.0' }]) + ), + }) + ); + for (const dependency of [...dependencies, ...Object.keys(alignedDependencies)]) { + const dependencyDir = join(packageRoot, 'node_modules', dependency); + await mkdir(dependencyDir, { recursive: true }); + const dependencyManifest = { + version: + installedVersions[dependency] ?? + (dependency in alignedDependencies ? packageDependencies[dependency] : '1.0.0'), + }; + if (dependency === '@harperfast/rocksdb-js') { + dependencyManifest.dependencies = Object.fromEntries( + Object.keys(alignedDependencies).map((dep) => [dep, packageDependencies[dep]]) + ); + } + await writeFile(join(dependencyDir, 'package.json'), JSON.stringify(dependencyManifest)); + } + await writeFile( + join(binDir, 'npm'), + `#!/bin/sh +printf '%s\\n' "$2" >> "$QUERY_LOG" +attempt=$(grep -Fxc "$2" "$QUERY_LOG") +if [ "$FAILED_RANGE" = "$2" ] && [ "$attempt" -le "$FAILED_ATTEMPTS" ]; then + exit 1 +fi +if [ "$MISSING_RANGE" = "$2" ]; then + printf '{"error":{"code":"E404","summary":"No match found for version"}}\\n' + exit 1 +fi +if [ "$EMPTY_RANGE" = "$2" ]; then + printf '[]\\n' + exit +fi +if [ "$INVALID_RANGE" = "$2" ]; then + printf 'null\\n' + exit +fi +if [ "$ALL_RANGE_VERSIONS_CURRENT" = 1 ]; then + printf '["1.0.0"]\\n' + exit +fi +case "$2" in + fastify@*) printf '["1.0.0"]\\n' ;; + @aws-sdk/client-s3@*) printf '["1.0.0", "1.0.1"]\\n' ;; + *) printf 'stub npm: unmodelled query %s\\n' "$2" >&2; exit 1 ;; +esac +` + ); + await chmod(join(binDir, 'npm'), 0o755); + return { + binDir, + packageRoot, + queryLog, + allRangeVersionsCurrent, + failedRange, + failedAttempts, + ...registryResponses, + cleanup: () => rm(tempDir, { recursive: true, force: true }), + }; +} + +function runCheck(fixture) { + return spawnSync(process.execPath, [script, fixture.packageRoot], { + encoding: 'utf8', + env: { + ...process.env, + PATH: `${fixture.binDir}:${process.env.PATH}`, + QUERY_LOG: fixture.queryLog, + ALL_RANGE_VERSIONS_CURRENT: fixture.allRangeVersionsCurrent ? '1' : '0', + FAILED_RANGE: fixture.failedRange, + FAILED_ATTEMPTS: String(fixture.failedAttempts), + MISSING_RANGE: fixture.missingRange ?? '', + EMPTY_RANGE: fixture.emptyRange ?? '', + INVALID_RANGE: fixture.invalidRange ?? '', + }, + }); +}