refactor(tests): Update test suite and API hooks for AppController architecture
This commit is contained in:
@@ -88,7 +88,7 @@ def mock_app() -> Generator[App, None, None]:
|
||||
Mock version of the App for simple unit tests that don't need a loop.
|
||||
"""
|
||||
with (
|
||||
patch('gui_2.load_config', return_value={
|
||||
patch('src.models.load_config', return_value={
|
||||
'ai': {'provider': 'gemini', 'model': 'gemini-2.5-flash-lite'},
|
||||
'projects': {'paths': [], 'active': ''},
|
||||
'gui': {'show_windows': {}}
|
||||
@@ -97,16 +97,19 @@ def mock_app() -> Generator[App, None, None]:
|
||||
patch('gui_2.project_manager'),
|
||||
patch('gui_2.session_logger'),
|
||||
patch('gui_2.immapp.run'),
|
||||
patch.object(App, '_load_active_project'),
|
||||
patch.object(App, '_fetch_models'),
|
||||
patch('src.app_controller.AppController._load_active_project'),
|
||||
patch('src.app_controller.AppController._fetch_models'),
|
||||
patch.object(App, '_load_fonts'),
|
||||
patch.object(App, '_post_init'),
|
||||
patch.object(App, '_prune_old_logs'),
|
||||
patch.object(App, '_init_ai_and_hooks'),
|
||||
patch('src.app_controller.AppController._prune_old_logs'),
|
||||
patch('src.app_controller.AppController.start_services'),
|
||||
patch('src.app_controller.AppController._init_ai_and_hooks'),
|
||||
patch('gui_2.PerformanceMonitor')
|
||||
):
|
||||
app = App()
|
||||
yield app
|
||||
if hasattr(app, 'controller'):
|
||||
app.controller.stop_services()
|
||||
if hasattr(app, 'shutdown'):
|
||||
app.shutdown()
|
||||
|
||||
@@ -117,7 +120,7 @@ def app_instance() -> Generator[App, None, None]:
|
||||
Matches the pattern used in test_token_viz.py and test_gui_phase4.py.
|
||||
"""
|
||||
with (
|
||||
patch('gui_2.load_config', return_value={
|
||||
patch('src.models.load_config', return_value={
|
||||
'ai': {'provider': 'gemini', 'model': 'gemini-2.5-flash-lite'},
|
||||
'projects': {'paths': [], 'active': ''},
|
||||
'gui': {'show_windows': {}}
|
||||
@@ -126,22 +129,28 @@ def app_instance() -> Generator[App, None, None]:
|
||||
patch('gui_2.project_manager'),
|
||||
patch('gui_2.session_logger'),
|
||||
patch('gui_2.immapp.run'),
|
||||
patch.object(App, '_load_active_project'),
|
||||
patch.object(App, '_fetch_models'),
|
||||
patch('src.app_controller.AppController._load_active_project'),
|
||||
patch('src.app_controller.AppController._fetch_models'),
|
||||
patch.object(App, '_load_fonts'),
|
||||
patch.object(App, '_post_init'),
|
||||
patch.object(App, '_prune_old_logs'),
|
||||
patch.object(App, '_init_ai_and_hooks'),
|
||||
patch('src.app_controller.AppController._prune_old_logs'),
|
||||
patch('src.app_controller.AppController.start_services'),
|
||||
patch('src.app_controller.AppController._init_ai_and_hooks'),
|
||||
patch('gui_2.PerformanceMonitor')
|
||||
):
|
||||
app = App()
|
||||
yield app
|
||||
# Cleanup: Ensure background threads and asyncio loop are stopped
|
||||
if hasattr(app, 'controller'):
|
||||
app.controller.stop_services()
|
||||
|
||||
if hasattr(app, 'shutdown'):
|
||||
app.shutdown()
|
||||
|
||||
if hasattr(app, '_loop') and not app._loop.is_closed():
|
||||
tasks = [t for t in asyncio.all_tasks(app._loop) if not t.done()]
|
||||
# Use controller._loop for cleanup
|
||||
loop = getattr(app.controller, '_loop', None) if hasattr(app, 'controller') else None
|
||||
if loop and not loop.is_closed():
|
||||
tasks = [t for t in asyncio.all_tasks(loop) if not t.done()]
|
||||
if tasks:
|
||||
# Cancel tasks so they can be gathered
|
||||
for task in tasks:
|
||||
@@ -149,14 +158,14 @@ def app_instance() -> Generator[App, None, None]:
|
||||
# We can't really run the loop if it's already stopping or thread is dead,
|
||||
# but we try to be clean.
|
||||
try:
|
||||
if app._loop.is_running():
|
||||
app._loop.call_soon_threadsafe(app._loop.stop)
|
||||
if loop.is_running():
|
||||
loop.call_soon_threadsafe(loop.stop)
|
||||
except: pass
|
||||
|
||||
# Finally close the loop if we can
|
||||
try:
|
||||
if not app._loop.is_running():
|
||||
app._loop.close()
|
||||
if not loop.is_running():
|
||||
loop.close()
|
||||
except: pass
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
from gui_2 import App
|
||||
from events import UserRequestEvent
|
||||
from src.events import UserRequestEvent
|
||||
|
||||
@pytest.fixture
|
||||
def mock_gui() -> App:
|
||||
with (
|
||||
patch('gui_2.load_config', return_value={
|
||||
patch('src.models.load_config', return_value={
|
||||
"ai": {"provider": "gemini", "model": "model-1"},
|
||||
"projects": {"paths": [], "active": ""},
|
||||
"gui": {"show_windows": {}}
|
||||
@@ -15,8 +15,8 @@ def mock_gui() -> App:
|
||||
patch('gui_2.project_manager.migrate_from_legacy_config', return_value={}),
|
||||
patch('gui_2.project_manager.save_project'),
|
||||
patch('gui_2.session_logger.open_session'),
|
||||
patch('gui_2.App._init_ai_and_hooks'),
|
||||
patch('gui_2.App._fetch_models')
|
||||
patch('src.app_controller.AppController._init_ai_and_hooks'),
|
||||
patch('src.app_controller.AppController._fetch_models')
|
||||
):
|
||||
gui = App()
|
||||
return gui
|
||||
|
||||
@@ -43,7 +43,7 @@ def test_add_ticket_logic(mock_app: App):
|
||||
mock_imgui.get_window_draw_list.return_value.add_rect_filled = MagicMock()
|
||||
|
||||
# We also need to mock _push_mma_state_update
|
||||
with patch.object(mock_app, '_push_mma_state_update') as mock_push:
|
||||
with patch.object(mock_app.controller, '_push_mma_state_update') as mock_push:
|
||||
mock_app._render_mma_dashboard()
|
||||
|
||||
# Verify ticket was added
|
||||
@@ -93,7 +93,7 @@ def test_delete_ticket_logic(mock_app: App):
|
||||
mock_imgui.ImVec2 = MagicMock
|
||||
mock_imgui.ImVec4 = MagicMock
|
||||
|
||||
with patch('gui_2.C_LBL', MagicMock()), patch.object(mock_app, '_push_mma_state_update') as mock_push:
|
||||
with patch('gui_2.C_LBL', MagicMock()), patch.object(mock_app.controller, '_push_mma_state_update') as mock_push:
|
||||
# Render T-001
|
||||
mock_app._render_ticket_dag_node(mock_app.active_tickets[0], tickets_by_id, children_map, rendered)
|
||||
|
||||
@@ -110,8 +110,8 @@ def test_track_discussion_toggle(mock_app: App):
|
||||
patch('gui_2.imgui') as mock_imgui,
|
||||
patch('gui_2.project_manager.load_track_history', return_value=["@2026-03-01 12:00:00\n[User]\nTrack Hello"]) as mock_load,
|
||||
patch('gui_2.project_manager.str_to_entry', side_effect=lambda s, roles: {"ts": "12:00", "role": "User", "content": s.split("\n")[-1]}),
|
||||
patch.object(mock_app, '_flush_disc_entries_to_project') as mock_flush,
|
||||
patch.object(mock_app, '_switch_discussion') as mock_switch
|
||||
patch.object(mock_app.controller, '_flush_disc_entries_to_project') as mock_flush,
|
||||
patch.object(mock_app.controller, '_switch_discussion') as mock_switch
|
||||
):
|
||||
# Track calls to ensure we only return 'changed=True' once to avoid loops
|
||||
calls = {"Track Discussion": 0}
|
||||
@@ -163,9 +163,9 @@ def test_track_discussion_toggle(mock_app: App):
|
||||
|
||||
def test_push_mma_state_update(mock_app: App):
|
||||
mock_app.active_tickets = [{"id": "T-001", "description": "desc", "status": "todo", "assigned_to": "tier3-worker", "depends_on": []}]
|
||||
with patch('project_manager.save_track_state') as mock_save, \
|
||||
patch('project_manager.load_track_state', return_value=None):
|
||||
mock_app._push_mma_state_update()
|
||||
with patch('src.project_manager.save_track_state') as mock_save, \
|
||||
patch('src.project_manager.load_track_state', return_value=None):
|
||||
mock_app.controller._push_mma_state_update()
|
||||
|
||||
assert len(mock_app.active_track.tickets) == 1
|
||||
assert mock_app.active_track.tickets[0].id == "T-001"
|
||||
|
||||
Reference in New Issue
Block a user