# system.bash venv activation — before/after comparison

## Problem

`_handle_system_bash` sanitizes the subprocess environment to `SAFE_ENV_KEYS` but
never prepends the project's virtual environment `bin/` to `PATH`. This means
`pip`, `python`, and any venv-installed CLI tools are not found (or resolve to
system Python, installing packages in the wrong place).

## Root cause

- `PATH` is inherited from the runner process, which doesn't include the
  project venv's `bin/`.
- `VIRTUAL_ENV` is passed through via `SAFE_ENV_KEYS` but it's informational
  only — it does not affect binary resolution.
- macOS doesn't ship `pip` in `/usr/bin`, so `pip` is simply not found.

## Fix

`_resolve_project_python_bin()` in `message_handler.py`:
1. Checks active kernel sessions for a `python_path` (O(1), in memory).
2. Falls back to `env_manager.detect_environments()` (cached, 60s TTL) for
   venv/poetry/pipenv/conda environments.
3. Prepends the resolved `bin/` directory to `PATH` and sets `VIRTUAL_ENV`.

---

## pip / pip3 / python -m pip — installing skop-runner

### Before fix — with .venv in project

```
pip install skop-runner          → exit=127  /bin/sh: pip: command not found
pip3 install skop-runner         → exit=1    error: externally-managed-environment (PEP 668, system pip3 refuses)
python -m pip install skop-runner → exit=1   No module named pip (runner's own Python has no pip)
```

All 3 variants fail. `pip` doesn't exist on PATH. `pip3` finds the system one
which is blocked by PEP 668. `python -m pip` uses the runner's own Python
which has no pip module.

### Before fix — no venv in project

```
pip install skop-runner          → exit=127  /bin/sh: pip: command not found
pip3 install skop-runner         → exit=1    error: externally-managed-environment
python -m pip install skop-runner → exit=1   No module named pip
```

Identical to "with venv" — the venv is never activated so it makes no
difference whether it exists.

### After fix — with .venv in project

```
pip install skop-runner          → exit=0  Successfully installed skop-runner-0.1.6 (+ 70 deps)
  → which skop-runner            → exit=0  <project>/.venv/bin/skop-runner
  → skop-runner --help           → exit=0  Usage: skop-runner [OPTIONS] COMMAND [ARGS]...
  → skop_runner.__file__         → <project>/.venv/lib/python3.12/site-packages/skop_runner/__init__.py

pip3 install skop-runner         → exit=0  Successfully installed skop-runner-0.1.6
  → which skop-runner            → exit=0  <project>/.venv/bin/skop-runner
  → skop-runner --help           → exit=0  Usage: skop-runner [OPTIONS] COMMAND [ARGS]...
  → skop_runner.__file__         → <project>/.venv/lib/python3.12/site-packages/skop_runner/__init__.py

python -m pip install skop-runner → exit=0 Successfully installed skop-runner-0.1.6
  → which skop-runner             → exit=0 <project>/.venv/bin/skop-runner
  → skop-runner --help            → exit=0 Usage: skop-runner [OPTIONS] COMMAND [ARGS]...
  → skop_runner.__file__          → <project>/.venv/lib/python3.12/site-packages/skop_runner/__init__.py
```

All 3 variants succeed. Package, CLI entry point, and importable module all
land in the project's `.venv`.

### After fix — no venv in project

No project venv detected, so no activation. Commands fall through to whatever
is on the runner's inherited PATH (conda base in this case).

```
pip install skop-runner          → exit=0  installed in conda base (system fallback)
pip3 install skop-runner         → exit=0  installed in conda base
python -m pip install skop-runner → exit=0 installed in conda base
```

### pip summary matrix

**Before fix**: 0/6 succeed

| Command | With .venv | No venv |
|---------|-----------|---------|
| `pip install skop-runner` | exit=127, pip not found | exit=127, pip not found |
| `pip3 install skop-runner` | exit=1, PEP 668 blocked | exit=1, PEP 668 blocked |
| `python -m pip install skop-runner` | exit=1, No module named pip | exit=1, No module named pip |

**After fix**: 6/6 succeed

| Command | With .venv | No venv |
|---------|-----------|---------|
| `pip install skop-runner` | exit=0, project .venv | exit=0, conda base |
| `pip3 install skop-runner` | exit=0, project .venv | exit=0, conda base |
| `python -m pip install skop-runner` | exit=0, project .venv | exit=0, conda base |

---

## uv add / uv pip install / uv run pip install — installing skop-runner

`uv` is found at `/Users/.../.local/bin/uv` on the inherited PATH in all cases.

Tested across 4 project configurations:
1. `.venv` + `pyproject.toml`
2. `.venv` only (no `pyproject.toml`)
3. No `.venv`, no `pyproject.toml`
4. `pyproject.toml` only (no `.venv`)

### Before fix

#### With .venv + pyproject.toml

```
uv add skop-runner               → exit=0  Installed 92 packages
  → which skop-runner            → exit=0  /Users/.../.local/bin/skop-runner       ← NOT project venv
  → skop_runner.__file__         → ModuleNotFoundError                             ← python can't find it

uv pip install skop-runner       → exit=0  Installed
  → which skop-runner            → exit=0  /Users/.../skoplabs/runner/.venv/bin/skop-runner  ← RUNNER's venv, not project
  → skop_runner.__file__         → /Users/.../skoplabs/runner/.venv/lib/.../skop_runner/__init__.py  ← RUNNER's venv
```

`uv pip install` silently contaminated the runner's own .venv because
`VIRTUAL_ENV` pointed there (leaked from the runner process).

#### With .venv only (no pyproject.toml)

```
uv add skop-runner               → exit=2  error: No pyproject.toml found

uv pip install skop-runner       → exit=0  Installed
  → which skop-runner            → exit=0  /Users/.../skoplabs/runner/.venv/bin/skop-runner  ← RUNNER's venv
  → skop_runner.__file__         → /Users/.../skoplabs/runner/.venv/lib/.../skop_runner/__init__.py  ← RUNNER's venv
```

#### No venv, no pyproject.toml

```
uv add skop-runner               → exit=2  error: No pyproject.toml found

uv pip install skop-runner       → exit=0  Installed
  → which skop-runner            → exit=0  /Users/.../skoplabs/runner/.venv/bin/skop-runner  ← RUNNER's venv
  → skop_runner.__file__         → /Users/.../skoplabs/runner/.venv/lib/.../skop_runner/__init__.py  ← RUNNER's venv
```

#### pyproject.toml only (no .venv)

```
uv add skop-runner               → exit=0  Installed 92 packages (uv auto-creates .venv)
  → which skop-runner            → exit=0  /Users/.../.local/bin/skop-runner       ← NOT project venv
  → skop_runner.__file__         → ModuleNotFoundError                             ← python can't find it

uv pip install skop-runner       → exit=0  Installed
  → which skop-runner            → exit=0  /Users/.../skoplabs/runner/.venv/bin/skop-runner  ← RUNNER's venv
  → skop_runner.__file__         → /Users/.../skoplabs/runner/.venv/lib/.../skop_runner/__init__.py  ← RUNNER's venv
```

### After fix

#### With .venv + pyproject.toml

```
uv add skop-runner               → exit=0  Installed 92 packages
  → which skop-runner            → exit=0  <project>/.venv/bin/skop-runner         ← project venv
  → skop-runner --help           → exit=0  Usage: skop-runner [OPTIONS] COMMAND [ARGS]...
  → skop_runner.__file__         → <project>/.venv/lib/python3.12/site-packages/skop_runner/__init__.py

uv pip install skop-runner       → exit=0  Installed
  → which skop-runner            → exit=0  <project>/.venv/bin/skop-runner         ← project venv
  → skop-runner --help           → exit=0  Usage: skop-runner [OPTIONS] COMMAND [ARGS]...
  → skop_runner.__file__         → <project>/.venv/lib/python3.12/site-packages/skop_runner/__init__.py

uv run pip install skop-runner   → exit=0  Installed
  → (same as above, project venv)
```

All 3 variants install into the project's `.venv` and the CLI works.

#### With .venv only (no pyproject.toml)

```
uv add skop-runner               → exit=2  error: No pyproject.toml found

uv pip install skop-runner       → exit=0  Installed
  → which skop-runner            → exit=0  <project>/.venv/bin/skop-runner         ← project venv
  → skop-runner --help           → exit=0  Usage: skop-runner [OPTIONS] COMMAND [ARGS]...
  → skop_runner.__file__         → <project>/.venv/lib/python3.12/site-packages/skop_runner/__init__.py
```

#### No venv, no pyproject.toml

```
uv add skop-runner               → exit=2  error: No pyproject.toml found

uv pip install skop-runner       → exit=2  error: No virtual environment found    ← correct refusal

uv run pip install skop-runner   → exit=0  installed in conda base (system fallback)
```

#### pyproject.toml only (no .venv)

```
uv add skop-runner               → exit=0  Installed (uv auto-creates .venv)
  → which skop-runner            → exit=0  /Users/.../skoplabs/runner/.venv/bin/skop-runner  ← runner's venv (python not updated)
  → skop_runner.__file__         → ModuleNotFoundError (python resolves to runner's, not uv's auto-created venv)

uv pip install skop-runner       → exit=0  Installed
  → (same issue — uv uses auto-created venv but python still points elsewhere)
```

Note: when `uv add` auto-creates a `.venv`, `python` on PATH doesn't
automatically point to it. This is a `uv` behavior, not a bug in our fix.

### uv summary matrix

**Before fix**:

| Command | .venv + pyproject | .venv only | No venv | pyproject only |
|---------|-------------------|------------|---------|----------------|
| `uv add skop-runner` | exit=0, but `python` can't import (wrong PATH) | exit=2 | exit=2 | exit=0, same import issue |
| `uv pip install skop-runner` | exit=0, **runner's .venv** (wrong target) | exit=0, **runner's .venv** | exit=0, **runner's .venv** | exit=0, **runner's .venv** |
| `uv run pip install skop-runner` | exit=0, project .venv | exit=2 | exit=2 | exit=2 |

Critical bug: `uv pip install` contaminated the runner's own .venv in all 4 cases.

**After fix**:

| Command | .venv + pyproject | .venv only | No venv | pyproject only |
|---------|-------------------|------------|---------|----------------|
| `uv add skop-runner` | exit=0, **project .venv** | exit=2 | exit=2 | exit=0, python mismatch* |
| `uv pip install skop-runner` | exit=0, **project .venv** | exit=0, **project .venv** | exit=2 (correct refusal) | exit=0, python mismatch* |
| `uv run pip install skop-runner` | exit=0, **project .venv** | exit=0, project .venv | exit=0, conda base | exit=0, conda base |

\* When no pre-existing `.venv` exists and `uv` auto-creates one, `python` on
PATH doesn't point to it. This is expected uv behavior (not our bug).

---

## uv tool install / pipx install / bare pip install (global CLI)

These tests run outside of `system.bash` — they test the **user's own terminal**
installing `skop-runner` as a global CLI tool. This is the recommended install
path documented in the runner README.

### uv tool install skop-runner

```
$ uv tool install skop-runner
→ Resolved 93 packages in 974ms
→ Installed 93 packages in 141ms
→ Installed 1 executable: skop-runner

$ which skop-runner
→ /Users/.../.local/bin/skop-runner
  (symlink → /Users/.../.local/share/uv/tools/skop-runner/bin/skop-runner)

$ skop-runner --help
→ exit=0  Usage: skop-runner [OPTIONS] COMMAND [ARGS]...

$ python3 -c "import skop_runner"
→ ModuleNotFoundError (GOOD — isolated from current python)

$ uv tool uninstall skop-runner
→ Uninstalled 1 executable: skop-runner

$ which skop-runner
→ (not found — clean uninstall)
```

Installed in its own isolated venv at `~/.local/share/uv/tools/skop-runner/`.
Only the `skop-runner` CLI binary is symlinked to `~/.local/bin/`.
Dependencies never touch the user's project environments.

### pipx install skop-runner

```
$ pipx install skop-runner
→ installed package skop-runner 0.1.6, installed using Python 3.14.3
→ These apps are now available: skop-runner

$ which skop-runner
→ /Users/.../.local/bin/skop-runner
  (symlink → /Users/.../.local/pipx/venvs/skop-runner/bin/skop-runner)

$ skop-runner --help
→ exit=0  Usage: skop-runner [OPTIONS] COMMAND [ARGS]...

$ python3 -c "import skop_runner"
→ ModuleNotFoundError (GOOD — isolated from current python)

$ pipx uninstall skop-runner
→ uninstalled skop-runner!

$ which skop-runner
→ (not found — clean uninstall)
```

Installed in its own isolated venv at `~/.local/pipx/venvs/skop-runner/`.
Same isolation as `uv tool`.

### Bare pip install (no venv — user's terminal)

```
$ pip install skop-runner
→ command not found (macOS has no system pip)

$ pip3 install skop-runner
→ error: externally-managed-environment (PEP 668)
→ hint: See PEP 668 for the detailed specification.

$ python3 -m pip install skop-runner
→ error: externally-managed-environment (PEP 668)
```

All 3 variants fail on modern macOS. System Python is "externally managed"
and refuses global pip installs. This is why the README recommends
`uv tool` or `pipx` instead.

### Global install summary matrix

| Method | Installs? | Isolated? | CLI works? | Clean uninstall? |
|--------|-----------|-----------|------------|------------------|
| `uv tool install skop-runner` | exit=0 | Yes (own venv in `~/.local/share/uv/tools/`) | Yes | Yes |
| `pipx install skop-runner` | exit=0 | Yes (own venv in `~/.local/pipx/venvs/`) | Yes | Yes |
| `pip install skop-runner` | exit=127, not found | N/A | N/A | N/A |
| `pip3 install skop-runner` | exit=1, PEP 668 | N/A | N/A | N/A |
| `python3 -m pip install skop-runner` | exit=1, PEP 668 | N/A | N/A | N/A |

---

## Test methodology

All runs used the same approach:
1. Create a temp directory with the specified configuration
   (`.venv` via `python -m venv`, `pyproject.toml`, both, or neither).
2. Instantiate `MessageHandler` with `MockKernelBackend` (kernel is not
   involved in bash execution) and `FileManager` rooted at the temp dir.
3. Send `system.bash` JSON-RPC requests through `handle_message()`.
4. For each successful install, verify: `which skop-runner`,
   `skop-runner --help`, and `import skop_runner; print(skop_runner.__file__)`.

The "before" was tested by stashing the fix (`git stash`), running the script,
then restoring with `git stash pop`.

## Automated tests

See `test_system_bash.py` — 6 new tests added:

| Test | What it verifies |
|------|------------------|
| `test_pip_resolves_to_venv` | `which pip` returns the project venv's pip |
| `test_python_resolves_to_venv` | `which python` returns the project venv's python |
| `test_virtual_env_set_in_bash` | `$VIRTUAL_ENV` points at the project venv |
| `test_pip_install_targets_venv` | `pip install` puts packages in the venv |
| `test_session_python_path_takes_priority` | Active kernel session's env overrides detected envs |
| `test_no_venv_falls_back_gracefully` | No venv present — bash still works, no crash |
