""" Phase 1 of metadata_promotion_20260624. Verifies: 1. self.active_tickets load boundaries convert dicts to Ticket 2. conductor_tech_lead.topological_sort returns list[Ticket] 3. gui_2.py consumer sites use direct field access (not .get()) 4. app_controller.py consumer sites use direct field access (not .get()) """ import inspect from unittest.mock import patch from src.mma import Ticket class TestActiveTicketsType: def test_active_tickets_annotation_is_list_of_ticket(self) -> None: """self.active_tickets type hint must be list[Ticket], not list[Metadata].""" from src.app_controller import AppController src_text = inspect.getsource(AppController.__init__) assert "list[Ticket]" in src_text, ( "AppController.__init__ must declare self.active_tickets: list[Ticket]" ) assert "list[Metadata]" not in src_text.split("self.active_tickets")[1].split("\n")[0], ( "AppController.__init__ must NOT declare self.active_tickets: list[Metadata]" ) class TestActiveTicketsLoadBoundaries: def test_load_at_data_converts_dicts_to_tickets(self) -> None: """_deserialize_active_track_result boundary must wrap dicts as Ticket.""" from src.app_controller import AppController with patch.object(AppController, "load_config", return_value={ 'ai': {'provider': 'gemini', 'model': 'gemini-2.5-flash-lite'}, 'projects': {'paths': [], 'active': ''}, 'gui': {'show_windows': {}}, }), patch.object(AppController, "save_config"), \ patch.object(AppController, "_prune_old_logs"), \ patch.object(AppController, "start_services"), \ patch.object(AppController, "_init_ai_and_hooks"): ctrl = AppController.__new__(AppController) ctrl.__init__() at_data = { "id": "track-x", "title": "Track X", "tickets": [ {"id": "T1", "description": "first", "status": "todo"}, {"id": "T2", "description": "second", "status": "todo"}, ], } ctrl._deserialize_active_track_result(at_data) assert ctrl.active_tickets, "load path should populate active_tickets" for t in ctrl.active_tickets: assert isinstance(t, Ticket), ( f"active_tickets must contain Ticket instances, got {type(t).__name__}: {t!r}" ) def test_load_active_tickets_beads_branch_converts_dicts_to_tickets(self) -> None: """_load_active_tickets (beads branch) must wrap bead dicts as Ticket.""" from src.app_controller import AppController from src.mma import Ticket ctrl = AppController.__new__(AppController) ctrl._last_request_errors = [] ctrl.ui_project_execution_mode = "beads" ctrl.ui_files_base_dir = None class _Bead: def __init__(self, bid: str, title: str, desc: str, status: str) -> None: self.id = bid; self.title = title; self.description = desc; self.status = status with patch.object(AppController, "_load_beads_from_path_result") as mock_load: mock_load.return_value = (lambda: type("R", (), {"ok": True, "data": [ _Bead("B1", "T1", "first", "todo"), _Bead("B2", "T2", "second", "todo") ]})()) ctrl._load_active_tickets() for t in ctrl.active_tickets: assert isinstance(t, Ticket), ( f"beads branch must populate active_tickets with Ticket instances, got {type(t).__name__}" ) class TestTopologicalSortReturnsTicketList: def test_topological_sort_returns_ticket_instances(self) -> None: """conductor_tech_lead.topological_sort must return list[Ticket].""" from src import conductor_tech_lead sig = inspect.signature(conductor_tech_lead.topological_sort) assert sig.return_annotation is not inspect.Signature.empty assert "Ticket" in str(sig.return_annotation), ( f"topological_sort return annotation must reference Ticket, got {sig.return_annotation}" ) class TestGuiConsumersDirectFieldAccess: def test_reorder_ticket_uses_direct_field_access(self) -> None: """gui_2.App._reorder_ticket must use t.id / t.depends_on (not .get()).""" import inspect from src import gui_2 src = inspect.getsource(gui_2.App._reorder_ticket) assert "t.get(" not in src, ( "_reorder_ticket must not call t.get() — use t.id and t.depends_on directly" ) def test_bulk_execute_uses_direct_field_access(self) -> None: """gui_2.App.bulk_execute must use t.id (not .get()).""" import inspect from src import gui_2 src = inspect.getsource(gui_2.App.bulk_execute) assert "t.get(" not in src, ( "bulk_execute must not call t.get() — use t.id directly" ) def test_bulk_skip_uses_direct_field_access(self) -> None: """gui_2.App.bulk_skip must use t.id (not .get()).""" import inspect from src import gui_2 src = inspect.getsource(gui_2.App.bulk_skip) assert "t.get(" not in src, ( "bulk_skip must not call t.get() — use t.id directly" ) def test_bulk_block_uses_direct_field_access(self) -> None: """gui_2.App.bulk_block must use t.id (not .get()).""" import inspect from src import gui_2 src = inspect.getsource(gui_2.App.bulk_block) assert "t.get(" not in src, ( "bulk_block must not call t.get() — use t.id directly" ) def test_cb_block_ticket_uses_direct_field_access(self) -> None: """gui_2.App._cb_block_ticket must use direct field access (not .get()).""" import inspect from src import gui_2 src = inspect.getsource(gui_2.App._cb_block_ticket) assert "t.get(" not in src, ( "_cb_block_ticket must not call t.get() — use direct field access" ) def test_cb_unblock_ticket_uses_direct_field_access(self) -> None: """gui_2.App._cb_unblock_ticket must use direct field access (not .get()).""" import inspect from src import gui_2 src = inspect.getsource(gui_2.App._cb_unblock_ticket) assert "t.get(" not in src, ( "_cb_unblock_ticket must not call t.get() — use direct field access" ) def test_dag_cycle_check_uses_direct_field_access(self) -> None: """gui_2._dag_cycle_check_result must use t.id / t.depends_on (not .get()).""" import inspect from src import gui_2 src = inspect.getsource(gui_2._dag_cycle_check_result) assert "t.get(" not in src, ( "_dag_cycle_check_result must not call t.get() — use t.id and t.depends_on directly" ) class TestAppControllerConsumersDirectFieldAccess: def test_cb_ticket_retry_uses_direct_field_access(self) -> None: """app_controller._cb_ticket_retry must use t.id (not .get()).""" import inspect from src import app_controller src = inspect.getsource(app_controller.AppController._cb_ticket_retry) assert "t.get(" not in src, ( "_cb_ticket_retry must not call t.get() — use t.id directly" ) def test_cb_ticket_skip_uses_direct_field_access(self) -> None: """app_controller._cb_ticket_skip must use t.id (not .get()).""" import inspect from src import app_controller src = inspect.getsource(app_controller.AppController._cb_ticket_skip) assert "t.get(" not in src, ( "_cb_ticket_skip must not call t.get() — use t.id directly" ) def test_approve_ticket_uses_direct_field_access(self) -> None: """app_controller.approve_ticket must use t.id (not .get()).""" import inspect from src import app_controller src = inspect.getsource(app_controller.AppController.approve_ticket) assert "t.get(" not in src, ( "approve_ticket must not call t.get() — use t.id directly" ) def test_mutate_dag_uses_direct_field_access(self) -> None: """app_controller.mutate_dag must use t.id and t.depends_on (not .get()).""" import inspect from src import app_controller src = inspect.getsource(app_controller.AppController.mutate_dag) assert "t.get(" not in src, ( "mutate_dag must not call t.get() — use t.id and t.depends_on directly" )