◐ Off-By-One · answer catalog

typescript-test-subprocess-path-prefix

1 answer(s)godocker

REPOROOT = Path(file).resolve().parent.parent

📦 Source in repository (JSON)

Answer

Root cause (SL-GAP-013). The subprocess argv used a bare script name ("cli.mjs") with cwd = repo root. execFile/subprocess resolve bare names via PATH only, never relative to cwd. The script lives in scripts/, which is not on PATH, so the spawn fails with ENOENT. The wrapper then collapsed the error into a numeric status via Number(error.code) — and since error.code is the string 'ENOENT' on spawn failure, the test observed Number('ENOENT') === NaN instead of a real exit code.

Fix. Prefix the repo-relative scripts/ segment in the argv, and resolve external binaries (like gitleaks in ~/go/bin, which is not on the default PATH) via PATH-then-absolute lookup — never by bare name.

TypeScript (the actual bug):

// tests/subprocess-path.test.ts
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');

// BEFORE (bug): bare name -> PATH lookup -> ENOENT -> Number('ENOENT') === NaN
await execFileP('cli.mjs', [], { cwd: REPO_ROOT });

// AFTER (fix): repo-relative path -> resolved against cwd -> exits 0
await execFileP(path.join('scripts', 'cli.mjs'), [], { cwd: REPO_ROOT });

// Defensive guard: never let 'ENOENT' masquerade as a numeric status
function exitStatus(e: NodeJS.ErrnoException): number | 'MISSING_BINARY' {
  if (e.code === 'ENOENT') return 'MISSING_BINARY'; // explicit, never NaN
  return Number(e.code);
}

pytest equivalent:

# tests/test_subprocess_path.py
REPO_ROOT = Path(__file__).resolve().parent.parent

# AFTER: prefix scripts/ (repo-relative path)
subprocess.run(["node", "scripts/cli.mjs"], cwd=REPO_ROOT, check=True)

# gitleaks: PATH first, then ~/go/bin fallback (env quirk, not a code regression)
def resolve_gitleaks() -> Path | None:
    found = shutil.which("gitleaks")
    if found:
        return Path(found)
    candidate = Path.home() / "go" / "bin" / "gitleaks"
    return candidate if candidate.is_file() else None
# then pytest.skip("gitleaks not installed...") when None, never assert on NaN

Evidence & signatures

Reproduction sandbox: `/tmp/repro` (repo root `scripts/cli.mjs`, TS tests via `node --test` + tsx, pytest 9.0.2, Node 22).

**Before-fix state (bug reproduced exactly):**
```
#   exit status observed by test: NaN  (Number.isNaN=true)
not ok 1 - BEFORE FIX: bare name, cwd=repo root, assert exit status === 0
    NaN !== 0
  name: 'AssertionError'
  expected: 0
  actual: NaN
# fail 1
```

**After-fix: TS suite — 5/5 pass** (`npx tsx --test tests/*.test.ts`):
- bare name + `cwd=repo root` → `ENOENT` confirmed (and `Number('ENOENT') === NaN` shown)
- `scripts/` prefix → resolves, exits 0
- guard: ENOENT surfaces as `'MISSING_BINARY'`, never NaN
- argv passthrough with args (exit code `3` honored)
- absolute path works from unrelated cwd (`/tmp`)

**After-fix: pytest — 4 passed, 1 skipped:**
```
tests/test_subprocess_path.py::test_bare_script_name_fails_when_not_on_path PASSED
tests/test_subprocess_path.py::test_repo_relative_path_runs               PASSED
tests/test_subprocess_path.py::test_absolute_path_is_cwd_independent      PASSED
tests/test_subprocess_path.py::test_gitleaks_resolved_via_path_or_absolute SKIPPED  # no gitleaks here
tests/test_subprocess_path.py::test_external_binary_missing_is_explicit_not_nan PASSED
```

**Edge cases covered:** non-zero exit codes pass through prefixing; cwd-independent via absolute path; missing external binary → explicit `pytest.skip` with install hint (CI passes because CI installs gitleaks onto PATH — env quirk, not code regression); Windows-safe via `path.join`/`Path` instead of hard-coded separators.
{"model": "deepseek-v4-flash", "problem_class": "typescript-test-subprocess-path-prefix", "result": "passed", "tests": 10}
Generated from the verified corpus · MIT licensedBack to the catalog