feat(gui): implement Phases 2-5 of Comprehensive GUI UX track
- Add cost tracking with new cost_tracker.py module - Enhance Track Proposal modal with editable titles and goals - Add Conductor Setup summary and New Track creation form to MMA Dashboard - Implement Task DAG editing (add/delete tickets) and track-scoped discussion - Add visual polish: color-coded statuses, tinted progress bars, and node indicators - Support live worker streaming from AI providers to GUI panels - Fix numerous integration test regressions and stabilize headless service
This commit is contained in:
177
tests/test_gui_phase4.py
Normal file
177
tests/test_gui_phase4.py
Normal file
@@ -0,0 +1,177 @@
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user