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