Skip to content

Commit 3d075b7

Browse files
committed
test: Add API key detection tests and CI debug script
- Add comprehensive tests for API key detection in different environments - Add debug script to check CI environment and API key availability - Add debug step to CI workflow to diagnose API key issues - Related to issue #124
1 parent 8030dee commit 3d075b7

File tree

3 files changed

+263
-0
lines changed

3 files changed

+263
-0
lines changed

.github/workflows/tests.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@ jobs:
2828
python -m pip install --upgrade pip
2929
pip install -e ".[dev]"
3030
31+
- name: Debug CI Environment
32+
env:
33+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
34+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
35+
GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_AI_API_KEY }}
36+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
37+
run: |
38+
python debug_ci_env.py
39+
3140
- name: Run tests
3241
env:
3342
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}

debug_ci_env.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/usr/bin/env python3
2+
"""Debug script to check environment in CI."""
3+
4+
import os
5+
import sys
6+
7+
print("=== CI Environment Debug ===")
8+
print(f"Python version: {sys.version}")
9+
print(f"CI: {os.environ.get('CI', 'not set')}")
10+
print(f"GITHUB_ACTIONS: {os.environ.get('GITHUB_ACTIONS', 'not set')}")
11+
print(f"GITHUB_WORKFLOW: {os.environ.get('GITHUB_WORKFLOW', 'not set')}")
12+
13+
# Check for API keys (don't print values)
14+
api_keys = {
15+
"OPENAI_API_KEY": os.environ.get("OPENAI_API_KEY"),
16+
"ANTHROPIC_API_KEY": os.environ.get("ANTHROPIC_API_KEY"),
17+
"GOOGLE_AI_API_KEY": os.environ.get("GOOGLE_AI_API_KEY"),
18+
"HF_TOKEN": os.environ.get("HF_TOKEN"),
19+
}
20+
21+
print("\n=== API Keys Status ===")
22+
for key, value in api_keys.items():
23+
if value:
24+
print(f"{key}: SET (length: {len(value)})")
25+
else:
26+
print(f"{key}: NOT SET")
27+
28+
# Try loading API keys through our function
29+
print("\n=== Testing load_api_keys_optional ===")
30+
try:
31+
from orchestrator.utils.api_keys_flexible import load_api_keys_optional
32+
loaded = load_api_keys_optional()
33+
print(f"Loaded {len(loaded)} API keys: {list(loaded.keys())}")
34+
except Exception as e:
35+
print(f"Error loading API keys: {e}")
36+
37+
# Try initializing models
38+
print("\n=== Testing init_models ===")
39+
try:
40+
from orchestrator import init_models
41+
registry = init_models()
42+
models = registry.list_models()
43+
print(f"Initialized {len(models)} models")
44+
if models:
45+
print(f"First 5 models: {models[:5]}")
46+
except Exception as e:
47+
print(f"Error initializing models: {e}")
48+
import traceback
49+
traceback.print_exc()

tests/test_api_key_detection.py

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
"""Test API key detection in different environments."""
2+
3+
import os
4+
import pytest
5+
from pathlib import Path
6+
from unittest.mock import patch, MagicMock
7+
8+
from orchestrator.utils.api_keys_flexible import (
9+
load_api_keys_optional,
10+
get_missing_providers,
11+
ensure_api_key
12+
)
13+
14+
15+
class TestAPIKeyDetection:
16+
"""Test API key detection functionality."""
17+
18+
def test_load_api_keys_from_environment(self):
19+
"""Test loading API keys from environment variables."""
20+
# Set test environment variables
21+
test_keys = {
22+
"OPENAI_API_KEY": "test-openai-key",
23+
"ANTHROPIC_API_KEY": "test-anthropic-key",
24+
"GOOGLE_AI_API_KEY": "test-google-key",
25+
"HF_TOKEN": "test-hf-token"
26+
}
27+
28+
with patch.dict(os.environ, test_keys, clear=False):
29+
# Should load from environment
30+
loaded_keys = load_api_keys_optional()
31+
32+
assert len(loaded_keys) >= 4 # At least our test keys
33+
assert loaded_keys.get("openai") == "test-openai-key"
34+
assert loaded_keys.get("anthropic") == "test-anthropic-key"
35+
assert loaded_keys.get("google") == "test-google-key"
36+
assert loaded_keys.get("huggingface") == "test-hf-token"
37+
38+
def test_load_api_keys_github_actions(self):
39+
"""Test loading API keys in GitHub Actions environment."""
40+
test_keys = {
41+
"GITHUB_ACTIONS": "true",
42+
"CI": "true",
43+
"OPENAI_API_KEY": "gh-secret-openai",
44+
"ANTHROPIC_API_KEY": "gh-secret-anthropic"
45+
}
46+
47+
with patch.dict(os.environ, test_keys, clear=True):
48+
# Should detect GitHub Actions and use environment variables
49+
loaded_keys = load_api_keys_optional()
50+
51+
assert loaded_keys.get("openai") == "gh-secret-openai"
52+
assert loaded_keys.get("anthropic") == "gh-secret-anthropic"
53+
# Should not have keys that weren't set
54+
assert "google" not in loaded_keys
55+
assert "huggingface" not in loaded_keys
56+
57+
def test_load_api_keys_from_dotenv_file(self):
58+
"""Test loading API keys from .env file."""
59+
# Create a temporary .env file
60+
import tempfile
61+
with tempfile.NamedTemporaryFile(mode='w', suffix='.env', delete=False) as f:
62+
f.write("OPENAI_API_KEY=file-openai-key\n")
63+
f.write("ANTHROPIC_API_KEY=file-anthropic-key\n")
64+
temp_env_path = Path(f.name)
65+
66+
try:
67+
# Mock the home directory .env path
68+
with patch('orchestrator.utils.api_keys_flexible.Path.home') as mock_home:
69+
mock_home.return_value = temp_env_path.parent
70+
with patch.object(Path, 'exists') as mock_exists:
71+
def exists_side_effect(self):
72+
# Only our temp file exists
73+
return str(self) == str(temp_env_path.parent / ".orchestrator" / ".env")
74+
75+
mock_exists.side_effect = exists_side_effect
76+
77+
# Clear environment to ensure we load from file
78+
with patch.dict(os.environ, {}, clear=True):
79+
# This should load from our temp .env file
80+
loaded_keys = load_api_keys_optional()
81+
82+
# Note: dotenv loading in tests can be tricky
83+
# The test mainly verifies the code path works
84+
finally:
85+
# Clean up
86+
temp_env_path.unlink()
87+
88+
def test_get_missing_providers(self):
89+
"""Test identifying missing API key providers."""
90+
test_keys = {
91+
"OPENAI_API_KEY": "test-key",
92+
"ANTHROPIC_API_KEY": "test-key"
93+
}
94+
95+
# Mock the load_api_keys_optional to return only our test keys
96+
with patch('orchestrator.utils.api_keys_flexible.load_api_keys_optional') as mock_load:
97+
mock_load.return_value = {"openai": "test-key", "anthropic": "test-key"}
98+
99+
# Check all providers
100+
missing = get_missing_providers()
101+
assert "google" in missing
102+
assert "huggingface" in missing
103+
assert "openai" not in missing
104+
assert "anthropic" not in missing
105+
106+
# Check specific providers
107+
missing_specific = get_missing_providers({"openai", "google"})
108+
assert "google" in missing_specific
109+
assert "openai" not in missing_specific
110+
assert len(missing_specific) == 1
111+
112+
def test_ensure_api_key_success(self):
113+
"""Test ensuring API key when it exists."""
114+
with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}):
115+
key = ensure_api_key("openai")
116+
assert key == "test-key"
117+
118+
def test_ensure_api_key_missing(self):
119+
"""Test ensuring API key when it's missing."""
120+
# Mock load_api_keys_optional to return empty dict
121+
with patch('orchestrator.utils.api_keys_flexible.load_api_keys_optional') as mock_load:
122+
mock_load.return_value = {}
123+
124+
with pytest.raises(EnvironmentError) as exc_info:
125+
ensure_api_key("openai")
126+
127+
assert "Missing API key for openai" in str(exc_info.value)
128+
assert "OPENAI_API_KEY" in str(exc_info.value)
129+
130+
def test_debug_logging_output(self, capsys):
131+
"""Test that debug logging works correctly."""
132+
test_keys = {
133+
"GITHUB_ACTIONS": "true",
134+
"OPENAI_API_KEY": "test-key-12345",
135+
"ANTHROPIC_API_KEY": "test-key-67890"
136+
}
137+
138+
with patch.dict(os.environ, test_keys, clear=True):
139+
load_api_keys_optional()
140+
141+
# Check debug output
142+
captured = capsys.readouterr()
143+
assert "Running in GitHub Actions" in captured.out
144+
assert "Using environment variables from GitHub secrets" in captured.out
145+
assert "Found API key for openai (length: 14)" in captured.out
146+
assert "Found API key for anthropic (length: 14)" in captured.out
147+
assert "No API key found for google" in captured.out
148+
assert "Total API keys found: 2" in captured.out
149+
# Should not log actual key values
150+
assert "test-key-12345" not in captured.out
151+
assert "test-key-67890" not in captured.out
152+
153+
154+
class TestModelInitialization:
155+
"""Test model initialization with API keys."""
156+
157+
@pytest.mark.skipif(
158+
not any(os.environ.get(key) for key in ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"]),
159+
reason="No API keys available for model testing"
160+
)
161+
def test_init_models_with_api_keys(self):
162+
"""Test that init_models works when API keys are available."""
163+
from orchestrator import init_models
164+
165+
# Should initialize successfully
166+
registry = init_models()
167+
168+
# Should have at least one model
169+
models = registry.list_models()
170+
assert len(models) > 0
171+
172+
# Check that API key providers have models
173+
available_keys = load_api_keys_optional()
174+
for provider in available_keys:
175+
# Should have at least one model from each provider with keys
176+
provider_models = [m for m in models if m.startswith(f"{provider}:")]
177+
assert len(provider_models) > 0, f"No models found for provider {provider}"
178+
179+
def test_init_models_without_api_keys(self):
180+
"""Test that init_models handles missing API keys gracefully."""
181+
# Clear all API keys
182+
api_key_env_vars = ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GOOGLE_AI_API_KEY", "HF_TOKEN"]
183+
184+
# Save current values
185+
saved_env = {key: os.environ.get(key) for key in api_key_env_vars}
186+
187+
try:
188+
# Clear API keys
189+
for key in api_key_env_vars:
190+
if key in os.environ:
191+
del os.environ[key]
192+
193+
from orchestrator import init_models
194+
195+
# Should still initialize (might only have local models)
196+
registry = init_models()
197+
198+
# Should return a registry even with no models
199+
assert registry is not None
200+
201+
finally:
202+
# Restore environment
203+
for key, value in saved_env.items():
204+
if value is not None:
205+
os.environ[key] = value

0 commit comments

Comments
 (0)