headroom/tests/test_install
Abhay Singh 18e5680be3
fix(install): only validate requested targets on the manual path (#1659)
## Description

`resolve_targets()` runs the provider-scope "unsupported targets"
validation
**before** it dispatches on `provider_mode`:

```python
if scope == ConfigScope.PROVIDER.value:
    unsupported = [t for t in requested if t and t not in valid]
    if unsupported:
        raise click.ClickException("Provider scope supports only ...; unsupported targets: ...")

if provider_mode == ALL:  return [t.value for t in valid_targets]   # ignores `requested`
if provider_mode == AUTO: ...                                        # ignores `requested`
# manual: filters `requested`
```

But `all` and `auto` never consult the requested target list — only the
manual
path does. So an unsupported entry that those modes would simply ignore
instead
makes the call raise. Concretely:

```
headroom install apply --scope provider --providers all --target cursor
→ ClickException: Provider scope supports only claude, codex, openclaw, and
  opencode; unsupported targets: cursor
```

...when it should just return the full provider set. (`--target` is a
click
`Choice` that accepts all 7 targets regardless of scope/mode, so this is
reachable from the CLI.) The user-scope equivalent,
`resolve_targets("all",
["cursor"])`, happily ignores `cursor` and returns all user targets — so
provider
scope is inconsistent with user scope for identical, ignored input.

Closes: no issue filed — found while auditing `install` target
resolution.

## Fix

Move the provider-scope validation so it runs only on the manual path
(the only
mode that reads `requested`). `all` returns the full provider set and
`auto`
returns detected/default targets, neither raising on an ignored
requested list.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)

## Changes Made

- `headroom/install/planner.py`: move the provider-scope "unsupported
targets" check below the `all`/`auto` dispatch, into the manual path.
- `tests/test_install/test_planner.py`: regression tests — `all` and
`auto` ignore an unsupported requested target under provider scope; the
manual path still rejects it.
- `CHANGELOG.md`: Bug Fixes entry under Unreleased.

## Testing

- [x] New tests added for the fixed behavior
(`tests/test_install/test_planner.py`)
- [x] Linting passes (`ruff check`) and formatting is clean (`ruff
format --check`)
- [ ] Full `pytest` deferred to CI (see Real Behavior Proof for the
local-OOM reason).

```text
$ uv run ruff check headroom/install/planner.py tests/test_install/test_planner.py
All checks passed!
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.12.11, headroom built from this
branch. Importing `headroom` loads the torch/transformers stack and a
full `pytest` gets OOM-killed on this box, so I verified the control
flow with a dependency-free script (only stdlib) and left the full
pytest to CI.
- Exact command / steps: replicated `resolve_targets`'s control flow
(with the fix, `click.ClickException` stubbed, and stand-in target
lists) in a standalone script — no `headroom` import — and exercised
`all`/`auto`/`manual` under provider scope with an unsupported `cursor`
entry, plus the user-scope and manual-dedup regressions.
- Observed result: `all`/`auto` return the provider targets without
raising, `manual` still raises on the unsupported target, and the
pre-existing manual-dedup and user-scope behavior is unchanged:

```text
OK: all + [cursor] + provider -> provider set (no raise)
OK: auto + [cursor] + provider -> [claude, codex] (no raise)
OK: manual + [cursor] + provider -> raises (preserved)
OK: manual dedupe/filter + user-scope all unchanged
PLANNER LOGIC VERIFIED
```

- Not tested: a full `headroom install apply` end-to-end (would require
the heavy stack and a real deployment); the change is confined to pure
target-list resolution. Full local `pytest` deferred to CI (OOM, per
above).

## 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 — ran
lint + a standalone logic check; full pytest deferred to CI (local OOM,
disclosed above)
- [x] I have updated the CHANGELOG.md if applicable

## Additional Notes

- No new dependencies; a control-flow move plus tests.

---------

Co-authored-by: JD Davis <mxjerrett@gmail.com>
2026-07-12 16:30:31 -05: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: harden persistent install startup (#1851) 2026-07-10 00:40:34 -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