""" Tests for core/cot_rag.py — Chain-of-Thought RAG (EMNLP 2025). """ from __future__ import annotations from unittest.mock import MagicMock class TestDecomposeQuestion: def test_returns_list_of_strings(self) -> None: from core.cot_rag import decompose_question mock_llm = MagicMock(return_value="1. What is X?\n2. How does Y work?\n3. What is Z?") steps = decompose_question("Complex multi-hop question", mock_llm, max_steps=4) assert isinstance(steps, list) assert len(steps) >= 1 assert all(isinstance(s, str) for s in steps) def test_respects_max_steps(self) -> None: from core.cot_rag import decompose_question mock_llm = MagicMock( return_value="1. Step one\n2. Step two\n3. Step three\n4. Step four\n5. Step five" ) steps = decompose_question("question", mock_llm, max_steps=3) assert len(steps) <= 3 def test_fallback_on_llm_failure(self) -> None: from core.cot_rag import decompose_question mock_llm = MagicMock(side_effect=RuntimeError("LLM down")) steps = decompose_question("What is the capital of France?", mock_llm) assert len(steps) >= 1 assert "France" in steps[0] or "capital" in steps[0].lower() or len(steps[0]) > 0 def test_parses_numbered_list(self) -> None: from core.cot_rag import decompose_question mock_llm = MagicMock( return_value="1. Find the revenue figure\n2. Calculate the growth rate" ) steps = decompose_question("question", mock_llm) assert "Find the revenue figure" in steps assert "Calculate the growth rate" in steps def test_handles_empty_response(self) -> None: from core.cot_rag import decompose_question mock_llm = MagicMock(return_value="") steps = decompose_question("question", mock_llm) # Should return at least one step (fallback) assert len(steps) >= 1 class TestSynthesizeStep: def test_returns_string(self) -> None: from core.cot_rag import synthesize_step mock_llm = MagicMock(return_value="The answer is 42.") result = synthesize_step("What is the answer?", ["Context chunk 1"], mock_llm) assert isinstance(result, str) assert len(result) > 0 def test_no_chunks_returns_not_found(self) -> None: from core.cot_rag import synthesize_step mock_llm = MagicMock() result = synthesize_step("What is X?", [], mock_llm) assert "not found" in result.lower() or "no relevant" in result.lower() mock_llm.assert_not_called() def test_handles_llm_failure(self) -> None: from core.cot_rag import synthesize_step mock_llm = MagicMock(side_effect=RuntimeError("timeout")) result = synthesize_step("question", ["chunk"], mock_llm) assert isinstance(result, str) # should not raise class TestSynthesizeFinal: def test_returns_answer_and_token_count(self) -> None: from core.cot_rag import ReasoningStep, synthesize_final steps = [ ReasoningStep( step_number=1, thought="Find revenue", sub_query="revenue 2023", retrieved=["Revenue was $2.3B"], sources=["report.pdf"], intermediate="Revenue was $2.3B in 2023", ) ] mock_llm = MagicMock(return_value="The revenue grew to $2.3B.") answer, tokens = synthesize_final("What was the revenue?", steps, mock_llm) assert isinstance(answer, str) assert len(answer) > 0 assert isinstance(tokens, int) assert tokens > 0 class TestRunCoTRAG: def test_returns_cot_result(self) -> None: from core.cot_rag import run_cot_rag from models import QueryMode, RetrievalContext mock_llm = MagicMock( side_effect=[ "1. Find what X is\n2. Find how X works", # decompose "X is a framework for RAG", # step 1 synthesis "X works by retrieving chunks", # step 2 synthesis "X is a RAG framework that retrieves chunks.", # final synthesis ] ) def mock_retrieve(req): return RetrievalContext( query=req.question, results=[], query_mode=QueryMode.HYBRID, ) result = run_cot_rag( question="What is X and how does it work?", collection="default", retrieve_fn=mock_retrieve, llm_fn=mock_llm, max_steps=2, ) assert result.question == "What is X and how does it work?" assert isinstance(result.answer, str) assert result.num_steps == 2 assert isinstance(result.reasoning_steps, list) assert result.latency_ms > 0 def test_warns_on_no_context(self) -> None: from core.cot_rag import run_cot_rag from models import QueryMode, RetrievalContext mock_llm = MagicMock( side_effect=[ "1. Step one", "Not found in context.", "No answer available.", ] ) def mock_retrieve(req): return RetrievalContext( query=req.question, results=[], query_mode=QueryMode.HYBRID, ) result = run_cot_rag( question="Unknown question", collection="empty", retrieve_fn=mock_retrieve, llm_fn=mock_llm, max_steps=1, ) assert len(result.warnings) > 0 def test_trace_markdown_property(self) -> None: from core.cot_rag import CoTResult, ReasoningStep result = CoTResult( question="test?", answer="answer", reasoning_steps=[ ReasoningStep( step_number=1, thought="Find X", sub_query="X", retrieved=["chunk"], sources=["doc.txt"], intermediate="X is Y", ) ], all_sources=["doc.txt"], total_chunks=1, tokens_used=100, latency_ms=500.0, num_steps=1, ) md = result.trace_as_markdown assert "Step 1" in md assert "Find X" in md assert "answer" in md class TestQueryClassifier: def test_who_what_routes_low(self) -> None: # Just testing the classify function doesn't error from core.light_rag import classify_query assert classify_query("Who is the CEO?") == "low" assert classify_query("What is machine learning?") == "low" def test_summarize_routes_high(self) -> None: from core.light_rag import classify_query assert classify_query("Summarize the key themes") == "high" assert classify_query("Why is this important overall?") == "high"