0ef5fcb1c5
Security / Dependency audit (pip-audit) (push) Has been cancelled
Security / CodeQL (javascript-typescript) (push) Has been cancelled
Security / CodeQL (python) (push) Has been cancelled
Security / Secret scan (gitleaks) (push) Has been cancelled
rust / test (ubuntu) (push) Has been cancelled
rust / simulator e2e (macos-latest) (push) Has been cancelled
rust / simulator e2e (ubuntu-latest) (push) Has been cancelled
rust / simulator e2e (windows-latest) (push) Has been cancelled
rust / wheels (aarch64-apple-darwin) (push) Has been cancelled
rust / wheels (x86_64-unknown-linux-gnu) (push) Has been cancelled
rust / wheels (x86_64-apple-darwin) (push) Has been cancelled
rust / audit (push) Has been cancelled
rust / parity (nightly, allowed to fail during Phase 0) (push) Has been cancelled
CI / commitlint (push) Has been skipped
Dev Containers / validate (.devcontainer/devcontainer.json, default) (push) Failing after 0s
Dev Containers / validate (.devcontainer/memory-stack/devcontainer.json, memory-stack) (push) Failing after 0s
Dev Containers / validate-worktree (push) Failing after 0s
CI / changes (push) Failing after 4s
Deploy Documentation / validate (push) Has been skipped
Deploy Documentation / deploy (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, claude) (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, codex) (push) Failing after 1s
Install Native E2E / install-native (ubuntu-latest) (push) Failing after 1s
OpenCode Plugin / typecheck + build + test (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, copilot) (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
Wrap E2E / docker-wrap-e2e (push) Failing after 1s
Wrap Native E2E / wrap-native (ubuntu-latest) (push) Failing after 1s
Init E2E / docker-init-e2e (push) Failing after 4s
Merge Conflicts / merge-conflicts (push) Failing after 4s
CI / lint (push) Has been cancelled
CI / build-wheel (push) Has been cancelled
CI / build-wheel-windows (push) Has been cancelled
CI / prefetch-model (push) Has been cancelled
CI / test-dashboard-ui (push) Has been cancelled
CI / test (1) (push) Has been cancelled
CI / test (2) (push) Has been cancelled
CI / test (3) (push) Has been cancelled
CI / test (4) (push) Has been cancelled
CI / test-extras (push) Has been cancelled
CI / test-agno (push) Has been cancelled
CI / build (push) Has been cancelled
CI / workflow-validation (push) Has been cancelled
CI / docker-native-e2e (push) Has been cancelled
CI / windows-native-wrapper (push) Has been cancelled
CI / macos-native-wrapper (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / promote-latest (push) Has been cancelled
Init Native E2E / init-native (macos-latest, claude) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, codex) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, copilot) (push) Has been cancelled
Install Native E2E / install-native (macos-latest) (push) Has been cancelled
Wrap Native E2E / wrap-native (macos-latest) (push) Has been cancelled
305 lines
10 KiB
Python
305 lines
10 KiB
Python
"""Coverage for update_check / cli.update helper functions and branches."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.metadata as md
|
|
import sysconfig
|
|
|
|
import pytest
|
|
from click.testing import CliRunner
|
|
|
|
from headroom import update_check as uc
|
|
from headroom.cli import update as up
|
|
from headroom.cli.main import main
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _env(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("HEADROOM_WORKSPACE_DIR", str(tmp_path))
|
|
monkeypatch.setenv("HEADROOM_UPDATE_CHECK", "on")
|
|
monkeypatch.delenv("HEADROOM_STATELESS", raising=False)
|
|
monkeypatch.delenv("CI", raising=False)
|
|
monkeypatch.delenv("HEADROOM_IN_DOCKER", raising=False)
|
|
monkeypatch.delenv("CONDA_PREFIX", raising=False)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# cli.update helpers
|
|
# --------------------------------------------------------------------------- #
|
|
def test_norm_normalizes_and_lowercases():
|
|
assert up._norm("/Foo/Bar").endswith("/foo/bar")
|
|
assert up._norm(None) == ""
|
|
assert up._norm(r"C:\\X\\Y").count("\\") == 0
|
|
|
|
|
|
def test_in_virtualenv_via_prefix(monkeypatch):
|
|
monkeypatch.setattr(up.sys, "prefix", "/venv")
|
|
monkeypatch.setattr(up.sys, "base_prefix", "/usr")
|
|
assert up._in_virtualenv() is True
|
|
|
|
|
|
def test_in_virtualenv_via_conda(monkeypatch):
|
|
monkeypatch.setattr(up.sys, "prefix", "/x")
|
|
monkeypatch.setattr(up.sys, "base_prefix", "/x")
|
|
monkeypatch.setenv("CONDA_PREFIX", "/opt/conda")
|
|
assert up._in_virtualenv() is True
|
|
|
|
|
|
def test_in_virtualenv_false(monkeypatch):
|
|
monkeypatch.setattr(up.sys, "prefix", "/x")
|
|
monkeypatch.setattr(up.sys, "base_prefix", "/x")
|
|
assert up._in_virtualenv() is False
|
|
|
|
|
|
def test_in_docker_env_flag(monkeypatch):
|
|
monkeypatch.setenv("HEADROOM_IN_DOCKER", "1")
|
|
assert up._in_docker() is True
|
|
|
|
|
|
def test_in_docker_default_false():
|
|
# No HEADROOM_IN_DOCKER and (almost certainly) no /.dockerenv on the runner.
|
|
assert isinstance(up._in_docker(), bool)
|
|
|
|
|
|
def test_editable_install_true(monkeypatch):
|
|
class _D:
|
|
def read_text(self, name):
|
|
return '{"dir_info": {"editable": true}}'
|
|
|
|
monkeypatch.setattr(md, "distribution", lambda name: _D())
|
|
assert up._is_editable_install() is True
|
|
|
|
|
|
def test_editable_install_false_when_not_editable(monkeypatch):
|
|
class _D:
|
|
def read_text(self, name):
|
|
return '{"url": "https://pypi.org", "archive_info": {}}'
|
|
|
|
monkeypatch.setattr(md, "distribution", lambda name: _D())
|
|
assert up._is_editable_install() is False
|
|
|
|
|
|
def test_editable_install_false_when_no_direct_url(monkeypatch):
|
|
class _D:
|
|
def read_text(self, name):
|
|
return None
|
|
|
|
monkeypatch.setattr(md, "distribution", lambda name: _D())
|
|
assert up._is_editable_install() is False
|
|
|
|
|
|
def test_editable_install_swallows_errors(monkeypatch):
|
|
def _boom(name):
|
|
raise RuntimeError("nope")
|
|
|
|
monkeypatch.setattr(md, "distribution", _boom)
|
|
assert up._is_editable_install() is False
|
|
|
|
|
|
def test_package_location_handles_missing(monkeypatch):
|
|
def _boom(name):
|
|
raise md.PackageNotFoundError(name)
|
|
|
|
monkeypatch.setattr(md, "distribution", _boom)
|
|
assert up._package_location() is None
|
|
|
|
|
|
def test_user_site_and_membership(monkeypatch):
|
|
monkeypatch.setattr(up, "_user_site", lambda: "/home/u/.local/site")
|
|
assert up._is_user_site_install("/home/u/.local/site/headroom_ai") is True
|
|
assert up._is_user_site_install("/home/u/.local/site") is True
|
|
assert up._is_user_site_install("/usr/lib/python3/site") is False
|
|
assert up._is_user_site_install(None) is False
|
|
|
|
|
|
def test_user_site_no_sibling_prefix_match(monkeypatch):
|
|
# Path-segment containment: "/.../site" must NOT match "/.../site-packages".
|
|
monkeypatch.setattr(up, "_user_site", lambda: "/home/u/.local/site")
|
|
assert up._is_user_site_install("/home/u/.local/site-packages/x") is False
|
|
|
|
|
|
def test_format_cmd_quotes_spaces(monkeypatch):
|
|
monkeypatch.setattr(up.sys, "platform", "linux")
|
|
out = up._format_cmd(["/path with space/python", "-m", "pip", "install", "-U", "headroom-ai"])
|
|
assert "'/path with space/python'" in out
|
|
|
|
|
|
def test_format_cmd_windows(monkeypatch):
|
|
monkeypatch.setattr(up.sys, "platform", "win32")
|
|
out = up._format_cmd([r"C:\\Program Files\\Python\\python.exe", "-m", "pip"])
|
|
assert "Program Files" in out and out.endswith("-m pip")
|
|
|
|
|
|
def test_externally_managed_true(tmp_path, monkeypatch):
|
|
(tmp_path / "EXTERNALLY-MANAGED").write_text("[externally-managed]")
|
|
monkeypatch.setattr(sysconfig, "get_path", lambda key: str(tmp_path))
|
|
assert up._is_externally_managed() is True
|
|
|
|
|
|
def test_externally_managed_false(tmp_path, monkeypatch):
|
|
monkeypatch.setattr(sysconfig, "get_path", lambda key: str(tmp_path))
|
|
assert up._is_externally_managed() is False
|
|
|
|
|
|
def test_user_site_real_returns_str_or_empty():
|
|
assert isinstance(up._user_site(), str)
|
|
|
|
|
|
def test_source_checkout_real_is_bool():
|
|
assert isinstance(up._is_source_checkout(), bool)
|
|
|
|
|
|
def test_package_location_real_runs():
|
|
# Either a normalized path string or None, but the call must not raise.
|
|
assert up._package_location() is None or isinstance(up._package_location(), str)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"platform,needle",
|
|
[("darwin", "brew"), ("win32", "pipx"), ("linux", "distro")],
|
|
)
|
|
def test_managed_env_guidance(monkeypatch, platform, needle):
|
|
monkeypatch.setattr(up.sys, "platform", platform)
|
|
assert needle in up._managed_env_guidance()
|
|
|
|
|
|
def test_spec_with_and_without_extras():
|
|
assert up._spec(None) == "headroom-ai"
|
|
assert up._spec("all") == "headroom-ai[all]"
|
|
assert up._spec("[proxy]") == "headroom-ai[proxy]"
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# cli.update command branches
|
|
# --------------------------------------------------------------------------- #
|
|
def test_update_aborts_on_decline(monkeypatch):
|
|
monkeypatch.setattr(up, "installed_version", lambda: "0.26.0")
|
|
monkeypatch.setattr(up, "fetch_latest_version", lambda **k: "0.27.0")
|
|
monkeypatch.setattr(up, "_is_source_checkout", lambda: False)
|
|
monkeypatch.setattr(up, "_is_editable_install", lambda: False)
|
|
monkeypatch.setattr(up, "_in_docker", lambda: False)
|
|
monkeypatch.setattr(up, "_in_virtualenv", lambda: True)
|
|
|
|
def _no_run(*a, **k):
|
|
raise AssertionError("should not run after decline")
|
|
|
|
monkeypatch.setattr(up.subprocess, "run", _no_run)
|
|
res = CliRunner().invoke(main, ["update"], input="n\n")
|
|
assert res.exit_code == 0
|
|
assert "Aborted" in res.output
|
|
|
|
|
|
def test_update_missing_tool_surfaces_command(monkeypatch):
|
|
monkeypatch.setattr(up, "installed_version", lambda: "0.26.0")
|
|
monkeypatch.setattr(up, "fetch_latest_version", lambda **k: "0.27.0")
|
|
monkeypatch.setattr(up, "_is_source_checkout", lambda: False)
|
|
monkeypatch.setattr(up, "_is_editable_install", lambda: False)
|
|
monkeypatch.setattr(up, "_in_docker", lambda: False)
|
|
monkeypatch.setattr(
|
|
up,
|
|
"detect_install_method",
|
|
lambda extras=None: up.InstallMethod(
|
|
kind="pipx", can_self_update=True, argv=["pipx", "upgrade", "headroom-ai"]
|
|
),
|
|
)
|
|
|
|
def _missing(*a, **k):
|
|
raise FileNotFoundError("pipx")
|
|
|
|
monkeypatch.setattr(up.subprocess, "run", _missing)
|
|
res = CliRunner().invoke(main, ["update", "--yes"])
|
|
assert res.exit_code != 0
|
|
assert "not found on PATH" in res.output
|
|
|
|
|
|
def test_update_externally_managed_refuses_via_command(monkeypatch):
|
|
monkeypatch.setattr(up, "installed_version", lambda: "0.26.0")
|
|
monkeypatch.setattr(up, "fetch_latest_version", lambda **k: "0.27.0")
|
|
monkeypatch.setattr(up, "_is_source_checkout", lambda: False)
|
|
monkeypatch.setattr(up, "_is_editable_install", lambda: False)
|
|
monkeypatch.setattr(up, "_in_docker", lambda: False)
|
|
monkeypatch.setattr(up, "_in_virtualenv", lambda: False)
|
|
monkeypatch.setattr(up, "_is_user_site_install", lambda loc: False)
|
|
monkeypatch.setattr(up, "_is_externally_managed", lambda: True)
|
|
res = CliRunner().invoke(main, ["update", "--yes"])
|
|
assert res.exit_code == 0
|
|
assert "PEP 668" in res.output
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# update_check helpers / branches
|
|
# --------------------------------------------------------------------------- #
|
|
def test_installed_version_present(monkeypatch):
|
|
monkeypatch.setattr(md, "version", lambda name: "0.26.0")
|
|
assert uc.installed_version() == "0.26.0"
|
|
|
|
|
|
def test_installed_version_not_found(monkeypatch):
|
|
def _boom(name):
|
|
raise md.PackageNotFoundError(name)
|
|
|
|
monkeypatch.setattr(md, "version", _boom)
|
|
assert uc.installed_version() is None
|
|
|
|
|
|
def test_is_source_checkout_real_is_bool():
|
|
assert isinstance(uc._is_source_checkout(), bool)
|
|
|
|
|
|
def test_in_docker_real_is_bool():
|
|
assert isinstance(uc._in_docker(), bool)
|
|
|
|
|
|
def test_select_latest_skips_invalid_versions():
|
|
data = {"releases": {"not-a-version": [{}], "0.26.0": [{}]}}
|
|
assert uc._select_latest(data, allow_pre=False) == "0.26.0"
|
|
|
|
|
|
def test_select_latest_info_fallback_invalid_returns_none():
|
|
data = {"releases": {}, "info": {"version": "not-a-version"}}
|
|
assert uc._select_latest(data, allow_pre=False) is None
|
|
|
|
|
|
def test_select_latest_info_fallback_prerelease_filtered():
|
|
data = {"releases": {}, "info": {"version": "1.0.0rc1"}}
|
|
assert uc._select_latest(data, allow_pre=False) is None
|
|
assert uc._select_latest(data, allow_pre=True) == "1.0.0rc1"
|
|
|
|
|
|
def test_run_check_disabled_returns_none(monkeypatch):
|
|
monkeypatch.setenv("HEADROOM_UPDATE_CHECK", "off")
|
|
monkeypatch.setattr(uc, "fetch_latest_version", lambda **k: pytest.fail("no fetch"))
|
|
assert uc.run_check() is None
|
|
|
|
|
|
def test_maybe_check_async_disabled_returns_none(monkeypatch):
|
|
monkeypatch.setenv("HEADROOM_UPDATE_CHECK", "off")
|
|
assert uc.maybe_check_async() is None
|
|
|
|
|
|
def test_format_update_notice_invalid_versions(monkeypatch):
|
|
uc.write_cache("not-a-version")
|
|
monkeypatch.setattr(uc, "_is_source_checkout", lambda: False)
|
|
monkeypatch.setattr(uc, "_in_docker", lambda: False)
|
|
monkeypatch.setattr(uc, "installed_version", lambda: "0.26.0")
|
|
assert uc.format_update_notice() is None
|
|
|
|
|
|
def test_fetch_latest_version_info_only(monkeypatch):
|
|
import json
|
|
|
|
payload = json.dumps({"releases": {}, "info": {"version": "0.30.0"}}).encode()
|
|
|
|
class _Resp:
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *a):
|
|
return False
|
|
|
|
def read(self):
|
|
return payload
|
|
|
|
monkeypatch.setattr(uc.urllib.request, "urlopen", lambda *a, **k: _Resp())
|
|
assert uc.fetch_latest_version() == "0.30.0"
|