From 4921a6715cac47d64b8da4d8dc2a331491f52dfe Mon Sep 17 00:00:00 2001 From: Ed_ Date: Fri, 6 Mar 2026 23:07:08 -0500 Subject: [PATCH] OK. --- .../archive/test_fixes_session_debrief.md | 5 +- src/mcp_client.py | 16 ++--- src/models.py | 61 ++++++++++--------- tests/test_mma_approval_indicators.py | 3 +- tests/test_mma_dashboard_refresh.py | 2 +- tests/test_phase6_engine.py | 6 +- tests/test_process_pending_gui_tasks.py | 2 +- 7 files changed, 51 insertions(+), 44 deletions(-) diff --git a/conductor/archive/test_fixes_session_debrief.md b/conductor/archive/test_fixes_session_debrief.md index f246d6a..5b942d5 100644 --- a/conductor/archive/test_fixes_session_debrief.md +++ b/conductor/archive/test_fixes_session_debrief.md @@ -15,10 +15,7 @@ Fixed 329/330 tests passing for asyncio_decoupling_refactor_20260306 track. - Issue in the _pending_gui_tasks queue - tracks aren't being processed ## CRITICAL MCP TOOL LESSON -When using manual-slop_edit_file, parameters are CAMEL CASE: -- oldString (NOT old_string) -- newString (NOT new_string) -- replaceAll (NOT replace_all) +When using manual-slop_edit_file, parameters are lower_snake_case: The tool schema shows camelCase. Never assume snake_case. Always verify params from schema. diff --git a/src/mcp_client.py b/src/mcp_client.py index df2c5ca..7123dc9 100644 --- a/src/mcp_client.py +++ b/src/mcp_client.py @@ -1,4 +1,4 @@ -# mcp_client.py +# mcp_client.py """ Note(Gemini): MCP-style file context tools for manual_slop. @@ -322,7 +322,7 @@ def set_file_slice(path: str, start_line: int, end_line: int, new_content: str) new_content += "\n" new_lines = new_content.splitlines(keepends=True) if new_content else [] lines[start_idx:end_idx] = new_lines - p.write_text("".join(lines), encoding="utf-8", newline="") + p.write_text("".join(lines), encoding="utf-8") return f"Successfully updated lines {start_line}-{end_line} in {path}" except Exception as e: return f"ERROR updating slice in '{path}': {e}" @@ -341,7 +341,7 @@ def edit_file(path: str, old_string: str, new_string: str, replace_all: bool = F if not old_string: return "ERROR: old_string cannot be empty" try: - content = p.read_text(encoding="utf-8", newline="") + content = p.read_text(encoding="utf-8") if old_string not in content: return f"ERROR: old_string not found in '{path}'" count = content.count(old_string) @@ -349,11 +349,11 @@ def edit_file(path: str, old_string: str, new_string: str, replace_all: bool = F return f"ERROR: Found {count} matches for old_string in '{path}'. Use replace_all=true or provide more context to make it unique." if replace_all: new_content = content.replace(old_string, new_string) - p.write_text(new_content, encoding="utf-8", newline="") + p.write_text(new_content, encoding="utf-8") return f"Successfully replaced {count} occurrences in '{path}'" else: new_content = content.replace(old_string, new_string, 1) - p.write_text(new_content, encoding="utf-8", newline="") + p.write_text(new_content, encoding="utf-8") return f"Successfully replaced 1 occurrence in '{path}'" except Exception as e: return f"ERROR editing '{path}': {e}" @@ -734,10 +734,10 @@ def get_tree(path: str, max_depth: int = 2) -> str: entries = [e for e in entries if not e.name.startswith('.') and e.name not in ('__pycache__', 'venv', 'env') and e.name != "history.toml" and not e.name.endswith("_history.toml")] for i, entry in enumerate(entries): is_last = (i == len(entries) - 1) - connector = "└── " if is_last else "├── " + connector = "└── " if is_last else "├── " lines.append(f"{prefix}{connector}{entry.name}") if entry.is_dir(): - extension = " " if is_last else "│ " + extension = " " if is_last else "│ " lines.extend(_build_tree(entry, current_depth + 1, prefix + extension)) return lines tree_lines = [f"{p.name}/"] + _build_tree(p, 1) @@ -1416,3 +1416,5 @@ MCP_TOOL_SPECS: list[dict[str, Any]] = [ } ] + + diff --git a/src/models.py b/src/models.py index 34f4ce6..ff85fde 100644 --- a/src/models.py +++ b/src/models.py @@ -17,7 +17,6 @@ def save_config(config: dict[str, Any]) -> None: with open(CONFIG_PATH, "wb") as f: tomli_w.dump(config, f) -# Global constants for agent tools AGENT_TOOL_NAMES = [ "run_powershell", "read_file", @@ -40,7 +39,6 @@ AGENT_TOOL_NAMES = [ ] def parse_history_entries(history_strings: list[str], roles: list[str]) -> list[dict[str, Any]]: - """Parse stored history strings back to disc entry dicts.""" import re entries = [] for raw in history_strings: @@ -64,10 +62,6 @@ def parse_history_entries(history_strings: list[str], roles: list[str]) -> list[ @dataclass class Ticket: - """ - Represents a discrete unit of work within a track. - """ - id: str description: str status: str = "todo" @@ -81,16 +75,13 @@ class Ticket: retry_count: int = 0 def mark_blocked(self, reason: str) -> None: - """Sets the ticket status to 'blocked' and records the reason.""" self.status = "blocked" self.blocked_reason = reason def mark_complete(self) -> None: - """Sets the ticket status to 'completed'.""" self.status = "completed" def get(self, key: str, default: Any = None) -> Any: - """Helper to provide dictionary-like access to dataclass fields.""" return getattr(self, key, default) def to_dict(self) -> Dict[str, Any]: @@ -127,16 +118,11 @@ class Ticket: @dataclass class Track: - """ - Represents a collection of tickets that together form an architectural track or epic. - """ - id: str description: str tickets: List[Ticket] = field(default_factory=list) def get_executable_tickets(self) -> List[Ticket]: - """Returns tickets that are ready to be executed (dependencies met).""" from src.dag_engine import TrackDAG dag = TrackDAG(self.tickets) return dag.get_ready_tasks() @@ -159,10 +145,6 @@ class Track: @dataclass class WorkerContext: - """ - State preserved for a specific worker throughout its ticket lifecycle. - """ - ticket_id: str model_name: str messages: List[Dict[str, Any]] = field(default_factory=list) @@ -172,17 +154,17 @@ class WorkerContext: class Metadata: id: str name: str - status: str - created_at: Union[str, Any] - updated_at: Union[str, Any] + status: Optional[str] = None + created_at: Optional[datetime.datetime] = None + updated_at: Optional[datetime.datetime] = None def to_dict(self) -> Dict[str, Any]: return { "id": self.id, "name": self.name, "status": self.status, - "created_at": str(self.created_at), - "updated_at": str(self.updated_at), + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None, } @classmethod @@ -193,16 +175,16 @@ class Metadata: try: created = datetime.datetime.fromisoformat(created) except ValueError: - pass + created = None if isinstance(updated, str): try: updated = datetime.datetime.fromisoformat(updated) except ValueError: - pass + updated = None return cls( id=data["id"], name=data.get("name", ""), - status=data.get("status", "todo"), + status=data.get("status"), created_at=created, updated_at=updated, ) @@ -215,16 +197,39 @@ class TrackState: tasks: List[Ticket] = field(default_factory=list) def to_dict(self) -> Dict[str, Any]: + serialized_discussion = [] + for item in self.discussion: + if isinstance(item, dict): + new_item = dict(item) + if "ts" in new_item and isinstance(new_item["ts"], datetime.datetime): + new_item["ts"] = new_item["ts"].isoformat() + serialized_discussion.append(new_item) + else: + serialized_discussion.append(item) return { "metadata": self.metadata.to_dict(), - "discussion": self.discussion, + "discussion": serialized_discussion, "tasks": [t.to_dict() for t in self.tasks], } @classmethod def from_dict(cls, data: Dict[str, Any]) -> "TrackState": + discussion = data.get("discussion", []) + parsed_discussion = [] + for item in discussion: + if isinstance(item, dict): + new_item = dict(item) + ts = new_item.get("ts") + if isinstance(ts, str): + try: + new_item["ts"] = datetime.datetime.fromisoformat(ts) + except ValueError: + pass + parsed_discussion.append(new_item) + else: + parsed_discussion.append(item) return cls( metadata=Metadata.from_dict(data["metadata"]), - discussion=data.get("discussion", []), + discussion=parsed_discussion, tasks=[Ticket.from_dict(t) for t in data.get("tasks", [])], ) diff --git a/tests/test_mma_approval_indicators.py b/tests/test_mma_approval_indicators.py index 9bc3849..4253f3a 100644 --- a/tests/test_mma_approval_indicators.py +++ b/tests/test_mma_approval_indicators.py @@ -32,9 +32,10 @@ def _make_app(**kwargs): app.ui_new_ticket_desc = "" app.ui_new_ticket_target = "" app.ui_new_ticket_deps = "" + app.ui_new_ticket_deps = "" + app.ui_selected_ticket_id = "" return app - def _make_imgui_mock(): m = MagicMock() m.begin_table.return_value = False diff --git a/tests/test_mma_dashboard_refresh.py b/tests/test_mma_dashboard_refresh.py index fae37b8..d6e594a 100644 --- a/tests/test_mma_dashboard_refresh.py +++ b/tests/test_mma_dashboard_refresh.py @@ -8,7 +8,7 @@ from src.gui_2 import App def app_instance() -> Any: with ( patch("src.models.load_config", return_value={"ai": {}, "projects": {}}), - patch("src.gui_2.save_config"), + patch("src.models.save_config"), patch("src.gui_2.project_manager"), patch("src.app_controller.project_manager") as mock_pm, patch("src.gui_2.session_logger"), diff --git a/tests/test_phase6_engine.py b/tests/test_phase6_engine.py index d9ea169..21f8337 100644 --- a/tests/test_phase6_engine.py +++ b/tests/test_phase6_engine.py @@ -47,7 +47,7 @@ def test_per_tier_model_persistence(): patch("src.gui_2.project_manager.load_project", return_value={}), patch("src.gui_2.project_manager.migrate_from_legacy_config", return_value={}), patch("src.gui_2.project_manager.save_project"), - patch("src.gui_2.save_config"), + patch("src.models.save_config"), patch("src.gui_2.theme.load_from_config"), patch("src.gui_2.ai_client.set_provider"), patch("src.gui_2.ai_client.list_models", return_value=["gpt-4", "claude-3"]), @@ -83,8 +83,10 @@ def test_retry_escalation(): with patch.object(engine.engine, "tick") as mock_tick: # First tick returns ticket, second tick returns empty list to stop loop - mock_tick.side_effect = [[ticket], []] + mock_tick.side_effect = iter([[ticket], []]) + engine.run() + engine.run() engine.run() assert ticket.retry_count == 1 diff --git a/tests/test_process_pending_gui_tasks.py b/tests/test_process_pending_gui_tasks.py index bf7cde5..9e18c48 100644 --- a/tests/test_process_pending_gui_tasks.py +++ b/tests/test_process_pending_gui_tasks.py @@ -8,7 +8,7 @@ from src.gui_2 import App def app_instance() -> Generator[App, None, None]: with ( patch('src.models.load_config', return_value={'ai': {'provider': 'gemini', 'model': 'gemini-2.5-flash-lite'}, 'projects': {}}), - patch('src.gui_2.save_config'), + patch('src.models.save_config'), patch('src.gui_2.project_manager'), patch('src.gui_2.session_logger'), patch('src.gui_2.immapp.run'),