import pytest from fastapi import HTTPException from app.schemas.ai import SituationalAnalysisRequest from app.services.ai_tools import web_search as web_search_module from app.services.ai_tools.schemas import SearchEvidence, WebSearchConfig, WebSearchProviderConfig from app.services.ai_tools.web_search import WebSearchClient from app.services import ai_client as ai_client_module from app.services.ai_client import AIProviderClient @pytest.mark.asyncio async def test_ai_client_analyze_logs_summary_without_prompt(monkeypatch): events = [] async def fake_emit_business_log(_logger, **payload): events.append(payload) async def fake_request(self, method, path, json=None, request_id=None, operation="request", payload_summary=None): return { "provider": "test-provider", "model": "test-model", "content": "ok", "content_blocks": [], "text_blocks": ["ok"], "thinking_blocks": [], "raw_response": {}, } monkeypatch.setattr(ai_client_module, "emit_business_log", fake_emit_business_log) monkeypatch.setattr(AIProviderClient, "_request", fake_request) client = AIProviderClient( service_url="http://provider.test", llm_config={"provider": "openai", "provider_api": "openai-completions", "model": "gpt-test", "api_key": "sk-secret"}, ) result = await client.analyze( SituationalAnalysisRequest( title="Sensitive title", objective="Do not store this full prompt", observations=["secret observation"], constraints=["secret constraint"], context={"source": "test", "private": "value"}, ), request_id="req-ai-test", ) assert result.model == "test-model" assert [event["event"] for event in events] == [ "ai.provider.analyze.start", "ai.provider.analyze.success", ] serialized = str(events) assert "Do not store this full prompt" not in serialized assert "secret observation" not in serialized assert "sk-secret" not in serialized start_context = events[0]["context"] assert start_context["model"] == "gpt-test" assert start_context["input_summary"]["objective_length"] == len("Do not store this full prompt") assert start_context["input_summary"]["observation_count"] == 1 assert start_context["input_summary"]["context_keys"] == ["private", "source"] @pytest.mark.asyncio async def test_ai_client_analyze_logs_failure(monkeypatch): events = [] async def fake_emit_business_log(_logger, **payload): events.append(payload) async def fake_request(self, method, path, json=None, request_id=None, operation="request", payload_summary=None): raise HTTPException(status_code=502, detail="provider failed") monkeypatch.setattr(ai_client_module, "emit_business_log", fake_emit_business_log) monkeypatch.setattr(AIProviderClient, "_request", fake_request) client = AIProviderClient(service_url="http://provider.test", llm_config={"provider": "openai", "model": "gpt-test"}) with pytest.raises(HTTPException): await client.analyze( SituationalAnalysisRequest(title="T", objective="O", observations=["one"]), request_id="req-ai-fail", ) assert events[-1]["event"] == "ai.provider.analyze.failed" assert events[-1]["level"] == "error" assert events[-1]["context"]["error_type"] == "HTTPException" @pytest.mark.asyncio async def test_web_search_logs_query_hash_without_query(monkeypatch): events = [] async def fake_emit_business_log(_logger, **payload): events.append(payload) async def fake_search_tavily(self, config, query, max_results, domains, freshness_days): return [ SearchEvidence( title="Example", url="https://example.test", snippet="result", source_provider="tavily", ) ] monkeypatch.setattr(web_search_module, "emit_business_log", fake_emit_business_log) monkeypatch.setattr(WebSearchClient, "_search_tavily", fake_search_tavily) client = WebSearchClient( WebSearchConfig( enabled=True, default_provider="tavily", providers={"tavily": WebSearchProviderConfig(provider="tavily", api_key="secret-key")}, ) ) results = await client.search("secret query text", max_results=1) assert len(results) == 1 assert [event["event"] for event in events] == [ "ai_tool.web_search.start", "ai_tool.web_search.success", ] serialized = str(events) assert "secret query text" not in serialized assert "secret-key" not in serialized assert events[0]["context"]["query_length"] == len("secret query text") assert events[1]["context"]["result_count"] == 1