diff --git a/headroom/cli/wrap.py b/headroom/cli/wrap.py index 966d150ac..c3d42af40 100644 --- a/headroom/cli/wrap.py +++ b/headroom/cli/wrap.py @@ -3598,9 +3598,16 @@ def _proxy_version(payload: dict[str, Any] | None) -> str | None: def _proxy_needs_version_restart(payload: dict[str, Any] | None) -> bool: """Return True when a running Headroom proxy uses a different package version.""" running_version = _proxy_version(payload) - running_release = _normalize_release_version(running_version) # -dev is a display marker for source builds; compare the base release so a - # dev CLI still restarts a stale proxy on a real version difference. + # dev CLI still restarts a stale proxy on a real version difference. The + # suffix must be stripped from the running version too: with it intact, + # "0.34.0-dev" fails the release regex, normalizes to None, and the check + # below silently never fires for source-built proxies. The scheme is always + # "-dev" (get_version in headroom/_version.py); any future "-rcN" or + # "-dev.N" style would need stripping here as well. + running_release = _normalize_release_version( + running_version.removesuffix("-dev") if running_version is not None else None + ) current_release = _normalize_release_version(_HEADROOM_VERSION.removesuffix("-dev")) return ( running_release is not None diff --git a/tests/test_cli/test_wrap_persistent.py b/tests/test_cli/test_wrap_persistent.py index 740b81afc..a3649b9ed 100644 --- a/tests/test_cli/test_wrap_persistent.py +++ b/tests/test_cli/test_wrap_persistent.py @@ -332,6 +332,28 @@ def test_proxy_version_restart_ignores_non_release_source_labels(monkeypatch) -> assert wrap_cli._proxy_needs_version_restart({"version": "0.29.0"}) is True +def test_proxy_version_restart_strips_dev_suffix_on_both_sides(monkeypatch) -> None: + """A dev-built proxy must be compared by its base release, like the CLI is. + + Regression: "0.34.0-dev" failed the release regex, normalized to None, and + the check never fired, so a stale source-built proxy was silently reused + forever (observed: a 21-day-old 0.34.0-dev proxy reused by a 0.37.0-dev + wrap). + """ + monkeypatch.setattr(wrap_cli, "_HEADROOM_VERSION", "0.37.0-dev") + assert wrap_cli._proxy_needs_version_restart({"version": "0.34.0-dev"}) is True + assert wrap_cli._proxy_needs_version_restart({"version": "0.37.0-dev"}) is False + assert wrap_cli._proxy_needs_version_restart({"version": "0.36.4"}) is True + + monkeypatch.setattr(wrap_cli, "_HEADROOM_VERSION", "0.37.0") + assert wrap_cli._proxy_needs_version_restart({"version": "0.36.4-dev"}) is True + + # Unparseable running versions still never trigger a restart. + assert wrap_cli._proxy_needs_version_restart({"version": "unknown"}) is False + assert wrap_cli._proxy_needs_version_restart(None) is False + assert wrap_cli._proxy_needs_version_restart({}) is False + + def test_ensure_proxy_restarts_ephemeral_proxy_for_openai_api_url_mismatch(monkeypatch) -> None: calls: list[object] = [] health = {