From 876a6eb3ba9974d0eadc1fd3f628dafdc56d8716 Mon Sep 17 00:00:00 2001 From: historia Date: Fri, 28 Aug 2026 04:18:56 -0400 Subject: fix: system python used instead of venv python, breaking whisper transcription --- app/backends/envs.py | 11 +++++++++-- app/tests/test_backends_envs.py | 33 +++++++++++++++++++++++++++------ 2 files changed, 36 insertions(+), 8 deletions(-) (limited to 'app') diff --git a/app/backends/envs.py b/app/backends/envs.py index ff6d1bf..494c3e2 100644 --- a/app/backends/envs.py +++ b/app/backends/envs.py @@ -89,9 +89,16 @@ def env_exists(env_dir: Optional[Path] = None) -> bool: def is_managed_env() -> bool: - """True when the current process is already running inside the app venv.""" + """True when the current process is already running inside the app venv. + + Compares sys.prefix (the environment the current interpreter belongs to) + with ENV_DIR instead of the interpreter path: a venv's bin/python is + typically a symlink to the base interpreter, so resolving sys.executable + would also make the bare system python look like the managed env and + silently skip the bootstrap (and with it the requirements install). + """ try: - return Path(sys.executable).resolve() == env_python().resolve() + return Path(sys.prefix).resolve() == ENV_DIR.resolve() except OSError: return False diff --git a/app/tests/test_backends_envs.py b/app/tests/test_backends_envs.py index 2f0f910..d3b35c1 100644 --- a/app/tests/test_backends_envs.py +++ b/app/tests/test_backends_envs.py @@ -1,7 +1,9 @@ """Tests for the managed Python environment (backends/envs.py).""" import json +import os import sys +import tempfile import unittest from pathlib import Path from unittest.mock import patch @@ -63,15 +65,34 @@ class EnvPathTests(unittest.TestCase): return_value=Path("/no/such/path/python")): self.assertFalse(envs.env_exists()) - def test_is_managed_env_compares_resolved_executable(self): - fake_env_python = Path("/tmp/opencode/managed-env/bin/python") - with patch.object(envs, "env_python", return_value=fake_env_python), \ - patch.object(sys, "executable", str(fake_env_python)): + def test_is_managed_env_compares_prefix_to_env_dir(self): + # sys.prefix is the environment the current interpreter belongs to: + # the env dir for its python, the base install (/usr, ...) otherwise. + with patch.object(envs, "ENV_DIR", Path(sys.prefix)): self.assertTrue(envs.is_managed_env()) - with patch.object(envs, "env_python", return_value=fake_env_python), \ - patch.object(sys, "executable", "/usr/bin/python3"): + with patch.object(envs, "ENV_DIR", + Path(tempfile.gettempdir()) / "managed-env"): self.assertFalse(envs.is_managed_env()) + def test_is_managed_env_not_fooled_by_symlinked_venv_python(self): + # Regression: a venv's bin/python is a symlink to the base + # interpreter, so resolving sys.executable made the bare system + # python compare equal to env_python() and pass as the managed env — + # silently skipping the bootstrap (and the requirements install). + # The check must use sys.prefix, which stays the base install's. + if sys.platform == "win32": + self.skipTest("requires POSIX symlinks") + with tempfile.TemporaryDirectory() as tmp: + env_dir = Path(tmp) / "tts" + bin_dir = env_dir / "bin" + bin_dir.mkdir(parents=True) + os.symlink(sys.executable, bin_dir / "python") + with patch.object(envs, "ENV_DIR", env_dir): + # The bare system interpreter: sys.executable resolves to + # exactly what the venv python symlink points at, yet its + # prefix is not the env dir, so this must stay False. + self.assertFalse(envs.is_managed_env()) + class CreateEnvTests(unittest.TestCase): def test_create_env_invokes_venv_module(self): -- cgit v1.2.3