From af06a4212ca25d99145f16c0b8478f6cec47aa7e Mon Sep 17 00:00:00 2001 From: dougollerenshaw Date: Thu, 19 Sep 2024 12:37:30 -0700 Subject: [PATCH 1/2] Added logic for handling a missing api key --- codeaide/logic/chat_handler.py | 105 +++++++++++++++++++++++++++++---- codeaide/ui/chat_window.py | 84 +++++++++++++++----------- codeaide/utils/api_utils.py | 93 +++++++++++++++++++++++------ 3 files changed, 217 insertions(+), 65 deletions(-) diff --git a/codeaide/logic/chat_handler.py b/codeaide/logic/chat_handler.py index eefc68e..8de7586 100644 --- a/codeaide/logic/chat_handler.py +++ b/codeaide/logic/chat_handler.py @@ -1,7 +1,15 @@ import json import os - -from codeaide.utils.api_utils import parse_response, send_api_request +import sys +import re +import traceback +from codeaide.utils.api_utils import ( + parse_response, + send_api_request, + get_api_client, + save_api_key, + MissingAPIKeyException, +) from codeaide.utils.constants import MAX_RETRIES, MAX_TOKENS from codeaide.utils.cost_tracker import CostTracker from codeaide.utils.environment_manager import EnvironmentManager @@ -18,17 +26,93 @@ def __init__(self): self.env_manager = EnvironmentManager() self.terminal_manager = TerminalManager() self.latest_version = "0.0" + self.api_client = None + self.api_key_set = False + self.current_service = "anthropic" # Default service + + def check_api_key(self): + if self.api_client is None: + self.api_client = get_api_client(self.current_service) + + if self.api_client: + self.api_key_set = True + return True, None + else: + self.api_key_set = False + return False, self.get_api_key_instructions(self.current_service) + + def get_api_key_instructions(self, service): + if service == "anthropic": + return ( + "It looks like you haven't set up your Anthropic API key yet. " + "Here's how to get started:\n\n" + "1. Go to https://www.anthropic.com or https://console.anthropic.com to sign up or log in.\n" + "2. Navigate to your account settings or API section.\n" + "3. Generate a new API key.\n" + "4. Add some funds to your account to cover the cost of using the API (start with as little as $1).\n" + "5. Copy the API key and paste it here.\n\n" + "Once you've pasted your API key, I'll save it securely in a .env file in the root of your project. " + "This file is already in .gitignore, so it won't be shared if you push your code to a repository.\n\n" + "Please paste your Anthropic API key now:" + ) + else: + return f"Please enter your API key for {service.capitalize()}:" + + def validate_api_key(self, api_key): + # Remove leading/trailing whitespace and quotes + cleaned_key = api_key.strip().strip("'\"") + + # Check if the API key follows a general pattern for API keys + pattern = r"^[a-zA-Z0-9_-]{32,}$" + if len(cleaned_key) < 32: + return False, "API key is too short (should be at least 32 characters)" + elif not re.match(pattern, cleaned_key): + return ( + False, + "API key should only contain letters, numbers, underscores, and hyphens", + ) + return True, "" + + def handle_api_key_input(self, api_key): + cleaned_key = api_key.strip().strip("'\"") # Remove quotes and whitespace + is_valid, error_message = self.validate_api_key(cleaned_key) + if is_valid: + if save_api_key(self.current_service, cleaned_key): + # Try to get a new API client with the new key + self.api_client = get_api_client(self.current_service) + if self.api_client: + self.api_key_set = True + return True, "API key saved and verified successfully." + else: + return ( + False, + "API key saved, but verification failed. Please check your key and try again.", + ) + else: + return ( + False, + "Failed to save the API key. Please check your permissions and try again.", + ) + else: + return False, f"Invalid API key format: {error_message}. Please try again." def process_input(self, user_input): try: + if not self.api_key_set or self.api_client is None: + api_key_valid, message = self.check_api_key() + if not api_key_valid: + return {"type": "api_key_required", "message": message} + + # At this point, we know we have a valid API client self.conversation_history.append({"role": "user", "content": user_input}) for attempt in range(MAX_RETRIES): version_info = f"\n\nThe latest code version was {self.latest_version}. If you're making minor changes to the previous code, increment the minor version (e.g., 1.0 to 1.1). If you're creating entirely new code, increment the major version (e.g., 1.1 to 2.0). Ensure the new version is higher than {self.latest_version}." self.conversation_history[-1]["content"] += version_info - response = send_api_request(self.conversation_history, MAX_TOKENS) - print(f"Response (Attempt {attempt + 1}): {response}") + response = send_api_request( + self.api_client, self.conversation_history, MAX_TOKENS + ) if response is None: if attempt == MAX_RETRIES - 1: @@ -42,7 +126,7 @@ def process_input(self, user_input): try: parsed_response = parse_response(response) - if parsed_response[0] is None: # If parsing failed + if parsed_response[0] is None: raise ValueError("Failed to parse JSON response") ( @@ -88,8 +172,7 @@ def process_input(self, user_input): else: return {"type": "message", "message": text} - except (json.JSONDecodeError, ValueError) as e: - print(f"Error processing response (Attempt {attempt + 1}): {e}") + except (ValueError, json.JSONDecodeError) as e: if attempt < MAX_RETRIES - 1: error_prompt = f"\n\nThere was an error in your response: {e}. Please ensure you're using proper JSON formatting and incrementing the version number correctly. The latest version was {self.latest_version}, so the new version must be higher than this." self.conversation_history[-1]["content"] += error_prompt @@ -105,10 +188,10 @@ def process_input(self, user_input): } except Exception as e: - print("Unexpected error in process_input") + traceback.print_exc() return { - "type": "error", - "message": f"An unexpected error occurred: {str(e)}", + "type": "internal_error", + "message": f"An unexpected error occurred: {str(e)}. Please check the console window for the full traceback.", } @staticmethod @@ -125,14 +208,12 @@ def run_generated_code(self, filename, requirements): ) activation_command = self.env_manager.get_activation_command() - new_packages = self.env_manager.install_requirements(req_path) script_content = f""" clear # Clear the terminal echo "Activating environment..." {activation_command} - """ if new_packages: diff --git a/codeaide/ui/chat_window.py b/codeaide/ui/chat_window.py index 0f45930..6a1b418 100644 --- a/codeaide/ui/chat_window.py +++ b/codeaide/ui/chat_window.py @@ -1,8 +1,8 @@ import signal import sys +import traceback -from PyQt5.QtCore import Qt, QTimer, pyqtSignal -from PyQt5.QtGui import QColor, QFont +from PyQt5.QtCore import Qt, QTimer from PyQt5.QtWidgets import ( QApplication, QHBoxLayout, @@ -41,46 +41,33 @@ def __init__(self, chat_handler): self.chat_handler = chat_handler self.cost_tracker = chat_handler.cost_tracker self.code_popup = None + self.waiting_for_api_key = False self.setup_ui() self.add_to_chat("AI", INITIAL_MESSAGE) + self.check_api_key() - # Set up SIGINT handler signal.signal(signal.SIGINT, self.sigint_handler) - - # Allow CTRL+C to interrupt the Qt event loop self.timer = QTimer() - self.timer.start(500) # Timeout in ms - self.timer.timeout.connect(lambda: None) # Let the interpreter run each 500 ms + self.timer.start(500) + self.timer.timeout.connect(lambda: None) def setup_ui(self): central_widget = QWidget(self) self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) - main_layout.setSpacing(5) # Space between main components - main_layout.setContentsMargins(8, 8, 8, 8) # Margins around the entire window + main_layout.setSpacing(5) + main_layout.setContentsMargins(8, 8, 8, 8) - # Chat display self.chat_display = QTextEdit(self) self.chat_display.setReadOnly(True) self.chat_display.setStyleSheet( - f""" - background-color: {CHAT_WINDOW_BG}; - color: {CHAT_WINDOW_FG}; - border: 1px solid #ccc; - padding: 5px; - """ + f"background-color: {CHAT_WINDOW_BG}; color: {CHAT_WINDOW_FG}; border: 1px solid #ccc; padding: 5px;" ) main_layout.addWidget(self.chat_display, stretch=3) - # Input area self.input_text = QTextEdit(self) self.input_text.setStyleSheet( - f""" - background-color: {CHAT_WINDOW_BG}; - color: {USER_MESSAGE_COLOR}; - border: 1px solid #ccc; - padding: 5px; - """ + f"background-color: {CHAT_WINDOW_BG}; color: {USER_MESSAGE_COLOR}; border: 1px solid #ccc; padding: 5px;" ) self.input_text.setAcceptRichText(True) self.input_text.setFont(general_utils.set_font(USER_FONT)) @@ -89,9 +76,8 @@ def setup_ui(self): self.input_text.installEventFilter(self) main_layout.addWidget(self.input_text, stretch=1) - # Button layout button_layout = QHBoxLayout() - button_layout.setSpacing(5) # Space between buttons + button_layout.setSpacing(5) self.submit_button = QPushButton("Submit") self.submit_button.clicked.connect(self.on_submit) @@ -122,7 +108,6 @@ def eventFilter(self, obj, event): def on_submit(self): user_input = self.input_text.toPlainText().strip() if user_input: - print(f"ChatWindow: Received submission: {user_input}") self.add_to_chat("User", user_input) self.input_text.clear() self.display_thinking() @@ -135,11 +120,9 @@ def on_modify(self): self.input_text.ensureCursorVisible() def add_to_chat(self, sender, message): - print(f"ChatWindow: Adding to chat - {sender}: {message}") color = USER_MESSAGE_COLOR if sender == "User" else AI_MESSAGE_COLOR font = USER_FONT if sender == "User" else AI_FONT sender = AI_EMOJI if sender == "AI" else sender - html_message = general_utils.format_chat_message(sender, message, font, color) self.chat_display.append(html_message + "
") self.chat_display.ensureCursorVisible() @@ -148,11 +131,42 @@ def display_thinking(self): self.add_to_chat("AI", "Thinking... 🤔") def process_input(self, user_input): - response = self.chat_handler.process_input(user_input) - QTimer.singleShot(100, lambda: self.handle_response(response)) + try: + if self.waiting_for_api_key: + self.handle_api_key_input(user_input) + else: + response = self.chat_handler.process_input(user_input) + self.handle_response(response) + except Exception as e: + error_message = f"An unexpected error occurred: {str(e)}. Please check the console window for the full traceback." + self.add_to_chat("AI", error_message) + print("Unexpected error in ChatWindow process_input:", file=sys.stderr) + traceback.print_exc() + finally: + self.enable_ui_elements() - def handle_response(self, response): + def check_api_key(self): + api_key_valid, message = self.chat_handler.check_api_key() + if not api_key_valid: + self.add_to_chat("AI", message) + self.waiting_for_api_key = True + else: + self.waiting_for_api_key = False + + def handle_api_key_input(self, api_key): + success, message = self.chat_handler.handle_api_key_input(api_key) self.remove_thinking_messages() + if success: + self.waiting_for_api_key = False + self.add_to_chat( + "AI", + "Great! Your API key has been saved. What would you like to work on?", + ) + else: + self.add_to_chat("AI", message) + self.enable_ui_elements() + + def handle_response(self, response): self.enable_ui_elements() if response["type"] == "message": @@ -170,10 +184,11 @@ def handle_response(self, response): ) elif response["type"] == "code": self.add_to_chat("AI", response["message"]) - print("About to update or create code popup") self.update_or_create_code_popup(response) - print("Code popup updated or created") - elif response["type"] == "error": + elif response["type"] in ["error", "internal_error"]: + self.add_to_chat("AI", response["message"]) + elif response["type"] == "api_key_required": + self.waiting_for_api_key = True self.add_to_chat("AI", response["message"]) def remove_thinking_messages(self): @@ -238,5 +253,4 @@ def closeEvent(self, event): super().closeEvent(event) def sigint_handler(self, *args): - """Handler for the SIGINT signal.""" QApplication.quit() diff --git a/codeaide/utils/api_utils.py b/codeaide/utils/api_utils.py index 31bdd17..9abe89e 100644 --- a/codeaide/utils/api_utils.py +++ b/codeaide/utils/api_utils.py @@ -1,28 +1,80 @@ +import os import json import re - import anthropic from anthropic import APIError -from decouple import config +from decouple import config, AutoConfig from codeaide.utils.constants import AI_MODEL, MAX_TOKENS, SYSTEM_PROMPT -def get_anthropic_client(): +class MissingAPIKeyException(Exception): + def __init__(self, service): + self.service = service + super().__init__( + f"{service.upper()}_API_KEY not found in environment variables" + ) + + +def get_api_client(service="anthropic"): try: - api_key = config("ANTHROPIC_API_KEY", default=None) + # Force a reload of the configuration + auto_config = AutoConfig( + search_path=os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + ) + ) + api_key = auto_config(f"{service.upper()}_API_KEY", default=None) if api_key is None: - raise ValueError("ANTHROPIC_API_KEY not found in environment variables") - return anthropic.Anthropic(api_key=api_key) + raise MissingAPIKeyException(service) + + if service == "anthropic": + return anthropic.Anthropic(api_key=api_key) + # Add more elif blocks here for other API services + else: + raise ValueError(f"Unsupported service: {service}") + except MissingAPIKeyException: + # If the API key is missing, return None instead of raising the exception + return None except Exception as e: - print(f"Error initializing Anthropic API client: {str(e)}") + print(f"Error initializing {service.capitalize()} API client: {str(e)}") return None -client = get_anthropic_client() +def save_api_key(service, api_key): + try: + cleaned_key = api_key.strip().strip("'\"") # Remove quotes and whitespace + root_dir = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + ) + env_path = os.path.join(root_dir, ".env") + + if os.path.exists(env_path): + with open(env_path, "r") as file: + lines = file.readlines() + + key_exists = False + for i, line in enumerate(lines): + if line.startswith(f"{service.upper()}_API_KEY="): + lines[i] = f'{service.upper()}_API_KEY="{cleaned_key}"\n' + key_exists = True + break + + if not key_exists: + lines.append(f'{service.upper()}_API_KEY="{cleaned_key}"\n') + else: + lines = [f'{service.upper()}_API_KEY="{cleaned_key}"\n'] + + with open(env_path, "w") as file: + file.writelines(lines) + + return True + except Exception as e: + print(f"Error saving API key: {str(e)}") + return False -def send_api_request(conversation_history, max_tokens=MAX_TOKENS): +def send_api_request(client, conversation_history, max_tokens=MAX_TOKENS): system_prompt = SYSTEM_PROMPT try: print(f"\n\n{'='*50}\n") @@ -44,13 +96,13 @@ def send_api_request(conversation_history, max_tokens=MAX_TOKENS): if not content: print("Warning: Received empty response from API") - return None, True + return None return response except Exception as e: print(f"Error in API request: {str(e)}") - return None, True + return None def parse_response(response): @@ -73,14 +125,19 @@ def parse_response(response): return None, None, None, None, None, None -def check_api_connection(): +def check_api_connection(service="anthropic"): + client = get_api_client(service) try: - response = client.messages.create( - model=AI_MODEL, - max_tokens=100, - messages=[{"role": "user", "content": "Hi Claude, are we communicating?"}], - ) - return True, response.content[0].text.strip() + if service == "anthropic": + response = client.messages.create( + model=AI_MODEL, + max_tokens=100, + messages=[{"role": "user", "content": "Hi, are we communicating?"}], + ) + return True, response.content[0].text.strip() + # Add more elif blocks here for other API services + else: + raise ValueError(f"Unsupported service: {service}") except Exception as e: return False, str(e) From a9e7ccc4f06c111136e4af9f73ea36f86618d557 Mon Sep 17 00:00:00 2001 From: dougollerenshaw Date: Thu, 19 Sep 2024 13:00:42 -0700 Subject: [PATCH 2/2] Fixed tests, avoided formatting issue in input box --- codeaide/ui/chat_window.py | 12 ++++- codeaide/utils/api_utils.py | 48 ++++++------------ tests/utils/test_api_utils.py | 91 ++++++++++++++++++++++++++++++----- 3 files changed, 102 insertions(+), 49 deletions(-) diff --git a/codeaide/ui/chat_window.py b/codeaide/ui/chat_window.py index 6a1b418..de388f8 100644 --- a/codeaide/ui/chat_window.py +++ b/codeaide/ui/chat_window.py @@ -3,6 +3,7 @@ import traceback from PyQt5.QtCore import Qt, QTimer +from PyQt5.QtGui import QColor from PyQt5.QtWidgets import ( QApplication, QHBoxLayout, @@ -46,6 +47,8 @@ def __init__(self, chat_handler): self.add_to_chat("AI", INITIAL_MESSAGE) self.check_api_key() + self.input_text.setTextColor(QColor(CHAT_WINDOW_FG)) + signal.signal(signal.SIGINT, self.sigint_handler) self.timer = QTimer() self.timer.start(500) @@ -67,9 +70,14 @@ def setup_ui(self): self.input_text = QTextEdit(self) self.input_text.setStyleSheet( - f"background-color: {CHAT_WINDOW_BG}; color: {USER_MESSAGE_COLOR}; border: 1px solid #ccc; padding: 5px;" + f""" + background-color: {CHAT_WINDOW_BG}; + color: {CHAT_WINDOW_FG}; + border: 1px solid #ccc; + padding: 5px; + """ ) - self.input_text.setAcceptRichText(True) + self.input_text.setAcceptRichText(False) # Add this line self.input_text.setFont(general_utils.set_font(USER_FONT)) self.input_text.setFixedHeight(100) self.input_text.textChanged.connect(self.on_modify) diff --git a/codeaide/utils/api_utils.py b/codeaide/utils/api_utils.py index 9abe89e..bb269f1 100644 --- a/codeaide/utils/api_utils.py +++ b/codeaide/utils/api_utils.py @@ -25,17 +25,13 @@ def get_api_client(service="anthropic"): ) ) api_key = auto_config(f"{service.upper()}_API_KEY", default=None) - if api_key is None: - raise MissingAPIKeyException(service) + if api_key is None or api_key.strip() == "": + return None # Return None if API key is missing or empty - if service == "anthropic": + if service.lower() == "anthropic": return anthropic.Anthropic(api_key=api_key) - # Add more elif blocks here for other API services else: raise ValueError(f"Unsupported service: {service}") - except MissingAPIKeyException: - # If the API key is missing, return None instead of raising the exception - return None except Exception as e: print(f"Error initializing {service.capitalize()} API client: {str(e)}") return None @@ -77,29 +73,15 @@ def save_api_key(service, api_key): def send_api_request(client, conversation_history, max_tokens=MAX_TOKENS): system_prompt = SYSTEM_PROMPT try: - print(f"\n\n{'='*50}\n") - print( - f"Sending API request. The max tokens is {max_tokens}. Here's the conversation history:" - ) - for message in conversation_history: - print(f"{message['role']}: {message['content']}") - print("\n") - response = client.messages.create( model=AI_MODEL, max_tokens=max_tokens, messages=conversation_history, system=system_prompt, ) - - content = response.content[0].text if response.content else "" - - if not content: - print("Warning: Received empty response from API") + if not response.content: return None - return response - except Exception as e: print(f"Error in API request: {str(e)}") return None @@ -125,19 +107,17 @@ def parse_response(response): return None, None, None, None, None, None -def check_api_connection(service="anthropic"): - client = get_api_client(service) +def check_api_connection(): + client = get_api_client() + if client is None: + return False, "API key is missing or invalid" try: - if service == "anthropic": - response = client.messages.create( - model=AI_MODEL, - max_tokens=100, - messages=[{"role": "user", "content": "Hi, are we communicating?"}], - ) - return True, response.content[0].text.strip() - # Add more elif blocks here for other API services - else: - raise ValueError(f"Unsupported service: {service}") + response = client.messages.create( + model=AI_MODEL, + max_tokens=100, + messages=[{"role": "user", "content": "Hi Claude, are we communicating?"}], + ) + return True, response.content[0].text.strip() except Exception as e: return False, str(e) diff --git a/tests/utils/test_api_utils.py b/tests/utils/test_api_utils.py index 65421ed..ba288f5 100644 --- a/tests/utils/test_api_utils.py +++ b/tests/utils/test_api_utils.py @@ -1,14 +1,17 @@ import json from collections import namedtuple -from unittest.mock import Mock +from unittest.mock import Mock, patch import pytest +import anthropic from anthropic import APIError from codeaide.utils.api_utils import ( check_api_connection, parse_response, send_api_request, + get_api_client, + MissingAPIKeyException, ) from codeaide.utils.constants import AI_MODEL, MAX_TOKENS, SYSTEM_PROMPT @@ -23,6 +26,49 @@ ] +@pytest.fixture +def mock_anthropic_client(): + with patch("anthropic.Anthropic") as mock_anthropic: + mock_client = Mock() + mock_messages = Mock() + mock_client.messages = mock_messages + mock_anthropic.return_value = mock_client + yield mock_client + + +class TestGetApiClient: + def test_get_api_client_success(self, monkeypatch): + monkeypatch.setenv("ANTHROPIC_API_KEY", "test_key") + client = get_api_client() + assert client is not None + assert hasattr( + client, "messages" + ) # Check for a common attribute of Anthropic client + + @patch("codeaide.utils.api_utils.AutoConfig") + def test_get_api_client_missing_key(self, mock_auto_config, monkeypatch): + mock_config = Mock() + mock_config.return_value = None + mock_auto_config.return_value = mock_config + monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) + client = get_api_client() + assert client is None + + def test_get_api_client_empty_key(self, monkeypatch): + monkeypatch.setenv("ANTHROPIC_API_KEY", "") + client = get_api_client() + assert client is None + + @patch("codeaide.utils.api_utils.AutoConfig") + def test_get_api_client_unsupported_service(self, mock_auto_config): + mock_config = Mock() + mock_config.return_value = "dummy_key" + mock_auto_config.return_value = mock_config + + result = get_api_client("unsupported_service") + assert result is None + + class TestSendAPIRequest: def test_send_api_request_success(self, mock_anthropic_client): conversation_history = [{"role": "user", "content": "Hello, Claude!"}] @@ -30,7 +76,7 @@ def test_send_api_request_success(self, mock_anthropic_client): mock_response.content = [Mock(text="Hello! How can I assist you today?")] mock_anthropic_client.messages.create.return_value = mock_response - result = send_api_request(conversation_history) + result = send_api_request(mock_anthropic_client, conversation_history) mock_anthropic_client.messages.create.assert_called_once_with( model=AI_MODEL, @@ -46,9 +92,9 @@ def test_send_api_request_empty_response(self, mock_anthropic_client): mock_response.content = [] mock_anthropic_client.messages.create.return_value = mock_response - result = send_api_request(conversation_history) + result = send_api_request(mock_anthropic_client, conversation_history) - assert result == (None, True) + assert result is None def test_send_api_request_api_error(self, mock_anthropic_client): conversation_history = [{"role": "user", "content": "Hello, Claude!"}] @@ -59,9 +105,9 @@ def test_send_api_request_api_error(self, mock_anthropic_client): body={"error": {"message": "API Error"}}, ) - result = send_api_request(conversation_history) + result = send_api_request(mock_anthropic_client, conversation_history) - assert result == (None, True) + assert result is None def test_send_api_request_custom_max_tokens(self, mock_anthropic_client): conversation_history = [{"role": "user", "content": "Hello, Claude!"}] @@ -70,7 +116,9 @@ def test_send_api_request_custom_max_tokens(self, mock_anthropic_client): mock_response.content = [Mock(text="Hello! How can I assist you today?")] mock_anthropic_client.messages.create.return_value = mock_response - result = send_api_request(conversation_history, max_tokens=custom_max_tokens) + result = send_api_request( + mock_anthropic_client, conversation_history, max_tokens=custom_max_tokens + ) mock_anthropic_client.messages.create.assert_called_once_with( model=AI_MODEL, @@ -191,18 +239,35 @@ def test_parse_response_malformed_json(self): class TestAPIConnection: - def check_api_connection_success(self, mock_anthropic_client): + @patch("codeaide.utils.api_utils.get_api_client") + def test_check_api_connection_success(self, mock_get_api_client): + mock_client = Mock() mock_response = Mock() mock_response.content = [Mock(text="Yes, we are communicating.")] - mock_anthropic_client.messages.create.return_value = mock_response + mock_client.messages.create.return_value = mock_response + mock_get_api_client.return_value = mock_client + result = check_api_connection() + assert result[0] == True assert result[1] == "Yes, we are communicating." - def check_api_connection_failure(self, mock_anthropic_client): - mock_anthropic_client.messages.create.side_effect = Exception( - "Connection failed" - ) + @patch("codeaide.utils.api_utils.get_api_client") + def test_check_api_connection_failure(self, mock_get_api_client): + mock_client = Mock() + mock_client.messages.create.side_effect = Exception("Connection failed") + mock_get_api_client.return_value = mock_client + result = check_api_connection() + assert result[0] == False assert "Connection failed" in result[1] + + @patch("codeaide.utils.api_utils.get_api_client") + def test_check_api_connection_missing_key(self, mock_get_api_client): + mock_get_api_client.return_value = None + + result = check_api_connection() + + assert result[0] == False + assert "API key is missing or invalid" in result[1]