From 68dc9e92c6176bbe99c3436d74021dccf291f857 Mon Sep 17 00:00:00 2001 From: ZKDplz <2646995705@qq.com> Date: Thu, 5 Nov 2026 22:15:56 +0800 Subject: [PATCH] fix(doctor): respect installation owner in repair advice Signed-off-by: ZKDplz <2646995705@qq.com> --- loopx/doctor.py | 14 ++++-- tests/test_python_install_owner_platform.py | 51 +++++++++++++++++++-- 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/loopx/doctor.py b/loopx/doctor.py index 8d9efe0523..df1d6d37ec 100644 --- a/loopx/doctor.py +++ b/loopx/doctor.py @@ -28,7 +28,6 @@ from .paths import configured_runtime_route, default_runtime_route, global_registry_path from .python_install_owner import ( PythonInstallOwner, - interpreter_invocation, python_distribution_upgrade_command, resolve_python_install_owner, ) @@ -1223,9 +1222,16 @@ def collect_doctor( if canonical_agent_type and agent_type_uses_host_managed_skills(canonical_agent_type) else ( - "Run `loopx workflow-skills --install`, then " - "`loopx slash-commands --install`, and rerun doctor. Upgrade this " - f"installation with `{interpreter_invocation(sys.executable)} -m pip install --upgrade loopx`." + ( + f"Upgrade this installation with:\n\n```\n{install_freshness['upgrade_command']}\n```" + if install_freshness["upgrade_command"] + else ( + "Run `loopx workflow-skills --install`, then " + "`loopx slash-commands --install`, and rerun doctor. " + "Use the original package manager " + f"({python_distribution.get('installer') or 'unknown installer'}) to upgrade this installation." + ) + ) if python_distribution.get("available") else ( f"Run `{local_install_command(repo_root, skip_skills=externally_managed_skills)}` " diff --git a/tests/test_python_install_owner_platform.py b/tests/test_python_install_owner_platform.py index b4e2af2055..e3f7c263a3 100644 --- a/tests/test_python_install_owner_platform.py +++ b/tests/test_python_install_owner_platform.py @@ -10,16 +10,19 @@ from __future__ import annotations +import json import os import shutil import subprocess import sys +from importlib.metadata import PathDistribution from pathlib import Path import pytest -from loopx import python_install_owner -from loopx.doctor import build_install_freshness, collect_doctor +from loopx import doctor, python_install_owner +from loopx.cli import main +from loopx.doctor import build_install_freshness, collect_doctor, render_doctor_markdown from loopx.python_install_owner import ( PythonInstallOwner, python_distribution_upgrade_command, @@ -193,11 +196,51 @@ def test_windows_doctor_fix_hint_uses_the_call_operator( payload = collect_doctor() assert ( - f"`& {powershell_literal(PLAIN_WINDOWS_INTERPRETER)} " - "-m pip install --upgrade loopx`" + f"& {powershell_literal(PLAIN_WINDOWS_INTERPRETER)} " + "-m pip install --upgrade loopx" ) in str(payload["fix"]) +@pytest.mark.parametrize("installer", ["pipx", "uv", "custom-manager"]) +def test_doctor_fix_preserves_installation_owner( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], installer: str +) -> None: + prefix = tmp_path / "loopx-preview" + metadata = prefix / "loopx-1.4.0.dist-info" + metadata.mkdir(parents=True) + (metadata / "METADATA").write_text("Name: loopx\nVersion: 1.4.0\n", encoding="utf-8") + (metadata / "INSTALLER").write_text( + "pip\n" if installer == "pipx" else installer + "\n", encoding="utf-8" + ) + module = Path(doctor.__file__).as_posix() + (metadata / "RECORD").write_text(f"{module},,\n", encoding="utf-8") + if installer == "pipx": + (prefix / "pipx_metadata.json").write_text( + '{"environment":"loopx-preview"}', encoding="utf-8" + ) + monkeypatch.setattr(doctor, "distribution", lambda _name: PathDistribution(metadata)) + monkeypatch.setattr(sys, "prefix", str(prefix)) + # A missing launcher makes doctor display its repair guidance on every host. + monkeypatch.setenv("PATH", "") + + assert main(["--registry", str(tmp_path / "registry.json"), "--runtime-root", str(tmp_path / "runtime"), + "--format", "json", "doctor"]) == 1 + payload = json.loads(capsys.readouterr().out) + + assert payload["package"]["python_distribution"]["installer"] == installer + if installer == "pipx": + assert payload["install_freshness"]["upgrade_command"].startswith("pipx upgrade loopx-preview\n") + assert "pipx upgrade loopx-preview" in payload["fix"] + else: + assert payload["install_freshness"]["upgrade_command"] is None + assert "original package manager" in payload["fix"] + assert installer in payload["fix"] + assert "-m pip install" not in payload["fix"] + assert "loopx workflow-skills --install" in payload["fix"] + assert "loopx slash-commands --install" in payload["fix"] + assert payload["fix"] in render_doctor_markdown(payload) + + def test_native_host_prints_a_runnable_interpreter_line() -> None: first_line = pip_owner_upgrade_command().splitlines()[0]