| 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): |
| |
| 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 |
| |
| 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 |
| |
| 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 |
| 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 |
| |
| 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() |
|
|