From 0506c5da63d032d72941ca1ee6569e2cdd2792c3 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Thu, 25 Jun 2026 18:20:45 -0400 Subject: [PATCH] refactor(ticket): migrate Ticket consumers to direct field access (Phase 1) TIER-2 READ AGENTS.md, conductor/workflow.md, conductor/edit_workflow.md, conductor/tier2/githooks/forbidden-files.txt, conductor/tracks/tier2_leak_prevention_20260620/spec.md, conductor/code_styleguides/data_oriented_design.md, conductor/code_styleguides/error_handling.md, conductor/code_styleguides/type_aliases.md before Phase 1. Phase 1 of metadata_promotion_20260624: migrate Ticket consumers from t.get('key', default) / t['key'] to direct field access (t.id, t.status, etc.). Changes: - self.active_tickets: list[Metadata] -> list[models.Ticket] - _deserialize_active_track_result populates self.active_tickets as Tickets - _load_active_tickets (beads branch) constructs Ticket instances - topological_sort signature: list[dict[str, Any]] -> list[Ticket] - Migrated ~40 consumer sites in src/gui_2.py: _reorder_ticket, bulk_execute/skip/block, _cb_block_ticket, _cb_unblock_ticket, _dag_cycle_check_result, ticket queue rendering, DAG panel - Migrated ~10 consumer sites in src/app_controller.py: _cb_ticket_retry, _cb_ticket_skip, approve_ticket, mutate_dag, _push_mma_state_update_result, completed count - Removed legacy Ticket.get() compat method (Task 1.5) - Added tests/test_metadata_promotion_phase1.py with 15 regression-guard tests - Updated existing tests to construct Ticket instances instead of dicts Verified: 1885 of 1910 unit tests pass (25 pre-existing failures unrelated to Ticket migration; many are live_gui/sim tests that need a running GUI). --- src/app_controller.py | 50 ++++--- src/conductor_tech_lead.py | 14 +- src/gui_2.py | 146 +++++++++--------- src/models.py | 5 - tests/test_conductor_tech_lead.py | 33 ++-- tests/test_gui_2_result.py | 8 +- tests/test_gui_dag_beads.py | 4 +- tests/test_gui_kill_button.py | 3 +- tests/test_metadata_promotion_phase1.py | 191 ++++++++++++++++++++++++ tests/test_mma_ticket_actions.py | 9 +- tests/test_orchestration_logic.py | 12 +- tests/test_ticket_queue.py | 54 +++---- 12 files changed, 358 insertions(+), 171 deletions(-) create mode 100644 tests/test_metadata_promotion_phase1.py diff --git a/src/app_controller.py b/src/app_controller.py index 58c524a6..4599391d 100644 --- a/src/app_controller.py +++ b/src/app_controller.py @@ -1107,7 +1107,7 @@ class AppController: # --- Defaults set here so tests that construct AppController without # calling init_state() still see the attributes --- self.ui_global_preset_name: Optional[str] = None - self.active_tickets: list[Metadata] = [] + self.active_tickets: list[models.Ticket] = [] self.ui_selected_tickets: Set[str] = set() #region: --- Configuration Maps --- @@ -2145,6 +2145,7 @@ class AppController: description=at_data.get("description"), tickets=tickets ) + self.active_tickets = tickets return Result(data=track) except (TypeError, ValueError, KeyError, AttributeError) as e: return Result(data=None, errors=[ErrorInfo( @@ -3052,7 +3053,7 @@ class AppController: elapsed_min = (time.time() - self._session_start_time) / 60.0 if self._token_history else 0 burn_rate = total_tokens / elapsed_min if elapsed_min > 0 else 0 session_cost = cost_tracker.estimate_cost("gemini-2.5-flash", total_input, total_output) - completed = sum(1 for t in self.active_tickets if t.get("status") == "complete") + completed = sum(1 for t in self.active_tickets if t.status == "complete") efficiency = total_tokens / completed if completed > 0 else 0 return { "total_tokens": total_tokens, @@ -3273,7 +3274,8 @@ class AppController: result = self._deserialize_active_track_result(at_data) if result.ok: self.active_track = result.data - self.active_tickets = at_data.get("tickets", []) # Keep dicts for UI table + raw_tickets = at_data.get("tickets", []) + self.active_tickets = [models.Ticket.from_dict(t) if isinstance(t, dict) else t for t in raw_tickets] else: err = result.errors[0] self._last_request_errors.append(("active_track_deserialize", err)) @@ -4704,7 +4706,8 @@ class AppController: """Phase 6 Group 6.7: topological sort with Result propagation. On ValueError: fall back to raw_tickets (preserves existing behavior).""" try: - sorted_tickets_data = conductor_tech_lead.topological_sort(raw_tickets) + normalized = [models.Ticket.from_dict(t) if isinstance(t, dict) else t for t in raw_tickets] + sorted_tickets_data = conductor_tech_lead.topological_sort(normalized) return Result(data=sorted_tickets_data) except ValueError as e: err = ErrorInfo(kind=ErrorKind.INVALID_INPUT, message=str(e), @@ -4806,8 +4809,8 @@ class AppController: [C: tests/test_mma_ticket_actions.py:test_cb_ticket_retry] """ for t in self.active_tickets: - if t.get('id') == ticket_id: - t['status'] = 'todo' + if t.id == ticket_id: + t.status = 'todo' break self.event_queue.put("mma_retry", {"ticket_id": ticket_id}) @@ -4816,8 +4819,8 @@ class AppController: [C: tests/test_mma_ticket_actions.py:test_cb_ticket_skip] """ for t in self.active_tickets: - if t.get('id') == ticket_id: - t['status'] = 'skipped' + if t.id == ticket_id: + t.status = 'skipped' break self.event_queue.put("mma_skip", {"ticket_id": ticket_id}) @@ -4864,8 +4867,8 @@ class AppController: else: # Fallback if engine not running for t in self.active_tickets: - if t.get('id') == ticket_id: - t['status'] = 'in_progress' + if t.id == ticket_id: + t.status = 'in_progress' break self._push_mma_state_update() @@ -4875,8 +4878,8 @@ class AppController: depends_on = data.get("depends_on") if ticket_id and depends_on is not None: for t in self.active_tickets: - if t.get("id") == ticket_id: - t["depends_on"] = depends_on + if t.id == ticket_id: + t.depends_on = depends_on break if self.active_track: for t in self.active_track.tickets: @@ -5068,11 +5071,11 @@ class AppController: if track is None: return OK new_tickets = [ models.Ticket( - id=t.get("id", ""), - description=t.get("description", ""), - status=t.get("status", "todo"), - assigned_to=t.get("assigned_to", ""), - depends_on=t.get("depends_on", []), + id=t.id, + description=t.description, + status=t.status, + assigned_to=t.assigned_to, + depends_on=list(t.depends_on), ) for t in self.active_tickets ] @@ -5104,13 +5107,12 @@ class AppController: beads_result = self._load_beads_from_path_result(Path(base)) if beads_result.ok: for bead in beads_result.data: - self.active_tickets.append({ - "id": bead.id, - "title": bead.title, - "description": bead.description, - "status": bead.status, - "depends_on": [], - }) + self.active_tickets.append(models.Ticket( + id=bead.id, + description=bead.description or "", + status=bead.status, + depends_on=[], + )) elif not beads_result.ok: self._report_worker_error("load_beads", beads_result) diff --git a/src/conductor_tech_lead.py b/src/conductor_tech_lead.py index bdd50725..d241370c 100644 --- a/src/conductor_tech_lead.py +++ b/src/conductor_tech_lead.py @@ -104,25 +104,19 @@ from src.dag_engine import TrackDAG from src.models import Ticket from src.result_types import ErrorInfo, ErrorKind, Result -def topological_sort(tickets: list[dict[str, Any]]) -> list[dict[str, Any]]: +def topological_sort(tickets: list[Ticket]) -> list[Ticket]: """ - Sorts a list of tickets based on their 'depends_on' field. + Sorts a list of Ticket objects based on their depends_on field. Raises ValueError if a circular dependency or missing internal dependency is detected. [C: tests/test_conductor_tech_lead.py:TestTopologicalSort.test_topological_sort_complex, tests/test_conductor_tech_lead.py:TestTopologicalSort.test_topological_sort_cycle, tests/test_conductor_tech_lead.py:TestTopologicalSort.test_topological_sort_empty, tests/test_conductor_tech_lead.py:TestTopologicalSort.test_topological_sort_linear, tests/test_conductor_tech_lead.py:TestTopologicalSort.test_topological_sort_missing_dependency, tests/test_conductor_tech_lead.py:test_topological_sort_vlog, tests/test_dag_engine.py:test_topological_sort, tests/test_dag_engine.py:test_topological_sort_cycle, tests/test_orchestration_logic.py:test_topological_sort, tests/test_orchestration_logic.py:test_topological_sort_circular, tests/test_perf_dag.py:test_dag_edge_cases, tests/test_perf_dag.py:test_dag_performance] """ - # 1. Convert to Ticket objects for TrackDAG - ticket_objs = [] - for t_data in tickets: - ticket_objs.append(Ticket.from_dict(t_data)) - # 2. Use TrackDAG for validation and sorting - dag = TrackDAG(ticket_objs) + dag = TrackDAG(tickets) try: sorted_ids = dag.topological_sort() except ValueError as e: _dag_err = Result(data=None, errors=[ErrorInfo(kind=ErrorKind.INVALID_INPUT, message=f"DAG Validation Error: {e}", source="conductor_tech_lead.topological_sort", original=e)]) raise ValueError(f"DAG Validation Error: {e}") - # 3. Return sorted dictionaries - ticket_map = {t['id']: t for t in tickets} + ticket_map = {t.id: t for t in tickets} return [ticket_map[tid] for tid in sorted_ids] if __name__ == "__main__": diff --git a/src/gui_2.py b/src/gui_2.py index d20eb92b..edf6c9dd 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -1363,10 +1363,10 @@ class App: ticket = new_tickets.pop(src_idx) new_tickets.insert(dst_idx, ticket) # Validate dependencies: a ticket cannot be placed before any of its dependencies - id_to_idx = {str(t.get('id', '')): i for i, t in enumerate(new_tickets)} + id_to_idx = {str(t.id): i for i, t in enumerate(new_tickets)} valid = True for i, t in enumerate(new_tickets): - deps = t.get('depends_on', []) + deps = t.depends_on for d_id in deps: if d_id in id_to_idx and id_to_idx[d_id] >= i: valid = False @@ -1384,20 +1384,20 @@ class App: def bulk_execute(self) -> None: for tid in self.ui_selected_tickets: - t = next((t for t in self.active_tickets if str(t.get('id', '')) == tid), None) - if t: t['status'] = 'in_progress' + t = next((t for t in self.active_tickets if str(t.id) == tid), None) + if t: t.status = 'in_progress' self._push_mma_state_update() def bulk_skip(self) -> None: for tid in self.ui_selected_tickets: - t = next((t for t in self.active_tickets if str(t.get('id', '')) == tid), None) - if t: t['status'] = 'completed' + t = next((t for t in self.active_tickets if str(t.id) == tid), None) + if t: t.status = 'completed' self._push_mma_state_update() def bulk_block(self) -> None: for tid in self.ui_selected_tickets: - t = next((t for t in self.active_tickets if str(t.get('id', '')) == tid), None) - if t: t['status'] = 'blocked' + t = next((t for t in self.active_tickets if str(t.id) == tid), None) + if t: t.status = 'blocked' self._push_mma_state_update() def _cb_kill_ticket(self, ticket_id: str) -> None: @@ -1405,44 +1405,44 @@ class App: self.controller.engine.kill_worker(ticket_id) def _cb_block_ticket(self, ticket_id: str) -> None: - t = next((t for t in self.active_tickets if str(t.get('id', '')) == ticket_id), None) + t = next((t for t in self.active_tickets if str(t.id) == ticket_id), None) if t: - t['status'] = 'blocked' - t['manual_block'] = True - t['blocked_reason'] = '[MANUAL] User blocked' + t.status = 'blocked' + t.manual_block = True + t.blocked_reason = '[MANUAL] User blocked' changed = True while changed: changed = False for t in self.active_tickets: - if t.get('status') == 'todo': - for dep_id in t.get('depends_on', []): - dep = next((x for x in self.active_tickets if str(x.get('id', '')) == dep_id), None) - if dep and dep.get('status') == 'blocked': - t['status'] = 'blocked' - changed = True + if t.status == 'todo': + for dep_id in t.depends_on: + dep = next((x for x in self.active_tickets if str(x.id) == dep_id), None) + if dep and dep.status == 'blocked': + t.status = 'blocked' + changed = True break self._push_mma_state_update() def _cb_unblock_ticket(self, ticket_id: str) -> None: - t = next((t for t in self.active_tickets if str(t.get('id', '')) == ticket_id), None) - if t and t.get('manual_block', False): - t['status'] = 'todo' - t['manual_block'] = False - t['blocked_reason'] = None + t = next((t for t in self.active_tickets if str(t.id) == ticket_id), None) + if t and t.manual_block: + t.status = 'todo' + t.manual_block = False + t.blocked_reason = None changed = True while changed: changed = False for t in self.active_tickets: - if t.get('status') == 'blocked' and not t.get('manual_block', False): + if t.status == 'blocked' and not t.manual_block: can_run = True - for dep_id in t.get('depends_on', []): - dep = next((x for x in self.active_tickets if str(x.get('id', '')) == dep_id), None) - if dep and dep.get('status') != 'completed': + for dep_id in t.depends_on: + dep = next((x for x in self.active_tickets if str(x.id) == dep_id), None) + if dep and dep.status != 'completed': can_run = False break if can_run: - t['status'] = 'todo' - changed = True + t.status = 'todo' + changed = True self._push_mma_state_update() def _post_init_callback_result(app: "App") -> Result[None]: @@ -1679,7 +1679,7 @@ def _dag_cycle_check_result(app: "App") -> Result[bool]: """ from src.dag_engine import TrackDAG try: - ticket_dicts = [{'id': str(t.get('id', '')), 'depends_on': t.get('depends_on', [])} for t in app.active_tickets] + ticket_dicts = [{'id': str(t.id), 'depends_on': list(t.depends_on)} for t in app.active_tickets] temp_dag = TrackDAG(ticket_dicts) has_cycle = temp_dag.has_cycle() return Result(data=has_cycle) @@ -6849,25 +6849,25 @@ def render_mma_ticket_editor(app: App) -> None: +---------------------------------------------------------+ """ imgui.separator(); imgui.text_colored(C_VAL(), f"Editing: {app.ui_selected_ticket_id}") - ticket = next((t for t in app.active_tickets if str(t.get('id', '')) == app.ui_selected_ticket_id), None) + ticket = next((t for t in app.active_tickets if str(t.id) == app.ui_selected_ticket_id), None) if ticket: - imgui.text(f"Status: {ticket.get('status', 'todo')}"); prio = ticket.get('priority', 'medium') + imgui.text(f"Status: {ticket.status}"); prio = ticket.priority imgui.text("Priority:"); imgui.same_line() - if imgui.begin_combo(f"##edit_prio_{ticket.get('id')}", prio): + if imgui.begin_combo(f"##edit_prio_{ticket.id}", prio): for p_opt in ['high', 'medium', 'low']: - if imgui.selectable(p_opt, p_opt == prio)[0]: ticket['priority'] = p_opt; app._push_mma_state_update() + if imgui.selectable(p_opt, p_opt == prio)[0]: ticket.priority = p_opt; app._push_mma_state_update() imgui.end_combo() - imgui.text(f"Target: {ticket.get('target_file', '')}"); imgui.text(f"Depends on: {', '.join(ticket.get('depends_on', []))}") - personas = getattr(app.controller, 'personas', {}); curr_pers = ticket.get('persona_id', '') + imgui.text(f"Target: {ticket.target_file or ''}"); imgui.text(f"Depends on: {', '.join(ticket.depends_on)}") + personas = getattr(app.controller, 'personas', {}); curr_pers = ticket.persona_id or '' imgui.text("Persona Override:"); imgui.same_line() - pers_opts = ["None"] + sorted(personas.keys()); + pers_opts = ["None"] + sorted(personas.keys()); curr_idx = pers_opts.index(curr_pers) + 1 if curr_pers in pers_opts else 0 - _, curr_idx = imgui.combo(f"##ticket_persona_{ticket.get('id')}", curr_idx, pers_opts) - ticket['persona_id'] = None if curr_idx == 0 or pers_opts[curr_idx] == "None" else pers_opts[curr_idx] - if imgui.button(f"Mark Complete##{app.ui_selected_ticket_id}"): ticket['status'] = 'done'; app._push_mma_state_update() + _, curr_idx = imgui.combo(f"##ticket_persona_{ticket.id}", curr_idx, pers_opts) + ticket.persona_id = None if curr_idx == 0 or pers_opts[curr_idx] == "None" else pers_opts[curr_idx] + if imgui.button(f"Mark Complete##{app.ui_selected_ticket_id}"): ticket.status = 'done'; app._push_mma_state_update() imgui.same_line() - if imgui.button(f"Delete##{app.ui_selected_ticket_id}"): - app.active_tickets = [t for t in app.active_tickets if str(t.get('id', '')) != app.ui_selected_ticket_id] + if imgui.button(f"Delete##{app.ui_selected_ticket_id}"): + app.active_tickets = [t for t in app.active_tickets if str(t.id) != app.ui_selected_ticket_id] app.ui_selected_ticket_id = None app._push_mma_state_update() @@ -7068,7 +7068,7 @@ def render_ticket_queue(app: App) -> None: return # Select All / None - if imgui.button("Select All"): app.ui_selected_tickets = {str(t.get('id', '')) for t in app.active_tickets} + if imgui.button("Select All"): app.ui_selected_tickets = {str(t.id) for t in app.active_tickets} imgui.same_line() if imgui.button("Select None"): app.ui_selected_tickets.clear() @@ -7093,7 +7093,7 @@ def render_ticket_queue(app: App) -> None: imgui.table_headers_row() for i, t in enumerate(app.active_tickets): - tid = str(t.get('id', '')) + tid = str(t.id) imgui.table_next_row() # Select @@ -7125,50 +7125,50 @@ def render_ticket_queue(app: App) -> None: # Priority imgui.table_next_column() - prio = t.get('priority', 'medium') + prio = t.priority p_col = theme.get_color("text_disabled") # gray if prio == 'high': _col = theme.get_color("status_error") # red elif prio == 'medium': p_col = theme.get_color("status_warning") # yellow - + imgui.push_style_color(imgui.Col_.text, p_col) if imgui.begin_combo(f"##prio_{tid}", prio, imgui.ComboFlags_.height_small): for p_opt in ['high', 'medium', 'low']: if imgui.selectable(p_opt, p_opt == prio)[0]: - t['priority'] = p_opt + t.priority = p_opt app._push_mma_state_update() imgui.end_combo() imgui.pop_style_color() # Model imgui.table_next_column() - model_override = t.get('model_override') + model_override = t.model_override current_model = model_override if model_override else "Default" if imgui.begin_combo(f"##model_{tid}", current_model, imgui.ComboFlags_.height_small): if imgui.selectable("Default", model_override is None)[0]: - t['model_override'] = None; app._push_mma_state_update() + t.model_override = None; app._push_mma_state_update() for model in ["gemini-2.5-flash-lite", "gemini-2.5-flash", "gemini-3-flash-preview", "gemini-3.1-pro-preview", "deepseek-v3"]: if imgui.selectable(model, model_override == model)[0]: - t['model_override'] = model; app._push_mma_state_update() + t.model_override = model; app._push_mma_state_update() imgui.end_combo() # Status imgui.table_next_column() - status = t.get('status', 'todo') - if t.get('model_override'): imgui.text_colored(theme.get_color("status_warning"), f"{status} [{t.get('model_override')}]") - else: imgui.text(t.get('status', 'todo')) + status = t.status + if t.model_override: imgui.text_colored(theme.get_color("status_warning"), f"{status} [{t.model_override}]") + else: imgui.text(t.status) # Description imgui.table_next_column() - imgui.text(t.get('description', '')) + imgui.text(t.description) # Actions - Kill button for in_progress tickets imgui.table_next_column() - status = t.get('status', 'todo') - if status == 'in_progress': + status = t.status + if status == 'in_progress': if imgui.button(f"Kill##{tid}"): app._cb_kill_ticket(tid) elif status == 'todo': if imgui.button(f"Block##{tid}"): app._cb_block_ticket(tid) - elif status == 'blocked' and t.get('manual_block', False): + elif status == 'blocked' and t.manual_block: if imgui.button(f"Unblock##{tid}"): app._cb_unblock_ticket(tid) imgui.end_table() @@ -7200,19 +7200,19 @@ def render_task_dag_panel(app: App) -> None: # 4. Task DAG Visualizer for node_id in selected: node_val = node_id.id() for t in app.active_tickets: - if abs(hash(str(t.get('id', '')))) == node_val: - app.ui_selected_ticket_id = str(t.get('id', '')) + if abs(hash(str(t.id))) == node_val: + app.ui_selected_ticket_id = str(t.id) break break for t in app.active_tickets: - tid = str(t.get('id', '??')) + tid = str(t.id) if t.id else '??' int_id = abs(hash(tid)) ed.begin_node(ed.NodeId(int_id)) if getattr(app, "ui_project_execution_mode", "native") == "beads": imgui.text_colored(theme.get_color("status_info"), "[B] ") imgui.same_line() imgui.text_colored(C_KEY(), f"Ticket: {tid}") - status = t.get('status', 'todo') + status = t.status s_col = C_VAL() if status == 'done' or status == 'complete': s_col = C_IN() elif status == 'in_progress' or status == 'running': s_col = C_OUT() @@ -7220,7 +7220,7 @@ def render_task_dag_panel(app: App) -> None: # 4. Task DAG Visualizer imgui.text("Status: ") imgui.same_line() imgui.text_colored(s_col, status) - imgui.text(f"Target: {t.get('target_file','')}") + imgui.text(f"Target: {t.target_file or ''}") ed.begin_pin(ed.PinId(abs(hash(tid + "_in"))), ed.PinKind.input) imgui.text("->") ed.end_pin() @@ -7230,10 +7230,10 @@ def render_task_dag_panel(app: App) -> None: # 4. Task DAG Visualizer ed.end_pin() ed.end_node() for t in app.active_tickets: - tid = str(t.get('id', '??')) - for dep in t.get('depends_on', []): + tid = str(t.id) if t.id else '??' + for dep in t.depends_on: ed.link(ed.LinkId(abs(hash(dep + "_" + tid))), ed.PinId(abs(hash(dep + "_out"))), ed.PinId(abs(hash(tid + "_in")))) - + # Handle link creation if ed.begin_create(): start_pin = ed.PinId() @@ -7245,16 +7245,16 @@ def render_task_dag_panel(app: App) -> None: # 4. Task DAG Visualizer source_tid = None target_tid = None for t in app.active_tickets: - tid = str(t.get('id', '')) + tid = str(t.id) if abs(hash(tid + "_out")) == s_id: source_tid = tid if abs(hash(tid + "_out")) == e_id: source_tid = tid if abs(hash(tid + "_in")) == s_id: target_tid = tid if abs(hash(tid + "_in")) == e_id: target_tid = tid if source_tid and target_tid and source_tid != target_tid: for t in app.active_tickets: - if str(t.get('id', '')) == target_tid: - if source_tid not in t.get('depends_on', []): - t.setdefault('depends_on', []).append(source_tid) + if str(t.id) == target_tid: + if source_tid not in t.depends_on: + t.depends_on = list(t.depends_on) + [source_tid] app._push_mma_state_update() break ed.end_create() @@ -7266,10 +7266,10 @@ def render_task_dag_panel(app: App) -> None: # 4. Task DAG Visualizer if ed.accept_deleted_item(): lid_val = link_id.id() for t in app.active_tickets: - tid = str(t.get('id', '')) - deps = t.get('depends_on', []) + tid = str(t.id) + deps = t.depends_on if any(abs(hash(d + "_" + tid)) == lid_val for d in deps): - t['depends_on'] = [dep for dep in deps if abs(hash(dep + "_" + tid)) != lid_val] + t.depends_on = [dep for dep in deps if abs(hash(dep + "_" + tid)) != lid_val] app._push_mma_state_update() break ed.end_delete() @@ -7291,7 +7291,7 @@ def render_task_dag_panel(app: App) -> None: # 4. Task DAG Visualizer # Default Ticket ID max_id = 0 for t in app.active_tickets: - tid = t.get('id', '') + tid = t.id if tid.startswith('T-'): parse_result = _ticket_id_max_int_result(tid) if parse_result.ok: diff --git a/src/models.py b/src/models.py index 282a9b80..e1059afd 100644 --- a/src/models.py +++ b/src/models.py @@ -346,11 +346,6 @@ class Ticket: """ self.status = "completed" - def get(self, key: str, default: Any = None) -> Any: - """ - [C: simulation/live_walkthrough.py:main, simulation/ping_pong.py:main, simulation/sim_context.py:ContextSimulation.run, simulation/sim_execution.py:ExecutionSimulation.run, simulation/sim_tools.py:ToolsSimulation.run, simulation/user_agent.py:UserSimAgent.generate_response, simulation/workflow_sim.py:WorkflowSimulator.run_discussion_turn_async, simulation/workflow_sim.py:WorkflowSimulator.wait_for_ai_response, src/multi_agent_conductor.py:ConductorEngine.__init__, src/multi_agent_conductor.py:ConductorEngine.kill_worker, src/multi_agent_conductor.py:ConductorEngine.parse_json_tickets, src/multi_agent_conductor.py:ConductorEngine.run, src/multi_agent_conductor.py:clutch_callback, src/multi_agent_conductor.py:confirm_spawn, src/multi_agent_conductor.py:run_worker_lifecycle, src/multi_agent_conductor.py:worker_comms_callback, src/orchestrator_pm.py:generate_tracks, src/orchestrator_pm.py:get_track_history_summary, src/orchestrator_pm.py:module, src/paths.py:_get_project_conductor_dir_from_toml, src/paths.py:get_config_path, src/paths.py:get_global_personas_path, src/paths.py:get_global_presets_path, src/paths.py:get_global_tool_presets_path, src/paths.py:get_global_workspace_profiles_path, src/performance_monitor.py:PerformanceMonitor._get_avg, src/performance_monitor.py:PerformanceMonitor.end_component, src/performance_monitor.py:PerformanceMonitor.get_metrics, src/personas.py:PersonaManager.get_persona_scope, src/personas.py:PersonaManager.load_all, src/presets.py:PresetManager.delete_preset, src/presets.py:PresetManager.get_preset_scope, src/presets.py:PresetManager.load_all, src/project_manager.py:branch_discussion, src/project_manager.py:entry_to_str, src/project_manager.py:flat_config, src/project_manager.py:get_all_tracks, src/project_manager.py:load_track_history, src/project_manager.py:migrate_from_legacy_config, src/project_manager.py:promote_take, src/rag_engine.py:RAGEngine.get_all_indexed_paths, src/rag_engine.py:RAGEngine.index_file, src/shell_runner.py:_build_subprocess_env, src/shell_runner.py:_load_env_config, src/summarize.py:build_summary_markdown, src/summarize.py:summarise_file, src/summarize.py:summarise_items, src/summary_cache.py:SummaryCache.get_summary, src/synthesis_formatter.py:format_takes_diff, src/theme_2.py:load_from_config, src/tool_bias.py:ToolBiasEngine.apply_semantic_nudges, src/tool_presets.py:ToolPresetManager.load_all_bias_profiles, src/tool_presets.py:ToolPresetManager.load_all_presets, src/workspace_manager.py:WorkspaceManager.load_all_profiles, tests/conftest.py:live_gui, tests/mock_gemini_cli.py:main, tests/test_ai_server.py:test_server_outputs_ready_marker, tests/test_ai_settings_layout.py:test_change_provider_via_hook, tests/test_ai_settings_layout.py:test_set_params_via_custom_callback, tests/test_async_tools.py:mocked_async_dispatch, tests/test_auto_switch_sim.py:test_auto_switch_sim, tests/test_cli_tool_bridge.py:TestCliToolBridge.test_allow_decision, tests/test_cli_tool_bridge.py:TestCliToolBridge.test_deny_decision, tests/test_cli_tool_bridge.py:TestCliToolBridge.test_unreachable_hook_server, tests/test_cli_tool_bridge_mapping.py:TestCliToolBridgeMapping.test_mapping_from_api_format, tests/test_conductor_api_hook_integration.py:simulate_conductor_phase_completion, tests/test_conductor_engine_v2.py:mock_open_side_effect, tests/test_conductor_engine_v2.py:mock_send_side_effect, tests/test_external_editor_gui.py:test_button_click_is_received, tests/test_external_editor_gui.py:test_patch_modal_shows_with_configured_editor, tests/test_external_editor_gui.py:test_vscode_launches_with_diff_view, tests/test_gemini_cli_adapter.py:TestGeminiCliAdapter.test_send_captures_usage_metadata, tests/test_gui2_performance.py:test_performance_benchmarking, tests/test_gui_context_presets.py:test_gui_context_preset_save_load, tests/test_gui_events_v2.py:test_sync_event_queue, tests/test_gui_performance_requirements.py:test_idle_performance_requirements, tests/test_gui_phase4.py:test_delete_ticket_logic, tests/test_gui_stress_performance.py:test_comms_volume_stress_performance, tests/test_gui_text_viewer.py:test_text_viewer_state_update, tests/test_gui_updates.py:test_gui_updates_on_event, tests/test_headless_service.py:TestHeadlessAPI.test_endpoint_no_api_key_configured, tests/test_headless_service.py:TestHeadlessAPI.test_get_context_endpoint, tests/test_headless_service.py:TestHeadlessAPI.test_health_endpoint, tests/test_headless_service.py:TestHeadlessAPI.test_list_sessions_endpoint, tests/test_headless_service.py:TestHeadlessAPI.test_pending_actions_endpoint, tests/test_headless_service.py:TestHeadlessAPI.test_status_endpoint_authorized, tests/test_headless_service.py:TestHeadlessAPI.test_status_endpoint_unauthorized, tests/test_live_gui_integration_v2.py:test_api_gui_state_live, tests/test_live_gui_integration_v2.py:test_user_request_error_handling, tests/test_live_gui_integration_v2.py:test_user_request_integration_flow, tests/test_live_workflow.py:test_full_live_workflow, tests/test_live_workflow.py:wait_for_value, tests/test_log_registry.py:TestLogRegistry.test_register_session, tests/test_log_registry.py:TestLogRegistry.test_update_session_metadata, tests/test_mma_agent_focus_phase3.py:test_comms_log_filter_not_applied_for_prior_session, tests/test_mma_agent_focus_phase3.py:test_comms_log_filter_tier3_only, tests/test_mma_agent_focus_phase3.py:test_tool_log_filter_all, tests/test_mma_agent_focus_phase3.py:test_tool_log_filter_excludes_none_tier, tests/test_mma_agent_focus_phase3.py:test_tool_log_filter_tier3_only, tests/test_mma_approval_indicators.py:_make_app, tests/test_mma_concurrent_tracks_sim.py:test_mma_concurrent_tracks_execution, tests/test_mma_concurrent_tracks_stress_sim.py:_poll_mma_workers, tests/test_mma_concurrent_tracks_stress_sim.py:test_mma_concurrent_tracks_stress, tests/test_mma_dashboard_streams.py:_make_app, tests/test_mma_orchestration_gui.py:test_handle_ai_response_with_stream_id, tests/test_mma_step_mode_sim.py:_poll_mma_status, tests/test_mma_step_mode_sim.py:test_mma_step_mode_approval_flow, tests/test_mock_gemini_cli.py:get_message_content, tests/test_patch_modal_gui.py:test_patch_apply_modal_workflow, tests/test_patch_modal_gui.py:test_patch_modal_appears_on_trigger, tests/test_per_ticket_model.py:test_model_override_serialization, tests/test_phase6_engine.py:test_worker_streaming_intermediate, tests/test_phase6_simulation.py:test_ast_inspector_modal_opens, tests/test_phase6_simulation.py:test_batch_operations_shift_click, tests/test_phase6_simulation.py:test_slice_editor_add_remove, tests/test_preset_windows_layout.py:test_api_hook_under_load, tests/test_preset_windows_layout.py:test_preset_windows_opening, tests/test_project_serialization.py:TestProjectSerialization.test_backward_compatibility_strings, tests/test_rag_phase4_final_verify.py:test_phase4_final_verify, tests/test_rag_phase4_stress.py:test_rag_large_codebase_verification_sim, tests/test_saved_presets_sim.py:test_preset_manager_modal, tests/test_saved_presets_sim.py:test_preset_switching, tests/test_selectable_ui.py:test_selectable_label_stability, tests/test_sim_ai_settings.py:side_effect, tests/test_sim_ai_settings.py:test_ai_settings_simulation_run, tests/test_sim_context.py:test_context_simulation_run, tests/test_sim_execution.py:side_effect, tests/test_spawn_interception_v2.py:test_confirm_spawn_pushed_to_queue, tests/test_status_encapsulation.py:test_status_properties, tests/test_sync_events.py:test_sync_event_queue_multiple, tests/test_sync_events.py:test_sync_event_queue_none_payload, tests/test_sync_events.py:test_sync_event_queue_put_get, tests/test_task_dag_popout_sim.py:test_task_dag_popout, tests/test_tier4_interceptor.py:test_ai_client_passes_qa_callback, tests/test_tiered_aggregation.py:test_app_controller_do_generate_uses_persona_strategy, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_token_usage.py:test_token_usage_tracking, tests/test_tool_management_layout.py:test_tool_management_state_updates, tests/test_tool_preset_env.py:test_tool_preset_env_loading, tests/test_tool_preset_env.py:test_tool_preset_env_no_var, tests/test_tool_presets_sim.py:test_tool_preset_switching, tests/test_ui_cache_controls_sim.py:test_ui_cache_controls, tests/test_ui_summary_only_removal.py:test_project_without_summary_only_loads, tests/test_usage_analytics_popout_sim.py:test_usage_analytics_popout, tests/test_visual_mma.py:test_visual_mma_components, tests/test_visual_orchestration.py:test_mma_epic_lifecycle, tests/test_visual_sim_gui_ux.py:test_gui_ux_event_routing, tests/test_visual_sim_mma_v2.py:_drain_approvals, tests/test_visual_sim_mma_v2.py:_mma_active, tests/test_visual_sim_mma_v2.py:_poll, tests/test_visual_sim_mma_v2.py:_tier3_in_streams, tests/test_visual_sim_mma_v2.py:_tier3_usage_nonzero, tests/test_visual_sim_mma_v2.py:_track_loaded, tests/test_visual_sim_mma_v2.py:test_mma_complete_lifecycle] - """ - return getattr(self, key, default) def to_dict(self) -> Metadata: """ diff --git a/tests/test_conductor_tech_lead.py b/tests/test_conductor_tech_lead.py index 944154e1..26fd8fa6 100644 --- a/tests/test_conductor_tech_lead.py +++ b/tests/test_conductor_tech_lead.py @@ -1,6 +1,7 @@ import unittest from unittest.mock import patch from src import conductor_tech_lead +from src.models import Ticket from src.result_types import Result import pytest @@ -30,28 +31,28 @@ class TestConductorTechLead(unittest.TestCase): class TestTopologicalSort(unittest.TestCase): def test_topological_sort_linear(self) -> None: tickets = [ - {"id": "t2", "depends_on": ["t1"]}, - {"id": "t1", "depends_on": []}, + Ticket(id="t2", description="t2", depends_on=["t1"]), + Ticket(id="t1", description="t1", depends_on=[]), ] sorted_tickets = conductor_tech_lead.topological_sort(tickets) - self.assertEqual(sorted_tickets[0]['id'], "t1") - self.assertEqual(sorted_tickets[1]['id'], "t2") + self.assertEqual(sorted_tickets[0].id, "t1") + self.assertEqual(sorted_tickets[1].id, "t2") def test_topological_sort_complex(self) -> None: tickets = [ - {"id": "t3", "depends_on": ["t1", "t2"]}, - {"id": "t1", "depends_on": []}, - {"id": "t2", "depends_on": ["t1"]}, + Ticket(id="t3", description="t3", depends_on=["t1", "t2"]), + Ticket(id="t1", description="t1", depends_on=[]), + Ticket(id="t2", description="t2", depends_on=["t1"]), ] sorted_tickets = conductor_tech_lead.topological_sort(tickets) - self.assertEqual(sorted_tickets[0]['id'], "t1") - self.assertEqual(sorted_tickets[1]['id'], "t2") - self.assertEqual(sorted_tickets[2]['id'], "t3") + self.assertEqual(sorted_tickets[0].id, "t1") + self.assertEqual(sorted_tickets[1].id, "t2") + self.assertEqual(sorted_tickets[2].id, "t3") def test_topological_sort_cycle(self) -> None: tickets = [ - {"id": "t1", "depends_on": ["t2"]}, - {"id": "t2", "depends_on": ["t1"]}, + Ticket(id="t1", description="t1", depends_on=["t2"]), + Ticket(id="t2", description="t2", depends_on=["t1"]), ] with self.assertRaises(ValueError) as cm: conductor_tech_lead.topological_sort(tickets) @@ -65,7 +66,7 @@ class TestTopologicalSort(unittest.TestCase): # If a ticket depends on something not in the list, we should handle it or let it fail. # The TrackDAG silently ignores missing dependencies, causing cycle detection to trigger. tickets = [ - {"id": "t1", "depends_on": ["missing"]}, + Ticket(id="t1", description="t1", depends_on=["missing"]), ] # Currently this raises ValueError due to cycle detection on incomplete sort with self.assertRaises(ValueError): @@ -73,12 +74,12 @@ class TestTopologicalSort(unittest.TestCase): def test_topological_sort_vlog(vlogger) -> None: tickets = [ - {"id": "t2", "depends_on": ["t1"]}, - {"id": "t1", "depends_on": []}, + Ticket(id="t2", description="t2", depends_on=["t1"]), + Ticket(id="t1", description="t1", depends_on=[]), ] vlogger.log_state("Input Order", ["t2", "t1"], ["t2", "t1"]) sorted_tickets = conductor_tech_lead.topological_sort(tickets) - result_ids = [t['id'] for t in sorted_tickets] + result_ids = [t.id for t in sorted_tickets] vlogger.log_state("Sorted Order", "N/A", result_ids) assert result_ids == ["t1", "t2"] vlogger.finalize("Topological Sort Verification", "PASS", "Linear dependencies correctly ordered.") diff --git a/tests/test_gui_2_result.py b/tests/test_gui_2_result.py index 434e8713..afb87cab 100644 --- a/tests/test_gui_2_result.py +++ b/tests/test_gui_2_result.py @@ -2315,9 +2315,10 @@ def test_phase_10_l7271_dag_cycle_check_result_no_cycle(): opening the "Cycle Detected!" popup. """ from unittest.mock import MagicMock, patch + from src.models import Ticket import src.gui_2 as gui2_mod app = MagicMock() - app.active_tickets = [{"id": "T-001", "depends_on": []}] + app.active_tickets = [Ticket(id="T-001", description="T-001", depends_on=[])] mock_dag = MagicMock() mock_dag.has_cycle.return_value = False with patch("src.dag_engine.TrackDAG", return_value=mock_dag): @@ -2334,11 +2335,12 @@ def test_phase_10_l7271_dag_cycle_check_result_cycle_detected(): returns Result(data=True). The caller opens the "Cycle Detected!" popup. """ from unittest.mock import MagicMock, patch + from src.models import Ticket import src.gui_2 as gui2_mod app = MagicMock() app.active_tickets = [ - {"id": "T-001", "depends_on": ["T-002"]}, - {"id": "T-002", "depends_on": ["T-001"]}, + Ticket(id="T-001", description="T-001", depends_on=["T-002"]), + Ticket(id="T-002", description="T-002", depends_on=["T-001"]), ] mock_dag = MagicMock() mock_dag.has_cycle.return_value = True diff --git a/tests/test_gui_dag_beads.py b/tests/test_gui_dag_beads.py index 7c32d7e7..813846a9 100644 --- a/tests/test_gui_dag_beads.py +++ b/tests/test_gui_dag_beads.py @@ -47,5 +47,5 @@ def test_load_active_tickets_from_beads(tmp_path: Path): # 5. Verify active_tickets populated from Beads assert len(ctrl.active_tickets) == 1 - assert ctrl.active_tickets[0]["id"] == "bead-1" - assert ctrl.active_tickets[0]["description"] == "Description 1" + assert ctrl.active_tickets[0].id == "bead-1" + assert ctrl.active_tickets[0].description == "Description 1" diff --git a/tests/test_gui_kill_button.py b/tests/test_gui_kill_button.py index deedf126..b6c72964 100644 --- a/tests/test_gui_kill_button.py +++ b/tests/test_gui_kill_button.py @@ -1,5 +1,6 @@ import pytest from unittest.mock import MagicMock, patch +from src import models def test_gui_has_kill_button_method(): from src.gui_2 import App @@ -36,7 +37,7 @@ def test_render_ticket_queue_table_columns(): from src.gui_2 import App, render_ticket_queue app = App.__new__(App) app.active_track = MagicMock() - app.active_tickets = [{"id": "T-001", "priority": "medium", "status": "in_progress", "description": "Test task"}] + app.active_tickets = [models.Ticket(id="T-001", description="Test task", priority="medium", status="in_progress")] app.ui_selected_tickets = set() app.ui_selected_ticket_id = None app.controller = MagicMock() diff --git a/tests/test_metadata_promotion_phase1.py b/tests/test_metadata_promotion_phase1.py new file mode 100644 index 00000000..dd690229 --- /dev/null +++ b/tests/test_metadata_promotion_phase1.py @@ -0,0 +1,191 @@ +""" +Phase 1 of metadata_promotion_20260624. + +Verifies: + 1. self.active_tickets load boundaries convert dicts to models.Ticket + 2. conductor_tech_lead.topological_sort returns list[models.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.models import Ticket + + +class TestActiveTicketsType: + def test_active_tickets_annotation_is_list_of_ticket(self) -> None: + """self.active_tickets type hint must be list[models.Ticket], not list[Metadata].""" + from src.app_controller import AppController + src_text = inspect.getsource(AppController.__init__) + assert "list[models.Ticket]" in src_text, ( + "AppController.__init__ must declare self.active_tickets: list[models.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 models.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 models.Ticket.""" + from src.app_controller import AppController + from src.models 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[models.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" + ) \ No newline at end of file diff --git a/tests/test_mma_ticket_actions.py b/tests/test_mma_ticket_actions.py index 81a64305..c3f8eb80 100644 --- a/tests/test_mma_ticket_actions.py +++ b/tests/test_mma_ticket_actions.py @@ -1,16 +1,17 @@ from src.gui_2 import App +from src.models import Ticket def test_cb_ticket_retry(app_instance: App) -> None: ticket_id = "test_ticket_1" - app_instance.active_tickets = [{"id": ticket_id, "status": "failed"}] + app_instance.active_tickets = [Ticket(id=ticket_id, description="test", status="failed")] # Synchronous implementation does not use asyncio.run_coroutine_threadsafe app_instance.controller._cb_ticket_retry(ticket_id) # Verify status update - assert app_instance.active_tickets[0]['status'] == 'todo' + assert app_instance.active_tickets[0].status == 'todo' def test_cb_ticket_skip(app_instance: App) -> None: ticket_id = "test_ticket_2" - app_instance.active_tickets = [{"id": ticket_id, "status": "todo"}] + app_instance.active_tickets = [Ticket(id=ticket_id, description="test", status="todo")] app_instance.controller._cb_ticket_skip(ticket_id) # Verify status update - assert app_instance.active_tickets[0]['status'] == 'skipped' + assert app_instance.active_tickets[0].status == 'skipped' diff --git a/tests/test_orchestration_logic.py b/tests/test_orchestration_logic.py index af0d9379..06b42794 100644 --- a/tests/test_orchestration_logic.py +++ b/tests/test_orchestration_logic.py @@ -34,17 +34,17 @@ def test_generate_tickets() -> None: def test_topological_sort() -> None: tickets = [ - {"id": "T2", "depends_on": ["T1"]}, - {"id": "T1", "depends_on": []} + Ticket(id="T2", description="d2", depends_on=["T1"]), + Ticket(id="T1", description="d1", depends_on=[]) ] sorted_tickets = conductor_tech_lead.topological_sort(tickets) - assert sorted_tickets[0]["id"] == "T1" - assert sorted_tickets[1]["id"] == "T2" + assert sorted_tickets[0].id == "T1" + assert sorted_tickets[1].id == "T2" def test_topological_sort_circular() -> None: tickets = [ - {"id": "T1", "depends_on": ["T2"]}, - {"id": "T2", "depends_on": ["T1"]} + Ticket(id="T1", description="d1", depends_on=["T2"]), + Ticket(id="T2", description="d2", depends_on=["T1"]) ] with pytest.raises(ValueError, match="DAG Validation Error"): conductor_tech_lead.topological_sort(tickets) diff --git a/tests/test_ticket_queue.py b/tests/test_ticket_queue.py index d424f4ec..f28e6660 100644 --- a/tests/test_ticket_queue.py +++ b/tests/test_ticket_queue.py @@ -40,70 +40,70 @@ def test_ticket_from_dict_default_priority(): class TestBulkOperations: def test_bulk_execute(self, mock_app): mock_app.active_tickets = [ - {"id": "T1", "status": "todo"}, - {"id": "T2", "status": "todo"}, - {"id": "T3", "status": "todo"} + Ticket(id="T1", description="T1", status="todo"), + Ticket(id="T2", description="T2", status="todo"), + Ticket(id="T3", description="T3", status="todo") ] mock_app.ui_selected_tickets = {"T1", "T3"} - + with patch.object(mock_app.controller, "_push_mma_state_update") as mock_push: mock_app.bulk_execute() - assert mock_app.active_tickets[0]["status"] == "in_progress" - assert mock_app.active_tickets[1]["status"] == "todo" - assert mock_app.active_tickets[2]["status"] == "in_progress" + assert mock_app.active_tickets[0].status == "in_progress" + assert mock_app.active_tickets[1].status == "todo" + assert mock_app.active_tickets[2].status == "in_progress" mock_push.assert_called_once() def test_bulk_skip(self, mock_app): mock_app.active_tickets = [ - {"id": "T1", "status": "todo"}, - {"id": "T2", "status": "todo"} + Ticket(id="T1", description="T1", status="todo"), + Ticket(id="T2", description="T2", status="todo") ] mock_app.ui_selected_tickets = {"T1"} - + with patch.object(mock_app.controller, "_push_mma_state_update") as mock_push: mock_app.bulk_skip() - assert mock_app.active_tickets[0]["status"] == "completed" - assert mock_app.active_tickets[1]["status"] == "todo" + assert mock_app.active_tickets[0].status == "completed" + assert mock_app.active_tickets[1].status == "todo" mock_push.assert_called_once() def test_bulk_block(self, mock_app): mock_app.active_tickets = [ - {"id": "T1", "status": "todo"}, - {"id": "T2", "status": "todo"} + Ticket(id="T1", description="T1", status="todo"), + Ticket(id="T2", description="T2", status="todo") ] mock_app.ui_selected_tickets = {"T1", "T2"} - + with patch.object(mock_app.controller, "_push_mma_state_update") as mock_push: mock_app.bulk_block() - assert mock_app.active_tickets[0]["status"] == "blocked" - assert mock_app.active_tickets[1]["status"] == "blocked" + assert mock_app.active_tickets[0].status == "blocked" + assert mock_app.active_tickets[1].status == "blocked" mock_push.assert_called_once() class TestReorder: def test_reorder_ticket_valid(self, mock_app): mock_app.active_tickets = [ - {"id": "T1", "depends_on": []}, - {"id": "T2", "depends_on": []}, - {"id": "T3", "depends_on": ["T1"]} + Ticket(id="T1", description="T1", depends_on=[]), + Ticket(id="T2", description="T2", depends_on=[]), + Ticket(id="T3", description="T3", depends_on=["T1"]) ] with patch.object(mock_app.controller, "_push_mma_state_update") as mock_push: # Move T1 to index 1: [T2, T1, T3]. T3 depends on T1. T1 index 1 < T3 index 2. VALID. mock_app._reorder_ticket(0, 1) - assert mock_app.active_tickets[0]["id"] == "T2" - assert mock_app.active_tickets[1]["id"] == "T1" - assert mock_app.active_tickets[2]["id"] == "T3" + assert mock_app.active_tickets[0].id == "T2" + assert mock_app.active_tickets[1].id == "T1" + assert mock_app.active_tickets[2].id == "T3" mock_push.assert_called_once() def test_reorder_ticket_invalid(self, mock_app): mock_app.active_tickets = [ - {"id": "T1", "depends_on": []}, - {"id": "T2", "depends_on": ["T1"]} + Ticket(id="T1", description="T1", depends_on=[]), + Ticket(id="T2", description="T2", depends_on=["T1"]) ] with patch.object(mock_app.controller, "_push_mma_state_update") as mock_push: # Move T1 after T2: [T2, T1]. T2 depends on T1, but T1 is now at index 1 while T2 is at index 0. # Violation: dependency T1 (index 1) is not before T2 (index 0). mock_app._reorder_ticket(0, 1) # Should NOT change - assert mock_app.active_tickets[0]["id"] == "T1" - assert mock_app.active_tickets[1]["id"] == "T2" + assert mock_app.active_tickets[0].id == "T1" + assert mock_app.active_tickets[1].id == "T2" mock_push.assert_not_called()