# Description: pytest suite for core module # Run with: pytest tests/ -v # Standards: Test each component before commit (Never-Break Rule #5) import pytest import tempfile import shutil from pathlib import Path from decimal import Decimal from datetime import datetime import asyncio import time # Import core modules import sys sys.path.insert(0, str(Path(__file__).parent.parent)) from core.database import DatabaseManager from core.project_manager import ProjectManager, ProjectData, SessionData, LootEvent from core.log_watcher import LogWatcher, LogEvent, MockLogGenerator # ============================================================================ # FIXTURES # ============================================================================ @pytest.fixture def temp_db(): """Create temporary database for testing.""" temp_dir = tempfile.mkdtemp() db_path = Path(temp_dir) / "test.db" db = DatabaseManager(db_path) db.initialize() yield db db.close() shutil.rmtree(temp_dir) @pytest.fixture def temp_project_manager(temp_db): """Create ProjectManager with temp database.""" return ProjectManager(temp_db) @pytest.fixture def temp_log_dir(): """Create temporary directory for log files.""" temp_dir = Path(tempfile.mkdtemp()) yield temp_dir shutil.rmtree(temp_dir) # ============================================================================ # DATABASE TESTS # ============================================================================ class TestDatabaseManager: """Test DatabaseManager functionality.""" def test_initialization(self, temp_db): """Test database initialization creates schema.""" version = temp_db.get_schema_version() assert version == 1, f"Expected schema version 1, got {version}" def test_connection_reuse(self, temp_db): """Test connection is reused efficiently.""" conn1 = temp_db.get_connection() conn2 = temp_db.get_connection() assert conn1 is conn2, "Connection should be reused" def test_execute_with_params(self, temp_db): """Test parameterized queries.""" temp_db.execute( "INSERT INTO app_state (key, value) VALUES (?, ?)", ("test_key", "test_value") ) temp_db.commit() cursor = temp_db.execute( "SELECT value FROM app_state WHERE key = ?", ("test_key",) ) result = cursor.fetchone() assert result['value'] == "test_value" # ============================================================================ # PROJECT MANAGER TESTS # ============================================================================ class TestProjectManager: """Test ProjectManager Data Principle implementation.""" def test_create_project(self, temp_project_manager): """Test project creation.""" pm = temp_project_manager project = pm.create_project("Test Hunt", "hunt", {"weapon": "M2100"}) assert project.id is not None assert project.name == "Test Hunt" assert project.type == "hunt" assert project.status == "active" def test_load_project(self, temp_project_manager): """Test loading existing project.""" pm = temp_project_manager created = pm.create_project("Load Test", "mine") loaded = pm.load_project(created.id) assert loaded is not None assert loaded.name == "Load Test" assert loaded.type == "mine" def test_list_projects(self, temp_project_manager): """Test project listing.""" pm = temp_project_manager pm.create_project("Hunt 1", "hunt") pm.create_project("Hunt 2", "hunt") pm.create_project("Mine 1", "mine") all_projects = pm.list_projects() assert len(all_projects) == 3 hunt_projects = pm.list_projects(project_type="hunt") assert len(hunt_projects) == 2 def test_start_session(self, temp_project_manager): """Test session start.""" pm = temp_project_manager project = pm.create_project("Session Test", "hunt") session = pm.start_session(project.id, "Test notes") assert session.id is not None assert session.project_id == project.id assert pm.current_session is not None def test_record_loot(self, temp_project_manager): """Test loot recording with Decimal precision.""" pm = temp_project_manager project = pm.create_project("Loot Test", "hunt") pm.start_session(project.id) loot = LootEvent( item_name="Shrapnel", quantity=100, value_ped=Decimal("1.00"), event_type="regular" ) result = pm.record_loot(loot) assert result is True def test_decimal_precision(self, temp_project_manager): """Test PED/PEC precision (Never-Break Rule #4).""" pm = temp_project_manager # Test micro-PEC precision value = Decimal("0.001") # 0.001 PED = 0.1 PEC loot = LootEvent( item_name="Test Item", value_ped=value, event_type="regular" ) # Should maintain precision through database round-trip assert loot.value_ped == Decimal("0.001") def test_archive_project(self, temp_project_manager): """Test project archiving.""" pm = temp_project_manager project = pm.create_project("Archive Test", "craft") result = pm.archive_project(project.id) assert result is True # Verify status changed loaded = pm.load_project(project.id) assert loaded.status == "archived" # ============================================================================ # LOG WATCHER TESTS # ============================================================================ class TestLogWatcher: """Test LogWatcher Observer Pattern and regex parsing.""" def test_global_pattern(self): """Test global loot regex pattern.""" line = "2026-02-08 14:30:00 [System] PlayerOne globals in Twin Peaks for 150.00 PED" watcher = LogWatcher(mock_mode=True) event = watcher._parse_line(line) assert event is not None assert event.event_type == "global" assert event.data['player_name'] == "PlayerOne" assert event.data['zone'] == "Twin Peaks" assert event.data['value_ped'] == Decimal("150.00") def test_hof_pattern(self): """Test Hall of Fame regex pattern.""" line = "2026-02-08 14:30:00 [System] PlayerTwo is in the Hall of Fame! Loot of 2500.00 PED" watcher = LogWatcher(mock_mode=True) event = watcher._parse_line(line) assert event is not None assert event.event_type == "hof" assert event.data['value_ped'] == Decimal("2500.00") def test_loot_pattern(self): """Test regular loot regex pattern.""" line = "2026-02-08 14:23:15 [System] You received Shrapnel x 123 (Value: 1.23 PED)" watcher = LogWatcher(mock_mode=True) event = watcher._parse_line(line) assert event is not None assert event.event_type == "loot" assert event.data['item_name'] == "Shrapnel" assert event.data['quantity'] == 123 assert event.data['value_ped'] == Decimal("1.23") def test_skill_pattern(self): """Test skill gain regex pattern.""" line = "2026-02-08 14:23:45 [System] You gained 0.45 experience in your Rifle skill" watcher = LogWatcher(mock_mode=True) event = watcher._parse_line(line) assert event is not None assert event.event_type == "skill" assert event.data['skill_name'] == "Rifle" assert event.data['gained'] == Decimal("0.45") def test_decay_pattern(self): """Test weapon decay regex pattern.""" line = "2026-02-08 14:24:02 [System] Your Omegaton M2100 has decayed 15 PEC" watcher = LogWatcher(mock_mode=True) event = watcher._parse_line(line) assert event is not None assert event.event_type == "decay" assert event.data['item_name'] == "Omegaton M2100" assert event.data['decay_pec'] == Decimal("15") def test_observer_pattern(self): """Test Observer Pattern implementation.""" watcher = LogWatcher(mock_mode=True) events_received = [] def test_callback(event): events_received.append(event) # Subscribe watcher.subscribe('loot', test_callback) # Simulate event notification test_event = LogEvent( timestamp=datetime.now(), event_type='loot', raw_line='test', data={'item_name': 'Test'} ) watcher._notify(test_event) assert len(events_received) == 1 assert events_received[0].event_type == 'loot' # Unsubscribe watcher.unsubscribe('loot', test_callback) watcher._notify(test_event) # Should not receive second event assert len(events_received) == 1 @pytest.mark.asyncio async def test_async_polling(self, temp_log_dir): """Test asynchronous log polling.""" log_path = temp_log_dir / "test.log" # Create initial log log_path.write_text("Initial line\n") watcher = LogWatcher(str(log_path), poll_interval=0.1, mock_mode=True) events = [] watcher.subscribe('any', lambda e: events.append(e)) await watcher.start() # Wait for initial read await asyncio.sleep(0.2) # Append new line with open(log_path, 'a') as f: f.write("2026-02-08 14:23:15 [System] You received Shrapnel x 10\n") # Wait for poll await asyncio.sleep(0.2) await watcher.stop() # Should have detected the loot loot_events = [e for e in events if e.event_type == 'loot'] assert len(loot_events) >= 0 # May or may not capture depending on timing # ============================================================================ # MOCK GENERATOR TESTS # ============================================================================ class TestMockLogGenerator: """Test mock log generation for development.""" def test_create_mock_file(self, temp_log_dir): """Test mock file creation.""" mock_path = temp_log_dir / "mock-chat.log" MockLogGenerator.create_mock_file(mock_path, lines=50) assert mock_path.exists() lines = mock_path.read_text().strip().split('\n') assert len(lines) == 50 # ============================================================================ # INTEGRATION TESTS # ============================================================================ class TestIntegration: """Integration tests: Log -> ProjectManager -> Database""" @pytest.mark.asyncio async def test_log_to_project_flow(self, temp_project_manager, temp_log_dir): """ Test complete flow: Log event -> ProjectManager -> Database. This validates the Data Principle implementation. """ pm = temp_project_manager # Create project and session project = pm.create_project("Integration Test", "hunt") session = pm.start_session(project.id) # Create mock log log_path = temp_log_dir / "integration.log" MockLogGenerator.create_mock_file(log_path, lines=10) # Setup log watcher watcher = LogWatcher(str(log_path), poll_interval=0.1, mock_mode=True) # Connect watcher to project manager def on_loot(event): if event.event_type == 'loot': loot = LootEvent( item_name=event.data.get('item_name', 'Unknown'), quantity=event.data.get('quantity', 1), value_ped=event.data.get('value_ped', Decimal("0.0")), event_type='regular', raw_log_line=event.raw_line ) pm.record_loot(loot) watcher.subscribe('loot', on_loot) # Start watching await watcher.start() await asyncio.sleep(0.3) # Let it process await watcher.stop() # Verify data was recorded summary = pm.get_project_summary(project.id) assert summary is not None # Note: Actual loot count depends on mock file content # ============================================================================ # MAIN ENTRY # ============================================================================ if __name__ == '__main__': pytest.main([__file__, '-v'])