* feat(system): fix(windows-ci): combine pip bootstrap fix + venv activation in Verify step Combines both fixes needed to get windows-test.yml actually passing: 1. setup.sh: stop swallowing ensurepip errors (quiet + 2>/dev/null + || true was hiding real failures — pip was silently not installed). Add get-pip.py fallback and hard pip verification. 2. windows-test.yml: add 'source .venv/Scripts/activate' to Verify step. Each CI step gets a fresh shell — setup.sh's venv activation doesn't carry over, so drone wasn't on PATH in the subsequent step. Together, these should take Windows CI from the 'silent failure every run since creation' state to actually green. Supersedes PRs #330 and #331 which had the fixes on separate branches (neither green alone). Co-Authored-By: @devpulse <devpulse@aipass> * feat(system): chore(lint): ruff auto-fix sweep — 303 errors across 178 files (F401 unused imports + F541 f-string placeholders + F811 redefined); restored report_error re-export + added logger call in errors.py Co-Authored-By: @devpulse <devpulse@aipass> --------- Co-authored-by: @devpulse <devpulse@aipass>
343 lines
12 KiB
Python
343 lines
12 KiB
Python
# =================== AIPass ====================
|
|
# Name: test_critical_paths.py
|
|
# Description: Critical path tests for API branch core functions
|
|
# Version: 1.0.0
|
|
# Created: 2026-03-31
|
|
# Modified: 2026-03-31
|
|
# =============================================
|
|
|
|
"""
|
|
Critical path tests for the API branch.
|
|
|
|
Covers the 4 core functions that form the API request pipeline:
|
|
|
|
1. get_api_key() - Key retrieval from config JSON and secrets file
|
|
2. validate_key() - Key format validation per provider rules
|
|
3. get_response() - Main API call orchestrator
|
|
4. extract_response() - Response content extraction
|
|
|
|
All external dependencies are mocked. File-based tests use tmp_path.
|
|
"""
|
|
|
|
import json
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
|
|
|
|
# =============================================
|
|
# 1. get_api_key() tests
|
|
# =============================================
|
|
|
|
|
|
class TestGetApiKey:
|
|
"""Tests for get_api_key() — key retrieval from config and secrets sources."""
|
|
|
|
@patch("aipass.api.apps.handlers.auth.keys.json_handler")
|
|
@patch("aipass.api.apps.handlers.auth.keys.API_JSON_DIR")
|
|
def test_key_from_config_json(self, mock_api_json_dir, mock_jh, tmp_path):
|
|
"""Key found in api_connect_config.json is returned after validation."""
|
|
from aipass.api.apps.handlers.auth.keys import get_api_key
|
|
|
|
config_path = tmp_path / "api_connect_config.json"
|
|
config_data = {
|
|
"config": {
|
|
"providers": {
|
|
"openrouter": {
|
|
"api_key": "sk-or-v1-NOTREAL-test-000000000000000000000000000000000000"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
config_path.write_text(json.dumps(config_data), encoding="utf-8")
|
|
|
|
mock_api_json_dir.__truediv__ = lambda self, name: tmp_path / name
|
|
|
|
result = get_api_key("openrouter")
|
|
#do not add real api kets here
|
|
assert result == "sk-or-v1-NOTREAL-test-000000000000000000000000000000000000"
|
|
|
|
mock_jh.log_operation.assert_called_once()
|
|
|
|
@patch("aipass.api.apps.handlers.auth.keys.json_handler")
|
|
@patch("aipass.api.apps.handlers.auth.keys.API_JSON_DIR")
|
|
def test_key_from_secrets_file(self, mock_api_json_dir, mock_jh, tmp_path):
|
|
"""Key found in ~/.secrets/aipass/.env when config has no key."""
|
|
from aipass.api.apps.handlers.auth.keys import get_api_key
|
|
|
|
# Config file exists but has no key for openrouter
|
|
config_path = tmp_path / "api_connect_config.json"
|
|
config_data = {"config": {"providers": {}}}
|
|
config_path.write_text(json.dumps(config_data), encoding="utf-8")
|
|
mock_api_json_dir.__truediv__ = lambda self, name: tmp_path / name
|
|
|
|
# Create secrets file
|
|
secrets_dir = tmp_path / ".secrets" / "aipass"
|
|
secrets_dir.mkdir(parents=True)
|
|
env_file = secrets_dir / ".env"
|
|
env_file.write_text(
|
|
"OPENROUTER_API_KEY=sk-or-v1-NOTREAL-test-00000000000000000000000000\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
with patch("aipass.api.apps.handlers.auth.keys.Path") as mock_path_cls:
|
|
# Path.home() should return tmp_path so secrets resolve there
|
|
mock_path_cls.home.return_value = tmp_path
|
|
result = get_api_key("openrouter")
|
|
|
|
assert result == "sk-or-v1-NOTREAL-test-00000000000000000000000000"
|
|
|
|
@patch("aipass.api.apps.handlers.auth.keys.json_handler")
|
|
@patch("aipass.api.apps.handlers.auth.keys.API_JSON_DIR")
|
|
def test_no_key_found_returns_none(self, mock_api_json_dir, mock_jh, tmp_path):
|
|
"""Returns None when no key exists in any source."""
|
|
from aipass.api.apps.handlers.auth.keys import get_api_key
|
|
|
|
# Config file with no providers
|
|
config_path = tmp_path / "api_connect_config.json"
|
|
config_data = {"config": {"providers": {}}}
|
|
config_path.write_text(json.dumps(config_data), encoding="utf-8")
|
|
mock_api_json_dir.__truediv__ = lambda self, name: tmp_path / name
|
|
|
|
# No secrets file exists
|
|
with patch("aipass.api.apps.handlers.auth.keys.Path") as mock_path_cls:
|
|
mock_home = tmp_path / "nonexistent_home"
|
|
mock_path_cls.home.return_value = mock_home
|
|
result = get_api_key("openrouter")
|
|
|
|
assert result is None
|
|
|
|
@patch("aipass.api.apps.handlers.auth.keys.json_handler")
|
|
@patch("aipass.api.apps.handlers.auth.keys.API_JSON_DIR")
|
|
def test_invalid_key_format_returns_none(self, mock_api_json_dir, mock_jh, tmp_path):
|
|
"""Key exists in config but fails validation (wrong prefix) returns None."""
|
|
from aipass.api.apps.handlers.auth.keys import get_api_key
|
|
|
|
config_path = tmp_path / "api_connect_config.json"
|
|
config_data = {
|
|
"config": {
|
|
"providers": {
|
|
"openrouter": {
|
|
"api_key": "INVALID-PREFIX-key-that-is-long-enough"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
config_path.write_text(json.dumps(config_data), encoding="utf-8")
|
|
mock_api_json_dir.__truediv__ = lambda self, name: tmp_path / name
|
|
|
|
# No secrets file fallback
|
|
with patch("aipass.api.apps.handlers.auth.keys.Path") as mock_path_cls:
|
|
mock_path_cls.home.return_value = tmp_path / "no_home"
|
|
result = get_api_key("openrouter")
|
|
|
|
assert result is None
|
|
|
|
|
|
# =============================================
|
|
# 2. validate_key() tests
|
|
# =============================================
|
|
|
|
|
|
class TestValidateKey:
|
|
"""Tests for validate_key() — key format validation per provider rules."""
|
|
|
|
def test_valid_openrouter_key(self):
|
|
"""Valid openrouter key with correct prefix and length passes."""
|
|
from aipass.api.apps.handlers.auth.keys import validate_key
|
|
|
|
key = "sk-or-v1-NOTREAL-test-000000000000000000000000000000000000"
|
|
assert validate_key(key, "openrouter") is True
|
|
|
|
def test_key_too_short(self):
|
|
"""Key shorter than min_length fails validation."""
|
|
from aipass.api.apps.handlers.auth.keys import validate_key
|
|
|
|
key = "sk-or-v1-short"
|
|
assert len(key) < 20
|
|
assert validate_key(key, "openrouter") is False
|
|
|
|
def test_wrong_prefix(self):
|
|
"""Key with wrong prefix for provider fails validation."""
|
|
from aipass.api.apps.handlers.auth.keys import validate_key
|
|
|
|
key = "sk-wrong-prefix-but-long-enough-to-pass-length"
|
|
assert validate_key(key, "openrouter") is False
|
|
|
|
def test_empty_key(self):
|
|
"""Empty string key fails validation."""
|
|
from aipass.api.apps.handlers.auth.keys import validate_key
|
|
|
|
assert validate_key("", "openrouter") is False
|
|
|
|
def test_none_key(self):
|
|
"""None key fails validation."""
|
|
from aipass.api.apps.handlers.auth.keys import validate_key
|
|
|
|
assert validate_key(None, "openrouter") is False # type: ignore[arg-type]
|
|
|
|
def test_generic_provider_no_prefix_required(self):
|
|
"""Generic provider only checks min_length, no prefix required."""
|
|
from aipass.api.apps.handlers.auth.keys import validate_key
|
|
|
|
key = "any-key-that-is-long-enough"
|
|
assert validate_key(key, "unknown_provider") is True
|
|
|
|
def test_generic_provider_too_short(self):
|
|
"""Generic provider rejects keys shorter than 10 chars."""
|
|
from aipass.api.apps.handlers.auth.keys import validate_key
|
|
|
|
assert validate_key("short", "unknown_provider") is False
|
|
|
|
|
|
# =============================================
|
|
# 3. get_response() tests
|
|
# =============================================
|
|
|
|
|
|
MODULE = "aipass.api.apps.handlers.openrouter.client"
|
|
|
|
|
|
class TestGetResponse:
|
|
"""Tests for get_response() — main API call orchestrator."""
|
|
|
|
@patch(f"{MODULE}.track_usage")
|
|
@patch(f"{MODULE}.extract_response")
|
|
@patch(f"{MODULE}.make_api_request")
|
|
@patch(f"{MODULE}.get_cached_client")
|
|
@patch(f"{MODULE}.get_api_key")
|
|
@patch(f"{MODULE}.ensure_caller_config")
|
|
@patch(f"{MODULE}.get_caller_info")
|
|
def test_successful_call(
|
|
self,
|
|
mock_caller_info,
|
|
mock_ensure,
|
|
mock_get_key,
|
|
mock_get_client,
|
|
mock_make_req,
|
|
mock_extract,
|
|
mock_track,
|
|
):
|
|
"""Full successful pipeline: detect caller, get key, make request, extract, track."""
|
|
from aipass.api.apps.handlers.openrouter.client import get_response
|
|
|
|
mock_caller_info.return_value = {"caller_name": "test-branch"}
|
|
mock_get_key.return_value = "FAKE-sk-or-v1-testkey"
|
|
mock_get_client.return_value = MagicMock()
|
|
mock_make_req.return_value = MagicMock()
|
|
mock_extract.return_value = {
|
|
"content": "Hello, world!",
|
|
"id": "gen-abc123",
|
|
"model": "anthropic/claude-3.5-sonnet",
|
|
}
|
|
|
|
result = get_response(
|
|
"What is Python?", model="anthropic/claude-3.5-sonnet"
|
|
)
|
|
|
|
assert result is not None
|
|
assert result["content"] == "Hello, world!"
|
|
assert result["id"] == "gen-abc123"
|
|
mock_track.assert_called_once()
|
|
|
|
@patch(f"{MODULE}.get_api_key")
|
|
@patch(f"{MODULE}.ensure_caller_config")
|
|
@patch(f"{MODULE}.get_caller_info")
|
|
def test_no_model_returns_none(
|
|
self, mock_caller_info, mock_ensure, mock_get_key
|
|
):
|
|
"""Missing model parameter returns None without making API call."""
|
|
from aipass.api.apps.handlers.openrouter.client import get_response
|
|
|
|
mock_caller_info.return_value = {"caller_name": "test"}
|
|
|
|
result = get_response("What is Python?", model=None)
|
|
|
|
assert result is None
|
|
mock_get_key.assert_not_called()
|
|
|
|
@patch(f"{MODULE}.get_api_key")
|
|
@patch(f"{MODULE}.ensure_caller_config")
|
|
@patch(f"{MODULE}.get_caller_info")
|
|
def test_no_api_key_returns_none(
|
|
self, mock_caller_info, mock_ensure, mock_get_key
|
|
):
|
|
"""No API key available returns None."""
|
|
from aipass.api.apps.handlers.openrouter.client import get_response
|
|
|
|
mock_caller_info.return_value = {"caller_name": "test"}
|
|
mock_get_key.return_value = None
|
|
|
|
result = get_response(
|
|
"What is Python?", model="anthropic/claude-3.5-sonnet"
|
|
)
|
|
|
|
assert result is None
|
|
|
|
|
|
# =============================================
|
|
# 4. extract_response() tests
|
|
# =============================================
|
|
|
|
|
|
class TestExtractResponse:
|
|
"""Tests for extract_response() — response content extraction from OpenAI objects."""
|
|
|
|
def test_valid_response(self):
|
|
"""Extracts content, id, and model from a well-formed response."""
|
|
from aipass.api.apps.handlers.openrouter.client import extract_response
|
|
|
|
choice = MagicMock()
|
|
choice.message.content = "The answer is 42."
|
|
choice.finish_reason = "stop"
|
|
|
|
response = MagicMock()
|
|
response.choices = [choice]
|
|
response.id = "gen-xyz789"
|
|
response.model = "anthropic/claude-3.5-sonnet"
|
|
|
|
result = extract_response(response)
|
|
|
|
assert result is not None
|
|
assert result["content"] == "The answer is 42."
|
|
assert result["id"] == "gen-xyz789"
|
|
assert result["model"] == "anthropic/claude-3.5-sonnet"
|
|
|
|
def test_none_response(self):
|
|
"""None response returns None."""
|
|
from aipass.api.apps.handlers.openrouter.client import extract_response
|
|
|
|
assert extract_response(None) is None
|
|
|
|
def test_response_no_choices(self):
|
|
"""Response with empty choices list returns None."""
|
|
from aipass.api.apps.handlers.openrouter.client import extract_response
|
|
|
|
response = MagicMock()
|
|
response.choices = []
|
|
|
|
assert extract_response(response) is None
|
|
|
|
def test_response_no_content(self):
|
|
"""Response with choice but no content returns None."""
|
|
from aipass.api.apps.handlers.openrouter.client import extract_response
|
|
|
|
choice = MagicMock()
|
|
choice.message.content = None
|
|
|
|
response = MagicMock()
|
|
response.choices = [choice]
|
|
response.id = "gen-empty"
|
|
response.model = "test/model"
|
|
|
|
assert extract_response(response) is None
|
|
|
|
def test_response_missing_message(self):
|
|
"""Response choice without message attribute returns None."""
|
|
from aipass.api.apps.handlers.openrouter.client import extract_response
|
|
|
|
choice = MagicMock(spec=[]) # No attributes at all
|
|
response = MagicMock()
|
|
response.choices = [choice]
|
|
|
|
assert extract_response(response) is None
|