headroom/tests/test_cli/test_wrap_persistent.py
GUOHAO LIU b4205c68e6
fix(wrap): replace stale-proxy detection with Vite-style port fallback (#1406)
## Description

Replace the 180-line process-killing approach with a 15-line Vite-style
socket.bind probe: if port is busy (EADDRINUSE or EACCES), try the next
available port.

Pure stdlib -- no /proc, no lsof, no subprocess -- works identically on
Linux/macOS/Windows.

## Problem

When `headroom wrap <agent>` is killed without proper cleanup (window
close, SSH timeout, crash), the background proxy becomes orphaned and
holds the port. The next `headroom wrap` on the same port would wait
30-45 seconds then fail with a confusing error.

This PR takes a simpler, safer approach: find the next available port.
No process detection, no killing.

### Related issues

- **#589** (Port 8787 reserved by Windows) -- partially addressed:
EACCES is now skipped together with EADDRINUSE
- **#804** (Shared proxy killed by exiting session) -- already fixed
upstream via `_live_proxy_clients` marker files; this PR doesn't touch
that code

## Type of Change

- [x] New feature (non-breaking change that adds functionality)
- [x] Code refactoring (no functional changes)

## Changes Made

- `headroom/cli/wrap.py`: Add `_find_available_port()` -- socket.bind
loop that skips EADDRINUSE (busy) and EACCES (reserved/privileged)
ports, returns first available port in range. Replace `_ensure_proxy`
port-bind check with auto-fallback call to `_find_available_port`.
Remove `_ensure_port_free()` call from `_start_proxy()`. Remove 8 dead
functions: `_find_process_on_port`, `_linux_find_process_on_port`,
`_resolve_inode_to_pid`, `_is_headroom_proxy`, `_read_process_cmdline`,
`_kill_process`, `_ensure_port_free`, `_format_unbindable_port_error`.
- `tests/test_cli/test_wrap_helpers.py`: Remove 14 old
`TestEnsurePortFree` tests (mocked /proc parsing, process killing). Add
6 new `TestFindAvailablePort` tests covering: port free, first port
busy, multiple busy, EACCES skipped, unexpected error propagated, range
exhausted.
- `tests/test_cli/test_wrap_persistent.py`: Adapt persistence tests for
`_find_available_port` mock. Rewrite unbindable-port test to use new
error path.

Zero new dependencies. Zero changes to core proxy server, MCP,
compression, or providers.

## Testing

- [x] Unit tests pass (`python -m pytest tests/test_cli/ -v`)
- [x] New tests added for new functionality

### Test Output

```
> python -m pytest tests/test_cli/test_wrap_helpers.py::TestFindAvailablePort -v --no-header
============================= 6 passed ==============================
test_port_free_returns_same PASSED
test_port_busy_finds_next PASSED
test_multiple_busy_ports PASSED
test_propagates_unexpected_error PASSED
test_propagates_eaddrinuse_with_eacces PASSED
test_exhausts_range PASSED

> python -m pytest tests/test_cli/ -q
============================= 445 passed in 8.40s ==============================
```

## Real Behavior Proof

- Environment: Ubuntu 24.04 x86_64, Python 3.12.3
- Exact command / steps: Ran `python -m pytest
tests/test_cli/test_wrap_helpers.py::TestFindAvailablePort -v` -- 6/6
pass for port fallback. Ran full test suite `python -m pytest
tests/test_cli/ -q` -- 445/445 pass.
- Observed result: `_find_available_port(8787)` returns 8787 when free,
8788 when 8787 is busy. EACCES skipped same as EADDRINUSE. Non-retryable
errors (EADDRNOTAVAIL) propagate immediately.
- Not tested: Windows EACCES fallback (no Windows CI runner). macOS port
fallback (no macOS runner). Code path is identical across platforms
(stdlib socket only).

## 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] 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
2026-07-07 12:10:52 -05:00

840 lines
30 KiB
Python

from __future__ import annotations
import errno
import click
import pytest
import headroom.cli.wrap as wrap_cli
@pytest.fixture(autouse=True)
def _no_attached_wrappers(monkeypatch: pytest.MonkeyPatch) -> None:
"""Default: no other wrap clients attached, so restart paths are hermetic.
The ephemeral restart guards consult ``_live_proxy_clients``; without this, a
real ``headroom wrap`` session on the dev's machine could make these tests
flaky. Individual tests override this to simulate attached wrappers.
"""
monkeypatch.setattr(wrap_cli, "_live_proxy_clients", lambda *a, **kw: [])
class _Manifest:
profile = "default"
preset = "persistent-service"
supervisor_kind = "service"
health_url = "http://127.0.0.1:8787/readyz"
def test_ensure_proxy_recovers_matching_persistent_deployment(monkeypatch) -> None:
calls: list[str] = []
monkeypatch.setattr(wrap_cli, "_check_proxy", lambda port: False)
monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: _Manifest())
monkeypatch.setattr("headroom.install.health.probe_ready", lambda url: False)
monkeypatch.setattr(
"headroom.install.supervisors.start_supervisor",
lambda manifest: calls.append(f"start:{manifest.profile}"),
)
monkeypatch.setattr(
"headroom.install.runtime.wait_ready", lambda manifest, timeout_seconds=45: True
)
monkeypatch.setattr(
wrap_cli,
"_start_proxy",
lambda *args, **kwargs: (_ for _ in ()).throw(
AssertionError("ephemeral proxy should not start")
),
)
proc, actual_port = wrap_cli._ensure_proxy(8787, False)
assert proc is None
assert actual_port == 8787
assert calls == ["start:default"]
def test_ensure_proxy_recovers_persistent_deployment_when_socket_is_bound(monkeypatch) -> None:
calls: list[str] = []
monkeypatch.setattr(wrap_cli, "_check_proxy", lambda port: True)
monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: _Manifest())
monkeypatch.setattr("headroom.install.health.probe_ready", lambda url: False)
monkeypatch.setattr(
"headroom.install.supervisors.start_supervisor",
lambda manifest: calls.append(f"start:{manifest.profile}"),
)
monkeypatch.setattr(
"headroom.install.runtime.wait_ready", lambda manifest, timeout_seconds=45: True
)
proc, actual_port = wrap_cli._ensure_proxy(8787, False)
assert proc is None
assert actual_port == 8787
assert calls == ["start:default"]
def test_ensure_proxy_rejects_unhealthy_persistent_deployment(monkeypatch) -> None:
monkeypatch.setattr(wrap_cli, "_check_proxy", lambda port: True)
monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: _Manifest())
monkeypatch.setattr("headroom.install.health.probe_ready", lambda url: False)
monkeypatch.setattr(wrap_cli, "_recover_persistent_proxy", lambda port: False)
try:
wrap_cli._ensure_proxy(8787, False)
except click.ClickException as exc:
assert "is not healthy" in str(exc)
else:
raise AssertionError("expected unhealthy persistent deployment to raise")
def test_ensure_proxy_falls_back_when_persistent_manifest_is_stale(monkeypatch) -> None:
calls: list[str] = []
monkeypatch.setattr(wrap_cli, "_check_proxy", lambda port: False)
monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: _Manifest())
monkeypatch.setattr("headroom.install.health.probe_ready", lambda url: False)
monkeypatch.setattr(wrap_cli, "_recover_persistent_proxy", lambda port: False)
monkeypatch.setattr(wrap_cli, "_port_bind_error", lambda port: None)
monkeypatch.setattr(wrap_cli, "_find_available_port", lambda port, **kw: port)
monkeypatch.setattr(wrap_cli, "_start_proxy", lambda *args, **kwargs: calls.append("start"))
proc, actual_port = wrap_cli._ensure_proxy(8787, False)
assert proc is None
assert actual_port == 8787
assert calls == ["start"]
def test_ensure_proxy_reports_unbindable_port_before_starting_subprocess(monkeypatch) -> None:
calls: list[str] = []
monkeypatch.setattr(wrap_cli, "_check_proxy", lambda port: False)
monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: None)
monkeypatch.setattr(
wrap_cli,
"_find_available_port",
lambda port, **kw: (_ for _ in ()).throw(
OSError(errno.EADDRNOTAVAIL, "address not available")
),
)
monkeypatch.setattr(wrap_cli, "_start_proxy", lambda *args, **kwargs: calls.append("start"))
try:
wrap_cli._ensure_proxy(8787, False, agent_type="cursor")
except click.ClickException as exc:
message = str(exc)
else:
raise AssertionError("expected unbindable port to raise before starting proxy")
assert "Port 8787 is unavailable" in message
assert calls == []
def test_ensure_proxy_restarts_idle_stale_persistent_deployment(monkeypatch) -> None:
calls: list[str] = []
health = {
"version": "0.0.1",
"runtime": {"websocket_sessions": {"active_sessions": 0, "active_relay_tasks": 0}},
"config": {"pid": 12345},
}
monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: _Manifest())
monkeypatch.setattr("headroom.install.health.probe_ready", lambda url: True)
monkeypatch.setattr(wrap_cli, "_query_proxy_health", lambda port: health)
monkeypatch.setattr(
wrap_cli,
"_restart_persistent_proxy",
lambda manifest, port: calls.append(f"restart:{manifest.profile}:{port}") or True,
)
monkeypatch.setattr(
wrap_cli,
"_start_proxy",
lambda *args, **kwargs: (_ for _ in ()).throw(
AssertionError("ephemeral proxy should not start")
),
)
proc, actual_port = wrap_cli._ensure_proxy(8787, False)
assert proc is None
assert actual_port == 8787
assert calls == ["restart:default:8787"]
def test_ensure_proxy_leaves_active_stale_persistent_deployment_running(monkeypatch) -> None:
health = {
"version": "0.0.1",
"runtime": {"websocket_sessions": {"active_sessions": 1, "active_relay_tasks": 2}},
"config": {"pid": 12345},
}
monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: _Manifest())
monkeypatch.setattr("headroom.install.health.probe_ready", lambda url: True)
monkeypatch.setattr(wrap_cli, "_query_proxy_health", lambda port: health)
monkeypatch.setattr(
wrap_cli,
"_restart_persistent_proxy",
lambda *args, **kwargs: (_ for _ in ()).throw(
AssertionError("active deployment should not restart")
),
)
proc, actual_port = wrap_cli._ensure_proxy(8787, False)
assert proc is None
assert actual_port == 8787
def test_ensure_proxy_defers_persistent_restart_when_http_wrapper_attached(
monkeypatch,
) -> None:
"""A stale persistent proxy is left running while marker-tracked HTTP
wrappers are attached, even when WebSocket session count is zero."""
health = {
"version": "0.0.1",
"runtime": {"websocket_sessions": {"active_sessions": 0, "active_relay_tasks": 0}},
"config": {"pid": 12345},
}
monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: _Manifest())
monkeypatch.setattr("headroom.install.health.probe_ready", lambda url: True)
monkeypatch.setattr(wrap_cli, "_query_proxy_health", lambda port: health)
monkeypatch.setattr(wrap_cli, "_live_proxy_clients", lambda *a, **kw: [999])
monkeypatch.setattr(
wrap_cli,
"_restart_persistent_proxy",
lambda *args, **kwargs: (_ for _ in ()).throw(
AssertionError("attached persistent proxy should not restart")
),
)
monkeypatch.setattr(
wrap_cli,
"_start_proxy",
lambda *args, **kwargs: (_ for _ in ()).throw(
AssertionError("replacement proxy should not start")
),
)
proc, actual_port = wrap_cli._ensure_proxy(8787, False)
assert proc is None
assert actual_port == 8787
def test_find_persistent_manifest_prefers_default_profile(monkeypatch) -> None:
class DefaultManifest:
profile = "default"
port = 8787
class OtherManifest:
profile = "custom"
port = 8787
monkeypatch.setattr(
"headroom.install.state.list_manifests",
lambda: [OtherManifest(), DefaultManifest()],
)
manifest = wrap_cli._find_persistent_manifest(8787)
assert manifest.profile == "default"
def test_recover_persistent_proxy_reuses_healthy_deployment(monkeypatch) -> None:
monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: _Manifest())
monkeypatch.setattr("headroom.install.health.probe_ready", lambda url: True)
assert wrap_cli._recover_persistent_proxy(8787) is True
def test_recover_persistent_proxy_warns_for_task_deployment(monkeypatch) -> None:
class TaskManifest(_Manifest):
supervisor_kind = "task"
monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: TaskManifest())
monkeypatch.setattr("headroom.install.health.probe_ready", lambda url: False)
assert wrap_cli._recover_persistent_proxy(8787) is False
def test_ensure_proxy_restarts_idle_stale_ephemeral_proxy(monkeypatch) -> None:
calls: list[object] = []
health = {
"version": "0.0.1",
"runtime": {"websocket_sessions": {"active_sessions": 0, "active_relay_tasks": 0}},
"config": {"pid": "12345", "memory": False, "learn": False, "code_graph": False},
}
monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: None)
monkeypatch.setattr(wrap_cli, "_check_proxy", lambda port: len(calls) == 0)
monkeypatch.setattr(wrap_cli, "_query_proxy_health", lambda port: health)
monkeypatch.setattr(wrap_cli, "_port_bind_error", lambda port: None)
monkeypatch.setattr(
wrap_cli,
"_kill_proxy_by_pid",
lambda pid, port: calls.append(("kill", pid, port)) or True,
)
monkeypatch.setattr(
wrap_cli,
"_start_proxy",
lambda *args, **kwargs: calls.append(("start", args, kwargs)),
)
proc, actual_port = wrap_cli._ensure_proxy(8787, False)
assert proc is None
assert actual_port == 8787
assert calls[0] == ("kill", 12345, 8787)
assert calls[1][0] == "start"
def test_ensure_proxy_restarts_ephemeral_proxy_for_openai_api_url_mismatch(monkeypatch) -> None:
calls: list[object] = []
health = {
"version": wrap_cli._HEADROOM_VERSION,
"runtime": {"websocket_sessions": {"active_sessions": 0, "active_relay_tasks": 0}},
"config": {
"pid": "12345",
"memory": False,
"learn": False,
"code_graph": False,
"openai_api_url": "https://api.githubcopilot.com",
},
}
monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: None)
monkeypatch.setattr(wrap_cli, "_check_proxy", lambda port: len(calls) == 0)
monkeypatch.setattr(wrap_cli, "_query_proxy_health", lambda port: health)
monkeypatch.setattr(wrap_cli, "_port_bind_error", lambda port: None)
monkeypatch.setattr(
wrap_cli,
"_kill_proxy_by_pid",
lambda pid, port: calls.append(("kill", pid, port)) or True,
)
monkeypatch.setattr(
wrap_cli,
"_start_proxy",
lambda *args, **kwargs: calls.append(("start", args, kwargs)),
)
proc, actual_port = wrap_cli._ensure_proxy(
8787,
False,
openai_api_url="https://api.individual.githubcopilot.com",
)
assert proc is None
assert actual_port == 8787
assert calls[0] == ("kill", 12345, 8787)
assert calls[1][0] == "start"
assert calls[1][2]["openai_api_url"] == "https://api.individual.githubcopilot.com"
def test_ensure_proxy_reuses_agent_proxy_without_savings_profile(monkeypatch) -> None:
health = {
"version": wrap_cli._HEADROOM_VERSION,
"runtime": {"websocket_sessions": {"active_sessions": 0, "active_relay_tasks": 0}},
"config": {"pid": "12345", "memory": False, "learn": False, "code_graph": False},
}
monkeypatch.delenv("HEADROOM_SAVINGS_PROFILE", raising=False)
monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: None)
monkeypatch.setattr(wrap_cli, "_check_proxy", lambda port: True)
monkeypatch.setattr(wrap_cli, "_query_proxy_health", lambda port: health)
monkeypatch.setattr(
wrap_cli,
"_kill_proxy_by_pid",
lambda *args, **kwargs: (_ for _ in ()).throw(
AssertionError("default agent proxy should not restart for savings profile")
),
)
monkeypatch.setattr(
wrap_cli,
"_start_proxy",
lambda *args, **kwargs: (_ for _ in ()).throw(
AssertionError("replacement proxy should not start")
),
)
proc, actual_port = wrap_cli._ensure_proxy(8787, False, agent_type="codex")
assert proc is None
assert actual_port == 8787
def test_ensure_proxy_restarts_for_explicit_agent_savings_profile(monkeypatch) -> None:
calls: list[object] = []
health = {
"version": wrap_cli._HEADROOM_VERSION,
"runtime": {"websocket_sessions": {"active_sessions": 0, "active_relay_tasks": 0}},
"config": {"pid": "12345", "memory": False, "learn": False, "code_graph": False},
}
monkeypatch.setenv("HEADROOM_SAVINGS_PROFILE", "agent-90")
monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: None)
monkeypatch.setattr(wrap_cli, "_check_proxy", lambda port: len(calls) == 0)
monkeypatch.setattr(wrap_cli, "_query_proxy_health", lambda port: health)
monkeypatch.setattr(wrap_cli, "_port_bind_error", lambda port: None)
monkeypatch.setattr(
wrap_cli,
"_kill_proxy_by_pid",
lambda pid, port: calls.append(("kill", pid, port)) or True,
)
monkeypatch.setattr(
wrap_cli,
"_start_proxy",
lambda *args, **kwargs: calls.append(("start", args, kwargs)),
)
proc, actual_port = wrap_cli._ensure_proxy(8787, False, agent_type="codex")
assert proc is None
assert actual_port == 8787
assert calls[0] == ("kill", 12345, 8787)
assert calls[1][0] == "start"
def test_ensure_proxy_reuses_agent_proxy_with_savings_profile(monkeypatch) -> None:
health = {
"version": wrap_cli._HEADROOM_VERSION,
"runtime": {"websocket_sessions": {"active_sessions": 0, "active_relay_tasks": 0}},
"config": {
"pid": "12345",
"memory": False,
"learn": False,
"code_graph": False,
"savings_profile": "agent-90",
"target_ratio": 0.10,
"compress_user_messages": True,
"compress_system_messages": True,
"protect_recent": 2,
"protect_analysis_context": True,
"min_tokens_to_crush": 120,
"max_items_after_crush": 8,
"smart_crusher_with_compaction": False,
"accuracy_guard": "strict",
},
}
monkeypatch.setenv("HEADROOM_SAVINGS_PROFILE", "agent-90")
monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: None)
monkeypatch.setattr(wrap_cli, "_check_proxy", lambda port: True)
monkeypatch.setattr(wrap_cli, "_query_proxy_health", lambda port: health)
monkeypatch.setattr(
wrap_cli,
"_kill_proxy_by_pid",
lambda *args, **kwargs: (_ for _ in ()).throw(
AssertionError("configured proxy should not restart")
),
)
monkeypatch.setattr(
wrap_cli,
"_start_proxy",
lambda *args, **kwargs: (_ for _ in ()).throw(
AssertionError("replacement proxy should not start")
),
)
proc, actual_port = wrap_cli._ensure_proxy(8787, False, agent_type="cursor")
assert proc is None
assert actual_port == 8787
def test_ensure_proxy_leaves_active_stale_ephemeral_proxy_running(monkeypatch) -> None:
health = {
"version": "0.0.1",
"runtime": {"websocket_sessions": {"active_sessions": 2, "active_relay_tasks": 2}},
"config": {"pid": "12345", "memory": False, "learn": False, "code_graph": False},
}
monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: None)
monkeypatch.setattr(wrap_cli, "_check_proxy", lambda port: True)
monkeypatch.setattr(wrap_cli, "_query_proxy_health", lambda port: health)
monkeypatch.setattr(
wrap_cli,
"_kill_proxy_by_pid",
lambda *args, **kwargs: (_ for _ in ()).throw(
AssertionError("active proxy should not be killed")
),
)
monkeypatch.setattr(
wrap_cli,
"_start_proxy",
lambda *args, **kwargs: (_ for _ in ()).throw(
AssertionError("replacement proxy should not start")
),
)
proc, actual_port = wrap_cli._ensure_proxy(8787, False)
assert proc is None
assert actual_port == 8787
def test_ensure_proxy_defers_version_restart_when_http_wrapper_attached(monkeypatch) -> None:
"""A stale-version proxy is NOT restarted while a marker-tracked HTTP
wrapper is attached, even though the WebSocket session count is zero."""
health = {
"version": "0.0.1", # stale → version restart wanted
# No WebSocket relay sessions — the gap that let the old code kill it.
"runtime": {"websocket_sessions": {"active_sessions": 0, "active_relay_tasks": 0}},
"config": {"pid": "12345", "memory": False, "learn": False, "code_graph": False},
}
monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: None)
monkeypatch.setattr(wrap_cli, "_check_proxy", lambda port: True)
monkeypatch.setattr(wrap_cli, "_query_proxy_health", lambda port: health)
# Another HTTP wrapper (PID 999) is attached per the marker registry.
monkeypatch.setattr(wrap_cli, "_live_proxy_clients", lambda *a, **kw: [999])
monkeypatch.setattr(
wrap_cli,
"_kill_proxy_by_pid",
lambda *args, **kwargs: (_ for _ in ()).throw(
AssertionError("attached proxy must not be killed for a version restart")
),
)
monkeypatch.setattr(
wrap_cli,
"_start_proxy",
lambda *args, **kwargs: (_ for _ in ()).throw(
AssertionError("replacement proxy must not start")
),
)
proc, actual_port = wrap_cli._ensure_proxy(8787, False)
assert proc is None
assert actual_port == 8787
def test_ensure_proxy_defers_flag_restart_when_other_wrapper_attached(monkeypatch) -> None:
"""Requesting --memory must not restart the proxy out from under another
attached wrapper; reuse the running proxy as-is instead."""
health = {
"version": wrap_cli._HEADROOM_VERSION, # same version → no version restart
"runtime": {"websocket_sessions": {"active_sessions": 0, "active_relay_tasks": 0}},
# Running proxy lacks `memory`; this session asks for it.
"config": {"pid": "12345", "memory": False, "learn": False, "code_graph": False},
}
monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: None)
monkeypatch.setattr(wrap_cli, "_check_proxy", lambda port: True)
monkeypatch.setattr(wrap_cli, "_query_proxy_health", lambda port: health)
monkeypatch.setattr(wrap_cli, "_live_proxy_clients", lambda *a, **kw: [999])
monkeypatch.setattr(
wrap_cli,
"_kill_proxy_by_pid",
lambda *args, **kwargs: (_ for _ in ()).throw(
AssertionError("attached proxy must not be killed to add flags")
),
)
monkeypatch.setattr(
wrap_cli,
"_start_proxy",
lambda *args, **kwargs: (_ for _ in ()).throw(
AssertionError("replacement proxy must not start")
),
)
proc, actual_port = wrap_cli._ensure_proxy(8787, False, memory=True)
assert proc is None
assert actual_port == 8787
def test_ensure_proxy_restarts_for_flags_when_no_other_wrapper(monkeypatch) -> None:
"""Control: with no other wrapper attached, a missing-flag restart still
happens — the guard must not block the single-client upgrade path."""
calls: list[object] = []
health = {
"version": wrap_cli._HEADROOM_VERSION,
"runtime": {"websocket_sessions": {"active_sessions": 0, "active_relay_tasks": 0}},
"config": {"pid": "12345", "memory": False, "learn": False, "code_graph": False},
}
monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: None)
monkeypatch.setattr(wrap_cli, "_check_proxy", lambda port: len(calls) == 0)
monkeypatch.setattr(wrap_cli, "_query_proxy_health", lambda port: health)
monkeypatch.setattr(wrap_cli, "_live_proxy_clients", lambda *a, **kw: [])
monkeypatch.setattr(wrap_cli, "_port_bind_error", lambda port: None)
monkeypatch.setattr(
wrap_cli,
"_kill_proxy_by_pid",
lambda pid, port: calls.append(("kill", pid, port)) or True,
)
monkeypatch.setattr(
wrap_cli,
"_start_proxy",
lambda *args, **kwargs: calls.append(("start", args, kwargs)),
)
proc, actual_port = wrap_cli._ensure_proxy(8787, False, memory=True)
assert proc is None
assert actual_port == 8787
assert calls[0] == ("kill", 12345, 8787)
assert calls[1][0] == "start"
def test_ensure_proxy_restarts_persistent_deployment_for_feature_mismatch(monkeypatch) -> None:
"""Persistent deployment should restart when requested features differ from running config."""
calls: list[object] = []
health = {
"version": wrap_cli._HEADROOM_VERSION,
"runtime": {"websocket_sessions": {"active_sessions": 0, "active_relay_tasks": 0}},
"config": {
"pid": 12345,
"memory": False,
"learn": False,
"code_graph": False,
"openai_api_url": None,
},
}
monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: _Manifest())
monkeypatch.setattr("headroom.install.health.probe_ready", lambda url: True)
monkeypatch.setattr(wrap_cli, "_query_proxy_health", lambda port: health)
# Persistent proxy is running, so _check_proxy returns True
monkeypatch.setattr(wrap_cli, "_check_proxy", lambda port: True)
monkeypatch.setattr(wrap_cli, "_port_bind_error", lambda port: None)
monkeypatch.setattr(
wrap_cli,
"_kill_proxy_by_pid",
lambda pid, port: calls.append(("kill", pid, port)) or True,
)
monkeypatch.setattr(
wrap_cli,
"_start_proxy",
lambda *args, **kwargs: calls.append(("start", args, kwargs)),
)
# Request openai_api_url that differs from running config (None)
proc, actual_port = wrap_cli._ensure_proxy(
8787,
False,
openai_api_url="https://api.githubcopilot.com",
)
assert proc is None
assert actual_port == 8787
# Proxy should be killed and restarted due to openai_api_url mismatch
assert calls[0] == ("kill", 12345, 8787)
assert calls[1][0] == "start"
assert calls[1][2]["openai_api_url"] == "https://api.githubcopilot.com"
def test_ensure_proxy_restarts_persistent_deployment_for_memory_mismatch(monkeypatch) -> None:
"""Persistent deployment should restart when memory is requested but not enabled."""
calls: list[object] = []
health = {
"version": wrap_cli._HEADROOM_VERSION,
"runtime": {"websocket_sessions": {"active_sessions": 0, "active_relay_tasks": 0}},
"config": {
"pid": 12345,
"memory": False,
"learn": False,
"code_graph": False,
"openai_api_url": None,
},
}
monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: _Manifest())
monkeypatch.setattr("headroom.install.health.probe_ready", lambda url: True)
monkeypatch.setattr(wrap_cli, "_query_proxy_health", lambda port: health)
# Persistent proxy is running, so _check_proxy returns True
monkeypatch.setattr(wrap_cli, "_check_proxy", lambda port: True)
monkeypatch.setattr(wrap_cli, "_port_bind_error", lambda port: None)
monkeypatch.setattr(
wrap_cli,
"_kill_proxy_by_pid",
lambda pid, port: calls.append(("kill", pid, port)) or True,
)
monkeypatch.setattr(
wrap_cli,
"_start_proxy",
lambda *args, **kwargs: calls.append(("start", args, kwargs)),
)
# Request memory that differs from running config (False)
proc, actual_port = wrap_cli._ensure_proxy(8787, False, memory=True)
assert proc is None
assert actual_port == 8787
# Proxy should be killed and restarted due to memory mismatch
assert calls[0] == ("kill", 12345, 8787)
assert calls[1][0] == "start"
def test_ensure_proxy_restarts_recovered_persistent_for_openai_api_url_mismatch(
monkeypatch,
) -> None:
calls: list[object] = []
health = {
"version": wrap_cli._HEADROOM_VERSION,
"runtime": {"websocket_sessions": {"active_sessions": 0, "active_relay_tasks": 0}},
"config": {
"pid": 12345,
"memory": False,
"learn": False,
"code_graph": False,
"openai_api_url": None,
},
}
monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: _Manifest())
monkeypatch.setattr("headroom.install.health.probe_ready", lambda url: False)
monkeypatch.setattr(wrap_cli, "_recover_persistent_proxy", lambda port: True)
monkeypatch.setattr(wrap_cli, "_check_proxy", lambda port: True)
monkeypatch.setattr(wrap_cli, "_query_proxy_health", lambda port: health)
monkeypatch.setattr(
wrap_cli,
"_restart_persistent_proxy",
lambda manifest, port: calls.append(("restart", manifest.profile, port)) or True,
)
monkeypatch.setattr(
wrap_cli,
"_start_proxy",
lambda *args, **kwargs: (_ for _ in ()).throw(
AssertionError("ephemeral proxy should not start")
),
)
proc, actual_port = wrap_cli._ensure_proxy(
8787,
False,
openai_api_url="https://api.business.githubcopilot.com",
)
assert proc is None
assert actual_port == 8787
assert calls == [("restart", "default", 8787)]
def test_ensure_proxy_restarts_recovered_persistent_when_config_unavailable(monkeypatch) -> None:
calls: list[object] = []
monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: _Manifest())
monkeypatch.setattr("headroom.install.health.probe_ready", lambda url: False)
monkeypatch.setattr(wrap_cli, "_recover_persistent_proxy", lambda port: True)
monkeypatch.setattr(wrap_cli, "_check_proxy", lambda port: True)
monkeypatch.setattr(wrap_cli, "_query_proxy_health", lambda port: {"version": "x"})
monkeypatch.setattr(wrap_cli, "_query_proxy_config", lambda port: None)
monkeypatch.setattr(
wrap_cli,
"_restart_persistent_proxy",
lambda manifest, port: calls.append(("restart", manifest.profile, port)) or True,
)
monkeypatch.setattr(
wrap_cli,
"_start_proxy",
lambda *args, **kwargs: (_ for _ in ()).throw(
AssertionError("ephemeral proxy should not start")
),
)
proc, actual_port = wrap_cli._ensure_proxy(
8787,
False,
openai_api_url="https://api.business.githubcopilot.com",
)
assert proc is None
assert actual_port == 8787
assert calls == [("restart", "default", 8787)]
def test_ensure_proxy_reuses_persistent_deployment_when_features_match(monkeypatch) -> None:
"""Persistent deployment should be reused when all requested features match."""
health = {
"version": wrap_cli._HEADROOM_VERSION,
"runtime": {"websocket_sessions": {"active_sessions": 0, "active_relay_tasks": 0}},
"config": {
"pid": 12345,
"memory": True,
"learn": False,
"code_graph": False,
"openai_api_url": "https://api.githubcopilot.com",
},
}
monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: _Manifest())
monkeypatch.setattr("headroom.install.health.probe_ready", lambda url: True)
monkeypatch.setattr(wrap_cli, "_query_proxy_health", lambda port: health)
monkeypatch.setattr(
wrap_cli,
"_restart_persistent_proxy",
lambda *args, **kwargs: (_ for _ in ()).throw(
AssertionError("should not restart when features match")
),
)
monkeypatch.setattr(
wrap_cli,
"_start_proxy",
lambda *args, **kwargs: (_ for _ in ()).throw(
AssertionError("should not start ephemeral proxy when features match")
),
)
# Request same features as running config
proc, actual_port = wrap_cli._ensure_proxy(
8787,
False,
memory=True,
openai_api_url="https://api.githubcopilot.com",
)
assert proc is None
assert actual_port == 8787
def test_ensure_proxy_recovered_persistent_deployment_checks_feature_mismatch(monkeypatch) -> None:
"""Recovered persistent deployments must still restart on feature mismatch.
Regression guard for the recover path: when wrap requests a different
openai_api_url (Copilot subscription), do not early-return right after
recover; run the shared mismatch checks and restart if needed.
"""
calls: list[object] = []
health = {
"version": wrap_cli._HEADROOM_VERSION,
"runtime": {"websocket_sessions": {"active_sessions": 0, "active_relay_tasks": 0}},
"config": {
"pid": "12345",
"memory": False,
"learn": False,
"code_graph": False,
"openai_api_url": None,
},
}
monkeypatch.setattr(wrap_cli, "_find_persistent_manifest", lambda port: _Manifest())
monkeypatch.setattr("headroom.install.health.probe_ready", lambda url: False)
monkeypatch.setattr(wrap_cli, "_recover_persistent_proxy", lambda port: True)
monkeypatch.setattr(wrap_cli, "_check_proxy", lambda port: True)
monkeypatch.setattr(wrap_cli, "_query_proxy_health", lambda port: health)
monkeypatch.setattr(
wrap_cli,
"_restart_persistent_proxy",
lambda manifest, port: calls.append(("restart", manifest.profile, port)) or True,
)
monkeypatch.setattr(
wrap_cli,
"_start_proxy",
lambda *args, **kwargs: (_ for _ in ()).throw(
AssertionError("ephemeral proxy should not start")
),
)
proc, actual_port = wrap_cli._ensure_proxy(
8787,
False,
openai_api_url="https://api.githubcopilot.com",
)
assert proc is None
assert actual_port == 8787
assert calls == [("restart", "default", 8787)]