import unittest from unittest.mock import MagicMock, patch import gradio as gr from app import chat_wrapper, chat_agent_stream, switch_thread class TestSageRegressionV6_5(unittest.TestCase): def test_switch_thread_robustness(self): """Verifies that switch_thread handles list inputs from Gradio without crashing.""" t_state = {"tid1": {"history": ["msg1"], "title": "T1"}, "tid2": {"history": ["msg2"], "title": "T2"}} # Test string input h, tid, ud, um = switch_thread("tid1", t_state) self.assertEqual(h, ["msg1"]) self.assertEqual(tid, "tid1") # Test list input (Gradio often sends [value] for Dropdowns) h, tid, ud, um = switch_thread(["tid2"], t_state) self.assertEqual(h, ["msg2"]) self.assertEqual(tid, "tid2") # Test empty/None input h, tid, ud, um = switch_thread([], t_state) self.assertEqual(h, []) @patch('app.detect_language') @patch('app.get_oracle_data') def test_agent_role_alternation(self, mock_oracle, mock_detect): """Verifies Assistant -> Tool -> Execution -> Assistant sequence.""" mock_detect.return_value = "English" mock_oracle.return_value = {"wisdom": "The path is clear."} # Mock API return with patch('app.get_llm') as mock_llm, \ patch('app.TextIteratorStreamer') as mock_streamer, \ patch('app.retrieve_relevant_chunks') as mock_rag: mock_model = MagicMock() mock_processor = MagicMock() mock_llm.return_value = (mock_model, mock_processor) mock_rag.return_value = [] # 1st turn: Tool Call (LLM decides to call tool) # 2nd turn: Interpretation (LLM interprets the injected result) mock_inst = mock_streamer.return_value mock_inst.__iter__.side_effect = [ iter(["{\"name\":\"oracle_consultation\",\"arguments\":{\"topic\":\"peace\"}}"]), iter(["Peace flows like a river."]) ] # Using list(gen) triggers the full multi-turn loop gen = chat_agent_stream("ask oracle", [], None, None) responses = list(gen) # Verify Oracle was called mock_oracle.assert_called_with(name="Seeker", topic="peace", date_str="") # Verify final response self.assertIn("Peace", responses[-1]) @patch('app.detect_language') def test_chat_purification_logic(self, mock_detect): """Verifies that tags are stripped from streaming output.""" mock_detect.return_value = "English" with patch('app.get_llm') as mock_llm, \ patch('app.TextIteratorStreamer') as mock_streamer, \ patch('app.retrieve_relevant_chunks') as mock_rag: mock_model = MagicMock() mock_processor = MagicMock() mock_llm.return_value = (mock_model, mock_processor) mock_rag.return_value = [] # Mock streamer yielding text with a tool call mock_inst = mock_streamer.return_value mock_inst.__iter__.return_value = iter(["Hello", " {\"name\":\"oracle\"}", " Seeker"]) gen = chat_agent_stream("hi", [], None, None) yields = list(gen) # Ensure no yield contains the tags for y in yields: self.assertNotIn("", y) # Ensure the text is still preserved self.assertIn("Hello", yields[-1]) self.assertIn("Seeker", yields[-1]) def test_ui_sync_signatures(self): """Verifies that chat_wrapper returns 5 values for desktop/mobile sync.""" with patch('app.chat_agent_stream') as mock_agent: mock_agent.return_value = iter(["Response"]) history = [] threads = {"tid": {"title": "Chat", "history": []}} gen = chat_wrapper("hello", history, threads, "tid", None, None) # Must yield 5 items: h, t, upd_d, upd_m, a for val in gen: self.assertEqual(len(val), 5) self.assertIsInstance(val[2], dict) # gr.update() self.assertIsInstance(val[3], dict) # gr.update() if __name__ == "__main__": unittest.main()