headroom/tests/test_install
Abhay Singh b097ef3e25
fix(install): don't let host env override the manifest in persistent-docker (#2090)
## Description

In persistent-docker deployments a stale host env var can silently
override the value the deployment manifest pinned for the container.

`build_runtime_command` builds the `docker run` argv in two passes:

1. It emits the manifest's pinned env as `--env NAME=VALUE` (from
`base_env` plus the deployment env).
2. It then walks `os.environ` and, for every name matching a
`PASSTHROUGH_ENV_PREFIXES` prefix, appends a bare `--env NAME` so the
host value is forwarded into the container.

A manifest-pinned name and a host-exported name can collide when they
share a passthrough prefix. `HEADROOM_BACKEND` is the clearest case: the
manifest pins `--env HEADROOM_BACKEND=anthropic` in pass 1, and pass 2
also matches the `HEADROOM_` prefix and appends a bare `--env
HEADROOM_BACKEND`. Docker resolves duplicate `--env` flags last-wins,
and the bare passthrough comes last, so a stale host export
`HEADROOM_BACKEND=anyllm` wins and the container runs a different
backend than its deployment config says.

`start_persistent_docker` runs the resulting command through
`subprocess.run` with the parent process environment, so whatever the
operator happened to have exported leaks in and overrides the manifest.

The fix skips the bare passthrough for any name the manifest already
pins, so the pinned value stands while unrelated host secrets (API keys
and so on) are still passed through as before.

Closes #

## Type of Change

- [x] 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
- [ ] Code refactoring (no functional changes)

## Changes Made

- `headroom/install/runtime.py`: skip the bare `--env NAME` passthrough
when `NAME` is already pinned by the manifest (`and name not in
runtime_env`).
- `tests/test_install/test_runtime.py`: add
`test_build_runtime_command_docker_manifest_env_beats_host_passthrough`,
which exports a conflicting `HEADROOM_BACKEND` and asserts the command
keeps the manifest value and emits no bare passthrough for it.
- `CHANGELOG.md`: Bug Fixes entry.

## Testing

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

### Test Output

```text
$ uvx ruff@0.15.17 format headroom/install/runtime.py tests/test_install/test_runtime.py
2 files left unchanged
$ uvx ruff@0.15.17 check headroom/install/runtime.py tests/test_install/test_runtime.py
All checks passed!
$ python -m py_compile headroom/install/runtime.py tests/test_install/test_runtime.py
OK
```

## Real Behavior Proof

- Environment: local checkout, Python 3.11, `uvx ruff@0.15.17`.
- Exact command / steps: ran a standalone script that reproduces the
two-pass argv build and models Docker's duplicate `--env` last-wins
resolution, with the manifest pinning `HEADROOM_BACKEND=anthropic` and
the host exporting `HEADROOM_BACKEND=anyllm`.
- Observed result: the old build resolves the effective
`HEADROOM_BACKEND` to the host value `anyllm` (bare passthrough wins);
the new build keeps the manifest value `anthropic` and emits no bare
`HEADROOM_BACKEND` token, while a non-pinned passthrough
(`ANTHROPIC_API_KEY`) is still forwarded.
- Not tested: I did not run the full `pytest` suite locally because it
pulls in the ML stack; the new regression test is left for CI.

## Review Readiness

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

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable

## Additional Notes

The "unit tests pass locally" and "type checking" boxes are unchecked
because the full suite imports the ML dependencies, which I can't run in
this environment; the change is a pure function over
`build_runtime_command`, verified by the standalone proof above and
covered by the new regression test for CI.

Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-07-13 10:24:41 -04:00
..
test_health.py test: expand persistent install coverage 2026-04-11 18:24:15 -05:00
test_native_installers.py fix: B1 — retire ICM, RollingWindow, scoring, relevance + dependents 2026-05-02 12:23:17 -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(install): only validate requested targets on the manual path (#1659) 2026-07-12 16:30:31 -05:00
test_providers.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_runtime.py fix(install): don't let host env override the manifest in persistent-docker (#2090) 2026-07-13 10:24:41 -04:00
test_state.py fix(install): write deployment manifest atomically and tolerate corrupt manifests (#1303) 2026-07-09 14:07:05 -05:00
test_supervisors.py fix(install): pass sc.exe create as raw command line so binPath= quoting survives (#1654) (#1702) 2026-07-07 12:43:57 -05:00