Fix/onboard oauth ux (#38)
* "fix(onboard): guide ccproxy install and auth in OAuth flow
- Always show API Key / OAuth choice; prompt to install evoscientist[oauth]
when ccproxy missing (mirrors iMessage imsg install UX)
- Add _ccproxy_exe() helper: checks PATH then env bin dir (fixes conda envs
where shutil.which may not find newly installed binaries)
- Fix check_ccproxy_auth() false positive: ccproxy auth status exits 0 even
when not authenticated; detect via output content + filter structlog noise
- Silent install/login subprocesses; show browser URL as fallback
- Reset anthropic/openai auth_mode to api_key when switching to non-Anthropic/
OpenAI provider, preventing stale oauth config from triggering ccproxy error
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
* fix(onboard): update OAuth support details in README and zh-CN translation
This commit is contained in:
@@ -30,9 +30,26 @@ _DEFAULT_PORT = 8000
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def _ccproxy_exe() -> str | None:
|
||||||
|
"""Return the path to the ccproxy binary, or None if not found.
|
||||||
|
|
||||||
|
Checks PATH first, then the current Python environment's bin directory
|
||||||
|
(handles conda envs where newly installed binaries may not be visible
|
||||||
|
to shutil.which immediately after pip install).
|
||||||
|
"""
|
||||||
|
found = shutil.which("ccproxy")
|
||||||
|
if found:
|
||||||
|
return found
|
||||||
|
import sys as _sys
|
||||||
|
candidate = os.path.join(os.path.dirname(_sys.executable), "ccproxy")
|
||||||
|
if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
|
||||||
|
return candidate
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def is_ccproxy_available() -> bool:
|
def is_ccproxy_available() -> bool:
|
||||||
"""Check whether the ``ccproxy`` CLI binary is on PATH."""
|
"""Check whether the ``ccproxy`` CLI binary is available."""
|
||||||
return shutil.which("ccproxy") is not None
|
return _ccproxy_exe() is not None
|
||||||
|
|
||||||
|
|
||||||
def _summarize_auth_output(raw: str) -> str:
|
def _summarize_auth_output(raw: str) -> str:
|
||||||
@@ -77,23 +94,35 @@ def check_ccproxy_auth(provider: str = "claude_api") -> tuple[bool, str]:
|
|||||||
(is_valid, message) tuple.
|
(is_valid, message) tuple.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
exe = _ccproxy_exe() or "ccproxy"
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["ccproxy", "auth", "status", provider],
|
[exe, "auth", "status", provider],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=10,
|
timeout=10,
|
||||||
)
|
)
|
||||||
# ccproxy auth status exits 0 when authed
|
|
||||||
if result.returncode == 0:
|
|
||||||
summary = _summarize_auth_output(result.stdout)
|
|
||||||
return True, summary or "Authenticated"
|
|
||||||
# On failure, include stderr for diagnostics
|
|
||||||
raw = (result.stdout + result.stderr).strip()
|
|
||||||
# Strip ANSI escapes for cleaner error messages
|
|
||||||
import re as _re
|
import re as _re
|
||||||
|
|
||||||
|
raw = (result.stdout + result.stderr).strip()
|
||||||
clean = _re.sub(r"\x1b\[[0-9;]*m", "", raw)
|
clean = _re.sub(r"\x1b\[[0-9;]*m", "", raw)
|
||||||
return False, clean or "Not authenticated"
|
|
||||||
|
# Filter out structlog warning/noise lines, keep only status lines
|
||||||
|
status_lines = [
|
||||||
|
line for line in clean.splitlines()
|
||||||
|
if line.strip()
|
||||||
|
and not _re.match(r"\d{4}-\d{2}-\d{2}", line.strip())
|
||||||
|
and "warning" not in line.lower()
|
||||||
|
and "plugin" not in line.lower()
|
||||||
|
]
|
||||||
|
status_msg = " ".join(status_lines).strip()
|
||||||
|
|
||||||
|
# ccproxy auth status may exit 0 even when not authenticated —
|
||||||
|
# detect failure by checking output content
|
||||||
|
if result.returncode != 0 or "not authenticated" in clean.lower():
|
||||||
|
return False, status_msg or "Not authenticated"
|
||||||
|
|
||||||
|
summary = _summarize_auth_output(result.stdout)
|
||||||
|
return True, summary or "Authenticated"
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
return False, "ccproxy not found"
|
return False, "ccproxy not found"
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
@@ -131,8 +160,9 @@ def start_ccproxy(port: int = _DEFAULT_PORT) -> subprocess.Popen:
|
|||||||
RuntimeError: If ccproxy fails to become healthy within 10 seconds.
|
RuntimeError: If ccproxy fails to become healthy within 10 seconds.
|
||||||
FileNotFoundError: If ccproxy binary is not found.
|
FileNotFoundError: If ccproxy binary is not found.
|
||||||
"""
|
"""
|
||||||
|
exe = _ccproxy_exe() or "ccproxy"
|
||||||
proc = subprocess.Popen(
|
proc = subprocess.Popen(
|
||||||
["ccproxy", "serve", "--port", str(port)],
|
[exe, "serve", "--port", str(port)],
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=subprocess.DEVNULL,
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=subprocess.DEVNULL,
|
||||||
)
|
)
|
||||||
|
|||||||
+108
-20
@@ -768,19 +768,16 @@ def _step_anthropic_auth_mode(config: EvoScientistConfig) -> str:
|
|||||||
Returns:
|
Returns:
|
||||||
Selected auth mode: "api_key", "oauth", or "auto".
|
Selected auth mode: "api_key", "oauth", or "auto".
|
||||||
"""
|
"""
|
||||||
from ..ccproxy_manager import is_ccproxy_available, check_ccproxy_auth
|
from ..ccproxy_manager import _ccproxy_exe, is_ccproxy_available, check_ccproxy_auth
|
||||||
|
|
||||||
if not is_ccproxy_available():
|
ccproxy_available = is_ccproxy_available()
|
||||||
console.print(
|
|
||||||
" [dim]OAuth via ccproxy not available. "
|
|
||||||
'Install with: pip install "evoscientist[oauth]"[/dim]'
|
|
||||||
)
|
|
||||||
return "api_key"
|
|
||||||
|
|
||||||
choices = [
|
choices = [
|
||||||
Choice(title="API Key (direct Anthropic access)", value="api_key"),
|
Choice(title="API Key (direct Anthropic access)", value="api_key"),
|
||||||
Choice(
|
Choice(
|
||||||
title="Claude Code OAuth (via ccproxy — no API key needed)", value="oauth"
|
title="Claude Code OAuth (via ccproxy — no API key needed)"
|
||||||
|
+ ("" if ccproxy_available else " [requires: pip install evoscientist[oauth]]"),
|
||||||
|
value="oauth",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -800,6 +797,30 @@ def _step_anthropic_auth_mode(config: EvoScientistConfig) -> str:
|
|||||||
if auth_mode is None:
|
if auth_mode is None:
|
||||||
raise KeyboardInterrupt()
|
raise KeyboardInterrupt()
|
||||||
|
|
||||||
|
if auth_mode == "oauth" and not ccproxy_available:
|
||||||
|
console.print(" [yellow]✗ ccproxy not installed[/yellow]")
|
||||||
|
console.print()
|
||||||
|
install = questionary.confirm(
|
||||||
|
'Install ccproxy now? (pip install "evoscientist[oauth]")',
|
||||||
|
default=True,
|
||||||
|
style=WIZARD_STYLE,
|
||||||
|
qmark=f" {QMARK}",
|
||||||
|
).ask()
|
||||||
|
if install is None:
|
||||||
|
raise KeyboardInterrupt()
|
||||||
|
if install:
|
||||||
|
console.print()
|
||||||
|
if _install_ccproxy():
|
||||||
|
console.print(" [green]✓ ccproxy installed successfully.[/green]")
|
||||||
|
else:
|
||||||
|
console.print(" [yellow]Falling back to API key mode.[/yellow]")
|
||||||
|
return "api_key"
|
||||||
|
else:
|
||||||
|
console.print(
|
||||||
|
' [dim]Skipped. Install manually: pip install "evoscientist[oauth]"[/dim]'
|
||||||
|
)
|
||||||
|
return "api_key"
|
||||||
|
|
||||||
# If OAuth selected, check auth status and offer login
|
# If OAuth selected, check auth status and offer login
|
||||||
if auth_mode in ("oauth", "auto"):
|
if auth_mode in ("oauth", "auto"):
|
||||||
authed, msg = check_ccproxy_auth()
|
authed, msg = check_ccproxy_auth()
|
||||||
@@ -816,10 +837,17 @@ def _step_anthropic_auth_mode(config: EvoScientistConfig) -> str:
|
|||||||
if login:
|
if login:
|
||||||
console.print(" [dim]Opening browser for authentication...[/dim]")
|
console.print(" [dim]Opening browser for authentication...[/dim]")
|
||||||
try:
|
try:
|
||||||
subprocess.run(
|
proc = subprocess.run(
|
||||||
["ccproxy", "auth", "login", "claude_api"],
|
[_ccproxy_exe() or "ccproxy", "auth", "login", "claude_api"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
timeout=120,
|
timeout=120,
|
||||||
)
|
)
|
||||||
|
# Show browser URL in case browser didn't open automatically
|
||||||
|
for line in proc.stdout.splitlines():
|
||||||
|
if line.strip().startswith("https://"):
|
||||||
|
console.print(f" [dim]Visit: {line.strip()}[/dim]")
|
||||||
|
break
|
||||||
authed, msg = check_ccproxy_auth()
|
authed, msg = check_ccproxy_auth()
|
||||||
if authed:
|
if authed:
|
||||||
console.print(f" [green]✓ OAuth: {msg}[/green]")
|
console.print(f" [green]✓ OAuth: {msg}[/green]")
|
||||||
@@ -842,19 +870,16 @@ def _step_openai_auth_mode(config: EvoScientistConfig) -> str:
|
|||||||
Returns:
|
Returns:
|
||||||
Selected auth mode: "api_key" or "oauth".
|
Selected auth mode: "api_key" or "oauth".
|
||||||
"""
|
"""
|
||||||
from ..ccproxy_manager import is_ccproxy_available, check_ccproxy_auth
|
from ..ccproxy_manager import _ccproxy_exe, is_ccproxy_available, check_ccproxy_auth
|
||||||
|
|
||||||
if not is_ccproxy_available():
|
ccproxy_available = is_ccproxy_available()
|
||||||
console.print(
|
|
||||||
" [dim]OAuth via ccproxy not available. "
|
|
||||||
'Install with: pip install "evoscientist[oauth]"[/dim]'
|
|
||||||
)
|
|
||||||
return "api_key"
|
|
||||||
|
|
||||||
choices = [
|
choices = [
|
||||||
Choice(title="API Key (direct OpenAI access)", value="api_key"),
|
Choice(title="API Key (direct OpenAI access)", value="api_key"),
|
||||||
Choice(
|
Choice(
|
||||||
title="Codex OAuth (via ccproxy — no API key needed)", value="oauth"
|
title="Codex OAuth (via ccproxy — no API key needed)"
|
||||||
|
+ ("" if ccproxy_available else " [requires: pip install evoscientist[oauth]]"),
|
||||||
|
value="oauth",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -874,6 +899,30 @@ def _step_openai_auth_mode(config: EvoScientistConfig) -> str:
|
|||||||
if auth_mode is None:
|
if auth_mode is None:
|
||||||
raise KeyboardInterrupt()
|
raise KeyboardInterrupt()
|
||||||
|
|
||||||
|
if auth_mode == "oauth" and not ccproxy_available:
|
||||||
|
console.print(" [yellow]✗ ccproxy not installed[/yellow]")
|
||||||
|
console.print()
|
||||||
|
install = questionary.confirm(
|
||||||
|
'Install ccproxy now? (pip install "evoscientist[oauth]")',
|
||||||
|
default=True,
|
||||||
|
style=WIZARD_STYLE,
|
||||||
|
qmark=f" {QMARK}",
|
||||||
|
).ask()
|
||||||
|
if install is None:
|
||||||
|
raise KeyboardInterrupt()
|
||||||
|
if install:
|
||||||
|
console.print()
|
||||||
|
if _install_ccproxy():
|
||||||
|
console.print(" [green]✓ ccproxy installed successfully.[/green]")
|
||||||
|
else:
|
||||||
|
console.print(" [yellow]Falling back to API key mode.[/yellow]")
|
||||||
|
return "api_key"
|
||||||
|
else:
|
||||||
|
console.print(
|
||||||
|
' [dim]Skipped. Install manually: pip install "evoscientist[oauth]"[/dim]'
|
||||||
|
)
|
||||||
|
return "api_key"
|
||||||
|
|
||||||
# If OAuth selected, check auth status and offer login
|
# If OAuth selected, check auth status and offer login
|
||||||
if auth_mode == "oauth":
|
if auth_mode == "oauth":
|
||||||
authed, msg = check_ccproxy_auth("codex")
|
authed, msg = check_ccproxy_auth("codex")
|
||||||
@@ -890,10 +939,17 @@ def _step_openai_auth_mode(config: EvoScientistConfig) -> str:
|
|||||||
if login:
|
if login:
|
||||||
console.print(" [dim]Opening browser for authentication...[/dim]")
|
console.print(" [dim]Opening browser for authentication...[/dim]")
|
||||||
try:
|
try:
|
||||||
subprocess.run(
|
proc = subprocess.run(
|
||||||
["ccproxy", "auth", "login", "codex"],
|
[_ccproxy_exe() or "ccproxy", "auth", "login", "codex"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
timeout=120,
|
timeout=120,
|
||||||
)
|
)
|
||||||
|
# Show browser URL in case browser didn't open automatically
|
||||||
|
for line in proc.stdout.splitlines():
|
||||||
|
if line.strip().startswith("https://"):
|
||||||
|
console.print(f" [dim]Visit: {line.strip()}[/dim]")
|
||||||
|
break
|
||||||
authed, msg = check_ccproxy_auth("codex")
|
authed, msg = check_ccproxy_auth("codex")
|
||||||
if authed:
|
if authed:
|
||||||
console.print(f" [green]✓ Codex OAuth: {msg}[/green]")
|
console.print(f" [green]✓ Codex OAuth: {msg}[/green]")
|
||||||
@@ -1715,6 +1771,33 @@ def validate_imessage() -> tuple[bool, str]:
|
|||||||
return True, f"imsg{version_str} at {cli_path}"
|
return True, f"imsg{version_str} at {cli_path}"
|
||||||
|
|
||||||
|
|
||||||
|
def _install_ccproxy() -> bool:
|
||||||
|
"""Run pip install for ccproxy (evoscientist[oauth]).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if installation succeeded and ccproxy is available.
|
||||||
|
"""
|
||||||
|
from ..ccproxy_manager import is_ccproxy_available
|
||||||
|
|
||||||
|
try:
|
||||||
|
proc = subprocess.run(
|
||||||
|
[sys.executable, "-m", "pip", "install", "evoscientist[oauth]"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=120,
|
||||||
|
)
|
||||||
|
if proc.returncode != 0:
|
||||||
|
console.print(f" [red]✗ Installation failed:[/red]\n{proc.stderr.strip()}")
|
||||||
|
return False
|
||||||
|
return is_ccproxy_available()
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
console.print(" [red]✗ Installation timed out.[/red]")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
console.print(f" [red]✗ Installation failed: {e}[/red]")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _install_imsg() -> bool:
|
def _install_imsg() -> bool:
|
||||||
"""Run brew install for imsg CLI.
|
"""Run brew install for imsg CLI.
|
||||||
|
|
||||||
@@ -2353,6 +2436,11 @@ def run_onboard(skip_validation: bool = False) -> bool:
|
|||||||
elif provider == "openai":
|
elif provider == "openai":
|
||||||
auth_mode = _step_openai_auth_mode(config)
|
auth_mode = _step_openai_auth_mode(config)
|
||||||
config.openai_auth_mode = auth_mode
|
config.openai_auth_mode = auth_mode
|
||||||
|
else:
|
||||||
|
# Non-Anthropic/OpenAI provider: reset OAuth modes to avoid
|
||||||
|
# stale oauth config triggering ccproxy requirement on startup
|
||||||
|
config.anthropic_auth_mode = "api_key"
|
||||||
|
config.openai_auth_mode = "api_key"
|
||||||
|
|
||||||
# Step 2c: Provider API Key (skip for Ollama — no key needed,
|
# Step 2c: Provider API Key (skip for Ollama — no key needed,
|
||||||
# and for Anthropic/OpenAI pure OAuth — key provided by ccproxy)
|
# and for Anthropic/OpenAI pure OAuth — key provided by ccproxy)
|
||||||
|
|||||||
@@ -220,9 +220,7 @@ def _apply_auto_config(
|
|||||||
base_url = os.environ.get("OPENAI_BASE_URL", "")
|
base_url = os.environ.get("OPENAI_BASE_URL", "")
|
||||||
_is_openai_proxy = "127.0.0.1" in base_url or "localhost" in base_url
|
_is_openai_proxy = "127.0.0.1" in base_url or "localhost" in base_url
|
||||||
if _is_openai_proxy:
|
if _is_openai_proxy:
|
||||||
# ccproxy forces store=False. Setting `reasoning` triggers
|
# Skip reasoning kwarg for ccproxy — not needed and may cause issues.
|
||||||
# langchain-openai's Responses API path, which produces
|
|
||||||
# rs_ summary items that 404 on multi-turn. Skip entirely.
|
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
kwargs["reasoning"] = {"effort": "high", "summary": "auto"}
|
kwargs["reasoning"] = {"effort": "high", "summary": "auto"}
|
||||||
@@ -308,8 +306,8 @@ def get_chat_model(
|
|||||||
kwargs["base_url"] = base_url
|
kwargs["base_url"] = base_url
|
||||||
_is_openai_proxy = "127.0.0.1" in base_url or "localhost" in base_url
|
_is_openai_proxy = "127.0.0.1" in base_url or "localhost" in base_url
|
||||||
if _is_openai_proxy:
|
if _is_openai_proxy:
|
||||||
kwargs.setdefault("streaming", False) # ccproxy streaming incompatible
|
kwargs.setdefault("streaming", False) # ccproxy streaming format incompatible with langchain-openai
|
||||||
kwargs.setdefault("use_responses_api", False) # force Chat Completions
|
kwargs.setdefault("use_responses_api", True) # ccproxy Chat Completions does not support tool calling; Responses API does
|
||||||
api_key = os.environ.get("OPENAI_API_KEY", "")
|
api_key = os.environ.get("OPENAI_API_KEY", "")
|
||||||
if api_key:
|
if api_key:
|
||||||
kwargs["api_key"] = api_key
|
kwargs["api_key"] = api_key
|
||||||
|
|||||||
@@ -207,7 +207,8 @@ EvoSci onboard
|
|||||||
```
|
```
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> It walks you through provider selection, key validation, model choice, and workspace mode.
|
> It walks you through provider selection, key validation, model choice, and workspace mode.
|
||||||
|
> Supports OAuth sign-in for [Claude Code](https://claude.com/product/claude-code) and [Codex ClI](https://developers.openai.com/codex/cli/) users — no API key needed.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -413,7 +414,7 @@ Coming soon:
|
|||||||
- [x] 👋 Human-in-the-loop action approval
|
- [x] 👋 Human-in-the-loop action approval
|
||||||
- [x] 🦾 Agent-initiated human clarification
|
- [x] 🦾 Agent-initiated human clarification
|
||||||
- [x] 📑 Technical report on the way
|
- [x] 📑 Technical report on the way
|
||||||
- [ ] 🔐 OAuth sign-in (Anthropic, OpenAI, etc.)
|
- [x] 🔐 OAuth sign-in (Anthropic, OpenAI, etc.)
|
||||||
- [ ] 📺 Web app with workspace UI
|
- [ ] 📺 Web app with workspace UI
|
||||||
- [ ] 📹 Demo and tutorial in the works
|
- [ ] 📹 Demo and tutorial in the works
|
||||||
- [ ] 📊 Benchmark suite to be released
|
- [ ] 📊 Benchmark suite to be released
|
||||||
|
|||||||
+2
-1
@@ -217,6 +217,7 @@ EvoSci onboard
|
|||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> 向导将引导你完成供应商选择、密钥验证、模型选择和工作区模式设置。
|
> 向导将引导你完成供应商选择、密钥验证、模型选择和工作区模式设置。
|
||||||
|
> 支持 [Claude Code](https://claude.com/product/claude-code) 和 [Codex CLI](https://developers.openai.com/codex/cli/) 用户通过 OAuth 直连——无需 API Key。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -422,7 +423,7 @@ channel_enabled: "telegram,slack,feishu,qq"
|
|||||||
- [x] 👋 Human-in-the-loop 操作审批
|
- [x] 👋 Human-in-the-loop 操作审批
|
||||||
- [x] 🦾 智能体主动向人类澄清确认
|
- [x] 🦾 智能体主动向人类澄清确认
|
||||||
- [x] 📑 技术报告已发布
|
- [x] 📑 技术报告已发布
|
||||||
- [ ] 🔐 OAuth 登录(Anthropic、OpenAI 等)
|
- [x] 🔐 OAuth 登录(Anthropic、OpenAI 等)
|
||||||
- [ ] 📺 带工作区的 Web 应用界面
|
- [ ] 📺 带工作区的 Web 应用界面
|
||||||
- [ ] 📹 Demo 与教程正在制作中
|
- [ ] 📹 Demo 与教程正在制作中
|
||||||
- [ ] 📊 基准测试套件即将推出
|
- [ ] 📊 基准测试套件即将推出
|
||||||
|
|||||||
@@ -29,8 +29,10 @@ class TestIsCcproxyAvailable:
|
|||||||
assert is_ccproxy_available() is True
|
assert is_ccproxy_available() is True
|
||||||
mock_which.assert_called_once_with("ccproxy")
|
mock_which.assert_called_once_with("ccproxy")
|
||||||
|
|
||||||
|
@patch("os.access", return_value=False)
|
||||||
|
@patch("os.path.isfile", return_value=False)
|
||||||
@patch("shutil.which", return_value=None)
|
@patch("shutil.which", return_value=None)
|
||||||
def test_not_found(self, mock_which):
|
def test_not_found(self, mock_which, mock_isfile, mock_access):
|
||||||
assert is_ccproxy_available() is False
|
assert is_ccproxy_available() is False
|
||||||
|
|
||||||
|
|
||||||
@@ -49,7 +51,8 @@ class TestCheckCcproxyAuth:
|
|||||||
assert valid is True
|
assert valid is True
|
||||||
assert "Authenticated" in msg
|
assert "Authenticated" in msg
|
||||||
mock_run.assert_called_once()
|
mock_run.assert_called_once()
|
||||||
assert mock_run.call_args[0][0] == ["ccproxy", "auth", "status", "claude_api"]
|
cmd = mock_run.call_args[0][0]
|
||||||
|
assert cmd[1:] == ["auth", "status", "claude_api"]
|
||||||
|
|
||||||
@patch("subprocess.run")
|
@patch("subprocess.run")
|
||||||
def test_valid_auth_codex(self, mock_run):
|
def test_valid_auth_codex(self, mock_run):
|
||||||
@@ -58,7 +61,8 @@ class TestCheckCcproxyAuth:
|
|||||||
)
|
)
|
||||||
valid, msg = check_ccproxy_auth("codex")
|
valid, msg = check_ccproxy_auth("codex")
|
||||||
assert valid is True
|
assert valid is True
|
||||||
assert mock_run.call_args[0][0] == ["ccproxy", "auth", "status", "codex"]
|
cmd = mock_run.call_args[0][0]
|
||||||
|
assert cmd[1:] == ["auth", "status", "codex"]
|
||||||
|
|
||||||
@patch("subprocess.run")
|
@patch("subprocess.run")
|
||||||
def test_invalid_auth(self, mock_run):
|
def test_invalid_auth(self, mock_run):
|
||||||
|
|||||||
Reference in New Issue
Block a user