headroom/tests/test_install
Parideboy c3c921f2f7
test(install/windows): verify the PATH guard against the real HKCU registry (#3068)
## Description

Follow-up requested in review of #2972, on top of the merged fix for
#2970 (#2985). Test-only; no
production code is touched and the `HEADROOM_INSTALL_PATH_SCOPE`
mechanism is unchanged.

`test_powershell_installer_does_not_leak_into_user_path` currently
guards the fix by comparing the
entry count of `[Environment]::GetEnvironmentVariable('Path','User')`
across an installer run. That
infers success from the environment variable rather than verifying it,
and it leaves three gaps:

- The .NET getter expands `%USERPROFILE%`-style references, so it cannot
observe a change of the
  registry value kind (`REG_EXPAND_SZ` vs `REG_SZ`) at all.
- A count comparison passes when an entry is replaced or reordered
rather than appended.
- There is no restore path. If the guard regresses, the test reports the
leak and then leaves the
polluted value behind in the contributor's registry, which is precisely
the damage #2970
  described: the test that detects the pollution also causes it.

This PR reads `HKCU\Environment` directly instead, so the assertion
verifies the guard rather than
assuming it.

## Type of Change

- [ ] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [x] Code refactoring (no functional changes)

## Changes Made

- `tests/test_install/test_native_installers.py`: new
`_read_user_path_entry` helper returning the
raw `HKCU\Environment` `Path` value together with its registry kind (or
`None` when the value is
absent), and `_restore_user_path_entry` writing that exact value and
kind back. Both import
`winreg` inside the function body, so the module still imports on
non-Windows hosts.
- `tests/test_install/test_native_installers.py`:
`test_powershell_installer_does_not_leak_into_user_path`
now records the raw value before the run and asserts both that the
throwaway install dir is absent
from the value afterwards (naming the #2970 symptom in the failure
message) and that value and
kind are byte-identical. The PowerShell subprocess that counted PATH
entries is gone, so the test
  also spawns one process fewer.
- `tests/test_install/test_native_installers.py`: the test now runs
under `try/finally`. The
`finally` cleans up the fake docker state, which this test was missing
relative to its sibling

`test_powershell_native_installer_supports_persistent_docker_lifecycle`,
and restores the recorded
registry value only when it actually changed, so a passing run performs
zero registry writes and a
  regressed run cannot leave the contributor's PATH polluted.

The scope allow-list tests added by #2985 (`_ENSURE_PATH_SCOPE_HARNESS`,
`test_path_scope_accepts_process_case_insensitively`,
`test_path_scope_rejects_machine_and_invalid_values`)
are untouched.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
$ uv run pytest tests/test_install/test_native_installers.py -q
platform win32 -- Python 3.13.11, pytest-9.0.3, pluggy-1.6.0
collected 5 items

tests\test_install\test_native_installers.py s....                       [100%]

======================== 4 passed, 1 skipped in 23.59s ========================

$ uv run ruff check .
All checks passed!

$ uv run ruff format --check tests/test_install/test_native_installers.py
1 file already formatted

$ uv run mypy headroom --ignore-missing-imports
Success: no issues found in 521 source files
```

The strengthened assertion was proven to detect a regression by
temporarily neutralising the scope
override in `scripts/install.ps1` (`if ($false -and
$env:HEADROOM_INSTALL_PATH_SCOPE)`), so
`Ensure-PathEntry` writes the `User` scope unconditionally again:

```text
$ uv run pytest tests/test_install/test_native_installers.py -q -k does_not_leak_into_user_path
tests\test_install\test_native_installers.py:638: in test_powershell_installer_does_not_leak_into_user_path
    assert str(home) not in (after[0] if after else ""), (
E   AssertionError: installer leaked the throwaway install dir into the real User PATH:
E     C:\Users\<user>\AppData\Local\Temp\pytest-of-<user>\pytest-154\test_powershell_installer_does0\home

======================= 1 failed, 4 deselected in 2.72s =======================
```

## Real Behavior Proof

- Environment: Windows 11 Pro 10.0.26200, PowerShell 7, Python 3.13.11,
pytest 9.0.3, headroom at
  `main` (`a6ab359a`), provider Anthropic
- Exact command / steps: recorded the raw `HKCU\Environment` `Path`
value with
`python -c "import winreg; ...QueryValueEx(k,'Path')"`, capturing its
registry kind, entry count
and a SHA-256 of the value; ran the full installer test file on the
patched tree; re-read the
registry; then neutralised the scope override in `scripts/install.ps1`
as shown above, re-ran the
single leak test, and re-read the registry a third time to confirm the
failure path restored it.
- Observed result: baseline `kind 1 entries 21 sha256 683ee646a95b8a28`.
After the passing run the
value was identical (`kind 1 entries 21 sha256 683ee646a95b8a28`), so a
passing run writes nothing.
With the override neutralised the test failed as quoted above and the
registry read afterwards was
again byte-identical to the recorded backup (compared as an exact
`{value, kind}` match, `True`),
confirming the `finally` restore. After reverting `scripts/install.ps1`,
the full file is back to
  4 passed, 1 skipped with the registry still unchanged.
- Not tested: non-Windows hosts (the changed test is Windows-only and
already skipped elsewhere;
`scripts/install.sh` is untouched), elevated/admin installs, and the
`Machine` scope, which
  `Ensure-PathEntry` rejects outright.

One open question this change is positioned to catch but does not
resolve: on this host the
`HKCU\Environment` `Path` value is `REG_SZ` (kind `1`), not
`REG_EXPAND_SZ`. A real install persists
through `[Environment]::SetEnvironmentVariable(..., 'User')`, which is
the API class known to rewrite
that value, so it is possible that a production install silently
downgrades an expandable PATH and
freezes `%USERPROFILE%`-style entries. I have not verified whether
headroom's installer caused it on
this machine or whether the value was always `REG_SZ`, and this PR
deliberately does not chase it.
Happy to open a separate issue if that is worth investigating.

## Runtime Rollout Safety

- Rollout-managed feature(s): none (test-only change)
- Minimum rollout channel: n/a
- Stable/default behavior changed: no; no production code path is
modified
- Kill switch / disable path: n/a
- Unsafe override required: no
- Qualification impact: none
- Rollback path: revert this commit; the test returns to the entry-count
comparison

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 20:21:03 -07:00
..
test_codex_install.py fix(codex): detect keyring-backed ChatGPT auth (#2478) 2026-07-22 06:09:31 -07:00
test_health.py test: expand persistent install coverage 2026-04-11 18:24:15 -05:00
test_native_installers.py test(install/windows): verify the PATH guard against the real HKCU registry (#3068) 2026-08-17 20:21:03 -07:00
test_paths.py ci: restore green lint (reformat for ruff 0.15.17, fix mypy no-any-return, pin linters) (#1295) 2026-06-22 15:14:40 -05:00
test_planner.py fix(wrap): set xAI upstream for grok-build proxy (#2772) 2026-08-16 15:04:59 -07:00
test_providers.py fix(codex): rewrite config.toml properly so Codex will route through … (#2102) 2026-07-14 11:53:24 -04:00
test_proxy_mode_default.py fix(install): default to cache mode, matching headroom proxy (#1893 follow-up) (#2563) 2026-07-25 19:26:15 -07:00
test_runtime.py fix(install): use --userns=keep-id under Podman so bind-mount writes don't fail (#2846) 2026-08-08 01:30:48 -05:00
test_state.py fix(install): migrate deployments off the retired chopratejas image repo (#2427) 2026-07-19 22:15:01 -07:00
test_supervisors.py fix(install/windows): register persistent-task from S4U hidden XML (#2453) (#2459) 2026-08-12 00:10:23 -05:00