test(analyzer,serp): add tests for caching, single query, and parallel processing
- Add TestSingleQueryBugFix: verify SERP.query called once per analysis - Add TestPatentCaching: DB cache hit/miss, SERP query cache hit/miss - Add TestDynamicDateRange: rolling window, days_back param - Add TestFilesystemPDFCaching: skip download, redownload empty files - Add autouse mock_db fixture to prevent real DB connections in all tests
This commit is contained in:
+187
-2
@@ -1,11 +1,22 @@
|
||||
"""Tests for the high-level company analyzer orchestration."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, call
|
||||
from unittest.mock import Mock, patch, call, MagicMock
|
||||
from SPARC.analyzer import CompanyAnalyzer
|
||||
from SPARC.types import Patent, Patents, CompanyAnalysisResult, BatchAnalysisResult
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_db(mocker):
|
||||
"""Mock DatabaseClient for all tests so no real DB connection is needed."""
|
||||
mock_db_cls = mocker.patch("SPARC.analyzer.DatabaseClient")
|
||||
mock_db_instance = MagicMock()
|
||||
mock_db_instance.get_cached_patent.return_value = None
|
||||
mock_db_instance.get_cached_serp_query.return_value = None
|
||||
mock_db_cls.return_value = mock_db_instance
|
||||
return mock_db_instance
|
||||
|
||||
|
||||
class TestCompanyAnalyzer:
|
||||
"""Test the CompanyAnalyzer orchestration logic."""
|
||||
|
||||
@@ -17,7 +28,7 @@ class TestCompanyAnalyzer:
|
||||
|
||||
mock_llm.assert_called_once_with(api_key="test-key")
|
||||
|
||||
def test_analyze_company_full_pipeline(self, mocker):
|
||||
def test_analyze_company_full_pipeline(self, mocker, mock_db):
|
||||
"""Test complete company analysis pipeline."""
|
||||
# Mock all the dependencies
|
||||
mock_query = mocker.patch("SPARC.analyzer.SERP.query")
|
||||
@@ -178,6 +189,180 @@ class TestCompanyAnalyzer:
|
||||
assert "PDF not found" in result
|
||||
|
||||
|
||||
class TestSingleQueryBugFix:
|
||||
"""Test that SERP.query is only called once per company analysis."""
|
||||
|
||||
def test_analyze_company_safe_calls_query_once(self, mocker, mock_db):
|
||||
"""_analyze_company_safe should call SERP.query exactly once."""
|
||||
mock_query = mocker.patch("SPARC.analyzer.SERP.query")
|
||||
mock_save = mocker.patch("SPARC.analyzer.SERP.save_patents")
|
||||
mock_parse = mocker.patch("SPARC.analyzer.SERP.parse_patent_pdf")
|
||||
mock_minimize = mocker.patch("SPARC.analyzer.SERP.minimize_patent_for_llm")
|
||||
mock_llm = mocker.patch("SPARC.analyzer.LLMAnalyzer")
|
||||
|
||||
patent = Patent(patent_id="US123", pdf_link="http://example.com/test.pdf")
|
||||
mock_query.return_value = Patents(patents=[patent])
|
||||
|
||||
def save_side_effect(p):
|
||||
p.pdf_path = "patents/US123.pdf"
|
||||
return p
|
||||
|
||||
mock_save.side_effect = save_side_effect
|
||||
mock_parse.return_value = {"abstract": "Test"}
|
||||
mock_minimize.return_value = "Content"
|
||||
|
||||
mock_llm_instance = Mock()
|
||||
mock_llm_instance.analyze_patent_portfolio.return_value = "Analysis"
|
||||
mock_llm.return_value = mock_llm_instance
|
||||
|
||||
analyzer = CompanyAnalyzer()
|
||||
analyzer._analyze_company_safe("TestCorp")
|
||||
|
||||
# The key assertion: SERP.query called exactly once, not twice
|
||||
mock_query.assert_called_once_with("TestCorp")
|
||||
|
||||
def test_analyze_company_with_prefetched_patents_skips_query(self, mocker):
|
||||
"""analyze_company should not call SERP.query when patents are provided."""
|
||||
mock_query = mocker.patch("SPARC.analyzer.SERP.query")
|
||||
mock_save = mocker.patch("SPARC.analyzer.SERP.save_patents")
|
||||
mock_parse = mocker.patch("SPARC.analyzer.SERP.parse_patent_pdf")
|
||||
mock_minimize = mocker.patch("SPARC.analyzer.SERP.minimize_patent_for_llm")
|
||||
mock_llm = mocker.patch("SPARC.analyzer.LLMAnalyzer")
|
||||
|
||||
patent = Patent(patent_id="US123", pdf_link="http://example.com/test.pdf")
|
||||
prefetched = Patents(patents=[patent])
|
||||
|
||||
def save_side_effect(p):
|
||||
p.pdf_path = "patents/US123.pdf"
|
||||
return p
|
||||
|
||||
mock_save.side_effect = save_side_effect
|
||||
mock_parse.return_value = {"abstract": "Test"}
|
||||
mock_minimize.return_value = "Content"
|
||||
|
||||
mock_llm_instance = Mock()
|
||||
mock_llm_instance.analyze_patent_portfolio.return_value = "Analysis"
|
||||
mock_llm.return_value = mock_llm_instance
|
||||
|
||||
analyzer = CompanyAnalyzer()
|
||||
analyzer.analyze_company("TestCorp", patents=prefetched)
|
||||
|
||||
# SERP.query should never be called
|
||||
mock_query.assert_not_called()
|
||||
|
||||
|
||||
class TestPatentCaching:
|
||||
"""Test patent-level DB caching in the pipeline."""
|
||||
|
||||
def test_process_single_patent_uses_db_cache(self, mocker, mock_db):
|
||||
"""_process_single_patent returns cached content when available."""
|
||||
mock_save = mocker.patch("SPARC.analyzer.SERP.save_patents")
|
||||
|
||||
mock_db.get_cached_patent.return_value = {
|
||||
"patent_id": "US123",
|
||||
"minimized_content": "Cached minimized content",
|
||||
}
|
||||
|
||||
patent = Patent(patent_id="US123", pdf_link="http://example.com/test.pdf")
|
||||
result = CompanyAnalyzer._process_single_patent(patent, "TestCorp", mock_db)
|
||||
|
||||
assert result == {"patent_id": "US123", "content": "Cached minimized content"}
|
||||
# Should NOT download since cache hit
|
||||
mock_save.assert_not_called()
|
||||
|
||||
def test_process_single_patent_stores_to_db_cache(self, mocker, mock_db):
|
||||
"""_process_single_patent stores result in DB after processing."""
|
||||
mock_save = mocker.patch("SPARC.analyzer.SERP.save_patents")
|
||||
mock_parse = mocker.patch("SPARC.analyzer.SERP.parse_patent_pdf")
|
||||
mock_minimize = mocker.patch("SPARC.analyzer.SERP.minimize_patent_for_llm")
|
||||
|
||||
# No cache hit
|
||||
mock_db.get_cached_patent.return_value = None
|
||||
|
||||
patent = Patent(patent_id="US123", pdf_link="http://example.com/test.pdf")
|
||||
|
||||
def save_side_effect(p):
|
||||
p.pdf_path = "patents/US123.pdf"
|
||||
return p
|
||||
|
||||
mock_save.side_effect = save_side_effect
|
||||
mock_parse.return_value = {"abstract": "Test abstract"}
|
||||
mock_minimize.return_value = "Minimized content"
|
||||
|
||||
result = CompanyAnalyzer._process_single_patent(patent, "TestCorp", mock_db)
|
||||
|
||||
assert result == {"patent_id": "US123", "content": "Minimized content"}
|
||||
mock_db.store_patent.assert_called_once_with(
|
||||
patent_id="US123",
|
||||
company_name="TestCorp",
|
||||
pdf_link="http://example.com/test.pdf",
|
||||
raw_sections={"abstract": "Test abstract"},
|
||||
minimized_content="Minimized content",
|
||||
)
|
||||
|
||||
def test_serp_query_cache_hit_skips_api(self, mocker, mock_db):
|
||||
"""When SERP query is cached, API call is skipped."""
|
||||
mock_query = mocker.patch("SPARC.analyzer.SERP.query")
|
||||
mock_save = mocker.patch("SPARC.analyzer.SERP.save_patents")
|
||||
mock_parse = mocker.patch("SPARC.analyzer.SERP.parse_patent_pdf")
|
||||
mock_minimize = mocker.patch("SPARC.analyzer.SERP.minimize_patent_for_llm")
|
||||
mock_llm = mocker.patch("SPARC.analyzer.LLMAnalyzer")
|
||||
|
||||
# Simulate SERP cache hit
|
||||
mock_db.get_cached_serp_query.return_value = ["US123"]
|
||||
# Simulate patent cache hit too
|
||||
mock_db.get_cached_patent.return_value = {
|
||||
"patent_id": "US123",
|
||||
"minimized_content": "Cached content",
|
||||
}
|
||||
|
||||
mock_llm_instance = Mock()
|
||||
mock_llm_instance.analyze_patent_portfolio.return_value = "Analysis"
|
||||
mock_llm.return_value = mock_llm_instance
|
||||
|
||||
analyzer = CompanyAnalyzer()
|
||||
result = analyzer.analyze_company("TestCorp")
|
||||
|
||||
assert result == "Analysis"
|
||||
# SERP.query should NOT be called
|
||||
mock_query.assert_not_called()
|
||||
# No downloads should happen
|
||||
mock_save.assert_not_called()
|
||||
|
||||
def test_serp_query_cache_miss_stores_result(self, mocker, mock_db):
|
||||
"""When SERP query cache misses, result is stored after API call."""
|
||||
mock_query = mocker.patch("SPARC.analyzer.SERP.query")
|
||||
mock_save = mocker.patch("SPARC.analyzer.SERP.save_patents")
|
||||
mock_parse = mocker.patch("SPARC.analyzer.SERP.parse_patent_pdf")
|
||||
mock_minimize = mocker.patch("SPARC.analyzer.SERP.minimize_patent_for_llm")
|
||||
mock_llm = mocker.patch("SPARC.analyzer.LLMAnalyzer")
|
||||
|
||||
mock_db.get_cached_serp_query.return_value = None
|
||||
|
||||
patent = Patent(patent_id="US123", pdf_link="http://example.com/test.pdf")
|
||||
mock_query.return_value = Patents(patents=[patent])
|
||||
|
||||
def save_side_effect(p):
|
||||
p.pdf_path = "patents/US123.pdf"
|
||||
return p
|
||||
|
||||
mock_save.side_effect = save_side_effect
|
||||
mock_parse.return_value = {"abstract": "Test"}
|
||||
mock_minimize.return_value = "Content"
|
||||
|
||||
mock_llm_instance = Mock()
|
||||
mock_llm_instance.analyze_patent_portfolio.return_value = "Analysis"
|
||||
mock_llm.return_value = mock_llm_instance
|
||||
|
||||
analyzer = CompanyAnalyzer()
|
||||
analyzer.analyze_company("TestCorp")
|
||||
|
||||
mock_db.store_serp_query.assert_called_once()
|
||||
call_kwargs = mock_db.store_serp_query.call_args[1]
|
||||
assert call_kwargs["company_name"] == "TestCorp"
|
||||
assert call_kwargs["patent_ids"] == ["US123"]
|
||||
|
||||
|
||||
class TestBatchProcessing:
|
||||
"""Test multi-company batch processing functionality."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user