From bef833d415dda712f58e525a9ad5c67d4468b32a Mon Sep 17 00:00:00 2001 From: BigDataDZ <76271875+BigDataDZ@users.noreply.github.com> Date: Mon, 2 Nov 2026 17:46:36 +0800 Subject: [PATCH] fix(claude): reject unsupported Python launcher candidates Signed-off-by: BigDataDZ <76271875+BigDataDZ@users.noreply.github.com> --- loopx/claude_goal_mode/scripts/install.py | 11 ++++-- ...est_claude_goal_install_python_launcher.py | 36 +++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/loopx/claude_goal_mode/scripts/install.py b/loopx/claude_goal_mode/scripts/install.py index c51a8ba20b..8e357dbe01 100644 --- a/loopx/claude_goal_mode/scripts/install.py +++ b/loopx/claude_goal_mode/scripts/install.py @@ -138,7 +138,7 @@ def _shell_command(value: str, *, powershell: bool) -> str: def _launches_python(py: str) -> bool: - """True when running `py` actually starts an interpreter. + """True when running `py` starts a supported Python 3.11+ interpreter. `shutil.which` only proves a file is executable, not that it runs Python. On Windows it can return the 0-byte `WindowsApps` App Execution Alias, which @@ -147,7 +147,11 @@ def _launches_python(py: str) -> bool: bakes that path (the PreToolUse hook, the statusline, `/loopx`) then silently does nothing.""" try: - return subprocess.run([py, "-c", ""], capture_output=True, timeout=10).returncode == 0 + return subprocess.run( + [py, "-c", "import sys; raise SystemExit(sys.version_info < (3, 11))"], + capture_output=True, + timeout=10, + ).returncode == 0 except (OSError, subprocess.SubprocessError): return False @@ -155,7 +159,8 @@ def _launches_python(py: str) -> bool: def _python_cmd() -> str: """Interpreter to bake into hook / statusline / command strings — robust where only `python` (not `python3`) is registered, and where `python3` resolves to a - launcher that cannot run Python (the Windows `WindowsApps` alias).""" + launcher that cannot run supported Python (including the Windows + `WindowsApps` alias and an older PATH interpreter).""" for candidate in (shutil.which("python3"), shutil.which("python")): if candidate and _launches_python(candidate): return candidate diff --git a/tests/test_claude_goal_install_python_launcher.py b/tests/test_claude_goal_install_python_launcher.py index 83abf03f77..57ff86bf93 100644 --- a/tests/test_claude_goal_install_python_launcher.py +++ b/tests/test_claude_goal_install_python_launcher.py @@ -156,6 +156,42 @@ class Result: assert claude_install._launches_python("python3") is True +@pytest.mark.parametrize( + ("version", "supported"), + [((3, 8), False), ((3, 10), False), ((3, 11), True), ((3, 12), True)], +) +def test_interpreter_probe_requires_the_supported_python_floor( + monkeypatch: pytest.MonkeyPatch, version: tuple[int, int], supported: bool, +) -> None: + real_run = subprocess.run + + def probe(command, **kwargs): + # Execute the actual probe with a controlled reported version; no + # obsolete Python installation or host configuration is required. + return real_run( + [sys.executable, "-c", f"import sys; sys.version_info = {version!r}; " + command[2]], + **kwargs, + ) + + monkeypatch.setattr(claude_install.subprocess, "run", probe) + assert claude_install._launches_python("candidate-python") is supported + + +def test_python_cmd_skips_a_runnable_but_unsupported_python3(monkeypatch: pytest.MonkeyPatch) -> None: + real_run = subprocess.run + monkeypatch.setattr(claude_install.shutil, "which", lambda name: name) + + def probe(command, **kwargs): + version = (3, 10) if command[0] == "python3" else (3, 11) + return real_run( + [sys.executable, "-c", f"import sys; sys.version_info = {version!r}; " + command[2]], + **kwargs, + ) + + monkeypatch.setattr(claude_install.subprocess, "run", probe) + assert claude_install._python_cmd() == "python" + + def _literal_interpreter(tmp_path: Path, dirname: str, request: pytest.FixtureRequest) -> Path: """A real launcher for `sys.executable` under `tmp_path / dirname`.