import pytest from unittest.mock import MagicMock, patch from gui_2 import App from models import Track, Ticket import project_manager @pytest.fixture def mock_app() -> App: with ( patch('gui_2.load_config', return_value={ "ai": {"provider": "gemini", "model": "model-1"}, "projects": {"paths": [], "active": ""}, "gui": {"show_windows": {}} }), patch('gui_2.project_manager.load_project', return_value={}), 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('gui_2.App._prune_old_logs') ): app = App() app._discussion_names_dirty = True app._discussion_names_cache = [] app.active_track = Track(id="track-1", description="Test Track", tickets=[]) app.active_tickets = [] app.ui_files_base_dir = "." app.disc_roles = ["User", "AI"] app.active_discussion = "main" app.project = {"discussion": {"discussions": {"main": {"history": []}}}} return app def test_add_ticket_logic(mock_app: App): # Mock imgui calls to simulate clicking "Create" in the form with patch('gui_2.imgui') as mock_imgui: # Default return for any checkbox/input mock_imgui.checkbox.side_effect = lambda label, value: (False, value) mock_imgui.input_text.side_effect = lambda label, value, **kwargs: (False, value) mock_imgui.input_text_multiline.side_effect = lambda label, value, *args, **kwargs: (False, value) mock_imgui.input_int.side_effect = lambda label, value, *args, **kwargs: (False, value) mock_imgui.begin_table.return_value = False mock_imgui.collapsing_header.return_value = False mock_imgui.begin_combo.return_value = False # Simulate form state mock_app._show_add_ticket_form = True mock_app.ui_new_ticket_id = "T-001" mock_app.ui_new_ticket_desc = "Test Description" mock_app.ui_new_ticket_target = "test.py" mock_app.ui_new_ticket_deps = "T-000" # Configure mock_imgui.button to return True only for "Create" def button_side_effect(label): return label == "Create" mock_imgui.button.side_effect = button_side_effect # Mock other necessary imgui calls to avoid errors mock_imgui.begin_child.return_value = True # We also need to mock _push_mma_state_update with patch.object(mock_app, '_push_mma_state_update') as mock_push: mock_app._render_mma_dashboard() # Verify ticket was added assert len(mock_app.active_tickets) == 1 t = mock_app.active_tickets[0] assert t["id"] == "T-001" assert t["description"] == "Test Description" assert t["target_file"] == "test.py" assert t["depends_on"] == ["T-000"] assert t["status"] == "todo" assert t["assigned_to"] == "tier3-worker" # Verify form was closed assert mock_app._show_add_ticket_form == False # Verify push was called mock_push.assert_called_once() def test_delete_ticket_logic(mock_app: App): # Setup tickets mock_app.active_tickets = [ {"id": "T-001", "status": "todo", "depends_on": []}, {"id": "T-002", "status": "todo", "depends_on": ["T-001"]} ] tickets_by_id = {t['id']: t for t in mock_app.active_tickets} children_map = {"T-001": ["T-002"]} rendered = set() with patch('gui_2.imgui') as mock_imgui: # Configure mock_imgui.button to return True only for "Delete##T-001" def button_side_effect(label): return label == "Delete##T-001" mock_imgui.button.side_effect = button_side_effect mock_imgui.tree_node_ex.return_value = True with patch.object(mock_app, '_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) # Verify T-001 was deleted assert len(mock_app.active_tickets) == 1 assert mock_app.active_tickets[0]["id"] == "T-002" # Verify dependency cleanup assert mock_app.active_tickets[0]["depends_on"] == [] # Verify push was called mock_push.assert_called_once() def test_track_discussion_toggle(mock_app: App): with ( 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.object(mock_app, '_flush_disc_entries_to_project') as mock_flush, patch.object(mock_app, '_switch_discussion') as mock_switch ): # Track calls to ensure we only return 'changed=True' once to avoid loops calls = {"Track Discussion": 0} def checkbox_side_effect(label, value): if label == "Track Discussion": calls[label] += 1 # Only return True for 'changed' on the first call in the test changed = (calls[label] == 1) return changed, True return False, value mock_imgui.checkbox.side_effect = checkbox_side_effect mock_imgui.begin_combo.return_value = False mock_imgui.selectable.return_value = (False, False) mock_imgui.button.return_value = False mock_imgui.collapsing_header.return_value = True # For Discussions header mock_imgui.input_text.side_effect = lambda label, value, **kwargs: (False, value) mock_imgui.input_int.side_effect = lambda label, value, *args, **kwargs: (False, value) mock_imgui.begin_child.return_value = True # Mock clipper to avoid the while loop hang mock_clipper = MagicMock() mock_clipper.step.side_effect = [True, False] mock_clipper.display_start = 0 mock_clipper.display_end = 0 mock_imgui.ListClipper.return_value = mock_clipper mock_app._render_discussion_panel() assert mock_app._track_discussion_active == True mock_flush.assert_called() mock_load.assert_called_with("track-1", ".") assert len(mock_app.disc_entries) == 1 assert mock_app.disc_entries[0]["content"] == "Track Hello" # Now toggle OFF calls["Track Discussion"] = 0 # Reset for next call def checkbox_off_side_effect(label, value): if label == "Track Discussion": calls[label] += 1 return (calls[label] == 1), False return False, value mock_imgui.checkbox.side_effect = checkbox_off_side_effect mock_clipper.step.side_effect = [True, False] # Reset clipper mock_app._render_discussion_panel() assert mock_app._track_discussion_active == False mock_switch.assert_called_with(mock_app.active_discussion) 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('gui_2.project_manager.save_track_state') as mock_save, \ patch('gui_2.project_manager.load_track_state', return_value=None): mock_app._push_mma_state_update() assert len(mock_app.active_track.tickets) == 1 assert mock_app.active_track.tickets[0].id == "T-001" assert mock_save.called args, kwargs = mock_save.call_args assert args[0] == "track-1" state = args[1] assert state.metadata.id == "track-1" assert state.tasks == mock_app.active_track.tickets