""" Unit tests for agent.py helper functions: - _strip_think(text) - _extract_final_text(result) agent.py has heavy FastAPI/LangChain imports; conftest.py stubs them out so these pure functions can be imported and tested in isolation. """ import pytest # conftest.py has already installed all stubs into sys.modules. # The FastAPI app is instantiated at module level in agent.py — # with the mocked fastapi, that just creates a MagicMock() object # and the route decorators are no-ops. from agent import _strip_think, _extract_final_text, _extract_urls # ── _strip_think ─────────────────────────────────────────────────────────────── class TestStripThink: def test_removes_single_think_block(self): text = "internal reasoningFinal answer." assert _strip_think(text) == "Final answer." def test_removes_multiline_think_block(self): text = "\nLine one.\nLine two.\n\nResult here." assert _strip_think(text) == "Result here." def test_no_think_block_unchanged(self): text = "This is a plain answer with no think block." assert _strip_think(text) == text def test_removes_multiple_think_blocks(self): text = "step 1middlestep 2end" assert _strip_think(text) == "middleend" def test_strips_surrounding_whitespace(self): text = " stuff answer " assert _strip_think(text) == "answer" def test_empty_think_block(self): text = "Hello." assert _strip_think(text) == "Hello." def test_empty_string(self): assert _strip_think("") == "" def test_only_think_block_returns_empty(self): text = "nothing useful" assert _strip_think(text) == "" def test_think_block_with_nested_tags(self): text = "I should use bold hereDone." assert _strip_think(text) == "Done." def test_preserves_markdown(self): text = "plan## Report\n\n- Point one\n- Point two" result = _strip_think(text) assert result == "## Report\n\n- Point one\n- Point two" # ── _extract_final_text ──────────────────────────────────────────────────────── class TestExtractFinalText: def _ai_msg(self, content: str, tool_calls=None): """Create a minimal AIMessage-like object.""" class AIMessage: pass m = AIMessage() m.content = content m.tool_calls = tool_calls or [] return m def _human_msg(self, content: str): class HumanMessage: pass m = HumanMessage() m.content = content return m def test_returns_last_ai_message_content(self): result = { "messages": [ self._human_msg("what is 2+2"), self._ai_msg("The answer is 4."), ] } assert _extract_final_text(result) == "The answer is 4." def test_returns_last_of_multiple_ai_messages(self): result = { "messages": [ self._ai_msg("First response."), self._human_msg("follow-up"), self._ai_msg("Final response."), ] } assert _extract_final_text(result) == "Final response." def test_skips_empty_ai_messages(self): result = { "messages": [ self._ai_msg("Real answer."), self._ai_msg(""), # empty — should be skipped ] } assert _extract_final_text(result) == "Real answer." def test_strips_think_tags_from_ai_message(self): result = { "messages": [ self._ai_msg("reasoning hereClean reply."), ] } assert _extract_final_text(result) == "Clean reply." def test_falls_back_to_output_field(self): result = { "messages": [], "output": "Fallback output.", } assert _extract_final_text(result) == "Fallback output." def test_strips_think_from_output_field(self): result = { "messages": [], "output": "thoughtsActual output.", } assert _extract_final_text(result) == "Actual output." def test_returns_none_when_no_content(self): result = {"messages": []} assert _extract_final_text(result) is None def test_returns_none_when_no_messages_and_no_output(self): result = {"messages": [], "output": ""} # output is falsy → returns None assert _extract_final_text(result) is None def test_skips_non_ai_messages(self): result = { "messages": [ self._human_msg("user question"), ] } assert _extract_final_text(result) is None def test_handles_ai_message_with_tool_calls_but_no_content(self): """AIMessage that only has tool_calls (no content) should be skipped.""" msg = self._ai_msg("", tool_calls=[{"name": "web_search", "args": {}}]) result = {"messages": [msg]} assert _extract_final_text(result) is None def test_multiline_think_stripped_correctly(self): result = { "messages": [ self._ai_msg("\nLong\nreasoning\nblock\n\n## Report\n\nSome content."), ] } assert _extract_final_text(result) == "## Report\n\nSome content." # ── _extract_urls ────────────────────────────────────────────────────────────── class TestExtractUrls: def test_single_url(self): assert _extract_urls("check this out https://example.com please") == ["https://example.com"] def test_multiple_urls(self): urls = _extract_urls("see https://foo.com and https://bar.org/path?q=1") assert urls == ["https://foo.com", "https://bar.org/path?q=1"] def test_no_urls(self): assert _extract_urls("no links here at all") == [] def test_http_and_https(self): urls = _extract_urls("http://old.site and https://new.site") assert "http://old.site" in urls assert "https://new.site" in urls def test_url_at_start_of_message(self): assert _extract_urls("https://example.com is interesting") == ["https://example.com"] def test_url_only(self): assert _extract_urls("https://example.com/page") == ["https://example.com/page"] def test_url_with_path_and_query(self): url = "https://example.com/articles/123?ref=home&page=2" assert _extract_urls(url) == [url] def test_empty_string(self): assert _extract_urls("") == [] def test_does_not_include_surrounding_quotes(self): # URLs inside quotes should not include the quote character urls = _extract_urls('visit "https://example.com" today') assert urls == ["https://example.com"]