from __future__ import annotations import json import os from pathlib import Path from headroom.install.models import DeploymentManifest, ManagedMutation from headroom.install.providers import _apply_windows_env_scope, _remove_windows_env_scope from headroom.providers.claude.install import apply_provider_scope as apply_claude_provider_scope from headroom.providers.claude.install import revert_provider_scope as revert_claude_provider_scope from headroom.providers.codex.install import apply_provider_scope as apply_codex_provider_scope from headroom.providers.codex.install import revert_provider_scope as revert_codex_provider_scope def _manifest(tmp_path: Path) -> DeploymentManifest: return DeploymentManifest( profile="default", preset="persistent-service", runtime_kind="python", supervisor_kind="service", scope="provider", provider_mode="manual", targets=["claude", "codex"], port=8787, host="127.0.0.1", backend="anthropic", memory_db_path=str(tmp_path / "memory.db"), tool_envs={ "claude": {"ANTHROPIC_BASE_URL": "http://127.0.0.1:8787"}, "codex": {"OPENAI_BASE_URL": "http://127.0.0.1:8787/v1"}, }, ) def test_apply_and_revert_claude_provider_scope(monkeypatch, tmp_path: Path) -> None: settings_path = tmp_path / "settings.json" settings_path.write_text( json.dumps({"env": {"ANTHROPIC_API_KEY": "keep", "ANTHROPIC_BASE_URL": "https://old"}}) ) monkeypatch.setattr( "headroom.providers.claude.install.claude_settings_path", lambda: settings_path ) manifest = _manifest(tmp_path) mutation = apply_claude_provider_scope(manifest) payload = json.loads(settings_path.read_text()) assert payload["env"]["ANTHROPIC_BASE_URL"] == "http://127.0.0.1:8787" assert payload["env"]["ANTHROPIC_API_KEY"] == "keep" assert mutation is not None revert_claude_provider_scope(mutation, manifest) reverted = json.loads(settings_path.read_text()) assert reverted["env"]["ANTHROPIC_BASE_URL"] == "https://old" assert reverted["env"]["ANTHROPIC_API_KEY"] == "keep" def test_apply_and_revert_codex_provider_scope(monkeypatch, tmp_path: Path) -> None: config_path = tmp_path / "config.toml" config_path.write_text('model = "gpt-4o"\n') monkeypatch.setattr("headroom.providers.codex.install.codex_config_path", lambda: config_path) manifest = _manifest(tmp_path) mutation = apply_codex_provider_scope(manifest) content = config_path.read_text() assert 'model_provider = "headroom"' in content assert 'base_url = "http://127.0.0.1:8787/v1"' in content assert mutation is not None revert_codex_provider_scope(mutation, manifest) reverted = config_path.read_text() assert 'model_provider = "headroom"' not in reverted assert reverted.strip() == 'model = "gpt-4o"' def test_apply_openclaw_provider_scope_uses_manifest_port(monkeypatch, tmp_path: Path) -> None: recorded: list[list[str]] = [] monkeypatch.setattr("headroom.providers.openclaw.install.shutil_which", lambda name: "openclaw") monkeypatch.setattr( "headroom.providers.openclaw.install.resolve_headroom_command", lambda: ["headroom"], ) monkeypatch.setattr( "headroom.providers.openclaw.install._invoke_openclaw", lambda command: recorded.append(command), ) monkeypatch.setattr( "headroom.providers.openclaw.install.openclaw_config_path", lambda: tmp_path / "openclaw.json", ) manifest = _manifest(tmp_path) manifest.port = 9999 from headroom.providers.openclaw.install import ( apply_provider_scope as apply_openclaw_provider_scope, ) apply_openclaw_provider_scope(manifest) assert recorded == [["headroom", "wrap", "openclaw", "--no-auto-start", "--proxy-port", "9999"]] def test_windows_env_scope_restores_previous_values(monkeypatch, tmp_path: Path) -> None: manifest = _manifest(tmp_path) manifest.scope = "user" manifest.targets = ["claude"] manifest.base_env = {"HEADROOM_PORT": "8787"} manifest.tool_envs = {"claude": {"ANTHROPIC_BASE_URL": "http://127.0.0.1:8787"}} calls: list[list[str]] = [] previous_values = { "HEADROOM_PORT": "7777", "ANTHROPIC_BASE_URL": "https://old", } class Result: def __init__(self, stdout: str = "") -> None: self.stdout = stdout def fake_run(command: list[str], **kwargs): calls.append(command) script = command[-1] if "GetEnvironmentVariable" in script: name = script.split("GetEnvironmentVariable('", 1)[1].split("'", 1)[0] value = previous_values.get(name, "__HEADROOM_UNSET__") return Result(stdout=value) return Result() monkeypatch.setattr("headroom.install.providers.subprocess.run", fake_run) mutations = _apply_windows_env_scope(manifest) _remove_windows_env_scope(mutations) previous_by_name = {mutation.data["name"]: mutation.data["previous"] for mutation in mutations} assert previous_by_name["HEADROOM_PORT"] == "7777" assert previous_by_name["ANTHROPIC_BASE_URL"] == "https://old" assert any( "[Environment]::SetEnvironmentVariable('HEADROOM_PORT','7777','User')" in command[-1] for command in calls ) assert any( "[Environment]::SetEnvironmentVariable('ANTHROPIC_BASE_URL','https://old','User')" in command[-1] for command in calls ) def test_remove_windows_env_scope_requires_name_and_scope() -> None: try: _remove_windows_env_scope([ManagedMutation(target="env", kind="windows-env", data={})]) except ValueError as exc: assert "variable name" in str(exc) else: raise AssertionError("expected missing variable name to raise") try: _remove_windows_env_scope( [ManagedMutation(target="env", kind="windows-env", data={"name": "X", "scope": 1})] ) except ValueError as exc: assert "valid scope" in str(exc) else: raise AssertionError("expected invalid scope to raise") def test_apply_mutations_runs_openclaw_for_user_scope(monkeypatch, tmp_path: Path) -> None: manifest = _manifest(tmp_path) manifest.scope = "user" manifest.targets = ["openclaw"] manifest.base_env = {"HEADROOM_PORT": "8787"} manifest.tool_envs = {} if os.name == "nt": monkeypatch.setattr( "headroom.install.providers._apply_windows_env_scope", lambda deployment: [] ) else: monkeypatch.setattr( "headroom.install.providers._apply_unix_env_scope", lambda deployment: [] ) monkeypatch.setattr( "headroom.install.providers.apply_provider_scope_mutations", lambda deployment: [ManagedMutation(target="openclaw", kind="openclaw-wrap")], ) from headroom.install.providers import apply_mutations mutations = apply_mutations(manifest) assert [mutation.kind for mutation in mutations] == ["openclaw-wrap"]