Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 12 additions & 9 deletions client_discovery/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import logging
import os
from pathlib import Path
from typing import Any
from dotenv import load_dotenv

logger = logging.getLogger(__name__)
Expand All @@ -15,9 +16,9 @@
_dotenv_loaded = False


def load_and_validate_config() -> dict[str, any]:
def load_and_validate_config() -> dict[str, Any]:
"""
Locates and loads environment variables, validates them, and returns
Locates and loads environment variables and returns
a dictionary of configuration parameters.
"""
global _dotenv_loaded
Expand All @@ -27,13 +28,6 @@ def load_and_validate_config() -> dict[str, any]:
_dotenv_loaded = True

gemini_api_key = os.environ.get("GEMINI_API_KEY")
testing = os.environ.get("TESTING") == "true"

if not gemini_api_key:
if testing:
logger.warning("GEMINI_API_KEY is not set.")
else:
raise ValueError("GEMINI_API_KEY is required but missing from the environment.")

# Optional variables
jules_api_key = os.environ.get("JULES_API_KEY")
Expand Down Expand Up @@ -86,5 +80,14 @@ def load_and_validate_config() -> dict[str, any]:
}


def require_gemini_api_key(config: dict[str, Any] | None = None) -> str:
"""Returns a configured Gemini API key or raises if missing/empty."""
resolved_config = config if config is not None else load_and_validate_config()
gemini_api_key = str(resolved_config.get("GEMINI_API_KEY") or "").strip()
if not gemini_api_key:
raise ValueError("GEMINI_API_KEY is required but missing from the environment.")
return gemini_api_key


# Provide load_config alias
load_config = load_and_validate_config
10 changes: 6 additions & 4 deletions client_discovery/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

from dataclasses import asdict
import logging
import os
from pathlib import Path
from unittest.mock import Mock

from character import root_agent
from google.adk.agents.llm_agent import LlmAgent
from client_discovery.config import load_config, require_gemini_api_key

from client_discovery.core import (
generate_documents,
Expand Down Expand Up @@ -40,8 +40,10 @@ def build_intake_response(questionnaire: str) -> dict[str, object]:

# Resolve strategic analysis
strategic_analysis = None
gemini_api_key = os.environ.get("GEMINI_API_KEY")
if not gemini_api_key:
config = load_config()
try:
require_gemini_api_key(config)
except ValueError:
strategic_analysis = "DIA Agent Error: GEMINI_API_KEY is missing or invalid."
else:
try:
Expand All @@ -65,7 +67,7 @@ def build_intake_response(questionnaire: str) -> dict[str, object]:

draft = gemini_resp.text

jules_api_key = os.environ.get("JULES_API_KEY")
jules_api_key = str(config.get("JULES_API_KEY") or "").strip()
if not jules_api_key:
strategic_analysis = draft
else:
Expand Down
10 changes: 5 additions & 5 deletions tests/test_adversarial_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def test_obs_empty_password_auth_disabled():
"OBS_PORT": "4455",
"OBS_PASSWORD": "", # Empty password when auth is disabled
}
with patch.dict(os.environ, env):
with patch.dict(os.environ, env, clear=True):
creds = _resolve_obs_credentials()
assert creds is not None
assert creds[2] == ""
Expand All @@ -53,7 +53,7 @@ def test_obs_out_of_range_port():
"OBS_PORT": "-100",
"OBS_PASSWORD": "secretpassword",
}
with patch.dict(os.environ, env):
with patch.dict(os.environ, env, clear=True):
creds = _resolve_obs_credentials()
assert creds is None

Expand All @@ -67,7 +67,7 @@ def test_jules_whitespace_api_key():
env = {
"JULES_API_KEY": " ",
}
with patch.dict(os.environ, env):
with patch.dict(os.environ, env, clear=True):
# The API call will run because JULES_API_KEY is not empty, but it's invalid.
# It should fail and return the original draft content.
draft = "Original Draft content"
Expand Down Expand Up @@ -115,7 +115,7 @@ def test_obs_screenshot_connection_failure_graceful_handling():
"OBS_PORT": "4455",
"OBS_PASSWORD": "wrong_password",
}
with patch.dict(os.environ, env), patch("obsws_python.ReqClient") as mock_req:
with patch.dict(os.environ, env, clear=True), patch("obsws_python.ReqClient") as mock_req:
mock_req.side_effect = Exception("Connection refused")
res = trigger_obs_screenshot()
assert "failed" in res.lower()
Expand All @@ -131,7 +131,7 @@ def test_obs_replay_buffer_connection_failure_graceful_handling():
"OBS_PORT": "4455",
"OBS_PASSWORD": "wrong_password",
}
with patch.dict(os.environ, env), patch("obsws_python.ReqClient") as mock_req:
with patch.dict(os.environ, env, clear=True), patch("obsws_python.ReqClient") as mock_req:
mock_req.side_effect = Exception("Auth failure")
res = save_obs_replay_buffer()
assert "failed" in res.lower()
Expand Down
20 changes: 15 additions & 5 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from unittest.mock import patch
import pytest

from client_discovery.config import load_and_validate_config
from client_discovery.config import load_and_validate_config, require_gemini_api_key

@pytest.fixture(autouse=True)
def reset_dotenv_loaded():
Expand Down Expand Up @@ -41,9 +41,8 @@ def test_load_config_missing_gemini_key_in_production():
"TESTING": "false",
}
with patch.dict(os.environ, env, clear=True), patch("client_discovery.config.load_dotenv"):
with pytest.raises(ValueError) as excinfo:
load_and_validate_config()
assert "GEMINI_API_KEY is required" in str(excinfo.value)
config = load_and_validate_config()
assert config["GEMINI_API_KEY"] is None

def test_load_config_missing_gemini_key_in_testing():
env = {
Expand All @@ -52,7 +51,18 @@ def test_load_config_missing_gemini_key_in_testing():
with patch.dict(os.environ, env, clear=True), patch("client_discovery.config.load_dotenv"), patch("client_discovery.config.logger") as mock_logger:
config = load_and_validate_config()
assert config["GEMINI_API_KEY"] is None
mock_logger.warning.assert_any_call("GEMINI_API_KEY is not set.")
mock_logger.warning.assert_any_call(
"JULES_API_KEY is not set. Jules refinement will fall back to returning original drafts."
)


def test_require_gemini_api_key_raises_when_missing():
env = {"TESTING": "false"}
with patch.dict(os.environ, env, clear=True), patch("client_discovery.config.load_dotenv"):
config = load_and_validate_config()
with pytest.raises(ValueError) as excinfo:
require_gemini_api_key(config)
assert "GEMINI_API_KEY is required" in str(excinfo.value)

def test_load_config_missing_optional_keys():
env = {
Expand Down
18 changes: 9 additions & 9 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def test_score_opportunity_full_integration_tier():

def test_refine_with_jules_success():
env = {"JULES_API_KEY": "valid_key"}
with patch.dict(os.environ, env), patch("requests.post") as mock_post:
with patch.dict(os.environ, env, clear=True), patch("requests.post") as mock_post:
mock_response = mock_post.return_value
mock_response.status_code = 200
mock_response.json.return_value = {"refined_content": "Refined Draft"}
Expand All @@ -123,15 +123,15 @@ def test_refine_with_jules_success():

def test_refine_with_jules_missing_key():
env = {"JULES_API_KEY": ""}
with patch.dict(os.environ, env), patch("requests.post") as mock_post:
with patch.dict(os.environ, env, clear=True), patch("requests.post") as mock_post:
result = refine_with_jules("Original Draft", "CEO")
assert result == "Original Draft"
mock_post.assert_not_called()


def test_refine_with_jules_status_error():
env = {"JULES_API_KEY": "valid_key"}
with patch.dict(os.environ, env), patch("requests.post") as mock_post:
with patch.dict(os.environ, env, clear=True), patch("requests.post") as mock_post:
mock_response = mock_post.return_value
mock_response.status_code = 500

Expand All @@ -141,7 +141,7 @@ def test_refine_with_jules_status_error():

def test_refine_with_jules_timeout():
env = {"JULES_API_KEY": "valid_key"}
with patch.dict(os.environ, env), patch("requests.post") as mock_post:
with patch.dict(os.environ, env, clear=True), patch("requests.post") as mock_post:
mock_post.side_effect = requests.exceptions.Timeout("Connection timed out")

result = refine_with_jules("Original Draft", "CEO")
Expand All @@ -150,7 +150,7 @@ def test_refine_with_jules_timeout():

def test_refine_with_jules_request_exception():
env = {"JULES_API_KEY": "valid_key"}
with patch.dict(os.environ, env), patch("requests.post") as mock_post:
with patch.dict(os.environ, env, clear=True), patch("requests.post") as mock_post:
mock_post.side_effect = requests.exceptions.RequestException("Request error")

result = refine_with_jules("Original Draft", "CEO")
Expand All @@ -159,7 +159,7 @@ def test_refine_with_jules_request_exception():

def test_refine_with_jules_malformed_json():
env = {"JULES_API_KEY": "valid_key"}
with patch.dict(os.environ, env), patch("requests.post") as mock_post:
with patch.dict(os.environ, env, clear=True), patch("requests.post") as mock_post:
mock_response = mock_post.return_value
mock_response.status_code = 200
mock_response.json.side_effect = ValueError("Malformed JSON")
Expand All @@ -170,7 +170,7 @@ def test_refine_with_jules_malformed_json():

def test_refine_with_jules_missing_refined_content_key():
env = {"JULES_API_KEY": "valid_key"}
with patch.dict(os.environ, env), patch("requests.post") as mock_post:
with patch.dict(os.environ, env, clear=True), patch("requests.post") as mock_post:
mock_response = mock_post.return_value
mock_response.status_code = 200
mock_response.json.return_value = {"something_else": "here"}
Expand All @@ -181,14 +181,14 @@ def test_refine_with_jules_missing_refined_content_key():

def test_trigger_obs_screenshot_missing_credentials():
env = {"OBS_HOST": "", "OBS_PORT": "", "OBS_PASSWORD": ""}
with patch.dict(os.environ, env):
with patch.dict(os.environ, env, clear=True):
res = trigger_obs_screenshot()
assert "skipped" in res.lower() or "missing" in res.lower()


def test_save_obs_replay_buffer_missing_credentials():
env = {"OBS_HOST": "", "OBS_PORT": "", "OBS_PASSWORD": ""}
with patch.dict(os.environ, env):
with patch.dict(os.environ, env, clear=True):
res = save_obs_replay_buffer()
assert "skipped" in res.lower() or "missing" in res.lower()

Expand Down