From 2f1622579f60e6d0065ff336da17bd314595ea9b Mon Sep 17 00:00:00 2001 From: akou Date: Wed, 18 Mar 2026 16:57:06 +0800 Subject: [PATCH] fix: improve ccproxy OAuth install hint (#55) Co-authored-by: Dinos Papakostas Co-authored-by: Claude Opus 4.6 (1M context) --- EvoScientist/ccproxy_manager.py | 36 ++++++++++++++++++++++++++++++++- tests/test_ccproxy_manager.py | 21 +++++++++++++++++-- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/EvoScientist/ccproxy_manager.py b/EvoScientist/ccproxy_manager.py index 7afa385..e6d3e9a 100644 --- a/EvoScientist/ccproxy_manager.py +++ b/EvoScientist/ccproxy_manager.py @@ -49,6 +49,40 @@ def is_ccproxy_available() -> bool: return _ccproxy_exe() is not None +def _is_editable_install() -> bool: + """Return True if EvoScientist was installed in editable/development mode. + + Checks all matching distributions because a stale ``.egg-info`` in the + project root can shadow the real ``dist-info`` in site-packages. + """ + try: + import json + import importlib.metadata as _meta + + for dist in _meta.distributions(): + name = dist.metadata.get("Name", "") + if name.lower() != "evoscientist": + continue + direct_url = dist.read_text("direct_url.json") + if direct_url is not None: + data = json.loads(direct_url) + if data.get("dir_info", {}).get("editable", False) is True: + return True + except Exception: + pass + return False + + +def _oauth_install_hint() -> str: + """Return the appropriate install command depending on install method.""" + if _is_editable_install(): + return ( + "uv sync --extra oauth " + "or pip install -e '.[oauth]'" + ) + return "pip install 'evoscientist[oauth]'" + + def _summarize_auth_output(raw: str) -> str: """Extract key fields from ccproxy auth status output into a one-line summary. @@ -356,7 +390,7 @@ def maybe_start_ccproxy(config: EvoScientistConfig) -> subprocess.Popen | None: if not is_ccproxy_available(): raise RuntimeError( "ccproxy is required for OAuth mode but not found. " - "Install it with: pip install 'evoscientist[oauth]'" + f"Install it with: {_oauth_install_hint()}" ) # Check auth for each provider that uses OAuth diff --git a/tests/test_ccproxy_manager.py b/tests/test_ccproxy_manager.py index 63ede6b..6ae0716 100644 --- a/tests/test_ccproxy_manager.py +++ b/tests/test_ccproxy_manager.py @@ -334,13 +334,30 @@ class TestMaybeStartCcproxy: mock_anthropic_env.assert_called_once() mock_codex_env.assert_called_once() + @patch("EvoScientist.ccproxy_manager._is_editable_install", return_value=True) @patch("EvoScientist.ccproxy_manager.is_ccproxy_available", return_value=False) - def test_openai_oauth_raises_no_binary(self, mock_avail): + def test_openai_oauth_raises_no_binary_editable(self, mock_avail, mock_edit): config = MagicMock() config.anthropic_auth_mode = "api_key" config.openai_auth_mode = "oauth" - with pytest.raises(RuntimeError, match="not found"): + with pytest.raises(RuntimeError) as exc_info: maybe_start_ccproxy(config) + msg = str(exc_info.value) + assert "ccproxy is required for OAuth mode but not found" in msg + assert "uv sync --extra oauth" in msg + assert "pip install -e '.[oauth]'" in msg + + @patch("EvoScientist.ccproxy_manager._is_editable_install", return_value=False) + @patch("EvoScientist.ccproxy_manager.is_ccproxy_available", return_value=False) + def test_openai_oauth_raises_no_binary_pip(self, mock_avail, mock_edit): + config = MagicMock() + config.anthropic_auth_mode = "api_key" + config.openai_auth_mode = "oauth" + with pytest.raises(RuntimeError) as exc_info: + maybe_start_ccproxy(config) + msg = str(exc_info.value) + assert "ccproxy is required for OAuth mode but not found" in msg + assert "pip install 'evoscientist[oauth]'" in msg @patch( "EvoScientist.ccproxy_manager.check_ccproxy_auth",