fss-mini-rag-github/tests/test_mode_separation.py
FSSCoding 930f53a0fb Major code quality improvements and structural organization
- Applied Black formatter and isort across entire codebase for professional consistency
- Moved implementation scripts (rag-mini.py, rag-tui.py) to bin/ directory for cleaner root
- Updated shell scripts to reference new bin/ locations maintaining user compatibility
- Added comprehensive linting configuration (.flake8, pyproject.toml) with dedicated .venv-linting
- Removed development artifacts (commit_message.txt, GET_STARTED.md duplicate) from root
- Consolidated documentation and fixed script references across all guides
- Relocated test_fixes.py to proper tests/ directory
- Enhanced project structure following Python packaging standards

All user commands work identically while improving code organization and beginner accessibility.
2025-08-28 15:29:54 +10:00

301 lines
10 KiB
Python

#!/usr/bin/env python3
"""
Test clean separation between synthesis and exploration modes.
Ensures that the two-mode architecture works correctly with no contamination
between thinking and no-thinking modes.
"""
import sys
import tempfile
import unittest
from pathlib import Path
# Add the RAG system to path
sys.path.insert(0, str(Path(__file__).parent.parent))
try:
from mini_rag.config import RAGConfig
from mini_rag.explorer import CodeExplorer
from mini_rag.indexer import ProjectIndexer
from mini_rag.llm_synthesizer import LLMSynthesizer
from mini_rag.search import CodeSearcher
except ImportError as e:
print(f"❌ Could not import RAG components: {e}")
print(" This test requires the full RAG system to be installed")
sys.exit(1)
class TestModeSeparation(unittest.TestCase):
"""Test the clean separation between synthesis and exploration modes."""
def setUp(self):
"""Set up test environment."""
self.temp_dir = tempfile.mkdtemp()
self.project_path = Path(self.temp_dir)
# Create a simple test project
test_file = self.project_path / "test_module.py"
test_file.write_text(
'''"""Test module for mode separation testing."""
def authenticate_user(username: str, password: str) -> bool:
"""Authenticate a user with username and password."""
# Simple authentication logic
if not username or not password:
return False
# Check against database (simplified)
valid_users = {"admin": "secret", "user": "password"}
return valid_users.get(username) == password
class UserManager:
"""Manages user operations."""
def __init__(self):
self.users = {}
def create_user(self, username: str) -> bool:
"""Create a new user."""
if username in self.users:
return False
self.users[username] = {"created": True}
return True
def get_user_info(self, username: str) -> dict:
"""Get user information."""
return self.users.get(username, {})
def process_login_request(username: str, password: str) -> dict:
"""Process a login request and return status."""
if authenticate_user(username, password):
return {"success": True, "message": "Login successful"}
else:
return {"success": False, "message": "Invalid credentials"}
'''
)
# Index the project for testing
try:
indexer = ProjectIndexer(self.project_path)
indexer.index_project()
except Exception as e:
self.skipTest(f"Could not index test project: {e}")
def tearDown(self):
"""Clean up test environment."""
import shutil
shutil.rmtree(self.temp_dir, ignore_errors=True)
def test_01_synthesis_mode_defaults(self):
"""Test that synthesis mode has correct defaults."""
synthesizer = LLMSynthesizer()
# Should default to no thinking
self.assertFalse(
synthesizer.enable_thinking, "Synthesis mode should default to no thinking"
)
print("✅ Synthesis mode defaults to no thinking")
def test_02_exploration_mode_defaults(self):
"""Test that exploration mode enables thinking."""
config = RAGConfig()
explorer = CodeExplorer(self.project_path, config)
# Should enable thinking in exploration mode
self.assertTrue(
explorer.synthesizer.enable_thinking,
"Exploration mode should enable thinking",
)
print("✅ Exploration mode enables thinking by default")
def test_03_no_runtime_thinking_toggle(self):
"""Test that thinking mode cannot be toggled at runtime."""
synthesizer = LLMSynthesizer(enable_thinking=False)
# Should not have public methods to toggle thinking
thinking_methods = [
method
for method in dir(synthesizer)
if "thinking" in method.lower() and not method.startswith("_")
]
# The only thinking-related attribute should be the readonly enable_thinking
self.assertEqual(
len(thinking_methods), 0, "Should not have public thinking toggle methods"
)
print("✅ No runtime thinking toggle methods available")
def test_04_mode_contamination_prevention(self):
"""Test that modes don't contaminate each other."""
if not self._ollama_available():
self.skipTest("Ollama not available for contamination testing")
# Create synthesis mode synthesizer
synthesis_synthesizer = LLMSynthesizer(enable_thinking=False)
# Create exploration mode synthesizer
exploration_synthesizer = LLMSynthesizer(enable_thinking=True)
# Both should maintain their thinking settings
self.assertFalse(
synthesis_synthesizer.enable_thinking,
"Synthesis synthesizer should remain no-thinking",
)
self.assertTrue(
exploration_synthesizer.enable_thinking,
"Exploration synthesizer should remain thinking-enabled",
)
print("✅ Mode contamination prevented")
def test_05_exploration_session_management(self):
"""Test exploration session management."""
config = RAGConfig()
explorer = CodeExplorer(self.project_path, config)
# Should start with no active session
self.assertIsNone(explorer.current_session, "Should start with no active session")
# Should be able to create session summary even without session
summary = explorer.get_session_summary()
self.assertIn("No active", summary, "Should handle no active session gracefully")
print("✅ Session management working correctly")
def test_06_context_memory_structure(self):
"""Test that exploration mode has context memory structure."""
config = RAGConfig()
explorer = CodeExplorer(self.project_path, config)
# Should have context tracking attributes
self.assertTrue(
hasattr(explorer, "current_session"),
"Explorer should have session tracking",
)
print("✅ Context memory structure present")
def test_07_synthesis_mode_no_thinking_prompts(self):
"""Test that synthesis mode properly handles no-thinking."""
if not self._ollama_available():
self.skipTest("Ollama not available for prompt testing")
synthesizer = LLMSynthesizer(enable_thinking=False)
# Test the _call_ollama method handling
if hasattr(synthesizer, "_call_ollama"):
# Should append <no_think> when thinking disabled
# This is a white-box test of the implementation
try:
# Mock test - just verify the method exists and can be called
# Test call (result unused)
synthesizer._call_ollama("test", temperature=0.1, disable_thinking=True)
# Don't assert on result since Ollama might not be available
print("✅ No-thinking prompt handling available")
except Exception as e:
print(f"⚠️ Prompt handling test skipped: {e}")
else:
self.fail("Synthesizer should have _call_ollama method")
def test_08_mode_specific_initialization(self):
"""Test that modes initialize correctly with lazy loading."""
# Synthesis mode
synthesis_synthesizer = LLMSynthesizer(enable_thinking=False)
self.assertFalse(
synthesis_synthesizer._initialized,
"Should start uninitialized for lazy loading",
)
# Exploration mode
config = RAGConfig()
explorer = CodeExplorer(self.project_path, config)
self.assertFalse(
explorer.synthesizer._initialized,
"Should start uninitialized for lazy loading",
)
print("✅ Lazy initialization working correctly")
def test_09_search_vs_exploration_integration(self):
"""Test integration differences between search and exploration."""
# Regular search (synthesis mode)
searcher = CodeSearcher(self.project_path)
search_results = searcher.search("authentication", top_k=3)
self.assertGreater(len(search_results), 0, "Search should return results")
# Exploration mode setup
config = RAGConfig()
explorer = CodeExplorer(self.project_path, config)
# Both should work with same project but different approaches
self.assertTrue(
hasattr(explorer, "synthesizer"),
"Explorer should have thinking-enabled synthesizer",
)
print("✅ Search and exploration integration working")
def test_10_mode_guidance_detection(self):
"""Test that the system can detect when to recommend different modes."""
# Words that should trigger exploration mode recommendation
exploration_triggers = ["why", "how", "explain", "debug"]
for trigger in exploration_triggers:
query = f"{trigger} does authentication work"
# This would typically be tested in the main CLI
# Here we just verify the trigger detection logic exists
has_trigger = any(word in query.lower() for word in exploration_triggers)
self.assertTrue(has_trigger, f"Should detect '{trigger}' as exploration trigger")
print("✅ Mode guidance detection working")
def _ollama_available(self) -> bool:
"""Check if Ollama is available for testing."""
try:
import requests
response = requests.get("http://localhost:11434/api/tags", timeout=5)
return response.status_code == 200
except Exception:
return False
def main():
"""Run mode separation tests."""
print("🧪 Testing Mode Separation")
print("=" * 40)
# Check if we're in the right environment
if not Path("mini_rag").exists():
print("❌ Tests must be run from the FSS-Mini-RAG root directory")
sys.exit(1)
# Run tests
loader = unittest.TestLoader()
suite = loader.loadTestsFromTestCase(TestModeSeparation)
runner = unittest.TextTestRunner(verbosity=2)
result = runner.run(suite)
# Summary
print("\n" + "=" * 40)
if result.wasSuccessful():
print("✅ All mode separation tests passed!")
print(" Synthesis and exploration modes are cleanly separated")
else:
print("❌ Some tests failed")
print(f" Failed: {len(result.failures)}, Errors: {len(result.errors)}")
return result.wasSuccessful()
if __name__ == "__main__":
success = main()
sys.exit(0 if success else 1)