# tests/tasks/test_workflow_tasks.py """ Tests for workflow state management tasks. These tests verify: - Task signatures are correctly defined - Tasks are bound (have access to self) - Tasks return expected structure - Tasks follow ADR-007 (transitions) and ADR-010 (PostgreSQL durability) Note: These tests mock actual execution since they would require database access and state machine operations in production. """ import uuid from unittest.mock import patch class TestRecoverStaleWorkflowsTask: """Tests for the recover_stale_workflows task.""" def test_recover_stale_workflows_task_exists(self): """Test that recover_stale_workflows task is registered.""" import app.tasks.workflow # noqa: F401 from app.celery_app import celery_app assert "app.tasks.workflow.recover_stale_workflows" in celery_app.tasks def test_recover_stale_workflows_is_bound_task(self): """Test that recover_stale_workflows is a bound task.""" from app.tasks.workflow import recover_stale_workflows assert recover_stale_workflows.__bound__ is True def test_recover_stale_workflows_has_correct_name(self): """Test that recover_stale_workflows has the correct task name.""" from app.tasks.workflow import recover_stale_workflows assert ( recover_stale_workflows.name == "app.tasks.workflow.recover_stale_workflows" ) def test_recover_stale_workflows_returns_expected_structure(self): """Test that recover_stale_workflows returns expected result.""" from app.tasks.workflow import recover_stale_workflows result = recover_stale_workflows() assert isinstance(result, dict) assert "status" in result assert "recovered" in result assert result["status"] == "pending" assert result["recovered"] == 0 class TestExecuteWorkflowStepTask: """Tests for the execute_workflow_step task.""" def test_execute_workflow_step_task_exists(self): """Test that execute_workflow_step task is registered.""" import app.tasks.workflow # noqa: F401 from app.celery_app import celery_app assert "app.tasks.workflow.execute_workflow_step" in celery_app.tasks def test_execute_workflow_step_is_bound_task(self): """Test that execute_workflow_step is a bound task.""" from app.tasks.workflow import execute_workflow_step assert execute_workflow_step.__bound__ is True def test_execute_workflow_step_returns_expected_structure(self): """Test that execute_workflow_step returns expected result.""" from app.tasks.workflow import execute_workflow_step workflow_id = str(uuid.uuid4()) transition = "start_planning" result = execute_workflow_step(workflow_id, transition) assert isinstance(result, dict) assert "status" in result assert "workflow_id" in result assert "transition" in result assert result["workflow_id"] == workflow_id assert result["transition"] == transition def test_execute_workflow_step_with_various_transitions(self): """Test that execute_workflow_step handles various transition types.""" from app.tasks.workflow import execute_workflow_step workflow_id = str(uuid.uuid4()) transitions = [ "start", "complete_planning", "begin_implementation", "request_approval", "approve", "reject", "complete", ] for transition in transitions: result = execute_workflow_step(workflow_id, transition) assert result["transition"] == transition class TestHandleApprovalResponseTask: """Tests for the handle_approval_response task.""" def test_handle_approval_response_task_exists(self): """Test that handle_approval_response task is registered.""" import app.tasks.workflow # noqa: F401 from app.celery_app import celery_app assert "app.tasks.workflow.handle_approval_response" in celery_app.tasks def test_handle_approval_response_is_bound_task(self): """Test that handle_approval_response is a bound task.""" from app.tasks.workflow import handle_approval_response assert handle_approval_response.__bound__ is True def test_handle_approval_response_returns_expected_structure(self): """Test that handle_approval_response returns expected result.""" from app.tasks.workflow import handle_approval_response workflow_id = str(uuid.uuid4()) approved = True comment = "LGTM! Proceeding with deployment." result = handle_approval_response(workflow_id, approved, comment) assert isinstance(result, dict) assert "status" in result assert "workflow_id" in result assert "approved" in result assert result["workflow_id"] == workflow_id assert result["approved"] == approved def test_handle_approval_response_with_rejection(self): """Test that handle_approval_response handles rejection.""" from app.tasks.workflow import handle_approval_response workflow_id = str(uuid.uuid4()) result = handle_approval_response( workflow_id, approved=False, comment="Needs more test coverage" ) assert result["approved"] is False def test_handle_approval_response_without_comment(self): """Test that handle_approval_response handles missing comment.""" from app.tasks.workflow import handle_approval_response workflow_id = str(uuid.uuid4()) result = handle_approval_response(workflow_id, approved=True) assert result["status"] == "pending" class TestStartSprintWorkflowTask: """Tests for the start_sprint_workflow task.""" def test_start_sprint_workflow_task_exists(self): """Test that start_sprint_workflow task is registered.""" import app.tasks.workflow # noqa: F401 from app.celery_app import celery_app assert "app.tasks.workflow.start_sprint_workflow" in celery_app.tasks def test_start_sprint_workflow_is_bound_task(self): """Test that start_sprint_workflow is a bound task.""" from app.tasks.workflow import start_sprint_workflow assert start_sprint_workflow.__bound__ is True def test_start_sprint_workflow_returns_expected_structure(self): """Test that start_sprint_workflow returns expected result.""" from app.tasks.workflow import start_sprint_workflow project_id = str(uuid.uuid4()) sprint_id = str(uuid.uuid4()) result = start_sprint_workflow(project_id, sprint_id) assert isinstance(result, dict) assert "status" in result assert "sprint_id" in result assert result["sprint_id"] == sprint_id class TestStartStoryWorkflowTask: """Tests for the start_story_workflow task.""" def test_start_story_workflow_task_exists(self): """Test that start_story_workflow task is registered.""" import app.tasks.workflow # noqa: F401 from app.celery_app import celery_app assert "app.tasks.workflow.start_story_workflow" in celery_app.tasks def test_start_story_workflow_is_bound_task(self): """Test that start_story_workflow is a bound task.""" from app.tasks.workflow import start_story_workflow assert start_story_workflow.__bound__ is True def test_start_story_workflow_returns_expected_structure(self): """Test that start_story_workflow returns expected result.""" from app.tasks.workflow import start_story_workflow project_id = str(uuid.uuid4()) story_id = str(uuid.uuid4()) result = start_story_workflow(project_id, story_id) assert isinstance(result, dict) assert "status" in result assert "story_id" in result assert result["story_id"] == story_id class TestWorkflowTaskRouting: """Tests for workflow task queue routing.""" def test_workflow_tasks_route_to_default_queue(self): """Test that workflow tasks route to 'default' queue. Per the routing configuration, workflow tasks match 'app.tasks.*' which routes to the default queue. """ from app.celery_app import celery_app routes = celery_app.conf.task_routes # Workflow tasks match the generic 'app.tasks.*' pattern # since there's no specific 'app.tasks.workflow.*' route assert "app.tasks.*" in routes assert routes["app.tasks.*"]["queue"] == "default" def test_all_workflow_tasks_match_routing_pattern(self): """Test that all workflow task names match the routing pattern.""" task_names = [ "app.tasks.workflow.recover_stale_workflows", "app.tasks.workflow.execute_workflow_step", "app.tasks.workflow.handle_approval_response", "app.tasks.workflow.start_sprint_workflow", "app.tasks.workflow.start_story_workflow", ] for name in task_names: assert name.startswith("app.tasks.") class TestWorkflowTaskLogging: """Tests for workflow task logging behavior.""" def test_recover_stale_workflows_logs_execution(self): """Test that recover_stale_workflows logs when executed.""" from app.tasks.workflow import recover_stale_workflows with patch("app.tasks.workflow.logger") as mock_logger: recover_stale_workflows() mock_logger.info.assert_called_once() call_args = mock_logger.info.call_args[0][0] assert "stale" in call_args.lower() or "recover" in call_args.lower() def test_execute_workflow_step_logs_execution(self): """Test that execute_workflow_step logs when executed.""" from app.tasks.workflow import execute_workflow_step workflow_id = str(uuid.uuid4()) transition = "start_planning" with patch("app.tasks.workflow.logger") as mock_logger: execute_workflow_step(workflow_id, transition) mock_logger.info.assert_called_once() call_args = mock_logger.info.call_args[0][0] assert transition in call_args assert workflow_id in call_args def test_handle_approval_response_logs_execution(self): """Test that handle_approval_response logs when executed.""" from app.tasks.workflow import handle_approval_response workflow_id = str(uuid.uuid4()) with patch("app.tasks.workflow.logger") as mock_logger: handle_approval_response(workflow_id, approved=True) mock_logger.info.assert_called_once() call_args = mock_logger.info.call_args[0][0] assert workflow_id in call_args def test_start_sprint_workflow_logs_execution(self): """Test that start_sprint_workflow logs when executed.""" from app.tasks.workflow import start_sprint_workflow project_id = str(uuid.uuid4()) sprint_id = str(uuid.uuid4()) with patch("app.tasks.workflow.logger") as mock_logger: start_sprint_workflow(project_id, sprint_id) mock_logger.info.assert_called_once() call_args = mock_logger.info.call_args[0][0] assert sprint_id in call_args class TestWorkflowTaskSignatures: """Tests for workflow task signature creation.""" def test_execute_workflow_step_signature_creation(self): """Test that execute_workflow_step signature can be created.""" from app.tasks.workflow import execute_workflow_step workflow_id = str(uuid.uuid4()) transition = "approve" sig = execute_workflow_step.s(workflow_id, transition) assert sig is not None assert sig.args == (workflow_id, transition) def test_workflow_chain_creation(self): """Test that workflow tasks can be chained together.""" from celery import chain from app.tasks.workflow import ( start_sprint_workflow, ) project_id = str(uuid.uuid4()) sprint_id = str(uuid.uuid4()) str(uuid.uuid4()) # Build a chain (doesn't execute, just creates the workflow) workflow = chain( start_sprint_workflow.s(project_id, sprint_id), # In reality, these would use results from previous tasks ) assert workflow is not None