headroom/tests/test_cli
Ralf Escher 16c638bc21
fix: recover persistent proxy feature checks and reject non-Copilot exchange URL (#1465)
## Description

This PR fixes two related reliability issues in Copilot
wrap/subscription flows:

1. Recovered persistent proxy instances could be reused too early,
before validating requested feature-sensitive config (especially
`openai_api_url`), which could lead to wrong upstream routing.
2. Subscription token-exchange payloads could provide a non-Copilot API
URL; this is now rejected and we safely fall back to user-info/default
Copilot endpoint resolution.

Related: #488

## 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

- Updated persistent proxy recover path to:
- continue into feature checks when feature-sensitive options are
requested
- restart persistent deployment when config is missing/mismatched after
recovery
  - keep historical fast return for plain recover-only calls
- Hardened subscription exchange URL resolution:
  - accept exchange `api_url` only when it is a Copilot host
  - log warning and fall back when non-Copilot host is provided
- Added regression tests for:
- recovered persistent proxy feature mismatch and config-unavailable
restart behavior
  - non-Copilot exchange host rejection with/without user-info fallback

## Testing

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

### Test Output

```text
$ python -m pytest -q tests/test_copilot_auth.py tests/test_cli/test_wrap_persistent.py
============================= test session starts =============================
platform win32 -- Python 3.12.8, pytest-9.1.1, pluggy-1.6.0
rootdir: C:\Users\ralf.escher\Documents\headroom
collected 82 items

tests\test_copilot_auth.py ............................................. [ 54%]
...........                                                              [ 68%]
tests\test_cli\test_wrap_persistent.py ..........................        [100%]

============================= 82 passed in 1.60s ==============================
```

## Real Behavior Proof

- Environment:
  - Windows
  - Python 3.12.8
  - Local Headroom branch with this patch
  - Copilot subscription route through local proxy

- Exact command / steps:
  1. Start local proxy and run Copilot wrap in subscription mode.
  2. Execute chat-completions requests through proxy.
  3. Inspect runtime proxy logs for outbound target and inbound status.
  4. Run focused regression tests:
- `python -m pytest -q tests/test_copilot_auth.py
tests/test_cli/test_wrap_persistent.py`

- Observed result:
  - Outbound requests routed to Copilot business host:
    - `path=https://api.business.githubcopilot.com/chat/completions`
  - Successful proxy responses observed:
    - `path=/v1/chat/completions status=200`
  - Model activity logged during successful requests:
    - `PERF model=gpt-4.1 ...`
  - Regression tests pass (`82 passed`), covering both fixes.

- Not tested:
  - Full repository test suite
  - Full lint/typecheck across entire project
  - Non-Windows runtime verification in this run

## 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
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable

## Additional Notes

- This PR intentionally excludes incidental local edits to
`.github/copilot-instructions.md`.
- Scope is limited to this bug fix and regression coverage; linked as
related work to #488.
2026-06-28 15:26:38 -07:00
..
conftest.py feat(wrap): make tokensave the primary coding-task compressor, Serena the backup (#1230) 2026-06-25 16:55:37 -05:00
test_copilot_auth.py fix: support Copilot Business subscription auth (#641) 2026-06-12 20:46:38 -05:00
test_init_cli.py fix(codex): retag threads on init so Codex Desktop history stays visible (#961) (#1349) 2026-06-24 10:14:40 -05:00
test_init_enable_tool_search.py fix(init): set ENABLE_TOOL_SEARCH=true so Claude Code keeps deferring tools (#746) (#995) 2026-06-19 11:26:26 -05:00
test_install_cli.py fix(cli): harden all CLI surfaces + fix docs accuracy (#1491) 2026-06-27 14:48:43 -07:00
test_main_help_version.py feat(cli): add Docker-native install flow and parity docs 2026-04-10 23:27:24 -05:00
test_mcp.py fix(mcp): auto-register headroom MCP server in wrap claude/codex and init -g 2026-05-08 17:18:32 -07:00
test_serena_disable.py fix(serena): stop the Serena dashboard popup and make --no-serena actually disable Serena (#1003) 2026-06-14 23:32:46 -07:00
test_serena_migrate.py fix(serena): migrate stale Headroom-installed Serena entry on re-wrap (#1008) 2026-06-16 15:17:13 -05:00
test_subprocess_utf8_encoding.py fix(windows): pin UTF-8 encoding on text-mode subprocess calls (#1311) 2026-06-23 12:52:49 -05:00
test_tokensave_helpers.py feat(wrap): make tokensave the primary coding-task compressor, Serena the backup (#1230) 2026-06-25 16:55:37 -05:00
test_tokensave_setup.py feat(wrap): make tokensave the primary coding-task compressor, Serena the backup (#1230) 2026-06-25 16:55:37 -05:00
test_unwrap_claude.py fix(unwrap): remove ANTHROPIC_BASE_URL + ENABLE_TOOL_SEARCH and init hooks on unwrap (#992) 2026-06-19 11:30:59 -05:00
test_wrap_aider.py feat(proxy): per-project savings breakdown on the dashboard (claude, codex, aider, copilot, cursor) (#803) 2026-06-10 21:04:45 -05:00
test_wrap_bridge.py fix(cli): add explicit UTF-8 encoding to file I/O in wrap commands (#1126) (#1164) 2026-06-26 12:07:03 -05:00
test_wrap_claude_base_url.py fix(wrap): write env.ANTHROPIC_BASE_URL to settings.json so daemon-spawned conversations inherit proxy (#951) (#1078) 2026-06-18 11:21:04 -05:00
test_wrap_codex.py fix(mcp): register managed installs with a resolvable headroom command (#1386) 2026-06-26 23:39:00 -05:00
test_wrap_continue.py fix(cli): G1 remediation — non-string clobber, per-model systemMessage, openhands gate 2026-05-25 11:54:06 -07:00
test_wrap_copilot.py fix(cli): add explicit UTF-8 encoding to file I/O in wrap commands (#1126) (#1164) 2026-06-26 12:07:03 -05:00
test_wrap_goose.py refactor(cli): factor shared wrap-subcommand scaffolding 2026-05-26 11:22:50 -07:00
test_wrap_helpers.py feat(wrap): add --1m to preserve the 1M context window on wrap claude (#1158) (#1351) 2026-06-24 10:15:19 -05:00
test_wrap_hintfile_agents.py fix(cli): add explicit UTF-8 encoding to file I/O in wrap commands (#1126) (#1164) 2026-06-26 12:07:03 -05:00
test_wrap_openclaw.py fix: stabilize codex compression, stats, and proxy lifecycle 2026-05-09 13:47:53 -07:00
test_wrap_opencode.py fix(opencode): write local MCP config (#1381) 2026-06-26 12:23:54 -05:00
test_wrap_openhands.py fix(cli): G1 remediation — non-string clobber, per-model systemMessage, openhands gate 2026-05-25 11:54:06 -07:00
test_wrap_persistent.py fix: recover persistent proxy feature checks and reject non-Copilot exchange URL (#1465) 2026-06-28 15:26:38 -07:00
test_wrap_rtk_hook_patch.py fix: patch rtk hook script to use absolute path after register_claude_hooks (#571) 2026-06-26 14:09:37 -05:00
test_wrap_rtk_metrics.py fix(observability): G3 remediation — bound cardinality + wire dead metrics 2026-05-24 10:41:56 -07:00
test_wrap_vibe.py feat: Add support for Mistral Vibe CLI (#935) 2026-06-16 14:59:51 -05:00