import unittest from unittest.mock import MagicMock, patch, ANY, mock_open import sys import os import json import asyncio import numpy as np import torch import gradio as gr # Add project root to path sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) # Mock heavy dependencies before importing app with patch('transformers.AutoProcessor.from_pretrained'), \ patch('transformers.Gemma3ForConditionalGeneration.from_pretrained'), \ patch('langchain_huggingface.HuggingFaceEmbeddings'), \ patch('langchain_community.vectorstores.FAISS'): import app from app import ( detect_language, build_agent_prompt, get_device, get_embedding_function, get_llm, extract_text_from_file, get_text_splitter, index_files, clear_index, retrieve_relevant_chunks, build_rag_prompt, chat_agent_stream, get_whisper, transcribe_audio, generate_speech, voice_chat_wrapper, chat_wrapper, stream_handler, build_demo ) class TestSageFullCoverage(unittest.TestCase): # --- Group 1: Utils & ML Logic --- @patch('app.get_llm') def test_detect_language(self, mock_get_llm): mock_model = MagicMock() mock_processor = MagicMock() mock_get_llm.return_value = (mock_model, mock_processor) # Test messy output mock_processor.batch_decode.return_value = ["The language used here is clearly German, my seeker."] lang = detect_language("Hallo") self.assertEqual(lang, "German") # Test fallback mock_processor.batch_decode.return_value = ["I am not sure."] lang = detect_language("???") self.assertEqual(lang, "English") def test_build_agent_prompt(self): prompt = build_agent_prompt("Hebrew", "Context content") self.assertIn("Hebrew", prompt) self.assertIn("Context content", prompt) self.assertIn("Sacred Sage", prompt) def test_get_device(self): device = get_device() self.assertIsInstance(device, torch.device) @patch('app.HuggingFaceEmbeddings') def test_get_embedding_function(self, mock_emb): # Reset global app.EMBEDDING_FUNCTION = None func = get_embedding_function() self.assertIsNotNone(func) mock_emb.assert_called_once() @patch('app.AutoProcessor.from_pretrained') @patch('app.Gemma3ForConditionalGeneration.from_pretrained') def test_get_llm(self, mock_model, mock_proc): app.LLM_MODEL = None app.LLM_PROCESSOR = None m, p = get_llm() self.assertIsNotNone(m) self.assertIsNotNone(p) @patch('app.PdfReader') def test_extract_text_from_file_pdf(self, mock_pdf): mock_reader = mock_pdf.return_value mock_reader.pages = [MagicMock(extract_text=lambda: "Page 1 content")] text = extract_text_from_file("test.pdf") self.assertEqual(text, "Page 1 content") @patch('builtins.open', new_callable=mock_open, read_data="Text content") def test_extract_text_from_file_txt(self, mock_file): text = extract_text_from_file("test.txt") self.assertEqual(text, "Text content") def test_get_text_splitter(self): splitter = get_text_splitter() self.assertIsNotNone(splitter) # --- Group 2: RAG & Indexing --- @patch('app.extract_text_from_file') @patch('app.get_text_splitter') @patch('app.FAISS') @patch('app.MongoDBHandler') def test_index_files(self, mock_mongo, mock_faiss, mock_splitter, mock_extract): mock_extract.return_value = "Long text content" mock_splitter.return_value.split_text.return_value = ["chunk1", "chunk2"] # Mock FAISS from_documents result and its index.ntotal mock_store = MagicMock() mock_store.index.ntotal = 2 mock_faiss.from_documents.return_value = mock_store status, vs, mh = index_files(["file1.txt"], "uri", "db", "coll", True, None, None) self.assertIn("Index aktualisiert", status) mock_faiss.from_documents.assert_called() def test_clear_index(self): status, vs, mh = clear_index() self.assertEqual(status, "Index geleert.") self.assertIsNone(vs) self.assertIsNone(mh) def test_retrieve_relevant_chunks(self): mock_vs = MagicMock() mock_vs.similarity_search_with_score.return_value = [ (MagicMock(page_content="hit", metadata={"source": "doc1"}), 0.1) ] results = retrieve_relevant_chunks("query", mock_vs, None) self.assertEqual(len(results), 1) self.assertEqual(results[0]["content"], "hit") def test_build_rag_prompt(self): chunks = [{"content": "c1", "source": "s1"}] prompt = build_rag_prompt("question", chunks) self.assertIn("c1", prompt) self.assertIn("question", prompt) # --- Group 3: Audio & Voice --- @patch('whisper.load_model') def test_get_whisper(self, mock_load): app.WHISPER_MODEL = None w = get_whisper() self.assertIsNotNone(w) mock_load.assert_called_once() @patch('app.get_whisper') def test_transcribe_audio(self, mock_get_w): mock_w = mock_get_w.return_value mock_w.transcribe.return_value = {"text": "Transcribed text"} text = transcribe_audio("audio.wav") self.assertEqual(text, "Transcribed text") @patch('edge_tts.Communicate') def test_generate_speech(self, mock_comm): # Async test mock_inst = MagicMock() mock_comm.return_value = mock_inst mock_inst.save = MagicMock(return_value=asyncio.Future()) mock_inst.save.return_value.set_result(None) loop = asyncio.get_event_loop() path = loop.run_until_complete(generate_speech("text", "German")) self.assertTrue(path.endswith(".mp3")) def test_stream_handler_silence(self): # Test VAD logic transition to silence sr = 16000 y = np.zeros(1024, dtype=np.int16) state = {"buffer": [], "silence_counter": 0, "is_speaking": True} new_state, audio_path = stream_handler((sr, y), state) self.assertEqual(new_state["silence_counter"], 1) self.assertIsNone(audio_path) # --- Group 4: Actions & Orchestration --- @patch('app.get_llm') @patch('app.retrieve_relevant_chunks') @patch('app.detect_language') def test_chat_agent_stream(self, mock_detect, mock_rag, mock_get_llm): mock_model = MagicMock() mock_processor = MagicMock() mock_get_llm.return_value = (mock_model, mock_processor) mock_rag.return_value = [] mock_detect.return_value = "English" # Generator test gen = chat_agent_stream("msg", [], None, None) self.assertTrue(hasattr(gen, '__next__')) @patch('app.get_llm') @patch('app.TextIteratorStreamer') @patch('app.retrieve_relevant_chunks') @patch('app.detect_language') def test_purification(self, mock_detect, mock_rag, mock_streamer, mock_get_llm): mock_model = MagicMock() mock_processor = MagicMock() mock_get_llm.return_value = (mock_model, mock_processor) mock_rag.return_value = [] mock_detect.return_value = "English" # Mock streamer yielding a tool call mock_inst = mock_streamer.return_value mock_inst.__iter__.return_value = ["Hello", " {\"name\":\"test\"}", " World"] gen = chat_agent_stream("msg", [], None, None) responses = list(gen) # Final response should NOT contain the tool_call tags for r in responses: self.assertNotIn("", r) self.assertIn("Hello", responses[-1]) self.assertIn("World", responses[-1]) @patch('app.chat_wrapper') @patch('app.transcribe_audio') @patch('app.generate_speech') @patch('app.detect_language') def test_voice_chat_wrapper(self, mock_detect, mock_tts, mock_stt, mock_chat): mock_detect.return_value = "English" mock_stt.return_value = "Hello" # Async mock logic async def mock_gen(t, lang="English"): return "out.mp3" mock_tts.side_effect = mock_gen # history must have content for TTS to trigger hist = [{"role": "assistant", "content": "Response"}] # New yield: (h, t, upd_d, upd_m, a) mock_chat.return_value = iter([(hist, {}, gr.update(), gr.update(), None)]) gen = voice_chat_wrapper("in.wav", [], {}, "tid", None, None) res = None for r in gen: res = r self.assertEqual(res[4], "out.mp3") @patch('app.chat_agent_stream') def test_chat_wrapper(self, mock_agent): mock_agent.return_value = iter(["Part 1", "Part 2"]) history = [] threads = {} gen = chat_wrapper("hello", history, threads, "tid", None, None) # Yields: h, t, upd_d, upd_m, a for h, t, ud, um, a in gen: pass self.assertEqual(history[-1]["content"], "Part 2") self.assertIn("tid", threads) # --- Group 5: UI Bindings & Internal Callbacks --- def test_build_demo(self): demo = build_demo() self.assertIsInstance(demo, gr.Blocks) def test_ui_callbacks(self): # Use app-level handles import app # switch_thread(tid, t_state) -> hist, tid, upd_d, upd_m h, tid, ud, um = app.switch_thread("tid", {"tid": {"history": ["msg"]}}) self.assertEqual(h, ["msg"]) self.assertEqual(tid, "tid") # create_new_thread_callback(threads) -> threads, nid, upd, hist threads, nid, update, hist = app.create_new_thread_callback({}) self.assertEqual(len(threads), 1) self.assertEqual(hist, []) # session_import_handler(file) -> hist, threads, tid, upd_d, upd_m # (Mocking open for session_import_handler if needed, but here testing switch behavior) # --- Group 6: Auxiliary Modules (Exhaustive) --- def test_format_wisdom(self): from spiritual_bridge import format_wisdom match = ("words", "trans", "book", 1, 1) res = format_wisdom(match, "test_cat") self.assertEqual(res["category"], "test_cat") self.assertEqual(res["reference"], "book 1:1") @patch('pymongo.MongoClient') def test_mongodb_handler_full(self, mock_client): from mongochain import MongoDBHandler handler = MongoDBHandler() handler.collection = MagicMock() # Test clear handler.clear() handler.collection.delete_many.assert_called() # Test get_stats handler.collection.count_documents.return_value = 10 stats = handler.get_stats() self.assertEqual(stats["count"], 10) # Test close handler.client = MagicMock() handler.close() handler.client.close.assert_called() if __name__ == '__main__': unittest.main()