amplegest / tests /test_tools.py
Viney's picture
fix: expose CHECK_REQUIRED periods as field-level-disclosed evidence instead of hiding entire rows
d65dc97
Raw
History Blame Contribute Delete
10.5 kB
import json
from unittest.mock import patch, MagicMock
from agent.tools import get_financial_metrics, search_filing, search_transcript, search_news
def _sample_metrics():
return {
"ticker": "AAPL", "company_name": "Apple Inc.", "filing_date": "2024-11-01",
"period": "FY2024", "form_type": "10-K", "revenue": 391035000000.0,
"revenue_yoy_pct": 2.0, "eps": 6.11, "gross_margin": 0.461,
"operating_margin": 0.314, "free_cash_flow": 108807000000.0,
"guidance_disclosed": 0, "guidance_text": None, "ingested_at": "2026-05-04T12:00:00",
"period_basis": "annual", "report_date": "2024-09-28",
"accession": "0000320193-24-000123", "source_url": "https://www.sec.gov/example",
"metric_contexts": {
"revenue": {"source": "sec_companyfacts", "selection": "exact_annual", "concept": "Revenues"},
},
"quality_warnings": [], "data_quality_status": "VERIFIED",
}
@patch("agent.tools.metrics_db.get_all_metrics")
def test_get_metrics_found(mock_get):
mock_get.return_value = [_sample_metrics()]
result = get_financial_metrics.invoke({"ticker": "AAPL"})
assert "Apple Inc." in result
assert "$391.0B" in result
assert "6.11" in result
payload = json.loads(result)
assert payload["schema"] == "evidence.v1"
assert payload["status"] == "OK"
assert payload["records"][0]["ref"]["source"] == "metrics"
@patch("agent.tools.metrics_db.get_all_metrics")
def test_get_metrics_not_ingested(mock_get):
mock_get.return_value = []
result = get_financial_metrics.invoke({"ticker": "ZZZZ"})
assert "not ingested" in result.lower()
@patch("agent.tools.metrics_db.get_all_metrics")
def test_get_metrics_filters_unverified_rows_and_exposes_lineage(mock_get):
verified = _sample_metrics()
check_required = {
**_sample_metrics(),
"period": "Q32024",
"filing_date": "2024-08-28",
"accession": "0000320193-24-000099",
"source_url": "https://www.sec.gov/check-required-example",
"data_quality_status": "CHECK_REQUIRED",
"quality_warnings": ["free_cash_flow:fallback_non_sec"],
}
mock_get.return_value = [verified, check_required]
payload = json.loads(get_financial_metrics.invoke({"ticker": "AAPL"}))
assert payload["status"] == "OK"
assert len(payload["records"]) == 2
verified_record, check_required_record = payload["records"]
assert verified_record["metadata"]["data_quality_status"] == "VERIFIED"
assert "sec_companyfacts/exact_annual/Revenues" in verified_record["content"]
assert check_required_record["ref"]["source_url"] == check_required["source_url"]
assert check_required_record["metadata"]["accession"] == check_required["accession"]
assert check_required_record["metadata"]["data_quality_status"] == "CHECK_REQUIRED"
assert check_required_record["metadata"]["quality_warnings"] == [
"free_cash_flow:fallback_non_sec"
]
assert "Data Quality: CHECK_REQUIRED" in check_required_record["content"]
assert "free_cash_flow:fallback_non_sec" in check_required_record["content"]
@patch("agent.tools.metrics_db.get_all_metrics")
def test_get_metrics_all_unverified_is_empty(mock_get):
mock_get.return_value = [{
**_sample_metrics(),
"data_quality_status": "LEGACY_UNVERIFIED",
}]
payload = json.loads(get_financial_metrics.invoke({"ticker": "AAPL"}))
assert payload["status"] == "EMPTY"
assert payload["records"] == []
assert "none carries SEC period lineage" in payload["message"]
@patch("agent.tools.metrics_db.get_all_metrics")
def test_get_metrics_preserves_newest_first_order_with_check_required(mock_get):
newest = {
**_sample_metrics(),
"period": "Q12027",
"filing_date": "2026-05-20",
"data_quality_status": "CHECK_REQUIRED",
"quality_warnings": ["free_cash_flow:fallback_non_sec"],
}
older = {
**_sample_metrics(),
"period": "Q42026",
"filing_date": "2026-02-25",
}
mock_get.return_value = [newest, older]
payload = json.loads(get_financial_metrics.invoke({"ticker": "NVDA"}))
assert payload["status"] == "OK"
assert [record["metadata"]["period"] for record in payload["records"]] == [
"Q12027", "Q42026"
]
assert payload["records"][0]["metadata"]["data_quality_status"] == "CHECK_REQUIRED"
@patch("agent.tools.metrics_db.get_all_metrics")
def test_get_metrics_includes_check_required_but_excludes_legacy(mock_get):
check_required = {
**_sample_metrics(),
"period": "Q12025",
"data_quality_status": "CHECK_REQUIRED",
"quality_warnings": ["eps:fallback_non_sec"],
}
legacy = {
**_sample_metrics(),
"period": "FY2023",
"data_quality_status": "LEGACY_UNVERIFIED",
}
mock_get.return_value = [check_required, legacy]
payload = json.loads(get_financial_metrics.invoke({"ticker": "AAPL"}))
assert payload["status"] == "OK"
assert len(payload["records"]) == 1
assert payload["records"][0]["metadata"]["period"] == "Q12025"
assert payload["records"][0]["metadata"]["data_quality_status"] == "CHECK_REQUIRED"
@patch("agent.tools.vector_store.search")
def test_search_filing_returns_chunks(mock_search):
mock_search.return_value = [
{"text": "Revenue grew 5%.", "metadata": {"source": "10-Q", "section": "MD&A", "filing_date": "2024-11-01"}}
]
result = search_filing.invoke({"query": "revenue growth", "ticker": "AAPL"})
assert "Revenue grew 5%" in result
assert "10-Q" in result
payload = json.loads(result)
assert payload["status"] == "OK"
assert payload["records"][0]["ref"]["source"] == "10-Q"
@patch("agent.tools.vector_store.search")
def test_search_filing_header_uses_source_prefix_when_no_chunk_context(mock_search):
# chunk_context absent → fallback header must say "Source:" so the LLM can tag correctly
mock_search.return_value = [
{"text": "Margin expanded.", "metadata": {"source": "10-K", "section": "MD&A", "filing_date": "2024-11-01"}}
]
result = search_filing.invoke({"query": "margins", "ticker": "AAPL"})
assert "Source:" in result
assert "10-K" in result
@patch("agent.tools.vector_store.search")
def test_search_transcript_header_uses_source_prefix_when_no_chunk_context(mock_search):
mock_search.return_value = [
{"text": "Tim Cook: strong demand.", "metadata": {"date": "2024-11-01"}}
]
result = search_transcript.invoke({"query": "tone", "ticker": "AAPL"})
assert "Source:" in result
assert "transcript" in result
@patch("agent.tools.vector_store.search")
def test_search_filing_with_since_parameter(mock_search):
mock_search.return_value = [
{"text": "Recent revenue data.", "metadata": {"source": "10-Q", "section": "MD&A", "filing_date": "2025-01-15"}}
]
result = search_filing.invoke({"query": "revenue", "ticker": "AAPL", "since": "2024-01-01"})
assert "Recent revenue data" in result
# Verify min_filing_date was passed to the search function
mock_search.assert_called_once()
call_kwargs = mock_search.call_args[1]
assert call_kwargs.get("min_filing_date") == "2024-01-01"
@patch("agent.tools.vector_store.search")
def test_search_filing_no_data(mock_search):
mock_search.return_value = []
result = search_filing.invoke({"query": "revenue", "ticker": "ZZZZ"})
assert "ingest" in result.lower()
assert json.loads(result)["status"] == "EMPTY"
@patch("agent.tools.vector_store.search", side_effect=RuntimeError("index unavailable"))
def test_search_filing_error_is_structured(mock_search):
result = search_filing.invoke({"query": "revenue", "ticker": "AAPL"})
payload = json.loads(result)
assert payload["status"] == "ERROR"
assert payload["records"] == []
assert payload["error"]["code"] == "TOOL_EXECUTION_ERROR"
@patch("agent.tools.vector_store.search")
def test_search_transcript_returns_chunks(mock_search):
mock_search.return_value = [
{"text": "Tim Cook: Record quarter.", "metadata": {"source": "transcript", "date": "2024-11-01", "speaker": "mixed"}}
]
result = search_transcript.invoke({"query": "CEO commentary", "ticker": "AAPL"})
assert "Tim Cook" in result
@patch("agent.tools.vector_store.search")
def test_search_transcript_with_since_parameter(mock_search):
mock_search.return_value = [
{"text": "Recent CEO commentary.", "metadata": {"source": "transcript", "date": "2025-01-15"}}
]
result = search_transcript.invoke({"query": "CEO commentary", "ticker": "AAPL", "since": "2024-01-01"})
assert "Recent CEO commentary" in result
# Verify min_filing_date was passed to the search function
mock_search.assert_called_once()
call_kwargs = mock_search.call_args[1]
assert call_kwargs.get("min_filing_date") == "2024-01-01"
@patch.dict("os.environ", {"TAVILY_API_KEY": "test-key"})
@patch("agent.tools.news_cache.put")
@patch("agent.tools.news_cache.get", return_value=None)
@patch("agent.tools.metrics_db.get_all_metrics")
@patch("agent.tools.TavilyClient")
def test_search_news(mock_cls, mock_metrics, _mock_cache_get, _mock_cache_put):
import agent.tools
agent.tools._tavily_client = None # reset cached client so patch takes effect
mock_metrics.return_value = [_sample_metrics()]
mock_client = MagicMock()
mock_cls.return_value = mock_client
mock_client.search.return_value = {"results": [
{"title": "Apple beats estimates", "content": "Strong Q4.", "url": "https://example.com", "published_date": "2024-11-01"}
]}
result = search_news.invoke({"query": "earnings", "ticker": "AAPL", "days": 30})
assert "Apple beats estimates" in result
# Verify Tavily was called with ticker-grounded query and days/topic params
mock_client.search.assert_called_once()
call_kwargs = mock_client.search.call_args[1]
assert "AAPL" in call_kwargs.get("query", "")
assert call_kwargs.get("days") == 30
assert call_kwargs.get("topic") == "news"
@patch("agent.tools.news_cache.get", return_value="legacy unstructured result")
def test_search_news_legacy_cache_is_not_repackaged_as_evidence(_mock_cache_get):
payload = json.loads(search_news.invoke({
"query": "earnings", "ticker": "AAPL", "days": 30,
}))
assert payload["status"] == "EMPTY"
assert payload["records"] == []
assert "provenance" in payload["message"].lower()