CLAUDE.md@tests/harness · git:20261003.6c58192 · 2026-10-03 · sha256 10915f1bcfa1a962

CLAUDE.md@tests/harness git:20261003.6c58192A

Immutable. This exact content never changes and is served at /api/v1/blob/10915f1bcfa1a962.

# tests/harness/ - the shared helpers behind every script test

Plain `.ts` modules imported by the test files, never run on their own. Each helper that changes
behaviour keeps (or gains) its one assertion in `tests/harness.test.ts`.

## Invariants

- No harness module depends on a plugin's own code: `harness.test.ts` decodes `writePng` output
  itself for that reason. Imports run one way only: `tmp.ts` uses `run.ts`, `stub.ts` uses
  `tmp.ts`; `run.ts` and `test.ts` import no other helper.
- Nothing a case calls blocks the event loop: `runScript` spawns asynchronously, and the fixture
  wrappers (`withTempDir`, `withGitRepo`, `withStub`, `forEachShell`) await their callback, so the
  concurrent cases `test.ts` schedules really overlap. Only once-per-process probes stay
  synchronous (`bash --version`, `icacls`).
- Process creation is the suite's dominant cost, above all on Windows. A helper called once per
  case spawns nothing it can avoid: `shells.ts` memoises PATH scans and `bash --version` probes
  per PATH value and, outside CI, stops at the first working bash; `withGitRepo` copies a
  template repo `git init`ed once per process and writes its global gitconfig as a file instead
  of calling `git config`; `canDenyRead`/`canSymlinkDir` probe once per process.
- The full shell matrix is CI-only: `FULL_SHELL_MATRIX` (`CI` = `true`/`1`) is the one switch, and
  a local `forEachShell` runs the first shell present and reports the rest as skips.
- A capability helper answers `false` (a skip), never throws, and verifies by doing the real thing:
  `denyRead` re-reads the file and restores it when the read still works (root, FAT, a dropped
  ACE). `restoreRead` never throws, since it runs in a failing test's `finally`.
- `BASE_ENV_KEYS` in `run.ts` stays minimal: a key added there reaches every script under test,
  and a test's `opts.env` stops being the only source of script configuration.
- `runScript` reports `status: null` for a signal kill (the timeout included) and puts the spawn
  error message in `stderr` when the child never started.

## Windows mechanics

- CreateProcess ignores a shebang and only appends `.exe`, so on win32 `runScript` looks a bare
  command up in `opts.stubDirs` first, then invokes the file's shebang interpreter by name.
  `withStub` writes an extensionless `#!/bin/sh` stub (its body must be POSIX sh) plus a
  `name.cmd` shim calling it through `bash`.
- The `P2P2_ARGV<n>` preamble passes the shell and script paths slash-separated: dash resolves an
  `exec` target through execve and rejects `C:\...`. Its prefix tokens (`--posix`, `sh`) are the
  harness's own, never test data.
- A resolved `Shell` is an argv (`[bash, "--posix"]`, `[busybox, "sh"]`), never a generated
  wrapper script: an extensionless shim is not spawnable on Windows, where `bash --posix` is the
  only POSIX shell. `bash --posix` uses the first bash `bashShells()` finds; `/bin/bash` is always
  a candidate beside PATH.
- `perms.ts` spawns `icacls` directly, never through a shell (Git-Bash would rewrite `/deny` into a
  path), naming the account as `USERDOMAIN\USERNAME`; no `USERNAME` means incapable.