From b0a377962edcda8348fe8cf7b1893b05feae9c67 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Wed, 23 Sep 2026 09:03:55 -0600 Subject: [PATCH 1/2] fix: resolve the harper CLI through its bin field, from the project under test Resolution step 3 did require.resolve('harper/dist/bin/harper.js'), which throws ERR_PACKAGE_PATH_NOT_EXPORTED on every published harper 5.x (their exports map only "."). A bare catch swallowed it, so step 3 never resolved anything and consumers had to pass harperBinPath. Resolve the package's "." entry instead, walk up to the package.json named harper, and join its bin.harper. Resolve from the working directory rather than from this module: inside the harper repo, package self-reference yields the local build, whereas this module's location would find the registry harper npm auto-installs for our peer dependency (the #23 scenario, latent until now only because step 3 was broken). Only MODULE_NOT_FOUND falls through now; a harper that is installed but cannot be located fails with the remedy instead of silently skipping step 3. The node_modules/dist ambiguity warning shares step 4's ancestor walk so it compares against the same file step 4 would have picked. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0138qUYfQacRsPPqTR73vtbz Dispatch-Task: integration-testing-harper-bin-resolution --- README.md | 3 +- src/harperLifecycle.ts | 109 ++++++++++++++++++++++------------- test/harperLifecycle.test.ts | 104 ++++++++++++++++++++++++++++++++- 3 files changed, 171 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index aaecbcb..fe87575 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,8 @@ The Harper binary is resolved in the following order: 1. `harperBinPath` option passed directly to `startHarper()` 2. `HARPER_INTEGRATION_TEST_INSTALL_SCRIPT` environment variable (path to `dist/bin/harper.js`) -3. Auto-resolved from a `harper` package installed as a project dependency +3. Auto-resolved from a `harper` package installed as a project dependency — the one `require('harper')` resolves to from the working directory, located via its `bin` field. Inside the harper repo itself this is the local build (package self-reference), not the registry copy npm auto-installs for this package's peer dependency. +4. The nearest `dist/bin/harper.js` in the working directory or its ancestors **Options:** diff --git a/src/harperLifecycle.ts b/src/harperLifecycle.ts index 65a656c..a09487b 100644 --- a/src/harperLifecycle.ts +++ b/src/harperLifecycle.ts @@ -1,5 +1,5 @@ import { spawn, ChildProcess } from 'node:child_process'; -import { createWriteStream, existsSync, type WriteStream } from 'node:fs'; +import { createWriteStream, existsSync, readFileSync, type WriteStream } from 'node:fs'; import { join, basename, dirname } from 'node:path'; import { tmpdir } from 'node:os'; import { mkdtemp, mkdir, rm, cp } from 'node:fs/promises'; @@ -143,7 +143,8 @@ export interface StartHarperOptions { * If not provided, resolution order is: * 1. This option * 2. HARPER_INTEGRATION_TEST_INSTALL_SCRIPT environment variable - * 3. Auto-resolved from 'harper' package in node_modules + * 3. Auto-resolved from the 'harper' package the project (cwd) resolves + * 4. Auto-resolved from current directory or ancestors ('dist/bin/harper.js') */ harperBinPath?: string; } @@ -213,7 +214,7 @@ export interface ContextWithHarper extends SuiteContext, TestContext { * Resolution order: * 1. `harperBinPath` argument * 2. `HARPER_INTEGRATION_TEST_INSTALL_SCRIPT` environment variable - * 3. Auto-resolved from 'harper' package in node_modules + * 3. Auto-resolved from the 'harper' package the project (cwd) resolves, via its `bin` field * 4. Auto-resolved from current directory or ancestors ('dist/bin/harper.js') * * @returns The absolute path to the Harper CLI entry script @@ -236,29 +237,16 @@ function getHarperScript(harperBinPath?: string): string { return logResolvedHarperScript(envPath, 'HARPER_INTEGRATION_TEST_INSTALL_SCRIPT'); } - // 3. Auto-resolve from node_modules - try { - const require = createRequire(import.meta.url); - const resolved = require.resolve('harper/dist/bin/harper.js'); - if (existsSync(resolved)) { - return logResolvedHarperScript(resolved, 'node_modules'); - } - } catch { - // harper package not found in node_modules + // 3. Auto-resolve from the project's 'harper' package + const packageScript = resolveHarperPackageScript(process.cwd()); + if (packageScript && existsSync(packageScript)) { + return logResolvedHarperScript(packageScript, "'harper' package"); } // 4. Auto-resolve from current directory or ancestors - let currentDir = process.cwd(); - while (true) { - const potentialPath = join(currentDir, 'dist/bin/harper.js'); - if (existsSync(potentialPath)) { - return logResolvedHarperScript(potentialPath, 'ancestor dist'); - } - const parentDir = dirname(currentDir); - if (parentDir === currentDir) { - break; - } - currentDir = parentDir; + const ancestorScript = findAncestorDistScript(); + if (ancestorScript) { + return logResolvedHarperScript(ancestorScript, 'ancestor dist'); } throw new Error( @@ -270,6 +258,53 @@ function getHarperScript(harperBinPath?: string): string { ); } +/** + * Locates the CLI script of the `harper` package that `require('harper')` resolves to from + * `projectDir`, via the package's `bin` field: harper's `exports` map exposes only `"."`, so + * `dist/bin/harper.js` cannot be resolved as a subpath. + * + * Resolving from the project rather than from this module matters inside the harper repo itself: + * package self-reference yields its local build, whereas this module's location would find the + * registry `harper` that npm auto-installs to satisfy our peer dependency. + * + * Returns `undefined` when no `harper` is resolvable. Exported for tests, not from `index.ts`. + */ +export function resolveHarperPackageScript(projectDir: string): string | undefined { + try { + const entryPath = createRequire(join(projectDir, 'noop.js')).resolve('harper'); + let packageDir = dirname(entryPath); + while (true) { + const packageJsonPath = join(packageDir, 'package.json'); + if (existsSync(packageJsonPath)) { + const { name, bin } = JSON.parse(readFileSync(packageJsonPath, 'utf8')); + if (name === 'harper') return join(packageDir, bin.harper); + } + const parentDir = dirname(packageDir); + if (parentDir === packageDir) return undefined; + packageDir = parentDir; + } + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'MODULE_NOT_FOUND') return undefined; + throw new Error( + `Could not locate the Harper CLI script of the 'harper' package resolved from ${projectDir}. ` + + `Provide it via the harperBinPath option or HARPER_INTEGRATION_TEST_INSTALL_SCRIPT.`, + { cause: error } + ); + } +} + +/** Nearest `dist/bin/harper.js` in the current directory or its ancestors. */ +function findAncestorDistScript(): string | undefined { + let currentDir = process.cwd(); + while (true) { + const potentialPath = join(currentDir, 'dist/bin/harper.js'); + if (existsSync(potentialPath)) return potentialPath; + const parentDir = dirname(currentDir); + if (parentDir === currentDir) return undefined; + currentDir = parentDir; + } +} + const loggedHarperScripts = new Set(); /** @@ -278,29 +313,21 @@ const loggedHarperScripts = new Set(); * Which binary runs is the single most consequential (and previously invisible) * decision the harness makes: resolving a stale `harper` package from * node_modules while the caller expected their local build produces confusing - * wholesale test failures with no hint of the cause. When the `node_modules` - * path wins while `./dist/bin/harper.js` also exists, warn with the override. + * wholesale test failures with no hint of the cause. When the `harper` package + * wins over a different local `dist/bin/harper.js` that step 4 would have + * picked, warn with the override. */ function logResolvedHarperScript(scriptPath: string, source: string): string { if (loggedHarperScripts.has(scriptPath)) return scriptPath; loggedHarperScripts.add(scriptPath); console.log(`[integration-testing] Using Harper at ${scriptPath} (via ${source})`); - if (source === 'node_modules') { - // Mirror resolution step 4's ancestor walk so the warning fires even when - // tests run from a subdirectory of the repo that holds the local build. - let currentDir = process.cwd(); - while (true) { - const localDist = join(currentDir, 'dist/bin/harper.js'); - if (existsSync(localDist) && localDist !== scriptPath) { - console.warn( - `[integration-testing] Warning: resolved the 'harper' package from node_modules, but ${localDist} also exists. ` + - `If you meant to test the local build, set HARPER_INTEGRATION_TEST_INSTALL_SCRIPT=${localDist}.` - ); - break; - } - const parentDir = dirname(currentDir); - if (parentDir === currentDir) break; - currentDir = parentDir; + if (source === "'harper' package") { + const localDist = findAncestorDistScript(); + if (localDist && localDist !== scriptPath) { + console.warn( + `[integration-testing] Warning: resolved the 'harper' package, but ${localDist} also exists. ` + + `If you meant to test the local build, set HARPER_INTEGRATION_TEST_INSTALL_SCRIPT=${localDist}.` + ); } } return scriptPath; diff --git a/test/harperLifecycle.test.ts b/test/harperLifecycle.test.ts index 4e7a70a..a29e395 100644 --- a/test/harperLifecycle.test.ts +++ b/test/harperLifecycle.test.ts @@ -1,11 +1,11 @@ import { test, before, after } from 'node:test'; -import { ok, strictEqual, match, doesNotMatch, rejects } from 'node:assert'; +import { ok, strictEqual, match, doesNotMatch, rejects, throws } from 'node:assert'; import { spawn, type ChildProcess } from 'node:child_process'; import { once } from 'node:events'; -import { mkdtempSync, writeFileSync, rmSync, existsSync } from 'node:fs'; +import { mkdtempSync, mkdirSync, realpathSync, writeFileSync, rmSync, existsSync } from 'node:fs'; import { readFile, writeFile as writeFileAsync } from 'node:fs/promises'; import { tmpdir } from 'node:os'; -import { join } from 'node:path'; +import { dirname, join } from 'node:path'; import { setTimeout as sleep } from 'node:timers/promises'; import { fileURLToPath } from 'node:url'; import { @@ -18,6 +18,7 @@ import { runHarperCommand, HarperStartupError, buildHarperChildEnv, + resolveHarperPackageScript, type StartedHarperTestContext, } from '../src/harperLifecycle.ts'; import { @@ -143,6 +144,11 @@ for (const index of [0, 1]) { }); } setInterval(() => {}, 1000); +`, + // Starts Harper with no harperBinPath, so the binary comes from auto-resolution in this cwd. + 'resolve-runner.mjs': ` +const { runHarperCommand } = await import(process.env.HARPER_LIFECYCLE_URL); +await runHarperCommand({ args: [], env: {} }); `, 'worker-harper.mjs': ` import { parentPort, workerData } from 'node:worker_threads'; @@ -1085,3 +1091,95 @@ test('buildHarperChildEnv: isolates HOME/USERPROFILE to dataRootDir by default', strictEqual(env.HOME, '/tmp/data-root'); strictEqual(env.USERPROFILE, '/tmp/data-root'); }); + +// The shape every published harper 5.x has: `exports` maps only ".", so dist/bin/harper.js is not a +// resolvable subpath. +const HARPER_PACKAGE_JSON = { + name: 'harper', + exports: { '.': { types: './dist/index.d.ts', default: './dist/index.js' } }, + bin: { harper: 'dist/bin/harper.js' }, +}; +const BUILT_HARPER_FILES = ['dist/index.js', 'dist/bin/harper.js']; + +function writePackage(dir: string, packageJson: object, files: string[] = []): string { + mkdirSync(dir, { recursive: true }); + writeFileSync(join(dir, 'package.json'), JSON.stringify(packageJson)); + for (const file of files) { + mkdirSync(dirname(join(dir, file)), { recursive: true }); + writeFileSync(join(dir, file), ''); + } + return dir; +} + +/** Realpath, since that is what module resolution returns and the assertions compare against. */ +function makeResolutionRoot(): string { + return realpathSync(mkdtempSync(join(fixtureDir, 'resolution-'))); +} + +/** Starts Harper through `runHarperCommand` from `cwd`, with nothing but auto-resolution to pick the binary. */ +async function runAutoResolution(cwd: string): Promise<{ code: number | null; stdout: string; stderr: string }> { + const runner = spawn(process.execPath, [fixtures['resolve-runner.mjs']], { + cwd, + env: { + ...process.env, + HARPER_LIFECYCLE_URL: new URL('../src/harperLifecycle.ts', import.meta.url).href, + HARPER_INTEGRATION_TEST_INSTALL_SCRIPT: undefined, + }, + stdio: ['ignore', 'pipe', 'pipe'], + }); + let stdout = ''; + let stderr = ''; + runner.stdout.on('data', (chunk: Buffer) => (stdout += chunk)); + runner.stderr.on('data', (chunk: Buffer) => (stderr += chunk)); + const [code] = await once(runner, 'close'); + return { code, stdout, stderr }; +} + +test("auto-resolution finds the CLI of a 'harper' dependency whose exports map only '.'", async () => { + const projectDir = writePackage(makeResolutionRoot(), { name: 'consumer' }); + const harperDir = writePackage(join(projectDir, 'node_modules', 'harper'), HARPER_PACKAGE_JSON, BUILT_HARPER_FILES); + + const { code, stdout, stderr } = await runAutoResolution(projectDir); + + strictEqual(code, 0, stderr); + ok( + stdout.includes(`Using Harper at ${join(harperDir, 'dist', 'bin', 'harper.js')} (via 'harper' package)`), + stdout + ); +}); + +test('inside a harper checkout, auto-resolution uses its own build, not node_modules/harper or an outer build', async () => { + // A harper checkout nested in another harper build (harper-pro/core inside harper-pro) that also + // has the registry harper npm auto-installs for this package's peer dependency. + const outerDir = writePackage(makeResolutionRoot(), { name: 'outer' }, ['dist/bin/harper.js']); + const checkoutDir = writePackage(join(outerDir, 'core'), HARPER_PACKAGE_JSON, BUILT_HARPER_FILES); + writePackage(join(checkoutDir, 'node_modules', 'harper'), HARPER_PACKAGE_JSON, BUILT_HARPER_FILES); + + const { code, stdout, stderr } = await runAutoResolution(checkoutDir); + + strictEqual(code, 0, stderr); + ok( + stdout.includes(`Using Harper at ${join(checkoutDir, 'dist', 'bin', 'harper.js')} (via 'harper' package)`), + stdout + ); + doesNotMatch(stderr, /Warning: resolved the 'harper' package/); +}); + +test('resolveHarperPackageScript falls through when the resolved harper is not built', () => { + const projectDir = makeResolutionRoot(); + writePackage(join(projectDir, 'node_modules', 'harper'), HARPER_PACKAGE_JSON); + + strictEqual(resolveHarperPackageScript(projectDir), undefined); +}); + +test('resolveHarperPackageScript reports an installed harper it cannot resolve instead of skipping it', () => { + const projectDir = makeResolutionRoot(); + writePackage(join(projectDir, 'node_modules', 'harper'), { ...HARPER_PACKAGE_JSON, exports: { './cli': './dist/bin/harper.js' } }, BUILT_HARPER_FILES); + + throws( + () => resolveHarperPackageScript(projectDir), + (error: Error) => + /Could not locate the Harper CLI script/.test(error.message) && + (error.cause as NodeJS.ErrnoException).code === 'ERR_PACKAGE_PATH_NOT_EXPORTED' + ); +}); From 11bed5701b9a7d1d84eef178c68c916fbb51cbef Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Wed, 23 Sep 2026 09:18:08 -0600 Subject: [PATCH 2/2] fix: prefer an enclosing Harper build over the resolved harper package With step 3 working, the harper package npm auto-installs for our peer dependency outranked the project's own build wherever Node's self-reference does not apply: harper-pro (named @harperfast/harper-pro) and harper subdirectories with a nameless package.json. On every published harper 5.x the old step 3 never resolved, so the effective order has always been ancestor dist before any package; resolve the ancestor dist first and the only behavior change left is that the "not found" error becomes resolving the harper dependency. The node_modules/local-build ambiguity warning can no longer fire (a local build now wins before the package lookup), so it is removed. The package lookup also reads npm's string form of bin. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0138qUYfQacRsPPqTR73vtbz Dispatch-Task: integration-testing-harper-bin-resolution --- README.md | 4 +-- src/harperLifecycle.ts | 70 +++++++++++++++--------------------- test/harperLifecycle.test.ts | 27 +++++++------- 3 files changed, 46 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index fe87575..494af54 100644 --- a/README.md +++ b/README.md @@ -94,8 +94,8 @@ The Harper binary is resolved in the following order: 1. `harperBinPath` option passed directly to `startHarper()` 2. `HARPER_INTEGRATION_TEST_INSTALL_SCRIPT` environment variable (path to `dist/bin/harper.js`) -3. Auto-resolved from a `harper` package installed as a project dependency — the one `require('harper')` resolves to from the working directory, located via its `bin` field. Inside the harper repo itself this is the local build (package self-reference), not the registry copy npm auto-installs for this package's peer dependency. -4. The nearest `dist/bin/harper.js` in the working directory or its ancestors +3. The nearest `dist/bin/harper.js` in the working directory or its ancestors — the Harper build (harper, harper-pro) your tests run inside, which takes precedence over the `harper` package npm auto-installs for this package's peer dependency +4. A `harper` package installed as a project dependency: the one `require('harper')` resolves to from the working directory, located through its `bin` field **Options:** diff --git a/src/harperLifecycle.ts b/src/harperLifecycle.ts index a09487b..f8bff2d 100644 --- a/src/harperLifecycle.ts +++ b/src/harperLifecycle.ts @@ -143,8 +143,8 @@ export interface StartHarperOptions { * If not provided, resolution order is: * 1. This option * 2. HARPER_INTEGRATION_TEST_INSTALL_SCRIPT environment variable - * 3. Auto-resolved from the 'harper' package the project (cwd) resolves - * 4. Auto-resolved from current directory or ancestors ('dist/bin/harper.js') + * 3. Auto-resolved from current directory or ancestors ('dist/bin/harper.js') + * 4. Auto-resolved from the 'harper' package the project (cwd) resolves */ harperBinPath?: string; } @@ -214,8 +214,11 @@ export interface ContextWithHarper extends SuiteContext, TestContext { * Resolution order: * 1. `harperBinPath` argument * 2. `HARPER_INTEGRATION_TEST_INSTALL_SCRIPT` environment variable - * 3. Auto-resolved from the 'harper' package the project (cwd) resolves, via its `bin` field - * 4. Auto-resolved from current directory or ancestors ('dist/bin/harper.js') + * 3. Auto-resolved from current directory or ancestors ('dist/bin/harper.js') + * 4. Auto-resolved from the 'harper' package the project (cwd) resolves, via its `bin` field + * + * A Harper build enclosing the working directory (harper, harper-pro) is the product under test, + * so it precedes the `harper` package, which npm auto-installs there to satisfy our peer dependency. * * @returns The absolute path to the Harper CLI entry script * @throws {AssertionError} If the script cannot be found @@ -237,18 +240,26 @@ function getHarperScript(harperBinPath?: string): string { return logResolvedHarperScript(envPath, 'HARPER_INTEGRATION_TEST_INSTALL_SCRIPT'); } - // 3. Auto-resolve from the project's 'harper' package + // 3. Auto-resolve from current directory or ancestors + let currentDir = process.cwd(); + while (true) { + const potentialPath = join(currentDir, 'dist/bin/harper.js'); + if (existsSync(potentialPath)) { + return logResolvedHarperScript(potentialPath, 'ancestor dist'); + } + const parentDir = dirname(currentDir); + if (parentDir === currentDir) { + break; + } + currentDir = parentDir; + } + + // 4. Auto-resolve from the project's 'harper' dependency const packageScript = resolveHarperPackageScript(process.cwd()); if (packageScript && existsSync(packageScript)) { return logResolvedHarperScript(packageScript, "'harper' package"); } - // 4. Auto-resolve from current directory or ancestors - const ancestorScript = findAncestorDistScript(); - if (ancestorScript) { - return logResolvedHarperScript(ancestorScript, 'ancestor dist'); - } - throw new Error( `Harper CLI script not found. Provide the path via:\n` + ` - harperBinPath option: startHarper(ctx, { harperBinPath: '/path/to/dist/bin/harper.js' })\n` + @@ -261,11 +272,8 @@ function getHarperScript(harperBinPath?: string): string { /** * Locates the CLI script of the `harper` package that `require('harper')` resolves to from * `projectDir`, via the package's `bin` field: harper's `exports` map exposes only `"."`, so - * `dist/bin/harper.js` cannot be resolved as a subpath. - * - * Resolving from the project rather than from this module matters inside the harper repo itself: - * package self-reference yields its local build, whereas this module's location would find the - * registry `harper` that npm auto-installs to satisfy our peer dependency. + * `dist/bin/harper.js` cannot be resolved as a subpath. Resolved from the project rather than from + * this module, so it is the project's dependency even when this package is linked from elsewhere. * * Returns `undefined` when no `harper` is resolvable. Exported for tests, not from `index.ts`. */ @@ -277,7 +285,10 @@ export function resolveHarperPackageScript(projectDir: string): string | undefin const packageJsonPath = join(packageDir, 'package.json'); if (existsSync(packageJsonPath)) { const { name, bin } = JSON.parse(readFileSync(packageJsonPath, 'utf8')); - if (name === 'harper') return join(packageDir, bin.harper); + if (name === 'harper') { + const binPath = typeof bin === 'string' ? bin : bin?.harper; + return binPath ? join(packageDir, binPath) : undefined; + } } const parentDir = dirname(packageDir); if (parentDir === packageDir) return undefined; @@ -293,18 +304,6 @@ export function resolveHarperPackageScript(projectDir: string): string | undefin } } -/** Nearest `dist/bin/harper.js` in the current directory or its ancestors. */ -function findAncestorDistScript(): string | undefined { - let currentDir = process.cwd(); - while (true) { - const potentialPath = join(currentDir, 'dist/bin/harper.js'); - if (existsSync(potentialPath)) return potentialPath; - const parentDir = dirname(currentDir); - if (parentDir === currentDir) return undefined; - currentDir = parentDir; - } -} - const loggedHarperScripts = new Set(); /** @@ -313,23 +312,12 @@ const loggedHarperScripts = new Set(); * Which binary runs is the single most consequential (and previously invisible) * decision the harness makes: resolving a stale `harper` package from * node_modules while the caller expected their local build produces confusing - * wholesale test failures with no hint of the cause. When the `harper` package - * wins over a different local `dist/bin/harper.js` that step 4 would have - * picked, warn with the override. + * wholesale test failures with no hint of the cause. */ function logResolvedHarperScript(scriptPath: string, source: string): string { if (loggedHarperScripts.has(scriptPath)) return scriptPath; loggedHarperScripts.add(scriptPath); console.log(`[integration-testing] Using Harper at ${scriptPath} (via ${source})`); - if (source === "'harper' package") { - const localDist = findAncestorDistScript(); - if (localDist && localDist !== scriptPath) { - console.warn( - `[integration-testing] Warning: resolved the 'harper' package, but ${localDist} also exists. ` + - `If you meant to test the local build, set HARPER_INTEGRATION_TEST_INSTALL_SCRIPT=${localDist}.` - ); - } - } return scriptPath; } diff --git a/test/harperLifecycle.test.ts b/test/harperLifecycle.test.ts index a29e395..74335f5 100644 --- a/test/harperLifecycle.test.ts +++ b/test/harperLifecycle.test.ts @@ -1148,21 +1148,17 @@ test("auto-resolution finds the CLI of a 'harper' dependency whose exports map o ); }); -test('inside a harper checkout, auto-resolution uses its own build, not node_modules/harper or an outer build', async () => { - // A harper checkout nested in another harper build (harper-pro/core inside harper-pro) that also - // has the registry harper npm auto-installs for this package's peer dependency. - const outerDir = writePackage(makeResolutionRoot(), { name: 'outer' }, ['dist/bin/harper.js']); - const checkoutDir = writePackage(join(outerDir, 'core'), HARPER_PACKAGE_JSON, BUILT_HARPER_FILES); - writePackage(join(checkoutDir, 'node_modules', 'harper'), HARPER_PACKAGE_JSON, BUILT_HARPER_FILES); +test('a Harper build enclosing the working directory wins over node_modules/harper', async () => { + // harper-pro's shape: its own build, the registry harper npm auto-installs for this package's peer + // dependency, and tests run from a subdirectory whose package.json only sets the module type. + const productDir = writePackage(makeResolutionRoot(), { name: '@harperfast/harper-pro', bin: { harper: 'dist/bin/harper.js' } }, ['dist/bin/harper.js']); + writePackage(join(productDir, 'node_modules', 'harper'), HARPER_PACKAGE_JSON, BUILT_HARPER_FILES); + const testsDir = writePackage(join(productDir, 'integrationTests'), { type: 'module' }); - const { code, stdout, stderr } = await runAutoResolution(checkoutDir); + const { code, stdout, stderr } = await runAutoResolution(testsDir); strictEqual(code, 0, stderr); - ok( - stdout.includes(`Using Harper at ${join(checkoutDir, 'dist', 'bin', 'harper.js')} (via 'harper' package)`), - stdout - ); - doesNotMatch(stderr, /Warning: resolved the 'harper' package/); + ok(stdout.includes(`Using Harper at ${join(productDir, 'dist', 'bin', 'harper.js')} (via ancestor dist)`), stdout); }); test('resolveHarperPackageScript falls through when the resolved harper is not built', () => { @@ -1172,6 +1168,13 @@ test('resolveHarperPackageScript falls through when the resolved harper is not b strictEqual(resolveHarperPackageScript(projectDir), undefined); }); +test("resolveHarperPackageScript reads npm's string form of bin", () => { + const projectDir = makeResolutionRoot(); + const harperDir = writePackage(join(projectDir, 'node_modules', 'harper'), { ...HARPER_PACKAGE_JSON, bin: 'dist/bin/harper.js' }, BUILT_HARPER_FILES); + + strictEqual(resolveHarperPackageScript(projectDir), join(harperDir, 'dist', 'bin', 'harper.js')); +}); + test('resolveHarperPackageScript reports an installed harper it cannot resolve instead of skipping it', () => { const projectDir = makeResolutionRoot(); writePackage(join(projectDir, 'node_modules', 'harper'), { ...HARPER_PACKAGE_JSON, exports: { './cli': './dist/bin/harper.js' } }, BUILT_HARPER_FILES);