headroom/tests/test_install/test_runtime.py
Shubham Srivastava c3db8e47f8
fix(install): default docker image to headroomlabs-ai GHCR registry (#1867) (#2039)
## Description

The GitHub repository was transferred from `chopratejas/headroom` to
`headroomlabs-ai/headroom`. GitHub 301-redirects transferred repos for
web and git operations, but **GitHub Container Registry (GHCR) does
not** — the old package `ghcr.io/chopratejas/headroom` is now orphaned
and frozen (its `latest` tag stopped advancing at `0.27.0`), while CI
publishes new images to `ghcr.io/headroomlabs-ai/headroom` (the workflow
derives the path from `${{ github.repository }}`).

Headroom's install tooling still defaulted to the dead path, so
`headroom install apply --preset persistent-docker`, `headroom init`,
and the standalone install scripts all pulled a stale `0.27.0` image
instead of the current release.

This changes every docker-image **default** to
`ghcr.io/headroomlabs-ai/headroom:latest`.

Closes #1867

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

- `headroom/install/models.py` — `InstallManifest.image` default.
- `headroom/cli/install.py` — `--image` Click option default.
- `headroom/cli/init.py` — two `InstallManifest(...)` image args.
- `scripts/install.sh` / `scripts/install.ps1` — `IMAGE_DEFAULT` /
`$ImageDefault` plus the `--image` help-text default.
- `docker/docker-compose.native.yml` — image default in both services.
- `tests/test_install/test_planner.py` (4) and
`tests/test_install/test_runtime.py` (5) — updated the assertions that
pinned the old image (including `assert "<image>" in command`), so they
now verify the corrected registry threads through the planner and docker
runtime command.

Scope note: `github.com/chopratejas/...` links and the plugin
marketplace slug are intentionally **not** changed — GitHub redirects
those, so they still work. Only the genuinely-dead GHCR image references
are touched. No new dependency, no new abstraction.

## 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
$ uv run pytest tests/test_install/ -q
99 passed, 1 skipped in 48.34s

$ uv run ruff check headroom/install/models.py headroom/cli/install.py headroom/cli/init.py
All checks passed!

$ grep -rn "ghcr.io/chopratejas/headroom" headroom/ scripts/ docker/ tests/
# (no source matches — every default now points at headroomlabs-ai)
```

The install-test assertions pinned the old image, so they fail against
the old defaults and pass after the fix — they are the regression guard.

## Real Behavior Proof

- **Environment:** Windows 11, Python 3.13.5, headroom installed from
this branch (editable, via `uv`).
- **Exact command / steps:** The dead-registry claim is verifiable at
the registry level, independent of a release:
  ```text
docker pull ghcr.io/chopratejas/headroom:latest # old default → 0.27.0
(frozen / orphaned)
docker pull ghcr.io/headroomlabs-ai/headroom:latest # new default →
current release
  ```
And the install pipeline now emits the correct image (covered by
`tests/test_install/test_runtime.py`, which asserts the resolved docker
command contains `ghcr.io/headroomlabs-ai/headroom:latest`).
- **Observed result:** `grep` confirms no `ghcr.io/chopratejas/headroom`
default remains in code, scripts, compose, or tests;
`tests/test_install/` is green (99 passed) with the corrected image
asserted end-to-end through planner → runtime command.
- **Not tested:** A live `docker pull` of both tags on this specific
machine (no local Docker daemon guaranteed) — the registry difference is
reproducible by anyone running the two `docker pull` commands above; and
an end-to-end `headroom install apply --preset persistent-docker`
against a real Docker host.

## 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
- [x] I have updated the CHANGELOG.md if applicable

## Screenshots (if applicable)

N/A

## Additional Notes

- Documentation checklist item is N/A — no user-facing docs surface
beyond the CHANGELOG entry.
- `mypy` left unchecked — not run as part of this verification; the
change is a string-default swap with no type-level surface.
- The `--image` help text and `docker-compose.native.yml` were included
so every user-facing default is consistent; only the dead GHCR image
string was changed.

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-07-13 14:01:28 -04:00

696 lines
24 KiB
Python

from __future__ import annotations
import os
import signal
import subprocess
import sys
import types
from pathlib import Path
import pytest
from headroom.install.models import DeploymentManifest, InstallPreset
from headroom.install.runtime import (
_clear_pid,
_deployment_env,
_mount_source,
_read_pid,
_runtime_env,
_write_pid,
acquire_runtime_start_lock,
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/headroomlabs-ai/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/headroomlabs-ai/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_docker_manifest_env_beats_host_passthrough(
monkeypatch, tmp_path: Path
) -> None:
"""A manifest value must win over a conflicting host export.
The manifest pins ``HEADROOM_BACKEND=anthropic``; the host process has a
stale ``HEADROOM_BACKEND=anyllm`` exported. Both start with the
``HEADROOM_`` passthrough prefix, so without the dedupe the command emits
``--env HEADROOM_BACKEND=anthropic`` and then a bare ``--env
HEADROOM_BACKEND``. Docker resolves duplicate ``--env`` last-wins, so the
bare passthrough reads the host value and silently overrides the manifest,
diverging the container from its deployment config.
"""
monkeypatch.setattr(Path, "home", lambda: tmp_path)
monkeypatch.setenv("HEADROOM_BACKEND", "anyllm")
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", "HEADROOM_BACKEND": "anthropic"},
proxy_args=["--host", "127.0.0.1", "--port", "8787"],
)
command = build_runtime_command(manifest)
# The manifest value is emitted...
assert "HEADROOM_BACKEND=anthropic" in command
# ...and no bare `--env HEADROOM_BACKEND` follows it to pull in the host
# value. `in` on the list matches the exact token, so the pinned
# `HEADROOM_BACKEND=anthropic` element does not count here.
assert "HEADROOM_BACKEND" not 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/headroomlabs-ai/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_build_runtime_command_for_docker_does_not_duplicate_entrypoint(
monkeypatch, tmp_path: Path
) -> None:
"""The image ENTRYPOINT is already ``["headroom", "proxy"]`` (Dockerfile),
so the args appended after the image name must NOT re-add ``headroom proxy``
or Docker runs ``headroom proxy headroom proxy ...`` and Click aborts with
"Got unexpected extra arguments (headroom proxy)" (issue #833)."""
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/headroomlabs-ai/headroom:latest",
base_env={"HEADROOM_PORT": "8787"},
proxy_args=["--host", "127.0.0.1", "--port", "8787", "--backend", "anthropic"],
)
command = build_runtime_command(manifest)
# Everything after the image name is what Docker appends to the ENTRYPOINT.
image_idx = command.index(manifest.image)
container_args = command[image_idx + 1 :]
assert "headroom" not in container_args, (
f"container args re-add the ENTRYPOINT — got {container_args}"
)
assert "proxy" not in container_args, (
f"container args re-add the ENTRYPOINT — got {container_args}"
)
# The container must still bind on all interfaces and keep the real flags.
assert container_args[:2] == ["--host", "0.0.0.0"]
assert container_args[2:] == ["--port", "8787", "--backend", "anthropic"]
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/headroomlabs-ai/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_runtime_start_lock_is_nonblocking(monkeypatch, tmp_path: Path) -> None:
monkeypatch.setattr(Path, "home", lambda: tmp_path)
with acquire_runtime_start_lock("default") as first_acquired:
assert first_acquired is True
with acquire_runtime_start_lock("default") as second_acquired:
assert second_acquired is False
with acquire_runtime_start_lock("default") as acquired_after_release:
assert acquired_after_release is True
def test_runtime_start_lock_blocks_another_process(monkeypatch, tmp_path: Path) -> None:
monkeypatch.setattr(Path, "home", lambda: tmp_path)
script = (
"from headroom.install.runtime import acquire_runtime_start_lock\n"
"with acquire_runtime_start_lock('default') as acquired:\n"
" print(acquired)\n"
)
env = {
**os.environ,
"HOME": str(tmp_path),
"USERPROFILE": str(tmp_path),
"PYTHONPATH": str(Path.cwd()),
}
with acquire_runtime_start_lock("default") as acquired:
assert acquired is True
result = subprocess.run(
[sys.executable, "-c", script],
capture_output=True,
check=True,
env=env,
text=True,
)
assert result.stdout.strip() == "False"
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_detached_agent_closes_parent_log_fd(monkeypatch, tmp_path: Path) -> None:
"""The parent must close its copy of the log file after Popen.
The child inherits the descriptor, so leaving the parent's copy open
leaks one fd per call and pins the log file open against rotation.
"""
monkeypatch.setattr(Path, "home", lambda: tmp_path)
monkeypatch.setattr("headroom.install.runtime.resolve_headroom_command", lambda: ["headroom"])
monkeypatch.setattr("headroom.install.runtime.sys.platform", "linux")
captured: dict[str, object] = {}
class FakeProc:
pid = 999
def fake_popen(command: list[str], **kwargs):
captured["stdout"] = kwargs["stdout"]
captured["stderr"] = kwargs["stderr"]
return FakeProc()
monkeypatch.setattr("headroom.install.runtime.subprocess.Popen", fake_popen)
start_detached_agent("demo")
log_handle = captured["stdout"]
# Same handle is passed to both streams, and the parent closed it.
assert captured["stderr"] is log_handle
assert log_handle.closed is True
def test_start_detached_agent_closes_log_fd_when_popen_raises(monkeypatch, tmp_path: Path) -> None:
"""A Popen failure must not leak the just-opened log file handle."""
monkeypatch.setattr(Path, "home", lambda: tmp_path)
monkeypatch.setattr("headroom.install.runtime.resolve_headroom_command", lambda: ["headroom"])
monkeypatch.setattr("headroom.install.runtime.sys.platform", "linux")
captured: dict[str, object] = {}
def boom(command: list[str], **kwargs):
captured["stdout"] = kwargs["stdout"]
raise OSError("spawn failed")
monkeypatch.setattr("headroom.install.runtime.subprocess.Popen", boom)
with pytest.raises(OSError, match="spawn failed"):
start_detached_agent("demo")
assert captured["stdout"].closed is True
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.pid_alive", lambda pid: False)
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.pid_alive", lambda pid: True)
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"
def _python_service_manifest() -> DeploymentManifest:
return 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",
)
def test_runtime_status_reports_live_pid_without_terminating(monkeypatch, tmp_path: Path) -> None:
"""#1544: status on a live detached PID stays 'running' and never signals it."""
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("25212", encoding="utf-8")
def fail_kill(pid: int, sig: int) -> None:
raise AssertionError(f"status must not signal the live proxy (pid={pid}, sig={sig})")
monkeypatch.setattr("headroom.install.runtime.os.kill", fail_kill)
monkeypatch.setattr("headroom.install.runtime.pid_alive", lambda pid: True)
assert runtime_status(_python_service_manifest()) == "running"
assert pid_file.exists() # status left the deployment untouched
def test_runtime_status_survives_winerror87_systemerror(monkeypatch, tmp_path: Path) -> None:
"""#1544: a WinError 87 SystemError from the liveness probe yields 'stopped', not a crash."""
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("25212", encoding="utf-8")
# Force the psutil fast-path to bail so the os.kill fallback runs...
fake_psutil = types.SimpleNamespace(
pid_exists=lambda pid: (_ for _ in ()).throw(RuntimeError("no psutil"))
)
monkeypatch.setitem(sys.modules, "psutil", fake_psutil)
# ...where Windows surfaces WinError 87 as a SystemError, not an OSError.
monkeypatch.setattr(
"headroom._subprocess.os.kill",
lambda pid, sig: (_ for _ in ()).throw(SystemError("WinError 87")),
)
assert runtime_status(_python_service_manifest()) == "stopped"