d16be30771
* auto-claude: subtask-1-3 - Thread progress callback through MergePipeline and ConflictResolver Add progress_callback parameter to MergePipeline.merge_file() and ConflictResolver.resolve_conflicts(). MergePipeline emits per-file progress at the start of merge within the resolving stage (50-75%). ConflictResolver emits per-conflict resolution progress with details about current file, conflict count, and conflicts resolved so far. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * auto-claude: subtask-1-4 - Wire progress emission into CLI merge entry point. Add _create_merge_progress_callback() helper that returns emit_progress only when stdout is piped (subprocess mode from Electron), avoiding JSON pollution in interactive CLI sessions. Wire the callback into _try_smart_merge_inner() with progress emissions at key pipeline stages: ANALYZING, DETECTING_CONFLICTS, RESOLVING, COMPLETE, and ERROR. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix PR review issues: conflict counts, progress calculations, and cross-task leakage - Fix conflicts_found on COMPLETE/ERROR stages to use original conflict count - Fix off-by-one in progress percentage calculations (50-75% range) - Add JSON validation for MergeProgress before IPC transmission - Add taskId filtering to prevent cross-task progress event leakage - Limit log entries to 500 to prevent unbounded memory growth - Fix race condition: wait for terminal progress event before hiding overlay - Remove unused imports (ruff fixes) - Remove orphaned unreachable code in workspace.py - Fix test mocks to use optional config_dir argument Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
596 lines
26 KiB
Python
596 lines
26 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Tests for Client Creation and Token Validation
|
|
===============================================
|
|
|
|
Tests the client.py and simple_client.py module functionality including:
|
|
- Token validation before SDK initialization
|
|
- Encrypted token rejection
|
|
- Client creation with valid tokens
|
|
"""
|
|
|
|
import os
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
# Auth token env vars that need to be cleared between tests
|
|
AUTH_TOKEN_ENV_VARS = [
|
|
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
"ANTHROPIC_AUTH_TOKEN",
|
|
"ANTHROPIC_BASE_URL",
|
|
]
|
|
|
|
|
|
@pytest.fixture
|
|
def clear_auth_env():
|
|
"""Clear auth environment variables before and after each test."""
|
|
for var in AUTH_TOKEN_ENV_VARS:
|
|
os.environ.pop(var, None)
|
|
yield
|
|
for var in AUTH_TOKEN_ENV_VARS:
|
|
os.environ.pop(var, None)
|
|
|
|
|
|
class TestClientTokenValidation:
|
|
"""Tests for client token validation."""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def setup(self, clear_auth_env):
|
|
"""Use shared clear_auth_env fixture."""
|
|
pass
|
|
|
|
def test_create_client_rejects_encrypted_tokens(self, tmp_path, monkeypatch):
|
|
"""Verify create_client() rejects encrypted tokens."""
|
|
from core.client import create_client
|
|
|
|
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", "enc:test123456789012")
|
|
# Mock keychain to ensure encrypted token is the only source
|
|
monkeypatch.setattr("core.auth.get_token_from_keychain", lambda _config_dir=None: None)
|
|
# Mock decrypt_token to raise ValueError (simulates decryption failure)
|
|
# This ensures the encrypted token flows through to validate_token_not_encrypted
|
|
monkeypatch.setattr(
|
|
"core.auth.decrypt_token",
|
|
lambda t: (_ for _ in ()).throw(ValueError("Decryption not supported")),
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="encrypted format"):
|
|
create_client(tmp_path, tmp_path, "claude-sonnet-4", "coder")
|
|
|
|
def test_create_simple_client_rejects_encrypted_tokens(self, monkeypatch):
|
|
"""Verify create_simple_client() rejects encrypted tokens."""
|
|
from core.simple_client import create_simple_client
|
|
|
|
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", "enc:test123456789012")
|
|
# Mock keychain to ensure encrypted token is the only source
|
|
monkeypatch.setattr("core.auth.get_token_from_keychain", lambda _config_dir=None: None)
|
|
# Mock decrypt_token to raise ValueError (simulates decryption failure)
|
|
monkeypatch.setattr(
|
|
"core.auth.decrypt_token",
|
|
lambda t: (_ for _ in ()).throw(ValueError("Decryption not supported")),
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="encrypted format"):
|
|
create_simple_client(agent_type="merge_resolver")
|
|
|
|
def test_create_client_accepts_valid_plaintext_token(self, tmp_path, monkeypatch):
|
|
"""Verify create_client() accepts valid plaintext tokens and creates SDK client."""
|
|
valid_token = "sk-ant-oat01-valid-plaintext-token"
|
|
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", valid_token)
|
|
monkeypatch.setattr("core.auth.get_token_from_keychain", lambda _config_dir=None: None)
|
|
|
|
# Mock the SDK client to avoid actual initialization
|
|
mock_sdk_client = MagicMock()
|
|
with patch("core.client.ClaudeSDKClient", return_value=mock_sdk_client):
|
|
from core.client import create_client
|
|
|
|
client = create_client(tmp_path, tmp_path, "claude-sonnet-4", "coder")
|
|
|
|
# Verify SDK client was created
|
|
assert client is mock_sdk_client
|
|
|
|
def test_create_simple_client_accepts_valid_plaintext_token(self, monkeypatch):
|
|
"""Verify create_simple_client() accepts valid plaintext tokens and creates SDK client."""
|
|
valid_token = "sk-ant-oat01-valid-plaintext-token"
|
|
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", valid_token)
|
|
monkeypatch.setattr("core.auth.get_token_from_keychain", lambda _config_dir=None: None)
|
|
|
|
# Mock the SDK client to avoid actual initialization
|
|
mock_sdk_client = MagicMock()
|
|
with patch(
|
|
"core.simple_client.ClaudeSDKClient", return_value=mock_sdk_client
|
|
):
|
|
from core.simple_client import create_simple_client
|
|
|
|
client = create_simple_client(agent_type="merge_resolver")
|
|
|
|
# Verify SDK client was created
|
|
assert client is mock_sdk_client
|
|
|
|
def test_create_client_validates_token_before_sdk_init(
|
|
self, tmp_path, monkeypatch
|
|
):
|
|
"""Verify create_client() validates token format before SDK initialization."""
|
|
valid_token = "sk-ant-oat01-valid-token"
|
|
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", valid_token)
|
|
monkeypatch.setattr("core.auth.get_token_from_keychain", lambda _config_dir=None: None)
|
|
|
|
# Mock validate_token_not_encrypted to verify it's called
|
|
with patch(
|
|
"core.auth.validate_token_not_encrypted"
|
|
) as mock_validate, patch("core.client.ClaudeSDKClient"):
|
|
from core.client import create_client
|
|
|
|
create_client(tmp_path, tmp_path, "claude-sonnet-4", "coder")
|
|
|
|
# Verify validation was called with the token
|
|
mock_validate.assert_called_once_with(valid_token)
|
|
|
|
def test_create_simple_client_validates_token_before_sdk_init(self, monkeypatch):
|
|
"""Verify create_simple_client() validates token format before SDK initialization."""
|
|
valid_token = "sk-ant-oat01-valid-token"
|
|
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", valid_token)
|
|
monkeypatch.setattr("core.auth.get_token_from_keychain", lambda _config_dir=None: None)
|
|
|
|
# Mock validate_token_not_encrypted to verify it's called
|
|
with patch(
|
|
"core.auth.validate_token_not_encrypted"
|
|
) as mock_validate, patch("core.simple_client.ClaudeSDKClient"):
|
|
from core.simple_client import create_simple_client
|
|
|
|
create_simple_client(agent_type="merge_resolver")
|
|
|
|
# Verify validation was called with the token
|
|
mock_validate.assert_called_once_with(valid_token)
|
|
|
|
|
|
class TestAPIProfileAuthentication:
|
|
"""Tests for API Profile authentication mode (e.g., z.ai, custom endpoints)."""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def setup(self, clear_auth_env):
|
|
"""Use shared clear_auth_env fixture."""
|
|
pass
|
|
|
|
def test_api_profile_mode_with_valid_token(self, tmp_path, monkeypatch):
|
|
"""API profile mode succeeds with ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN."""
|
|
api_token = "sk-api-test-token-123456"
|
|
api_endpoint = "https://api.z.ai/v1"
|
|
|
|
monkeypatch.setenv("ANTHROPIC_AUTH_TOKEN", api_token)
|
|
monkeypatch.setenv("ANTHROPIC_BASE_URL", api_endpoint)
|
|
# Ensure no OAuth token is set
|
|
monkeypatch.delenv("CLAUDE_CODE_OAUTH_TOKEN", raising=False)
|
|
|
|
# Mock the SDK client to avoid actual initialization
|
|
mock_sdk_client = MagicMock()
|
|
with patch("core.client.ClaudeSDKClient", return_value=mock_sdk_client):
|
|
from core.client import create_client
|
|
|
|
client = create_client(tmp_path, tmp_path, "glm-4", "coder")
|
|
|
|
# Verify SDK client was created
|
|
assert client is mock_sdk_client
|
|
|
|
# Verify CLAUDE_CODE_OAUTH_TOKEN was NOT set (API profile mode)
|
|
assert "CLAUDE_CODE_OAUTH_TOKEN" not in os.environ
|
|
|
|
# Verify ANTHROPIC_AUTH_TOKEN is still set
|
|
assert os.environ.get("ANTHROPIC_AUTH_TOKEN") == api_token
|
|
assert os.environ.get("ANTHROPIC_BASE_URL") == api_endpoint
|
|
|
|
def test_api_profile_mode_missing_token_raises_error(self, tmp_path, monkeypatch):
|
|
"""API profile mode raises ValueError when ANTHROPIC_AUTH_TOKEN is missing."""
|
|
api_endpoint = "https://api.z.ai/v1"
|
|
|
|
monkeypatch.setenv("ANTHROPIC_BASE_URL", api_endpoint)
|
|
# Don't set ANTHROPIC_AUTH_TOKEN - this should cause an error
|
|
monkeypatch.delenv("ANTHROPIC_AUTH_TOKEN", raising=False)
|
|
monkeypatch.delenv("CLAUDE_CODE_OAUTH_TOKEN", raising=False)
|
|
|
|
from core.client import create_client
|
|
|
|
with pytest.raises(ValueError, match=r"API profile mode active.*ANTHROPIC_AUTH_TOKEN is not set"):
|
|
create_client(tmp_path, tmp_path, "glm-4", "coder")
|
|
|
|
def test_api_profile_mode_empty_token_raises_error(self, tmp_path, monkeypatch):
|
|
"""API profile mode raises ValueError when ANTHROPIC_AUTH_TOKEN is empty string."""
|
|
api_endpoint = "https://api.z.ai/v1"
|
|
|
|
monkeypatch.setenv("ANTHROPIC_BASE_URL", api_endpoint)
|
|
monkeypatch.setenv("ANTHROPIC_AUTH_TOKEN", "") # Empty string
|
|
monkeypatch.delenv("CLAUDE_CODE_OAUTH_TOKEN", raising=False)
|
|
|
|
from core.client import create_client
|
|
|
|
with pytest.raises(ValueError, match=r"API profile mode active.*ANTHROPIC_AUTH_TOKEN is not set"):
|
|
create_client(tmp_path, tmp_path, "glm-4", "coder")
|
|
|
|
def test_oauth_mode_without_base_url(self, tmp_path, monkeypatch):
|
|
"""OAuth mode is used when ANTHROPIC_BASE_URL is not set."""
|
|
oauth_token = "sk-ant-oat01-oauth-token"
|
|
|
|
# Don't set ANTHROPIC_BASE_URL - this should trigger OAuth mode
|
|
monkeypatch.delenv("ANTHROPIC_BASE_URL", raising=False)
|
|
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", oauth_token)
|
|
monkeypatch.setattr("core.auth.get_token_from_keychain", lambda _config_dir=None: None)
|
|
|
|
# Mock the SDK client
|
|
mock_sdk_client = MagicMock()
|
|
with patch("core.client.ClaudeSDKClient", return_value=mock_sdk_client):
|
|
from core.client import create_client
|
|
|
|
client = create_client(tmp_path, tmp_path, "claude-sonnet-4", "coder")
|
|
|
|
# Verify SDK client was created
|
|
assert client is mock_sdk_client
|
|
|
|
# Verify CLAUDE_CODE_OAUTH_TOKEN was set (OAuth mode)
|
|
assert os.environ.get("CLAUDE_CODE_OAUTH_TOKEN") == oauth_token
|
|
|
|
def test_api_profile_takes_precedence_over_oauth(self, tmp_path, monkeypatch):
|
|
"""
|
|
When both ANTHROPIC_BASE_URL and OAuth token are set, API profile mode wins.
|
|
|
|
create_client() explicitly removes CLAUDE_CODE_OAUTH_TOKEN in API profile mode
|
|
so the SDK uses ANTHROPIC_AUTH_TOKEN instead (SDK prioritizes OAuth over API keys).
|
|
"""
|
|
api_token = "sk-api-test-token-123456"
|
|
api_endpoint = "https://api.z.ai/v1"
|
|
oauth_token = "sk-ant-oat01-oauth-token"
|
|
|
|
# Set both API profile and OAuth
|
|
monkeypatch.setenv("ANTHROPIC_AUTH_TOKEN", api_token)
|
|
monkeypatch.setenv("ANTHROPIC_BASE_URL", api_endpoint)
|
|
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", oauth_token)
|
|
|
|
# Mock the SDK client and OAuth functions to verify OAuth path is NOT taken
|
|
mock_sdk_client = MagicMock()
|
|
with patch("core.client.ClaudeSDKClient", return_value=mock_sdk_client), \
|
|
patch("core.auth.require_auth_token") as mock_require, \
|
|
patch("core.auth.validate_token_not_encrypted") as mock_validate:
|
|
from core.client import create_client
|
|
|
|
client = create_client(tmp_path, tmp_path, "glm-4", "coder")
|
|
|
|
# Verify SDK client was created
|
|
assert client is mock_sdk_client
|
|
|
|
# Verify CLAUDE_CODE_OAUTH_TOKEN was removed (API profile mode)
|
|
assert "CLAUDE_CODE_OAUTH_TOKEN" not in os.environ
|
|
|
|
# Ensure OAuth flow was NOT used (this proves API profile path was taken)
|
|
mock_require.assert_not_called()
|
|
mock_validate.assert_not_called()
|
|
|
|
def test_empty_base_url_triggers_oauth_mode(self, tmp_path, monkeypatch):
|
|
"""Empty ANTHROPIC_BASE_URL should trigger OAuth mode, not API profile mode."""
|
|
oauth_token = "sk-ant-oat01-oauth-token"
|
|
|
|
# Set empty ANTHROPIC_BASE_URL - should be treated as "not set"
|
|
monkeypatch.setenv("ANTHROPIC_BASE_URL", "")
|
|
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", oauth_token)
|
|
monkeypatch.setattr("core.auth.get_token_from_keychain", lambda _config_dir=None: None)
|
|
|
|
# Mock require_auth_token to verify it's called (OAuth mode)
|
|
with patch("core.auth.require_auth_token", return_value=oauth_token):
|
|
mock_sdk_client = MagicMock()
|
|
with patch("core.client.ClaudeSDKClient", return_value=mock_sdk_client):
|
|
from core.client import create_client
|
|
|
|
client = create_client(tmp_path, tmp_path, "claude-sonnet-4", "coder")
|
|
|
|
# Verify SDK client was created
|
|
assert client is mock_sdk_client
|
|
|
|
@pytest.mark.parametrize("endpoint", [
|
|
"https://api.z.ai/v1",
|
|
"https://api.example.com",
|
|
"http://localhost:8080/v1",
|
|
"https://custom-gateway.com/anthropic-proxy",
|
|
])
|
|
def test_api_profile_with_various_endpoints(self, tmp_path, monkeypatch, endpoint):
|
|
"""API profile mode works with various endpoint formats."""
|
|
api_token = "sk-api-test-token-123456"
|
|
|
|
monkeypatch.setenv("ANTHROPIC_AUTH_TOKEN", api_token)
|
|
monkeypatch.setenv("ANTHROPIC_BASE_URL", endpoint)
|
|
monkeypatch.delenv("CLAUDE_CODE_OAUTH_TOKEN", raising=False)
|
|
|
|
mock_sdk_client = MagicMock()
|
|
with patch("core.client.ClaudeSDKClient", return_value=mock_sdk_client):
|
|
from core.client import create_client
|
|
|
|
client = create_client(tmp_path, tmp_path, "glm-4", "coder")
|
|
|
|
assert client is mock_sdk_client
|
|
assert os.environ.get("ANTHROPIC_BASE_URL") == endpoint
|
|
|
|
def test_oauth_mode_without_any_token_raises_error(self, tmp_path, monkeypatch):
|
|
"""OAuth mode raises ValueError when no OAuth token is available."""
|
|
# Don't set any auth tokens
|
|
monkeypatch.delenv("CLAUDE_CODE_OAUTH_TOKEN", raising=False)
|
|
monkeypatch.delenv("ANTHROPIC_AUTH_TOKEN", raising=False)
|
|
monkeypatch.delenv("ANTHROPIC_BASE_URL", raising=False)
|
|
|
|
# Mock keychain to return None
|
|
monkeypatch.setattr("core.auth.get_token_from_keychain", lambda _config_dir=None: None)
|
|
|
|
from core.client import create_client
|
|
|
|
with pytest.raises(ValueError, match="No OAuth token found"):
|
|
create_client(tmp_path, tmp_path, "claude-sonnet-4", "coder")
|
|
|
|
|
|
class TestAPIProfileAuthenticationIntegration:
|
|
"""Integration tests verifying the complete auth flow behavior."""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def setup(self, clear_auth_env):
|
|
"""Use shared clear_auth_env fixture."""
|
|
pass
|
|
|
|
def test_sdk_env_vars_includes_api_profile_vars(self, monkeypatch):
|
|
"""Verify get_sdk_env_vars() passes ANTHROPIC_AUTH_TOKEN and ANTHROPIC_BASE_URL."""
|
|
from core.auth import get_sdk_env_vars
|
|
|
|
api_token = "sk-api-test-token"
|
|
api_endpoint = "https://api.z.ai/v1"
|
|
|
|
monkeypatch.setenv("ANTHROPIC_AUTH_TOKEN", api_token)
|
|
monkeypatch.setenv("ANTHROPIC_BASE_URL", api_endpoint)
|
|
|
|
sdk_env = get_sdk_env_vars()
|
|
|
|
assert sdk_env.get("ANTHROPIC_AUTH_TOKEN") == api_token
|
|
assert sdk_env.get("ANTHROPIC_BASE_URL") == api_endpoint
|
|
|
|
def test_sdk_env_vars_excludes_oauth_in_api_profile_mode(self, monkeypatch):
|
|
"""Verify SDK env vars don't include CLAUDE_CODE_OAUTH_TOKEN in API profile mode."""
|
|
from core.auth import get_sdk_env_vars
|
|
|
|
api_token = "sk-api-test-token"
|
|
api_endpoint = "https://api.z.ai/v1"
|
|
oauth_token = "sk-ant-oat01-oauth-token"
|
|
|
|
# Set both API profile and OAuth
|
|
monkeypatch.setenv("ANTHROPIC_AUTH_TOKEN", api_token)
|
|
monkeypatch.setenv("ANTHROPIC_BASE_URL", api_endpoint)
|
|
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", oauth_token)
|
|
|
|
sdk_env = get_sdk_env_vars()
|
|
|
|
# SDK_ENV_VARS doesn't include CLAUDE_CODE_OAUTH_TOKEN
|
|
# (it's set separately in create_client())
|
|
assert "CLAUDE_CODE_OAUTH_TOKEN" not in sdk_env
|
|
assert sdk_env.get("ANTHROPIC_AUTH_TOKEN") == api_token
|
|
assert sdk_env.get("ANTHROPIC_BASE_URL") == api_endpoint
|
|
|
|
def test_api_profile_mode_does_not_validate_oauth_token(self, tmp_path, monkeypatch):
|
|
"""In API profile mode, OAuth token validation is skipped."""
|
|
api_token = "sk-api-test-token"
|
|
api_endpoint = "https://api.z.ai/v1"
|
|
encrypted_oauth_token = "enc:encrypted-oauth-token" # Invalid encrypted format
|
|
|
|
monkeypatch.setenv("ANTHROPIC_AUTH_TOKEN", api_token)
|
|
monkeypatch.setenv("ANTHROPIC_BASE_URL", api_endpoint)
|
|
# Even with a bogus encrypted OAuth token, API profile mode should work
|
|
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", encrypted_oauth_token)
|
|
|
|
# Mock the SDK client
|
|
mock_sdk_client = MagicMock()
|
|
with patch("core.client.ClaudeSDKClient", return_value=mock_sdk_client):
|
|
from core.client import create_client
|
|
|
|
# Should NOT raise ValueError about encrypted token
|
|
# because OAuth validation is skipped in API profile mode
|
|
client = create_client(tmp_path, tmp_path, "glm-4", "coder")
|
|
|
|
assert client is mock_sdk_client
|
|
|
|
def test_oauth_mode_validates_token_even_with_api_env_vars_set(self, tmp_path, monkeypatch):
|
|
"""In OAuth mode (no BASE_URL), token validation happens even if ANTHROPIC_AUTH_TOKEN is set."""
|
|
api_token = "sk-api-test-token" # This exists but should be ignored in OAuth mode
|
|
encrypted_oauth_token = "enc:encrypted-oauth-token" # Invalid encrypted format
|
|
|
|
# Set ANTHROPIC_AUTH_TOKEN but NOT ANTHROPIC_BASE_URL - this is OAuth mode
|
|
monkeypatch.setenv("ANTHROPIC_AUTH_TOKEN", api_token)
|
|
monkeypatch.delenv("ANTHROPIC_BASE_URL", raising=False)
|
|
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", encrypted_oauth_token)
|
|
monkeypatch.setattr("core.auth.get_token_from_keychain", lambda _config_dir=None: None)
|
|
|
|
from core.client import create_client
|
|
|
|
# Should raise ValueError about encrypted token because we're in OAuth mode
|
|
with pytest.raises(ValueError, match="encrypted format"):
|
|
create_client(tmp_path, tmp_path, "claude-sonnet-4", "coder")
|
|
|
|
|
|
class TestAPIProfileAuthenticationEdgeCases:
|
|
"""Edge case tests for API profile authentication."""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def setup(self, clear_auth_env):
|
|
"""Use shared clear_auth_env fixture."""
|
|
pass
|
|
|
|
def test_whitespace_base_url_treated_as_empty(self, tmp_path, monkeypatch):
|
|
"""Whitespace-only ANTHROPIC_BASE_URL is trimmed and treated as empty (OAuth mode)."""
|
|
oauth_token = "sk-ant-oat01-oauth-token"
|
|
|
|
# Set whitespace-only ANTHROPIC_BASE_URL - should be trimmed to empty string
|
|
monkeypatch.setenv("ANTHROPIC_BASE_URL", " ")
|
|
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", oauth_token)
|
|
monkeypatch.setattr("core.auth.get_token_from_keychain", lambda _config_dir=None: None)
|
|
|
|
# Mock the SDK client
|
|
mock_sdk_client = MagicMock()
|
|
with patch("core.client.ClaudeSDKClient", return_value=mock_sdk_client):
|
|
from core.client import create_client
|
|
|
|
# Should use OAuth mode (whitespace is trimmed)
|
|
client = create_client(tmp_path, tmp_path, "claude-sonnet-4", "coder")
|
|
|
|
# Verify SDK client was created successfully
|
|
assert client is mock_sdk_client
|
|
|
|
def test_unicode_base_url(self, tmp_path, monkeypatch):
|
|
"""API profile mode works with Unicode characters in endpoint URL."""
|
|
api_token = "sk-api-test-token-123456"
|
|
# Using an IDN (Internationalized Domain Name)
|
|
api_endpoint = "https://münchen.example.com/v1"
|
|
|
|
monkeypatch.setenv("ANTHROPIC_AUTH_TOKEN", api_token)
|
|
monkeypatch.setenv("ANTHROPIC_BASE_URL", api_endpoint)
|
|
|
|
mock_sdk_client = MagicMock()
|
|
with patch("core.client.ClaudeSDKClient", return_value=mock_sdk_client):
|
|
from core.client import create_client
|
|
|
|
client = create_client(tmp_path, tmp_path, "glm-4", "coder")
|
|
|
|
assert client is mock_sdk_client
|
|
assert os.environ.get("ANTHROPIC_BASE_URL") == api_endpoint
|
|
|
|
def test_api_token_with_special_characters(self, tmp_path, monkeypatch):
|
|
"""API profile mode works with tokens containing special characters."""
|
|
# Tokens with various formats
|
|
test_tokens = [
|
|
"sk-api-simple",
|
|
"sk-api-with-dashes-and_underscores",
|
|
"sk.api.with.dots",
|
|
"sk_api_with_123456_numbers",
|
|
]
|
|
|
|
api_endpoint = "https://api.example.com/v1"
|
|
|
|
for token in test_tokens:
|
|
monkeypatch.setenv("ANTHROPIC_AUTH_TOKEN", token)
|
|
monkeypatch.setenv("ANTHROPIC_BASE_URL", api_endpoint)
|
|
|
|
mock_sdk_client = MagicMock()
|
|
with patch("core.client.ClaudeSDKClient", return_value=mock_sdk_client):
|
|
from core.client import create_client
|
|
|
|
client = create_client(tmp_path, tmp_path, "glm-4", "coder")
|
|
|
|
assert client is mock_sdk_client
|
|
assert os.environ.get("ANTHROPIC_AUTH_TOKEN") == token
|
|
|
|
|
|
class TestSimpleClientAPIProfileAuthentication:
|
|
"""Tests for API Profile authentication mode in create_simple_client()."""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def setup(self, clear_auth_env):
|
|
"""Use shared clear_auth_env fixture."""
|
|
pass
|
|
|
|
def test_simple_client_api_profile_mode_with_valid_token(self, monkeypatch):
|
|
"""create_simple_client() works with API profile mode."""
|
|
api_token = "sk-api-test-token-123456"
|
|
api_endpoint = "https://api.z.ai/v1"
|
|
|
|
monkeypatch.setenv("ANTHROPIC_AUTH_TOKEN", api_token)
|
|
monkeypatch.setenv("ANTHROPIC_BASE_URL", api_endpoint)
|
|
monkeypatch.delenv("CLAUDE_CODE_OAUTH_TOKEN", raising=False)
|
|
|
|
mock_sdk_client = MagicMock()
|
|
with patch("core.simple_client.ClaudeSDKClient", return_value=mock_sdk_client):
|
|
from core.simple_client import create_simple_client
|
|
|
|
client = create_simple_client(agent_type="merge_resolver")
|
|
|
|
# Verify SDK client was created
|
|
assert client is mock_sdk_client
|
|
|
|
# Verify CLAUDE_CODE_OAUTH_TOKEN was NOT set (API profile mode)
|
|
assert "CLAUDE_CODE_OAUTH_TOKEN" not in os.environ
|
|
|
|
def test_simple_client_api_profile_mode_missing_token_raises_error(self, monkeypatch):
|
|
"""create_simple_client() raises ValueError when API profile mode but no token."""
|
|
api_endpoint = "https://api.z.ai/v1"
|
|
|
|
monkeypatch.setenv("ANTHROPIC_BASE_URL", api_endpoint)
|
|
monkeypatch.delenv("ANTHROPIC_AUTH_TOKEN", raising=False)
|
|
monkeypatch.delenv("CLAUDE_CODE_OAUTH_TOKEN", raising=False)
|
|
|
|
from core.simple_client import create_simple_client
|
|
|
|
with pytest.raises(ValueError, match=r"API profile mode active.*ANTHROPIC_AUTH_TOKEN is not set"):
|
|
create_simple_client(agent_type="merge_resolver")
|
|
|
|
def test_simple_client_oauth_mode_without_base_url(self, monkeypatch):
|
|
"""create_simple_client() uses OAuth mode when ANTHROPIC_BASE_URL is not set."""
|
|
oauth_token = "sk-ant-oat01-oauth-token"
|
|
|
|
monkeypatch.delenv("ANTHROPIC_BASE_URL", raising=False)
|
|
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", oauth_token)
|
|
monkeypatch.setattr("core.auth.get_token_from_keychain", lambda _config_dir=None: None)
|
|
|
|
mock_sdk_client = MagicMock()
|
|
with patch("core.simple_client.ClaudeSDKClient", return_value=mock_sdk_client):
|
|
from core.simple_client import create_simple_client
|
|
|
|
client = create_simple_client(agent_type="merge_resolver")
|
|
|
|
# Verify SDK client was created
|
|
assert client is mock_sdk_client
|
|
|
|
# Verify CLAUDE_CODE_OAUTH_TOKEN was set (OAuth mode)
|
|
assert os.environ.get("CLAUDE_CODE_OAUTH_TOKEN") == oauth_token
|
|
|
|
def test_simple_client_api_profile_takes_precedence_over_oauth(self, monkeypatch):
|
|
"""
|
|
When both ANTHROPIC_BASE_URL and OAuth token are set, API profile mode wins.
|
|
|
|
create_simple_client() explicitly removes CLAUDE_CODE_OAUTH_TOKEN in API profile mode
|
|
so the SDK uses ANTHROPIC_AUTH_TOKEN instead (SDK prioritizes OAuth over API keys).
|
|
"""
|
|
api_token = "sk-api-test-token-123456"
|
|
api_endpoint = "https://api.z.ai/v1"
|
|
oauth_token = "sk-ant-oat01-oauth-token"
|
|
|
|
# Set both API profile and OAuth
|
|
monkeypatch.setenv("ANTHROPIC_AUTH_TOKEN", api_token)
|
|
monkeypatch.setenv("ANTHROPIC_BASE_URL", api_endpoint)
|
|
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", oauth_token)
|
|
|
|
# Mock the SDK client and OAuth functions to verify OAuth path is NOT taken
|
|
mock_sdk_client = MagicMock()
|
|
with patch("core.simple_client.ClaudeSDKClient", return_value=mock_sdk_client), \
|
|
patch("core.auth.require_auth_token") as mock_require, \
|
|
patch("core.auth.validate_token_not_encrypted") as mock_validate:
|
|
from core.simple_client import create_simple_client
|
|
|
|
client = create_simple_client(agent_type="merge_resolver")
|
|
|
|
# Verify SDK client was created
|
|
assert client is mock_sdk_client
|
|
|
|
# Verify CLAUDE_CODE_OAUTH_TOKEN was removed (API profile mode)
|
|
assert "CLAUDE_CODE_OAUTH_TOKEN" not in os.environ
|
|
|
|
# Ensure OAuth flow was NOT used (this proves API profile path was taken)
|
|
mock_require.assert_not_called()
|
|
mock_validate.assert_not_called()
|
|
|
|
def test_simple_client_whitespace_base_url_triggers_oauth_mode(self, monkeypatch):
|
|
"""Whitespace-only ANTHROPIC_BASE_URL is trimmed and treated as empty (OAuth mode)."""
|
|
oauth_token = "sk-ant-oat01-oauth-token"
|
|
|
|
# Set whitespace-only ANTHROPIC_BASE_URL - should be trimmed to empty string
|
|
monkeypatch.setenv("ANTHROPIC_BASE_URL", " ")
|
|
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", oauth_token)
|
|
monkeypatch.setattr("core.auth.get_token_from_keychain", lambda _config_dir=None: None)
|
|
|
|
mock_sdk_client = MagicMock()
|
|
with patch("core.simple_client.ClaudeSDKClient", return_value=mock_sdk_client):
|
|
from core.simple_client import create_simple_client
|
|
|
|
# Should use OAuth mode (whitespace is trimmed)
|
|
client = create_simple_client(agent_type="merge_resolver")
|
|
|
|
# Verify SDK client was created successfully
|
|
assert client is mock_sdk_client
|