from __future__ import annotations import signal from pathlib import Path from headroom.install.models import DeploymentManifest, InstallPreset from headroom.install.runtime import ( _clear_pid, _deployment_env, _mount_source, _read_pid, _runtime_env, _write_pid, build_runtime_command, resolve_headroom_command, run_foreground, runtime_status, start_detached_agent, start_persistent_docker, stop_runtime, wait_ready, ) def test_build_runtime_command_for_docker_includes_deployment_env( monkeypatch, tmp_path: Path ) -> None: monkeypatch.setattr(Path, "home", lambda: tmp_path) manifest = DeploymentManifest( profile="default", preset="persistent-docker", runtime_kind="docker", supervisor_kind="none", scope="user", provider_mode="manual", targets=["claude"], port=8787, host="127.0.0.1", backend="anthropic", image="ghcr.io/chopratejas/headroom:latest", base_env={"HEADROOM_PORT": "8787"}, proxy_args=["--host", "127.0.0.1", "--port", "8787"], ) command = build_runtime_command(manifest) joined = " ".join(command) assert command[:3] == ["docker", "run", "--rm"] assert "HEADROOM_DEPLOYMENT_PROFILE=default" in joined assert "HEADROOM_DEPLOYMENT_PRESET=persistent-docker" in joined assert "127.0.0.1:8787:8787" in joined assert "ghcr.io/chopratejas/headroom:latest" in command # Canonical Headroom filesystem contract (issue #175) forwarded into # the container. assert "HEADROOM_WORKSPACE_DIR=/tmp/headroom-home/.headroom" in command assert "HEADROOM_CONFIG_DIR=/tmp/headroom-home/.headroom/config" in command def test_build_runtime_command_for_docker_matches_wrapper_parity( monkeypatch, tmp_path: Path ) -> None: monkeypatch.setattr(Path, "home", lambda: tmp_path) monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key") monkeypatch.setenv("OPENAI_API_KEY", "test-openai") manifest = DeploymentManifest( profile="default", preset="persistent-docker", runtime_kind="docker", supervisor_kind="none", scope="user", provider_mode="manual", targets=["claude"], port=8787, host="127.0.0.1", backend="anthropic", image="ghcr.io/chopratejas/headroom:latest", base_env={"HEADROOM_PORT": "8787"}, proxy_args=["--host", "127.0.0.1", "--port", "8787"], ) command = build_runtime_command(manifest) assert (tmp_path / ".headroom").is_dir() assert (tmp_path / ".claude").is_dir() assert (tmp_path / ".codex").is_dir() assert (tmp_path / ".gemini").is_dir() assert "--env" in command joined = " ".join(command) assert "ANTHROPIC_API_KEY" in joined assert "OPENAI_API_KEY" in joined def test_resolve_headroom_command_prefers_headroom_binary(monkeypatch) -> None: monkeypatch.setattr( "shutil.which", lambda name: "/usr/bin/headroom" if name == "headroom" else None ) assert resolve_headroom_command() == ["/usr/bin/headroom"] def test_resolve_headroom_command_falls_back_to_python_module(monkeypatch) -> None: monkeypatch.setattr("shutil.which", lambda name: None) monkeypatch.setattr("headroom.install.runtime.sys.executable", "/usr/bin/python") assert resolve_headroom_command() == ["/usr/bin/python", "-m", "headroom.cli"] def test_runtime_env_and_mount_source(monkeypatch) -> None: manifest = DeploymentManifest( profile="default", preset="persistent-service", runtime_kind="python", supervisor_kind="service", scope="user", provider_mode="manual", targets=[], port=8787, host="127.0.0.1", backend="anthropic", base_env={"EXTRA": "1"}, ) monkeypatch.setattr("headroom.install.runtime.os.environ", {"BASE": "x"}) assert _deployment_env(manifest) == { "HEADROOM_DEPLOYMENT_PROFILE": "default", "HEADROOM_DEPLOYMENT_PRESET": "persistent-service", "HEADROOM_DEPLOYMENT_RUNTIME": "python", "HEADROOM_DEPLOYMENT_SUPERVISOR": "service", "HEADROOM_DEPLOYMENT_SCOPE": "user", } assert _runtime_env(manifest)["BASE"] == "x" assert _runtime_env(manifest)["EXTRA"] == "1" assert _runtime_env(manifest)["HEADROOM_DEPLOYMENT_PROFILE"] == "default" monkeypatch.setattr("headroom.install.runtime.sys.platform", "win32") assert _mount_source("C:\\Users\\me", ".headroom") == "C:\\Users\\me\\.headroom" monkeypatch.setattr("headroom.install.runtime.sys.platform", "linux") assert _mount_source("/home/me", ".headroom") == "/home/me/.headroom" def test_build_runtime_command_python_and_docker_user(monkeypatch, tmp_path: Path) -> None: monkeypatch.setattr("headroom.install.runtime.sys.executable", "/usr/bin/python") manifest = DeploymentManifest( profile="default", preset="persistent-service", runtime_kind="python", supervisor_kind="service", scope="user", provider_mode="manual", targets=[], port=8787, host="127.0.0.1", backend="anthropic", proxy_args=["--host", "127.0.0.1", "--port", "8787"], ) assert build_runtime_command(manifest) == [ "/usr/bin/python", "-m", "headroom.cli", "proxy", "--host", "127.0.0.1", "--port", "8787", ] monkeypatch.setattr(Path, "home", lambda: tmp_path) monkeypatch.setattr("headroom.install.runtime.sys.platform", "linux") monkeypatch.setattr("headroom.install.runtime.os.getuid", lambda: 1000, raising=False) monkeypatch.setattr("headroom.install.runtime.os.getgid", lambda: 1001, raising=False) docker_manifest = DeploymentManifest( profile="default", preset="persistent-docker", runtime_kind="docker", supervisor_kind="none", scope="user", provider_mode="manual", targets=[], port=8787, host="127.0.0.1", backend="anthropic", image="ghcr.io/chopratejas/headroom:latest", base_env={"HEADROOM_PORT": "8787"}, proxy_args=["--host", "127.0.0.1", "--port", "8787"], ) command = build_runtime_command(docker_manifest) assert "--user" in command assert "1000:1001" in command def test_read_pid_handles_invalid_content(monkeypatch, tmp_path: Path) -> None: monkeypatch.setattr(Path, "home", lambda: tmp_path) pid_file = tmp_path / ".headroom" / "deploy" / "default" / "runner.pid" pid_file.parent.mkdir(parents=True) pid_file.write_text("not-a-pid", encoding="utf-8") assert _read_pid("default") is None _clear_pid("default") assert not pid_file.exists() def test_write_read_and_clear_pid(monkeypatch, tmp_path: Path) -> None: monkeypatch.setattr(Path, "home", lambda: tmp_path) _write_pid("default", 456) assert _read_pid("default") == 456 _clear_pid("default") assert _read_pid("default") is None def test_run_foreground_and_detached_helpers(monkeypatch, tmp_path: Path) -> None: monkeypatch.setattr(Path, "home", lambda: tmp_path) monkeypatch.setattr( "headroom.install.runtime.build_runtime_command", lambda manifest: ["headroom", "proxy"] ) monkeypatch.setattr("headroom.install.runtime._runtime_env", lambda manifest: {"ENV": "1"}) signal_calls: list[int] = [] monkeypatch.setattr( "headroom.install.runtime.signal.signal", lambda sig, fn: signal_calls.append(sig) ) class FakeProc: def __init__(self, returncode: int = 0, pid: int = 321) -> None: self.returncode = returncode self.pid = pid self.terminated = False self.killed = False def wait(self, timeout: int | None = None) -> int: return self.returncode def poll(self): return None if not self.terminated else self.returncode def terminate(self) -> None: self.terminated = True def kill(self) -> None: self.killed = True fake_proc = FakeProc(returncode=7) popen_calls: list[tuple[list[str], dict]] = [] def fake_popen(command: list[str], **kwargs): popen_calls.append((command, kwargs)) return fake_proc monkeypatch.setattr("headroom.install.runtime.subprocess.Popen", fake_popen) manifest = DeploymentManifest( profile="default", preset="persistent-service", runtime_kind="python", supervisor_kind="service", scope="user", provider_mode="manual", targets=[], port=8787, host="127.0.0.1", backend="anthropic", ) assert run_foreground(manifest) == 7 assert popen_calls[0][0] == ["headroom", "proxy"] assert signal.SIGINT in signal_calls assert signal.SIGTERM in signal_calls assert _read_pid("default") is None monkeypatch.setattr("headroom.install.runtime.resolve_headroom_command", lambda: ["headroom"]) monkeypatch.setattr("headroom.install.runtime.sys.platform", "win32") monkeypatch.setattr("headroom.install.runtime.subprocess.DETACHED_PROCESS", 1, raising=False) monkeypatch.setattr( "headroom.install.runtime.subprocess.CREATE_NEW_PROCESS_GROUP", 2, raising=False ) fake_proc_nt = FakeProc() monkeypatch.setattr( "headroom.install.runtime.subprocess.Popen", lambda command, **kwargs: fake_proc_nt ) assert start_detached_agent("demo") is fake_proc_nt monkeypatch.setattr("headroom.install.runtime.sys.platform", "linux") fake_proc_posix = FakeProc() monkeypatch.setattr( "headroom.install.runtime.subprocess.Popen", lambda command, **kwargs: fake_proc_posix ) assert start_detached_agent("demo") is fake_proc_posix def test_start_stop_wait_and_runtime_status_branches(monkeypatch, tmp_path: Path) -> None: calls: list[list[str]] = [] monkeypatch.setattr( "headroom.install.runtime.subprocess.run", lambda command, **kwargs: calls.append(command) or type("Result", (), {"stdout": ""})(), ) monkeypatch.setattr( "headroom.install.runtime.build_runtime_command", lambda manifest: [ "docker", "run", "--rm", "--name", "demo", "-p", "127.0.0.1:8787:8787", "image", ], ) manifest = DeploymentManifest( profile="default", preset=InstallPreset.PERSISTENT_DOCKER.value, runtime_kind="docker", supervisor_kind="none", scope="user", provider_mode="manual", targets=[], port=8787, host="127.0.0.1", backend="anthropic", container_name="headroom-default", ) start_persistent_docker(manifest) assert calls == [ ["docker", "rm", "-f", "headroom-default"], [ "docker", "run", "-d", "--restart", "unless-stopped", "--name", "headroom-default", "-p", "127.0.0.1:8787:8787", "image", ], ] monkeypatch.setattr(Path, "home", lambda: tmp_path) python_manifest = DeploymentManifest( profile="default", preset="persistent-service", runtime_kind="python", supervisor_kind="service", scope="user", provider_mode="manual", targets=[], port=8787, host="127.0.0.1", backend="anthropic", health_url="http://127.0.0.1:8787/health", ) _write_pid("default", 123) killed: list[tuple[int, int]] = [] monkeypatch.setattr( "headroom.install.runtime.os.kill", lambda pid, sig: killed.append((pid, sig)) ) stop_runtime(python_manifest) assert killed == [(123, signal.SIGTERM)] assert _read_pid("default") is None _write_pid("default", 124) monkeypatch.setattr( "headroom.install.runtime.os.kill", lambda pid, sig: (_ for _ in ()).throw(OSError("gone")), ) stop_runtime(python_manifest) assert _read_pid("default") is None probe_results = iter([False, False, True]) sleeps: list[int] = [] monkeypatch.setattr("headroom.install.runtime.probe_ready", lambda url: next(probe_results)) monkeypatch.setattr( "headroom.install.runtime.time.sleep", lambda seconds: sleeps.append(seconds) ) assert wait_ready(python_manifest, timeout_seconds=3) is True assert sleeps == [1, 1] monkeypatch.setattr("headroom.install.runtime.probe_ready", lambda url: False) sleeps.clear() assert wait_ready(python_manifest, timeout_seconds=2) is False assert sleeps == [1, 1] class Result: def __init__(self, stdout: str = "") -> None: self.stdout = stdout monkeypatch.setattr( "headroom.install.runtime.subprocess.run", lambda command, **kwargs: Result(stdout=""), ) assert runtime_status(manifest) == "stopped" assert runtime_status(python_manifest) == "stopped" _write_pid("default", 125) monkeypatch.setattr( "headroom.install.runtime.os.kill", lambda pid, sig: (_ for _ in ()).throw(OSError()) ) assert runtime_status(python_manifest) == "stopped" def test_stop_runtime_for_docker_stops_and_removes_container(monkeypatch) -> None: calls: list[list[str]] = [] manifest = DeploymentManifest( profile="default", preset="persistent-docker", runtime_kind="docker", supervisor_kind="none", scope="user", provider_mode="manual", targets=[], port=8787, host="127.0.0.1", backend="anthropic", container_name="headroom-default", ) monkeypatch.setattr( "headroom.install.runtime.subprocess.run", lambda command, **kwargs: calls.append(command), ) stop_runtime(manifest) assert calls == [ ["docker", "stop", "headroom-default"], ["docker", "rm", "-f", "headroom-default"], ] def test_runtime_status_reads_container_and_pid_state(monkeypatch, tmp_path: Path) -> None: docker_manifest = DeploymentManifest( profile="default", preset="persistent-docker", runtime_kind="docker", supervisor_kind="none", scope="user", provider_mode="manual", targets=[], port=8787, host="127.0.0.1", backend="anthropic", container_name="headroom-default", ) class Result: def __init__(self, stdout: str = "") -> None: self.stdout = stdout monkeypatch.setattr( "headroom.install.runtime.subprocess.run", lambda command, **kwargs: Result(stdout="headroom-default\n"), ) assert runtime_status(docker_manifest) == "running" monkeypatch.setattr(Path, "home", lambda: tmp_path) pid_file = tmp_path / ".headroom" / "deploy" / "default" / "runner.pid" pid_file.parent.mkdir(parents=True) pid_file.write_text("123", encoding="utf-8") monkeypatch.setattr("headroom.install.runtime.os.kill", lambda pid, sig: None) python_manifest = DeploymentManifest( profile="default", preset="persistent-service", runtime_kind="python", supervisor_kind="service", scope="user", provider_mode="manual", targets=[], port=8787, host="127.0.0.1", backend="anthropic", ) assert runtime_status(python_manifest) == "running"