diff --git a/codeframe/agents/frontend_worker_agent.py b/codeframe/agents/frontend_worker_agent.py index ff40a752..02be5b5c 100644 --- a/codeframe/agents/frontend_worker_agent.py +++ b/codeframe/agents/frontend_worker_agent.py @@ -88,18 +88,33 @@ def _build_system_prompt(self) -> str: - Ensure proper TypeScript typing (no 'any' types) """ - async def execute_task(self, task: Task, project_id: int = 1) -> Dict[str, Any]: + async def execute_task(self, task: Dict[str, Any], project_id: int = 1) -> Dict[str, Any]: """ Execute frontend task: generate React component. Args: - task: Task to execute with component specification + task: Task dictionary with id, project_id, task_number, title, description, etc. project_id: Project ID for WebSocket broadcasts Returns: Execution result with status and output """ - self.current_task = task + # Extract commonly used fields from task dict + task_id = task["id"] + task_title = task["title"] + task_description = task["description"] + + # Create Task object for self.current_task (used by blocker creation) + self.current_task = Task( + id=task_id, + project_id=task.get("project_id"), + issue_id=task.get("issue_id"), + task_number=task.get("task_number", ""), + parent_issue_number=task.get("parent_issue_number", ""), + title=task_title, + description=task_description, + assigned_to=task.get("assigned_to"), + ) try: # Broadcast task started @@ -110,7 +125,7 @@ async def execute_task(self, task: Task, project_id: int = 1) -> Dict[str, Any]: await broadcast_task_status( self.websocket_manager, project_id, - task.id, + task_id, "in_progress", agent_id=self.agent_id, progress=0, @@ -118,10 +133,10 @@ async def execute_task(self, task: Task, project_id: int = 1) -> Dict[str, Any]: except Exception as e: logger.debug(f"Failed to broadcast task status: {e}") - logger.info(f"Frontend agent {self.agent_id} executing task {task.id}: {task.title}") + logger.info(f"Frontend agent {self.agent_id} executing task {task_id}: {task_title}") # Parse task description for component spec - component_spec = self._parse_component_spec(task.description) + component_spec = self._parse_component_spec(task_description) # Generate component code component_code = await self._generate_react_component(component_spec) @@ -151,7 +166,7 @@ async def execute_task(self, task: Task, project_id: int = 1) -> Dict[str, Any]: await broadcast_task_status( self.websocket_manager, project_id, - task.id, + task_id, "completed", agent_id=self.agent_id, progress=100, @@ -159,7 +174,7 @@ async def execute_task(self, task: Task, project_id: int = 1) -> Dict[str, Any]: except Exception as e: logger.debug(f"Failed to broadcast task status: {e}") - logger.info(f"Frontend agent {self.agent_id} completed task {task.id}") + logger.info(f"Frontend agent {self.agent_id} completed task {task_id}") # T076: Auto-commit task changes after successful completion if hasattr(self, "git_workflow") and self.git_workflow and file_paths: @@ -167,13 +182,13 @@ async def execute_task(self, task: Task, project_id: int = 1) -> Dict[str, Any]: # Convert file_paths dict to list of paths files_modified = [path for path in file_paths.values()] - # Convert Task object to dict for git_workflow + # Task is already a dict, extract fields for git_workflow task_dict = { - "id": task.id, - "project_id": task.project_id, - "task_number": task.task_number, - "title": task.title, - "description": task.description, + "id": task_id, + "project_id": task.get("project_id"), + "task_number": task.get("task_number", ""), + "title": task_title, + "description": task_description, } commit_sha = self.git_workflow.commit_task_changes( @@ -182,11 +197,11 @@ async def execute_task(self, task: Task, project_id: int = 1) -> Dict[str, Any]: # T082: Record commit SHA in database if commit_sha and self.db: - self.db.update_task_commit_sha(task.id, commit_sha) - logger.info(f"Task {task.id} committed with SHA: {commit_sha[:7]}") + self.db.update_task_commit_sha(task_id, commit_sha) + logger.info(f"Task {task_id} committed with SHA: {commit_sha[:7]}") except Exception as e: # T080: Graceful degradation - log warning but don't block task completion - logger.warning(f"Auto-commit failed for task {task.id} (non-blocking): {e}") + logger.warning(f"Auto-commit failed for task {task_id} (non-blocking): {e}") return { "status": "completed", @@ -196,7 +211,7 @@ async def execute_task(self, task: Task, project_id: int = 1) -> Dict[str, Any]: } except Exception as e: - logger.error(f"Frontend agent {self.agent_id} failed task {task.id}: {e}") + logger.error(f"Frontend agent {self.agent_id} failed task {task_id}: {e}") # Broadcast failure if self.websocket_manager: @@ -206,12 +221,12 @@ async def execute_task(self, task: Task, project_id: int = 1) -> Dict[str, Any]: await broadcast_task_status( self.websocket_manager, project_id, - task.id, + task_id, "failed", agent_id=self.agent_id, ) - except Exception as e: - logger.debug(f"Failed to broadcast task status: {e}") + except Exception as broadcast_err: + logger.debug(f"Failed to broadcast task status: {broadcast_err}") return {"status": "failed", "output": str(e), "error": str(e)} @@ -438,7 +453,7 @@ def _update_imports_exports(self, component_name: str, file_paths: Dict[str, str ) async def _run_and_check_linting( - self, task: Task, file_paths: Dict[str, str], project_id: int + self, task: Dict[str, Any], file_paths: Dict[str, str], project_id: int ) -> None: """ Run linting on created files and create blocker if critical errors found (T112). @@ -447,13 +462,14 @@ async def _run_and_check_linting( stores results in the database, and creates a blocker if critical errors are found. Args: - task: Task object + task: Task dictionary with id, project_id, task_number, title, description, etc. file_paths: Dict of created file paths project_id: Project ID for broadcasts Raises: ValueError: If linting fails with critical errors (blocker created) """ + task_id = task["id"] if not file_paths: logger.debug("No files created, skipping linting") return @@ -478,7 +494,7 @@ async def _run_and_check_linting( lint_runner = LintRunner(self.web_ui_root) logger.info( - f"Running linting on {len(files_to_lint)} TypeScript files for task {task.id}" + f"Running linting on {len(files_to_lint)} TypeScript files for task {task_id}" ) # Run linting @@ -488,7 +504,7 @@ async def _run_and_check_linting( if self.db: for result in lint_results: self.db.create_lint_result( - task_id=task.id, + task_id=task_id, linter=result.linter, error_count=result.error_count, warning_count=result.warning_count, @@ -508,10 +524,10 @@ async def _run_and_check_linting( blocker_type="SYNC", title=f"Linting failed: {total_errors} critical errors", description=blocker_description, - blocking_task_id=task.id, + blocking_task_id=task_id, ) - logger.error(f"Task {task.id} blocked by {total_errors} lint errors") + logger.error(f"Task {task_id} blocked by {total_errors} lint errors") # Broadcast lint failure via WebSocket (T119) if self.websocket_manager: @@ -523,7 +539,7 @@ async def _run_and_check_linting( project_id, { "type": "lint_failed", - "task_id": task.id, + "task_id": task_id, "error_count": total_errors, "timestamp": datetime.utcnow().isoformat(), }, @@ -536,7 +552,7 @@ async def _run_and_check_linting( # Log warnings (non-blocking) total_warnings = sum(r.warning_count for r in lint_results) if total_warnings > 0: - logger.warning(f"Task {task.id}: {total_warnings} lint warnings (non-blocking)") + logger.warning(f"Task {task_id}: {total_warnings} lint warnings (non-blocking)") # Broadcast lint success via WebSocket (T119) if self.websocket_manager: @@ -548,7 +564,7 @@ async def _run_and_check_linting( project_id, { "type": "lint_completed", - "task_id": task.id, + "task_id": task_id, "error_count": 0, "warning_count": total_warnings, "timestamp": datetime.utcnow().isoformat(), diff --git a/codeframe/agents/lead_agent.py b/codeframe/agents/lead_agent.py index 45e5be69..ee5bc350 100644 --- a/codeframe/agents/lead_agent.py +++ b/codeframe/agents/lead_agent.py @@ -2198,8 +2198,8 @@ async def _assign_and_execute_task(self, task: Task, retry_counts: Dict[int, int 7. Broadcast task status changes """ try: - # Determine agent type - task_dict = {"id": task.id, "title": task.title, "description": task.description} + # Determine agent type - use task.to_dict() for complete task data + task_dict = task.to_dict() agent_type = self.agent_assigner.assign_agent_type(task_dict) logger.info(f"Assigning task {task.id} ({task.title}) to {agent_type}") diff --git a/codeframe/agents/test_worker_agent.py b/codeframe/agents/test_worker_agent.py index 383c1e43..67cf2663 100644 --- a/codeframe/agents/test_worker_agent.py +++ b/codeframe/agents/test_worker_agent.py @@ -98,18 +98,33 @@ def _build_system_prompt(self) -> str: - Ensure proper async/await handling for async code """ - async def execute_task(self, task: Task, project_id: int = 1) -> Dict[str, Any]: + async def execute_task(self, task: Dict[str, Any], project_id: int = 1) -> Dict[str, Any]: """ Execute test generation task. Args: - task: Task with target code specification + task: Task dictionary with id, project_id, task_number, title, description, etc. project_id: Project ID for broadcasts Returns: Execution result with test status """ - self.current_task = task + # Extract commonly used fields from task dict + task_id = task["id"] + task_title = task["title"] + task_description = task["description"] + + # Create Task object for self.current_task (used by blocker creation) + self.current_task = Task( + id=task_id, + project_id=task.get("project_id"), + issue_id=task.get("issue_id"), + task_number=task.get("task_number", ""), + parent_issue_number=task.get("parent_issue_number", ""), + title=task_title, + description=task_description, + assigned_to=task.get("assigned_to"), + ) try: # Broadcast task started @@ -120,7 +135,7 @@ async def execute_task(self, task: Task, project_id: int = 1) -> Dict[str, Any]: await broadcast_task_status( self.websocket_manager, project_id, - task.id, + task_id, "in_progress", agent_id=self.agent_id, progress=0, @@ -128,10 +143,10 @@ async def execute_task(self, task: Task, project_id: int = 1) -> Dict[str, Any]: except Exception as e: logger.debug(f"Failed to broadcast task status: {e}") - logger.info(f"Test agent {self.agent_id} executing task {task.id}: {task.title}") + logger.info(f"Test agent {self.agent_id} executing task {task_id}: {task_title}") # Parse task for target code - test_spec = self._parse_test_spec(task.description) + test_spec = self._parse_test_spec(task_description) # Analyze target code code_analysis = self._analyze_target_code(test_spec.get("target_file")) @@ -147,7 +162,7 @@ async def execute_task(self, task: Task, project_id: int = 1) -> Dict[str, Any]: # Execute tests and self-correct if needed test_result = await self._execute_and_correct_tests( - test_file, test_spec, code_analysis, project_id, task.id + test_file, test_spec, code_analysis, project_id, task_id ) # Broadcast completion or failure @@ -159,7 +174,7 @@ async def execute_task(self, task: Task, project_id: int = 1) -> Dict[str, Any]: await broadcast_task_status( self.websocket_manager, project_id, - task.id, + task_id, final_status, agent_id=self.agent_id, progress=100, @@ -168,7 +183,7 @@ async def execute_task(self, task: Task, project_id: int = 1) -> Dict[str, Any]: logger.debug(f"Failed to broadcast task status: {e}") logger.info( - f"Test agent {self.agent_id} completed task {task.id}: " + f"Test agent {self.agent_id} completed task {task_id}: " f"{test_result['passed_count']}/{test_result['total_count']} tests passed" ) @@ -178,13 +193,13 @@ async def execute_task(self, task: Task, project_id: int = 1) -> Dict[str, Any]: # Convert test_file Path to string for files_modified files_modified = [str(test_file)] - # Convert Task object to dict for git_workflow + # Task is already a dict, extract fields for git_workflow task_dict = { - "id": task.id, - "project_id": task.project_id, - "task_number": task.task_number, - "title": task.title, - "description": task.description, + "id": task_id, + "project_id": task.get("project_id"), + "task_number": task.get("task_number", ""), + "title": task_title, + "description": task_description, } commit_sha = self.git_workflow.commit_task_changes( @@ -193,11 +208,11 @@ async def execute_task(self, task: Task, project_id: int = 1) -> Dict[str, Any]: # T082: Record commit SHA in database if commit_sha and self.db: - self.db.update_task_commit_sha(task.id, commit_sha) - logger.info(f"Task {task.id} committed with SHA: {commit_sha[:7]}") + self.db.update_task_commit_sha(task_id, commit_sha) + logger.info(f"Task {task_id} committed with SHA: {commit_sha[:7]}") except Exception as e: # T080: Graceful degradation - log warning but don't block task completion - logger.warning(f"Auto-commit failed for task {task.id} (non-blocking): {e}") + logger.warning(f"Auto-commit failed for task {task_id} (non-blocking): {e}") return { "status": final_status, @@ -208,7 +223,7 @@ async def execute_task(self, task: Task, project_id: int = 1) -> Dict[str, Any]: } except Exception as e: - logger.error(f"Test agent {self.agent_id} failed task {task.id}: {e}") + logger.error(f"Test agent {self.agent_id} failed task {task_id}: {e}") if self.websocket_manager: try: @@ -217,12 +232,12 @@ async def execute_task(self, task: Task, project_id: int = 1) -> Dict[str, Any]: await broadcast_task_status( self.websocket_manager, project_id, - task.id, + task_id, "failed", agent_id=self.agent_id, ) - except Exception as e: - logger.debug(f"Failed to broadcast task status: {e}") + except Exception as broadcast_err: + logger.debug(f"Failed to broadcast task status: {broadcast_err}") return {"status": "failed", "output": str(e), "error": str(e)} @@ -411,7 +426,9 @@ def _create_test_file(self, test_name: str, test_code: str) -> Path: return test_file - async def _run_and_check_linting(self, task: Task, test_file: Path, project_id: int) -> None: + async def _run_and_check_linting( + self, task: Dict[str, Any], test_file: Path, project_id: int + ) -> None: """ Run linting on test file and create blocker if critical errors found (T113). @@ -419,13 +436,15 @@ async def _run_and_check_linting(self, task: Task, test_file: Path, project_id: stores results in the database, and creates a blocker if critical errors are found. Args: - task: Task object + task: Task dictionary with id, project_id, task_number, title, description, etc. test_file: Path to created test file project_id: Project ID for broadcasts Raises: ValueError: If linting fails with critical errors (blocker created) """ + task_id = task["id"] + try: from codeframe.testing.lint_runner import LintRunner from codeframe.lib.lint_utils import format_lint_blocker @@ -434,7 +453,7 @@ async def _run_and_check_linting(self, task: Task, test_file: Path, project_id: # Initialize LintRunner lint_runner = LintRunner(self.project_root) - logger.info(f"Running linting on test file {test_file} for task {task.id}") + logger.info(f"Running linting on test file {test_file} for task {task_id}") # Run linting on test file lint_results = await lint_runner.run_lint([test_file]) @@ -443,7 +462,7 @@ async def _run_and_check_linting(self, task: Task, test_file: Path, project_id: if self.db: for result in lint_results: self.db.create_lint_result( - task_id=task.id, + task_id=task_id, linter=result.linter, error_count=result.error_count, warning_count=result.warning_count, @@ -463,10 +482,10 @@ async def _run_and_check_linting(self, task: Task, test_file: Path, project_id: blocker_type="SYNC", title=f"Linting failed: {total_errors} critical errors", description=blocker_description, - blocking_task_id=task.id, + blocking_task_id=task_id, ) - logger.error(f"Task {task.id} blocked by {total_errors} lint errors") + logger.error(f"Task {task_id} blocked by {total_errors} lint errors") # Broadcast lint failure via WebSocket (T119) if self.websocket_manager: @@ -478,7 +497,7 @@ async def _run_and_check_linting(self, task: Task, test_file: Path, project_id: project_id, { "type": "lint_failed", - "task_id": task.id, + "task_id": task_id, "error_count": total_errors, "timestamp": datetime.utcnow().isoformat(), }, @@ -491,7 +510,7 @@ async def _run_and_check_linting(self, task: Task, test_file: Path, project_id: # Log warnings (non-blocking) total_warnings = sum(r.warning_count for r in lint_results) if total_warnings > 0: - logger.warning(f"Task {task.id}: {total_warnings} lint warnings (non-blocking)") + logger.warning(f"Task {task_id}: {total_warnings} lint warnings (non-blocking)") # Broadcast lint success via WebSocket (T119) if self.websocket_manager: @@ -503,7 +522,7 @@ async def _run_and_check_linting(self, task: Task, test_file: Path, project_id: project_id, { "type": "lint_completed", - "task_id": task.id, + "task_id": task_id, "error_count": 0, "warning_count": total_warnings, "timestamp": datetime.utcnow().isoformat(), diff --git a/tests/agents/test_frontend_worker_agent.py b/tests/agents/test_frontend_worker_agent.py index 68bb0f0c..5a2cc0b0 100644 --- a/tests/agents/test_frontend_worker_agent.py +++ b/tests/agents/test_frontend_worker_agent.py @@ -8,7 +8,7 @@ from anthropic.types import Message, TextBlock from codeframe.agents.frontend_worker_agent import FrontendWorkerAgent -from codeframe.core.models import Task, AgentMaturity +from codeframe.core.models import AgentMaturity @pytest.fixture @@ -42,15 +42,25 @@ def mock_websocket_manager(): @pytest.fixture def sample_task(): - """Create sample task for testing.""" - return Task( - id=1, - title="Create UserCard component", - description="Create a UserCard component that displays user information", - status="pending", - priority=1, - workflow_step=1, - ) + """Create sample task dict for testing (matches LeadAgent's task.to_dict() output).""" + return { + "id": 1, + "project_id": 1, + "issue_id": 1, + "task_number": "T-001", + "parent_issue_number": "I-001", + "title": "Create UserCard component", + "description": "Create a UserCard component that displays user information", + "status": "pending", + "assigned_to": None, + "depends_on": "", + "can_parallelize": False, + "priority": 1, + "workflow_step": 1, + "requires_mcp": False, + "estimated_tokens": 0, + "actual_tokens": None, + } class TestFrontendWorkerAgentInitialization: @@ -329,20 +339,25 @@ async def test_execute_task_with_websocket_broadcasts( @pytest.mark.asyncio async def test_execute_task_json_spec(self, frontend_agent): """Test task execution with JSON specification.""" - json_task = Task( - id=2, - title="Create Button component", - description=json.dumps( + json_task = { + "id": 2, + "project_id": 1, + "issue_id": 1, + "task_number": "T-002", + "parent_issue_number": "I-001", + "title": "Create Button component", + "description": json.dumps( { "name": "Button", "description": "Reusable button component", "generate_types": False, } ), - status="pending", - priority=1, - workflow_step=1, - ) + "status": "pending", + "assigned_to": None, + "priority": 1, + "workflow_step": 1, + } result = await frontend_agent.execute_task(json_task, project_id=1) @@ -357,14 +372,19 @@ async def test_execute_task_json_spec(self, frontend_agent): async def test_execute_task_error_handling(self, frontend_agent): """Test task execution handles errors gracefully.""" # Create task with invalid spec that will cause error - invalid_task = Task( - id=3, - title="Invalid task", - description="Component: Name>", # Invalid component name - status="pending", - priority=1, - workflow_step=1, - ) + invalid_task = { + "id": 3, + "project_id": 1, + "issue_id": 1, + "task_number": "T-003", + "parent_issue_number": "I-001", + "title": "Invalid task", + "description": "Component: Name>", # Invalid component name + "status": "pending", + "assigned_to": None, + "priority": 1, + "workflow_step": 1, + } # Mock _create_component_files to raise error original_method = frontend_agent._create_component_files @@ -422,14 +442,19 @@ async def test_handle_file_already_exists(self, frontend_agent): existing_file = frontend_agent.components_dir / "Existing.tsx" existing_file.write_text("original content") - task = Task( - id=4, - title="Create Existing component", - description="Component: Existing", - status="pending", - priority=1, - workflow_step=1, - ) + task = { + "id": 4, + "project_id": 1, + "issue_id": 1, + "task_number": "T-004", + "parent_issue_number": "I-001", + "title": "Create Existing component", + "description": "Component: Existing", + "status": "pending", + "assigned_to": None, + "priority": 1, + "workflow_step": 1, + } result = await frontend_agent.execute_task(task, project_id=1) diff --git a/tests/agents/test_frontend_worker_auto_commit.py b/tests/agents/test_frontend_worker_auto_commit.py index 342e1c9d..9b971f99 100644 --- a/tests/agents/test_frontend_worker_auto_commit.py +++ b/tests/agents/test_frontend_worker_auto_commit.py @@ -8,7 +8,7 @@ from unittest.mock import Mock from codeframe.agents.frontend_worker_agent import FrontendWorkerAgent -from codeframe.core.models import Task, AgentMaturity +from codeframe.core.models import AgentMaturity @pytest.fixture @@ -58,25 +58,26 @@ async def test_frontend_worker_commits_after_successful_task( # Arrange frontend_agent.git_workflow = mock_git_workflow - task = Task( - id=10, - issue_id=1, - task_number="cf-2.3.1", - parent_issue_number="cf-2", - title="Create UserProfile component", - description="Build a React component for user profiles", - status="pending", - assigned_to="frontend-001", - depends_on=None, - can_parallelize=True, - priority=1, - workflow_step=5, - requires_mcp=False, - estimated_tokens=2000, - actual_tokens=0, - created_at="2025-01-01T00:00:00", - completed_at=None, - ) + task = { + "id": 10, + "project_id": 1, + "issue_id": 1, + "task_number": "cf-2.3.1", + "parent_issue_number": "cf-2", + "title": "Create UserProfile component", + "description": "Build a React component for user profiles", + "status": "pending", + "assigned_to": "frontend-001", + "depends_on": None, + "can_parallelize": True, + "priority": 1, + "workflow_step": 5, + "requires_mcp": False, + "estimated_tokens": 2000, + "actual_tokens": 0, + "created_at": "2025-01-01T00:00:00", + "completed_at": None, + } # Mock component generation (no API call) frontend_agent.client = None # Force fallback to template @@ -119,25 +120,26 @@ async def test_frontend_worker_no_commit_on_component_conflict( # Arrange frontend_agent.git_workflow = mock_git_workflow - task = Task( - id=11, - issue_id=1, - task_number="cf-2.3.2", - parent_issue_number="cf-2", - title="Create Dashboard component", - description="Duplicate component", - status="pending", - assigned_to="frontend-001", - depends_on=None, - can_parallelize=True, - priority=1, - workflow_step=5, - requires_mcp=False, - estimated_tokens=2000, - actual_tokens=0, - created_at="2025-01-01T00:00:00", - completed_at=None, - ) + task = { + "id": 11, + "project_id": 1, + "issue_id": 1, + "task_number": "cf-2.3.2", + "parent_issue_number": "cf-2", + "title": "Create Dashboard component", + "description": "Duplicate component", + "status": "pending", + "assigned_to": "frontend-001", + "depends_on": None, + "can_parallelize": True, + "priority": 1, + "workflow_step": 5, + "requires_mcp": False, + "estimated_tokens": 2000, + "actual_tokens": 0, + "created_at": "2025-01-01T00:00:00", + "completed_at": None, + } # Create conflicting file - use the actual component name from the template (frontend_agent.components_dir / "NewComponent.tsx").write_text("// existing") diff --git a/tests/agents/test_test_worker_agent.py b/tests/agents/test_test_worker_agent.py index b543e0bd..0acef976 100644 --- a/tests/agents/test_test_worker_agent.py +++ b/tests/agents/test_test_worker_agent.py @@ -7,7 +7,6 @@ from anthropic.types import Message, TextBlock from codeframe.agents.test_worker_agent import TestWorkerAgent -from codeframe.core.models import Task @pytest.fixture @@ -29,15 +28,25 @@ def test_agent(temp_tests_dir): @pytest.fixture def sample_task(): - """Create sample task for testing.""" - return Task( - id=1, - title="Create tests for UserService", - description="Generate tests for codeframe/services/user_service.py", - status="pending", - priority=1, - workflow_step=1, - ) + """Create sample task dict for testing (matches LeadAgent's task.to_dict() output).""" + return { + "id": 1, + "project_id": 1, + "issue_id": 1, + "task_number": "T-001", + "parent_issue_number": "I-001", + "title": "Create tests for UserService", + "description": "Generate tests for codeframe/services/user_service.py", + "status": "pending", + "assigned_to": None, + "depends_on": "", + "can_parallelize": False, + "priority": 1, + "workflow_step": 1, + "requires_mcp": False, + "estimated_tokens": 0, + "actual_tokens": None, + } class TestTestWorkerAgentInitialization: @@ -388,7 +397,7 @@ async def test_broadcast_test_result(self, test_agent, sample_task): counts = {"passed": 5, "failed": 1, "errors": 0, "total": 6} # This will attempt broadcast but gracefully handle no event loop - await test_agent._broadcast_test_result(1, sample_task.id, counts, False) + await test_agent._broadcast_test_result(1, sample_task["id"], counts, False) # In async context, it should broadcast results diff --git a/tests/agents/test_test_worker_auto_commit.py b/tests/agents/test_test_worker_auto_commit.py index e8a0e619..8acdab67 100644 --- a/tests/agents/test_test_worker_auto_commit.py +++ b/tests/agents/test_test_worker_auto_commit.py @@ -8,7 +8,7 @@ from unittest.mock import Mock from codeframe.agents.test_worker_agent import TestWorkerAgent -from codeframe.core.models import Task, AgentMaturity +from codeframe.core.models import AgentMaturity @pytest.fixture @@ -57,25 +57,26 @@ async def test_test_worker_commits_after_successful_task( # Arrange test_agent.git_workflow = mock_git_workflow - task = Task( - id=20, - issue_id=1, - task_number="cf-3.4.1", - parent_issue_number="cf-3", - title="Generate tests for auth module", - description='{"test_name": "test_auth", "target_file": "auth.py"}', - status="pending", - assigned_to="test-001", - depends_on=None, - can_parallelize=True, - priority=1, - workflow_step=8, - requires_mcp=False, - estimated_tokens=3000, - actual_tokens=0, - created_at="2025-01-01T00:00:00", - completed_at=None, - ) + task = { + "id": 20, + "project_id": 1, + "issue_id": 1, + "task_number": "cf-3.4.1", + "parent_issue_number": "cf-3", + "title": "Generate tests for auth module", + "description": '{"test_name": "test_auth", "target_file": "auth.py"}', + "status": "pending", + "assigned_to": "test-001", + "depends_on": None, + "can_parallelize": True, + "priority": 1, + "workflow_step": 8, + "requires_mcp": False, + "estimated_tokens": 3000, + "actual_tokens": 0, + "created_at": "2025-01-01T00:00:00", + "completed_at": None, + } # Mock test execution to pass immediately def mock_execute_tests(test_file): @@ -126,25 +127,26 @@ async def test_test_worker_commits_even_after_test_correction( test_agent.git_workflow = mock_git_workflow test_agent.max_correction_attempts = 2 - task = Task( - id=21, - issue_id=1, - task_number="cf-3.4.2", - parent_issue_number="cf-3", - title="Generate tests with correction", - description='{"test_name": "test_complex", "target_file": "complex.py"}', - status="pending", - assigned_to="test-001", - depends_on=None, - can_parallelize=True, - priority=1, - workflow_step=8, - requires_mcp=False, - estimated_tokens=3000, - actual_tokens=0, - created_at="2025-01-01T00:00:00", - completed_at=None, - ) + task = { + "id": 21, + "project_id": 1, + "issue_id": 1, + "task_number": "cf-3.4.2", + "parent_issue_number": "cf-3", + "title": "Generate tests with correction", + "description": '{"test_name": "test_complex", "target_file": "complex.py"}', + "status": "pending", + "assigned_to": "test-001", + "depends_on": None, + "can_parallelize": True, + "priority": 1, + "workflow_step": 8, + "requires_mcp": False, + "estimated_tokens": 3000, + "actual_tokens": 0, + "created_at": "2025-01-01T00:00:00", + "completed_at": None, + } # Mock tests to fail first, then pass attempt_count = 0 diff --git a/tests/agents/test_worker_task_dict_interface.py b/tests/agents/test_worker_task_dict_interface.py new file mode 100644 index 00000000..cdf886e9 --- /dev/null +++ b/tests/agents/test_worker_task_dict_interface.py @@ -0,0 +1,376 @@ +""" +Tests for worker agent task dictionary interface standardization. + +This test module verifies that: +1. LeadAgent passes complete task dictionaries to worker agents (using task.to_dict()) +2. FrontendWorkerAgent accepts task dictionaries (not Task objects) +3. TestWorkerAgent accepts task dictionaries (not Task objects) + +This standardizes on dictionary input to match BackendWorkerAgent and ReviewWorkerAgent. + +Issue: Interface mismatch between LeadAgent and worker agents - minimal dict with only +id/title/description was being passed, missing fields like project_id, task_number. +""" + +import pytest +from unittest.mock import Mock, AsyncMock, patch +from pathlib import Path + +from codeframe.core.models import Task, TaskStatus + + +class TestLeadAgentTaskDictCreation: + """Tests for LeadAgent creating complete task dictionaries.""" + + @pytest.mark.asyncio + async def test_assign_and_execute_task_passes_complete_task_dict( + self, tmp_path: Path + ): + """Test that _assign_and_execute_task passes complete task dict to agents. + + The task dict should include all fields from Task.to_dict(), not just + id/title/description. + """ + from codeframe.agents.lead_agent import LeadAgent + from codeframe.persistence.database import Database + + # Setup database + db = Database(":memory:") + db.initialize() + + project_id = db.create_project( + name="dict-test-project", + description="Test task dict creation", + source_type="empty", + workspace_path=str(tmp_path), + ) + + issue_id = db.create_issue({ + "project_id": project_id, + "issue_number": "TD-001", + "title": "Test Issue", + "description": "Test issue", + "priority": 1, + "workflow_step": 1, + }) + + task_id = db.create_task_with_issue( + project_id=project_id, + issue_id=issue_id, + task_number="TD-001-1", + parent_issue_number="TD-001", + title="Test Task", + description="Test task description", + status=TaskStatus.PENDING, + priority=2, + workflow_step=5, + can_parallelize=False, + ) + + # Create LeadAgent with mocked pool + with patch("codeframe.agents.lead_agent.AgentPoolManager") as mock_pool_class: + mock_pool = Mock() + mock_pool.get_or_create_agent.return_value = "backend-001" + mock_pool.mark_agent_busy.return_value = None + mock_pool.mark_agent_idle.return_value = None + + # Capture the task dict passed to execute_task + captured_task_dict = None + + async def capture_execute_task(task_dict): + nonlocal captured_task_dict + captured_task_dict = task_dict + return {"status": "completed"} + + mock_agent = Mock() + mock_agent.execute_task = capture_execute_task + mock_pool.get_agent_instance.return_value = mock_agent + mock_pool_class.return_value = mock_pool + + lead_agent = LeadAgent( + project_id=project_id, + db=db, + api_key="sk-ant-test-key", + ws_manager=None, + ) + lead_agent.agent_pool_manager = mock_pool + + # Mock review agent + with patch.object(lead_agent.agent_pool_manager, "get_or_create_agent") as mock_get: + mock_get.side_effect = ["backend-001", "review-001"] + + mock_review = Mock() + mock_review_report = Mock() + mock_review_report.status = "approved" + mock_review_report.overall_score = 9.0 + mock_review.execute_task = AsyncMock(return_value=mock_review_report) + + def get_instance(agent_id): + if "review" in agent_id: + return mock_review + return mock_agent + + mock_pool.get_agent_instance.side_effect = get_instance + + task = db.get_task(task_id) + await lead_agent._assign_and_execute_task(task, {}) + + # CRITICAL: Verify all required fields are present + assert captured_task_dict is not None, "execute_task was not called" + + # Core fields that must be present + assert "id" in captured_task_dict + assert "project_id" in captured_task_dict, \ + "project_id missing from task dict (needed for git workflow)" + assert "task_number" in captured_task_dict, \ + "task_number missing from task dict (needed for git workflow)" + assert "title" in captured_task_dict + assert "description" in captured_task_dict + + # Additional fields from to_dict() + assert "issue_id" in captured_task_dict + assert "parent_issue_number" in captured_task_dict + assert "status" in captured_task_dict + assert "priority" in captured_task_dict + assert "workflow_step" in captured_task_dict + + # Verify values + assert captured_task_dict["id"] == task_id + assert captured_task_dict["project_id"] == project_id + assert captured_task_dict["task_number"] == "TD-001-1" + + +class TestFrontendWorkerAgentDictInterface: + """Tests for FrontendWorkerAgent accepting dictionary input.""" + + @pytest.fixture + def temp_web_ui_dir(self, tmp_path): + """Create temporary web-ui directory structure.""" + web_ui = tmp_path / "web-ui" + components_dir = web_ui / "src" / "components" + components_dir.mkdir(parents=True) + return web_ui + + @pytest.fixture + def frontend_agent(self, temp_web_ui_dir): + """Create FrontendWorkerAgent for testing.""" + from codeframe.agents.frontend_worker_agent import FrontendWorkerAgent + + agent = FrontendWorkerAgent( + agent_id="frontend-dict-test-001", + provider="anthropic", + api_key="test-key", + ) + agent.web_ui_root = temp_web_ui_dir + agent.components_dir = temp_web_ui_dir / "src" / "components" + return agent + + @pytest.mark.asyncio + async def test_execute_task_accepts_dict_input(self, frontend_agent): + """Test that execute_task accepts a dictionary instead of Task object.""" + # Create task as dictionary (matching what LeadAgent will pass) + task_dict = { + "id": 42, + "project_id": 1, + "issue_id": 10, + "task_number": "FE-001-1", + "parent_issue_number": "FE-001", + "title": "Create TestComponent", + "description": '{"name": "TestComponent", "description": "A test component"}', + "status": "pending", + "assigned_to": None, + "depends_on": "", + "can_parallelize": False, + "priority": 2, + "workflow_step": 5, + "requires_mcp": False, + "estimated_tokens": 0, + "actual_tokens": None, + } + + # Mock the Claude API call + with patch.object(frontend_agent, "_generate_react_component") as mock_gen: + mock_gen.return_value = "export const TestComponent = () =>
Test
" + + # This should not raise AttributeError for task.id, task.title, etc. + result = await frontend_agent.execute_task(task_dict) + + assert result["status"] in ("completed", "failed") + + @pytest.mark.asyncio + async def test_execute_task_dict_access_patterns(self, frontend_agent): + """Test that all dictionary access patterns work correctly.""" + task_dict = { + "id": 99, + "project_id": 5, + "issue_id": 20, + "task_number": "FE-002-1", + "parent_issue_number": "FE-002", + "title": "Create Another Component", + "description": '{"name": "Another", "description": "Another component"}', + "status": "pending", + "assigned_to": None, + "depends_on": "", + "can_parallelize": False, + "priority": 1, + "workflow_step": 3, + "requires_mcp": False, + "estimated_tokens": 1000, + "actual_tokens": None, + } + + with patch.object(frontend_agent, "_generate_react_component") as mock_gen: + mock_gen.return_value = "export const Another = () =>
Another
" + + # The method should access task["id"], task["title"], etc. + # instead of task.id, task.title (which would fail for dict) + result = await frontend_agent.execute_task(task_dict) + + # Verify no AttributeError occurred + assert "status" in result + + +class TestTestWorkerAgentDictInterface: + """Tests for TestWorkerAgent accepting dictionary input.""" + + @pytest.fixture + def temp_tests_dir(self, tmp_path): + """Create temporary tests directory.""" + tests_dir = tmp_path / "tests" + tests_dir.mkdir() + return tests_dir + + @pytest.fixture + def test_agent(self, temp_tests_dir): + """Create TestWorkerAgent for testing.""" + from codeframe.agents.test_worker_agent import TestWorkerAgent + + agent = TestWorkerAgent( + agent_id="test-dict-test-001", + provider="anthropic", + api_key="test-key", + ) + agent.tests_dir = temp_tests_dir + agent.project_root = temp_tests_dir.parent + return agent + + @pytest.mark.asyncio + async def test_execute_task_accepts_dict_input(self, test_agent): + """Test that execute_task accepts a dictionary instead of Task object.""" + # Create task as dictionary (matching what LeadAgent will pass) + task_dict = { + "id": 55, + "project_id": 2, + "issue_id": 15, + "task_number": "TE-001-1", + "parent_issue_number": "TE-001", + "title": "Create tests for UserService", + "description": '{"test_name": "test_user", "target_file": "services/user.py"}', + "status": "pending", + "assigned_to": None, + "depends_on": "", + "can_parallelize": False, + "priority": 2, + "workflow_step": 7, + "requires_mcp": False, + "estimated_tokens": 500, + "actual_tokens": None, + } + + # Mock the Claude API call and test execution + with patch.object(test_agent, "_generate_pytest_tests") as mock_gen: + mock_gen.return_value = "def test_user(): pass" + + with patch.object(test_agent, "_execute_and_correct_tests") as mock_exec: + mock_exec.return_value = { + "passed": True, + "passed_count": 1, + "total_count": 1, + } + + # This should not raise AttributeError for task.id, etc. + result = await test_agent.execute_task(task_dict) + + assert result["status"] in ("completed", "failed") + + @pytest.mark.asyncio + async def test_execute_task_dict_access_patterns(self, test_agent): + """Test that all dictionary access patterns work correctly.""" + task_dict = { + "id": 77, + "project_id": 3, + "issue_id": 25, + "task_number": "TE-002-1", + "parent_issue_number": "TE-002", + "title": "Create tests for AuthService", + "description": '{"test_name": "test_auth", "target_file": "services/auth.py"}', + "status": "pending", + "assigned_to": None, + "depends_on": "", + "can_parallelize": False, + "priority": 1, + "workflow_step": 7, + "requires_mcp": False, + "estimated_tokens": 800, + "actual_tokens": None, + } + + with patch.object(test_agent, "_generate_pytest_tests") as mock_gen: + mock_gen.return_value = "def test_auth(): pass" + + with patch.object(test_agent, "_execute_and_correct_tests") as mock_exec: + mock_exec.return_value = { + "passed": True, + "passed_count": 1, + "total_count": 1, + } + + result = await test_agent.execute_task(task_dict) + + # Verify no AttributeError occurred + assert "status" in result + + +class TestTaskToDictMethod: + """Tests to verify Task.to_dict() produces the expected format.""" + + def test_task_to_dict_includes_all_required_fields(self): + """Verify Task.to_dict() includes all fields needed by worker agents.""" + task = Task( + id=1, + project_id=10, + issue_id=5, + task_number="T-001", + parent_issue_number="I-001", + title="Test Task", + description="Test description", + status=TaskStatus.PENDING, + assigned_to="agent-001", + depends_on="T-000", + can_parallelize=True, + priority=2, + workflow_step=3, + requires_mcp=True, + estimated_tokens=500, + actual_tokens=100, + ) + + task_dict = task.to_dict() + + # All worker agents need these fields + assert task_dict["id"] == 1 + assert task_dict["project_id"] == 10 + assert task_dict["issue_id"] == 5 + assert task_dict["task_number"] == "T-001" + assert task_dict["parent_issue_number"] == "I-001" + assert task_dict["title"] == "Test Task" + assert task_dict["description"] == "Test description" + assert task_dict["status"] == "pending" # String, not enum + assert task_dict["assigned_to"] == "agent-001" + assert task_dict["depends_on"] == "T-000" + assert task_dict["can_parallelize"] is True + assert task_dict["priority"] == 2 + assert task_dict["workflow_step"] == 3 + assert task_dict["requires_mcp"] is True + assert task_dict["estimated_tokens"] == 500 + assert task_dict["actual_tokens"] == 100