headroom/tests/test_copilot_native_mode.py
JD Davis 997a47992c
fix(copilot): preserve native enterprise model routing (#2998)
## Description

GitHub Copilot Enterprise/Business users without a BYOK provider key
were routed through Copilot CLI's single-model provider override. Native
model aliases and runtime `/model` switches were therefore forwarded
literally to the override and rejected with `400 model not supported`.
This change routes implicit GitHub OAuth through Copilot's native API
surface while retaining explicit subscription and provider-key behavior.

Closes #1910

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

- Added explicit `--native` routing and made it automatic for implicit
GitHub OAuth without BYOK.
- Clears every Copilot BYOK variable before native launch.
- Routes both OpenAI and Anthropic protocol targets through the resolved
tenant Copilot host.
- Preserves Enterprise/Business native aliases and runtime model
switching.
- Rejects BYOK-only options when native routing is selected.
- Refuses known Copilot bundles that do not reference `COPILOT_API_URL`,
avoiding silent proxy bypass.
- Preserves explicit `--subscription` and provider-key BYOK semantics.
- Added coverage for unreadable and unverifiable Copilot CLI bundles.

## 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
884 passed, 4 skipped in 103.11s
ruff check .: All checks passed
ruff format --check .: 1412 files already formatted
mypy headroom/providers/copilot/wrap.py headroom/cli/wrap.py:
Success: no issues found in 2 source files
```

Exact-head CI is entirely green on
`0aca48c096`.

## Real Behavior Proof

- Environment: macOS arm64/Python 3.13 locally; GitHub-hosted macOS and
Ubuntu native-wrap jobs.
- Exact command / steps: invoke `headroom wrap copilot` with implicit
OAuth and an Enterprise model alias; inspect the captured child/proxy
environment and resolved target URLs; exercise explicit native conflicts
and bundle-support probes.
- Observed result: native launch uses `COPILOT_API_URL`, clears all BYOK
state, and points both protocol targets at the tenant host. Native-wrap
jobs are green on macOS and Ubuntu for the refreshed head.
- Not tested: live request against a real Enterprise tenant; the
repository has no organization Enterprise credential available to CI.

## Runtime Rollout Safety

- Rollout-managed feature(s): implicit native Copilot routing for GitHub
OAuth sessions without BYOK.
- Minimum rollout channel: normal patch release.
- Stable/default behavior changed: implicit OAuth now uses native
routing; explicit subscription and BYOK paths are unchanged.
- Kill switch / disable path: use an explicit supported provider-key
BYOK configuration; native mode also fails closed when CLI support is
known absent.
- Unsafe override required: none.
- Qualification impact: native-wrap macOS/Ubuntu, Docker wrapper, full
Python matrix, and Copilot focused suites must pass.
- Rollback path: human revert of this PR restores the fixed-wire OAuth
behavior; no configuration migration is persisted.

## 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
- [x] I have made corresponding changes to the documentation — CLI help
and inline routing documentation; no separate guide required
- [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
- [x] I did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Screenshots (if applicable)

Not applicable; CLI routing change.

## Additional Notes

Human review only. No merge or auto-merge is configured. Refreshed from
main after #2996; the MCP cap `mcp>=1.28.1,<2.0.0` is preserved.
2026-08-25 21:46:31 -05:00

148 lines
5.7 KiB
Python

from __future__ import annotations
import pytest
from click.testing import CliRunner
from headroom.providers.copilot.wrap import (
COPILOT_BYOK_ENV_VARS,
COPILOT_NATIVE_API_URL_ENV,
build_launch_env,
build_native_launch_env,
native_api_url_supported,
)
def test_native_env_redirects_api_and_clears_all_byok_state() -> None:
seeded = dict.fromkeys(COPILOT_BYOK_ENV_VARS, "stale")
seeded["UNRELATED"] = "preserved"
env, _ = build_native_launch_env(port=8890, environ=seeded, project="repo name")
assert env[COPILOT_NATIVE_API_URL_ENV] == "http://127.0.0.1:8890/p/repo%20name"
assert env["UNRELATED"] == "preserved"
assert not any(variable in env for variable in COPILOT_BYOK_ENV_VARS)
def test_byok_builder_remains_disjoint_from_native_mode() -> None:
env, _ = build_launch_env(
port=8787,
provider_type="openai",
wire_api="responses",
environ={},
)
assert env["COPILOT_PROVIDER_BASE_URL"] == "http://127.0.0.1:8787/v1"
assert env["COPILOT_PROVIDER_WIRE_API"] == "responses"
assert COPILOT_NATIVE_API_URL_ENV not in env
def test_native_support_probe_distinguishes_unknown_and_unsupported(tmp_path) -> None:
local = tmp_path / "local"
assert native_api_url_supported(environ={"LOCALAPPDATA": str(local)}) is None
bundle = local / "copilot" / "pkg" / "platform" / "1.0" / "app.js"
bundle.parent.mkdir(parents=True)
bundle.write_text("no override here", encoding="utf-8")
assert native_api_url_supported(environ={"LOCALAPPDATA": str(local)}) is False
bundle.write_text("process.env.COPILOT_API_URL", encoding="utf-8")
assert native_api_url_supported(environ={"LOCALAPPDATA": str(local)}) is True
def test_native_support_probe_skips_unreadable_bundle(monkeypatch, tmp_path) -> None:
local = tmp_path / "local"
bundle = local / "copilot" / "pkg" / "platform" / "1.0" / "app.js"
bundle.parent.mkdir(parents=True)
bundle.write_text("process.env.COPILOT_API_URL", encoding="utf-8")
def _unreadable(*_args, **_kwargs):
raise OSError("synthetic unreadable bundle")
monkeypatch.setattr("builtins.open", _unreadable)
assert native_api_url_supported(environ={"LOCALAPPDATA": str(local)}) is False
def _invoke_native(
monkeypatch: pytest.MonkeyPatch,
extra: list[str] | None = None,
*,
support: bool | None = True,
):
from headroom.cli import wrap as wrap_mod
from headroom.cli.main import main
captured: dict[str, object] = {}
class Resolution:
token = "copilot-token"
api_url = "https://api.business.githubcopilot.com"
refresh_oauth_token = "refresh-token"
api_token_expires_at = 123.0
monkeypatch.setattr(wrap_mod.shutil, "which", lambda _name: "/usr/bin/copilot")
monkeypatch.setattr(wrap_mod, "_check_proxy", lambda _port: False)
monkeypatch.setattr(wrap_mod, "_require_copilot_subscription_resolution", lambda: Resolution())
monkeypatch.setattr(wrap_mod, "_native_api_url_supported", lambda **_kwargs: support)
monkeypatch.setattr(wrap_mod, "_launch_tool", lambda **kwargs: captured.update(kwargs))
result = CliRunner().invoke(
main,
["wrap", "copilot", "--native", "--port", "8890", *(extra or [])],
)
return result, captured
def test_implicit_oauth_uses_native_routing_without_flag(monkeypatch) -> None:
from headroom.cli import wrap as wrap_mod
from headroom.cli.main import main
captured: dict[str, object] = {}
monkeypatch.setattr(wrap_mod.shutil, "which", lambda _name: "/usr/bin/copilot")
monkeypatch.setattr(wrap_mod, "_check_proxy", lambda _port: False)
monkeypatch.setattr(wrap_mod, "has_oauth_auth", lambda: True)
monkeypatch.setattr(wrap_mod, "resolve_client_bearer_token", lambda: "oauth-token")
monkeypatch.setattr(
wrap_mod, "resolve_copilot_api_url", lambda _token: "https://api.githubcopilot.com"
)
monkeypatch.setattr(wrap_mod, "_native_api_url_supported", lambda **_kwargs: True)
monkeypatch.setattr(wrap_mod, "_launch_tool", lambda **kwargs: captured.update(kwargs))
result = CliRunner().invoke(
main,
["wrap", "copilot", "--port", "8890", "--", "--model", "claude-sonnet-5"],
)
assert result.exit_code == 0, result.output
env = captured["env"]
assert isinstance(env, dict)
assert COPILOT_NATIVE_API_URL_ENV in env
assert not any(variable in env for variable in COPILOT_BYOK_ENV_VARS)
def test_native_cli_routes_both_protocols_to_tenant_host(monkeypatch) -> None:
result, captured = _invoke_native(monkeypatch)
assert result.exit_code == 0, result.output
assert captured["openai_api_url"] == "https://api.business.githubcopilot.com"
assert captured["anthropic_api_url"] == "https://api.business.githubcopilot.com"
env = captured["env"]
assert isinstance(env, dict)
assert COPILOT_NATIVE_API_URL_ENV in env
assert not any(variable in env for variable in COPILOT_BYOK_ENV_VARS)
@pytest.mark.parametrize("extra", [["--wire-api", "responses"], ["--provider-type", "anthropic"]])
def test_native_cli_rejects_byok_only_options(monkeypatch, extra) -> None:
result, captured = _invoke_native(monkeypatch, extra)
assert result.exit_code != 0
assert not captured
def test_native_cli_refuses_known_unsupported_bundle(monkeypatch) -> None:
result, captured = _invoke_native(monkeypatch, support=False)
assert result.exit_code != 0
assert "COPILOT_API_URL" in result.output
assert not captured
def test_native_cli_reports_unknown_support_in_verbose_mode(monkeypatch) -> None:
result, captured = _invoke_native(monkeypatch, ["--verbose"], support=None)
assert result.exit_code == 0, result.output
assert "could not verify" in result.output
assert captured