Skip to content

Commit 8cdb1f3

Browse files
sjsyrekclaude
andauthored
test(harness): run deepl invocations against the tree's build, not a global install (#105)
23 suites shell out to the bare `deepl` command, which resolved from PATH: absent a global install they all failed with "command not found", and present one they silently tested the installed version instead of the working tree. A jest globalSetup now prepends a shim directory to PATH that execs dist/cli/index.js before workers spawn, so every CLI subprocess inherits it. CI's now-redundant `npm link` step is removed, and a unit suite pins that `deepl` resolves to the shim and reports the tree's version. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent f827117 commit 8cdb1f3

6 files changed

Lines changed: 70 additions & 2 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,4 @@ jobs:
2727
- run: npm run lint
2828
- run: npm run type-check
2929
- run: npm run build
30-
- name: Link CLI for E2E tests
31-
run: npm link
3230
- run: npm test

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515

1616
- **docs**: All install instructions now target `@deepl/cli` (10 occurrences across `docs/SYNC.md`, four example scripts, `examples/README.md`, and the git-hook template in `src/services/git-hooks.ts`). The README installation section documents three install paths — Homebrew (`brew install deepl/tap/deepl`, which manages Node itself), npm (`npm install -g @deepl/cli`), and from source — with an explicit Node.js 24 prerequisite, replacing the `better-sqlite3` native-compilation caveat (Xcode CLT / python3-make-gcc), which no longer applies. The `TROUBLESHOOTING.md` `NODE_MODULE_VERSION` / `npm rebuild better-sqlite3` entry is replaced by accurate guidance for the one remaining cache-degradation cause (running on Node < 24). Six stale `DeepLcom` GitHub URLs now point at the `DeepL` org directly. `CONTRIBUTING.md` states the Node 24 development prerequisite, and `SECURITY.md`'s supported-versions table reflects that only 2.x is a published, supported line.
1717
- **tests**: Removed the global manual mocks for `p-limit` and `fast-glob` (`tests/__mocks__/`). Because a manual mock for a node module is auto-applied to every suite, and `resetMocks: true` strips its implementation, `pLimit(n)` resolved to `undefined` and `fg(...)` resolved to `undefined` in all 236 suites — so no test exercised a real concurrency limit or a real glob walk, and two concurrency defects reached a release candidate undetected. The suites that need these mocked declare them explicitly with their own implementations, so nothing depended on the global versions. Added `tests/unit/concurrency-limiting.test.ts`, which asserts that `p-limit` rejects a non-positive concurrency, that peak overlap never exceeds the limit, and that `fast-glob` returns real paths — it fails if either global mock is reintroduced.
18+
- **tests**: Suites that shell out to the bare `deepl` command now run this tree's built CLI via a PATH shim installed in jest `globalSetup`, instead of whatever `deepl` happens to be globally installed. 23 suites (392 tests) previously required a global install — absent one they all failed with `command not found`, and present one they silently tested the installed version rather than the working tree. The shim lives in the real environment before jest workers spawn (a `setupFilesAfterEnv` hook cannot do this: test code sees a copied `process.env` that child processes never inherit). CI's now-redundant `npm link` step is removed, and a new unit suite pins that `deepl` resolves to the shim and reports the tree's version.
1819
- **tests**: `npm test` now fails fast with an actionable message when `dist/cli/index.js` is missing. `dist/` is gitignored and `npm test` does not build, yet all three test tiers execute the built CLI, so a fresh checkout previously failed hundreds of tests with errors that never mentioned the missing build.
1920
- **tests**: Jest's haste map no longer indexes `.claude/` (via `modulePathIgnorePatterns`). A leftover agent worktree under `.claude/worktrees/` duplicated the manual mocks in `tests/__mocks__/` and made every jest run emit a `jest-haste-map: duplicate manual mock found` warning; test files in worktrees were already excluded, but module indexing was not. Verified warning-free with a worktree present.
2021
- **BREAKING — package**: The package is now published as the scoped **`@deepl/cli`** (previously the unpublished working name `deepl-cli`). Scoped packages default to restricted visibility, so `publishConfig.access: "public"` is set explicitly — without it the publish fails. The `bin` name is unchanged: the command is still `deepl`, and scoping changes only the install string (`npm install -g @deepl/cli`). Repository, bugs, and homepage metadata now point at `github.com/DeepL/deepl-cli` directly instead of relying on the redirect from the legacy `DeepLcom` org name.

‎jest.config.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export default {
8282
],
8383

8484
// Setup files
85+
globalSetup: '<rootDir>/tests/global-setup.ts',
8586
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
8687

8788
// Test timeout

‎tests/global-setup.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import installHermeticDeepl from './hermetic-deepl';
2+
3+
/**
4+
* Runs in the main jest process before workers spawn, so the shim's PATH entry
5+
* lands in the real environment every worker (and every CLI subprocess the
6+
* tests spawn) inherits. A setupFilesAfterEnv hook cannot do this: test code
7+
* sees a copied process.env, and mutations there never reach child processes
8+
* spawned without an explicit env.
9+
*/
10+
export default function globalSetup(): void {
11+
installHermeticDeepl();
12+
}

‎tests/hermetic-deepl.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Puts a `deepl` shim on PATH that execs this repo's built CLI, so tests that
3+
* shell out to the bare `deepl` command always run dist/cli/index.js — never a
4+
* globally installed copy, which may be absent (failing every such test with
5+
* "command not found") or a different version than the tree under test.
6+
*
7+
* The shim directory is created once per jest worker process and advertised
8+
* through DEEPL_CLI_TEST_SHIM so subsequent suites in the same worker reuse it.
9+
*/
10+
11+
import * as fs from 'fs';
12+
import * as os from 'os';
13+
import * as path from 'path';
14+
15+
const CLI_ENTRY = path.join(process.cwd(), 'dist', 'cli', 'index.js');
16+
17+
export default function installHermeticDeepl(): void {
18+
const existing = process.env['DEEPL_CLI_TEST_SHIM'];
19+
if (existing && fs.existsSync(path.join(existing, 'deepl'))) return;
20+
21+
const shimDir = fs.mkdtempSync(path.join(os.tmpdir(), 'deepl-cli-shim-'));
22+
fs.writeFileSync(path.join(shimDir, 'deepl'), `#!/bin/sh\nexec node "${CLI_ENTRY}" "$@"\n`, {
23+
mode: 0o755,
24+
});
25+
process.env['DEEPL_CLI_TEST_SHIM'] = shimDir;
26+
process.env['PATH'] = `${shimDir}${path.delimiter}${process.env['PATH'] ?? ''}`;
27+
}

‎tests/unit/hermetic-deepl.test.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* Tests that the bare `deepl` command resolves to the test shim and executes
3+
* this tree's built CLI, so suites that shell out to `deepl` are independent
4+
* of any globally installed copy.
5+
*/
6+
7+
import { execSync } from 'child_process';
8+
import * as fs from 'fs';
9+
import * as path from 'path';
10+
11+
describe('hermetic deepl shim', () => {
12+
it('resolves `deepl` to the shim directory, ahead of any global install', () => {
13+
const resolved = execSync('command -v deepl', {
14+
encoding: 'utf-8',
15+
shell: '/bin/sh',
16+
}).trim();
17+
18+
expect(resolved).toBe(path.join(process.env['DEEPL_CLI_TEST_SHIM']!, 'deepl'));
19+
});
20+
21+
it('executes the built CLI from this tree', () => {
22+
const version = execSync('deepl --version', { encoding: 'utf-8' }).trim();
23+
const pkg = JSON.parse(
24+
fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf-8'),
25+
) as { version: string };
26+
27+
expect(version).toBe(pkg.version);
28+
});
29+
});

0 commit comments

Comments
 (0)