feat(backend): Add Celery worker infrastructure with task stubs

- Add Celery app configuration with Redis broker/backend
- Add task modules: agent, workflow, cost, git, sync
- Add task stubs for:
  - Agent execution (spawn, heartbeat, terminate)
  - Workflow orchestration (start sprint, checkpoint, code review)
  - Cost tracking (record usage, calculate, generate report)
  - Git operations (clone, commit, push, sync)
  - External sync (import issues, export updates)
- Add task tests directory structure
- Configure for production-ready Celery setup

Implements #18

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-30 02:08:14 +01:00
parent acfda1e9a9
commit 11da0d57a8
14 changed files with 3149 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
# tests/tasks/__init__.py
"""
Tests for Celery background tasks.
This module tests the Celery configuration and all task modules:
- agent: Agent execution tasks
- git: Git operation tasks
- sync: Issue synchronization tasks
- workflow: Workflow state management tasks
- cost: Cost tracking and aggregation tasks
"""

View File

@@ -0,0 +1,358 @@
# tests/tasks/test_agent_tasks.py
"""
Tests for agent execution tasks.
These tests verify:
- Task signatures are correctly defined
- Tasks are bound (have access to self)
- Tasks return expected structure
- Tasks handle various input scenarios
Note: These tests mock actual execution since they would require
LLM calls and database access in production.
"""
import pytest
from unittest.mock import patch, MagicMock
import uuid
class TestRunAgentStepTask:
"""Tests for the run_agent_step task."""
def test_run_agent_step_task_exists(self):
"""Test that run_agent_step task is registered."""
from app.celery_app import celery_app
import app.tasks.agent # noqa: F401
assert "app.tasks.agent.run_agent_step" in celery_app.tasks
def test_run_agent_step_is_bound_task(self):
"""Test that run_agent_step is a bound task (has access to self)."""
from app.tasks.agent import run_agent_step
# Bound tasks have __bound__=True, which means they receive 'self' as first arg
assert run_agent_step.__bound__ is True
def test_run_agent_step_has_correct_name(self):
"""Test that run_agent_step has the correct task name."""
from app.tasks.agent import run_agent_step
assert run_agent_step.name == "app.tasks.agent.run_agent_step"
def test_run_agent_step_returns_expected_structure(self):
"""Test that run_agent_step returns the expected result structure."""
from app.tasks.agent import run_agent_step
agent_instance_id = str(uuid.uuid4())
context = {"messages": [], "tools": []}
# Call the task directly (synchronously for testing)
result = run_agent_step(agent_instance_id, context)
assert isinstance(result, dict)
assert "status" in result
assert "agent_instance_id" in result
assert result["agent_instance_id"] == agent_instance_id
def test_run_agent_step_with_empty_context(self):
"""Test that run_agent_step handles empty context."""
from app.tasks.agent import run_agent_step
agent_instance_id = str(uuid.uuid4())
context = {}
result = run_agent_step(agent_instance_id, context)
assert result["status"] == "pending"
assert result["agent_instance_id"] == agent_instance_id
def test_run_agent_step_with_complex_context(self):
"""Test that run_agent_step handles complex context data."""
from app.tasks.agent import run_agent_step
agent_instance_id = str(uuid.uuid4())
context = {
"messages": [
{"role": "user", "content": "Create a new feature"},
{"role": "assistant", "content": "I will create the feature."},
],
"tools": ["create_file", "edit_file", "run_tests"],
"state": {"current_step": 3, "max_steps": 10},
"metadata": {"project_id": str(uuid.uuid4())},
}
result = run_agent_step(agent_instance_id, context)
assert result["status"] == "pending"
assert result["agent_instance_id"] == agent_instance_id
class TestSpawnAgentTask:
"""Tests for the spawn_agent task."""
def test_spawn_agent_task_exists(self):
"""Test that spawn_agent task is registered."""
from app.celery_app import celery_app
import app.tasks.agent # noqa: F401
assert "app.tasks.agent.spawn_agent" in celery_app.tasks
def test_spawn_agent_is_bound_task(self):
"""Test that spawn_agent is a bound task."""
from app.tasks.agent import spawn_agent
assert spawn_agent.__bound__ is True
def test_spawn_agent_has_correct_name(self):
"""Test that spawn_agent has the correct task name."""
from app.tasks.agent import spawn_agent
assert spawn_agent.name == "app.tasks.agent.spawn_agent"
def test_spawn_agent_returns_expected_structure(self):
"""Test that spawn_agent returns the expected result structure."""
from app.tasks.agent import spawn_agent
agent_type_id = str(uuid.uuid4())
project_id = str(uuid.uuid4())
initial_context = {"goal": "Implement user story"}
result = spawn_agent(agent_type_id, project_id, initial_context)
assert isinstance(result, dict)
assert "status" in result
assert "agent_type_id" in result
assert "project_id" in result
assert result["status"] == "spawned"
assert result["agent_type_id"] == agent_type_id
assert result["project_id"] == project_id
def test_spawn_agent_with_empty_initial_context(self):
"""Test that spawn_agent handles empty initial context."""
from app.tasks.agent import spawn_agent
agent_type_id = str(uuid.uuid4())
project_id = str(uuid.uuid4())
initial_context = {}
result = spawn_agent(agent_type_id, project_id, initial_context)
assert result["status"] == "spawned"
def test_spawn_agent_with_detailed_initial_context(self):
"""Test that spawn_agent handles detailed initial context."""
from app.tasks.agent import spawn_agent
agent_type_id = str(uuid.uuid4())
project_id = str(uuid.uuid4())
initial_context = {
"goal": "Implement authentication",
"constraints": ["Must use JWT", "Must support MFA"],
"assigned_issues": [str(uuid.uuid4()), str(uuid.uuid4())],
"autonomy_level": "MILESTONE",
}
result = spawn_agent(agent_type_id, project_id, initial_context)
assert result["status"] == "spawned"
assert result["agent_type_id"] == agent_type_id
assert result["project_id"] == project_id
class TestTerminateAgentTask:
"""Tests for the terminate_agent task."""
def test_terminate_agent_task_exists(self):
"""Test that terminate_agent task is registered."""
from app.celery_app import celery_app
import app.tasks.agent # noqa: F401
assert "app.tasks.agent.terminate_agent" in celery_app.tasks
def test_terminate_agent_is_bound_task(self):
"""Test that terminate_agent is a bound task."""
from app.tasks.agent import terminate_agent
assert terminate_agent.__bound__ is True
def test_terminate_agent_has_correct_name(self):
"""Test that terminate_agent has the correct task name."""
from app.tasks.agent import terminate_agent
assert terminate_agent.name == "app.tasks.agent.terminate_agent"
def test_terminate_agent_returns_expected_structure(self):
"""Test that terminate_agent returns the expected result structure."""
from app.tasks.agent import terminate_agent
agent_instance_id = str(uuid.uuid4())
reason = "Task completed successfully"
result = terminate_agent(agent_instance_id, reason)
assert isinstance(result, dict)
assert "status" in result
assert "agent_instance_id" in result
assert result["status"] == "terminated"
assert result["agent_instance_id"] == agent_instance_id
def test_terminate_agent_with_error_reason(self):
"""Test that terminate_agent handles error termination reasons."""
from app.tasks.agent import terminate_agent
agent_instance_id = str(uuid.uuid4())
reason = "Error: Budget limit exceeded"
result = terminate_agent(agent_instance_id, reason)
assert result["status"] == "terminated"
assert result["agent_instance_id"] == agent_instance_id
def test_terminate_agent_with_empty_reason(self):
"""Test that terminate_agent handles empty reason string."""
from app.tasks.agent import terminate_agent
agent_instance_id = str(uuid.uuid4())
reason = ""
result = terminate_agent(agent_instance_id, reason)
assert result["status"] == "terminated"
class TestAgentTaskRouting:
"""Tests for agent task queue routing."""
def test_agent_tasks_should_route_to_agent_queue(self):
"""Test that agent tasks are configured to route to 'agent' queue."""
from app.celery_app import celery_app
routes = celery_app.conf.task_routes
agent_route = routes.get("app.tasks.agent.*")
assert agent_route is not None
assert agent_route["queue"] == "agent"
def test_run_agent_step_routing(self):
"""Test that run_agent_step task routes to agent queue."""
from app.tasks.agent import run_agent_step
from app.celery_app import celery_app
# Get the routing configuration for this specific task
task_name = run_agent_step.name
routes = celery_app.conf.task_routes
# The task name matches the pattern "app.tasks.agent.*"
assert task_name.startswith("app.tasks.agent.")
assert "app.tasks.agent.*" in routes
assert routes["app.tasks.agent.*"]["queue"] == "agent"
class TestAgentTaskSignatures:
"""Tests for agent task signature creation (for async invocation)."""
def test_run_agent_step_signature_creation(self):
"""Test that run_agent_step signature can be created."""
from app.tasks.agent import run_agent_step
agent_instance_id = str(uuid.uuid4())
context = {"messages": []}
# Create a signature (delayed task)
sig = run_agent_step.s(agent_instance_id, context)
assert sig is not None
assert sig.args == (agent_instance_id, context)
def test_spawn_agent_signature_creation(self):
"""Test that spawn_agent signature can be created."""
from app.tasks.agent import spawn_agent
agent_type_id = str(uuid.uuid4())
project_id = str(uuid.uuid4())
initial_context = {}
sig = spawn_agent.s(agent_type_id, project_id, initial_context)
assert sig is not None
assert sig.args == (agent_type_id, project_id, initial_context)
def test_terminate_agent_signature_creation(self):
"""Test that terminate_agent signature can be created."""
from app.tasks.agent import terminate_agent
agent_instance_id = str(uuid.uuid4())
reason = "User requested termination"
sig = terminate_agent.s(agent_instance_id, reason)
assert sig is not None
assert sig.args == (agent_instance_id, reason)
def test_agent_task_chain_creation(self):
"""Test that agent tasks can be chained together."""
from celery import chain
from app.tasks.agent import spawn_agent, run_agent_step, terminate_agent
# Create a chain of tasks (this doesn't execute, just builds the chain)
agent_type_id = str(uuid.uuid4())
project_id = str(uuid.uuid4())
agent_instance_id = str(uuid.uuid4())
# Note: In real usage, the chain would pass results between tasks
workflow = chain(
spawn_agent.s(agent_type_id, project_id, {}),
# Further tasks would use the result from spawn_agent
)
assert workflow is not None
class TestAgentTaskLogging:
"""Tests for agent task logging behavior."""
def test_run_agent_step_logs_execution(self):
"""Test that run_agent_step logs when executed."""
from app.tasks.agent import run_agent_step
import logging
agent_instance_id = str(uuid.uuid4())
context = {}
with patch("app.tasks.agent.logger") as mock_logger:
run_agent_step(agent_instance_id, context)
mock_logger.info.assert_called_once()
call_args = mock_logger.info.call_args[0][0]
assert agent_instance_id in call_args
def test_spawn_agent_logs_execution(self):
"""Test that spawn_agent logs when executed."""
from app.tasks.agent import spawn_agent
agent_type_id = str(uuid.uuid4())
project_id = str(uuid.uuid4())
with patch("app.tasks.agent.logger") as mock_logger:
spawn_agent(agent_type_id, project_id, {})
mock_logger.info.assert_called_once()
call_args = mock_logger.info.call_args[0][0]
assert agent_type_id in call_args
assert project_id in call_args
def test_terminate_agent_logs_execution(self):
"""Test that terminate_agent logs when executed."""
from app.tasks.agent import terminate_agent
agent_instance_id = str(uuid.uuid4())
reason = "Test termination"
with patch("app.tasks.agent.logger") as mock_logger:
terminate_agent(agent_instance_id, reason)
mock_logger.info.assert_called_once()
call_args = mock_logger.info.call_args[0][0]
assert agent_instance_id in call_args
assert reason in call_args

View File

@@ -0,0 +1,321 @@
# tests/tasks/test_celery_config.py
"""
Tests for Celery application configuration.
These tests verify:
- Celery app is properly configured
- Queue routing is correctly set up per ADR-003
- Task discovery works for all task modules
- Beat schedule is configured for periodic tasks
"""
import pytest
from unittest.mock import patch, MagicMock
class TestCeleryAppConfiguration:
"""Tests for the Celery application instance configuration."""
def test_celery_app_is_created_with_correct_name(self):
"""Test that the Celery app is created with 'syndarix' as the name."""
from app.celery_app import celery_app
assert celery_app.main == "syndarix"
def test_celery_app_uses_redis_broker(self):
"""Test that Celery is configured to use Redis as the broker."""
from app.celery_app import celery_app
from app.core.config import settings
# The broker URL should match the settings
assert celery_app.conf.broker_url == settings.celery_broker_url
def test_celery_app_uses_redis_backend(self):
"""Test that Celery is configured to use Redis as the result backend."""
from app.celery_app import celery_app
from app.core.config import settings
assert celery_app.conf.result_backend == settings.celery_result_backend
def test_celery_uses_json_serialization(self):
"""Test that Celery is configured to use JSON for serialization."""
from app.celery_app import celery_app
assert celery_app.conf.task_serializer == "json"
assert celery_app.conf.result_serializer == "json"
assert "json" in celery_app.conf.accept_content
def test_celery_uses_utc_timezone(self):
"""Test that Celery is configured to use UTC timezone."""
from app.celery_app import celery_app
assert celery_app.conf.timezone == "UTC"
assert celery_app.conf.enable_utc is True
def test_celery_has_late_ack_enabled(self):
"""Test that late acknowledgment is enabled for task reliability."""
from app.celery_app import celery_app
# Per ADR-003: Late ack for reliability
assert celery_app.conf.task_acks_late is True
assert celery_app.conf.task_reject_on_worker_lost is True
def test_celery_prefetch_multiplier_is_one(self):
"""Test that worker prefetch is set to 1 for fair task distribution."""
from app.celery_app import celery_app
assert celery_app.conf.worker_prefetch_multiplier == 1
def test_celery_result_expiration(self):
"""Test that results expire after 24 hours."""
from app.celery_app import celery_app
# 86400 seconds = 24 hours
assert celery_app.conf.result_expires == 86400
def test_celery_has_time_limits_configured(self):
"""Test that task time limits are configured per ADR-003."""
from app.celery_app import celery_app
# 5 minutes soft limit, 10 minutes hard limit
assert celery_app.conf.task_soft_time_limit == 300
assert celery_app.conf.task_time_limit == 600
def test_celery_broker_connection_retry_enabled(self):
"""Test that broker connection retry is enabled on startup."""
from app.celery_app import celery_app
assert celery_app.conf.broker_connection_retry_on_startup is True
class TestQueueRoutingConfiguration:
"""Tests for Celery queue routing configuration per ADR-003."""
def test_default_queue_is_configured(self):
"""Test that 'default' is set as the default queue."""
from app.celery_app import celery_app
assert celery_app.conf.task_default_queue == "default"
def test_task_routes_are_configured(self):
"""Test that task routes are properly configured."""
from app.celery_app import celery_app
routes = celery_app.conf.task_routes
assert routes is not None
assert isinstance(routes, dict)
def test_agent_tasks_routed_to_agent_queue(self):
"""Test that agent tasks are routed to the 'agent' queue."""
from app.celery_app import celery_app
routes = celery_app.conf.task_routes
assert "app.tasks.agent.*" in routes
assert routes["app.tasks.agent.*"]["queue"] == "agent"
def test_git_tasks_routed_to_git_queue(self):
"""Test that git tasks are routed to the 'git' queue."""
from app.celery_app import celery_app
routes = celery_app.conf.task_routes
assert "app.tasks.git.*" in routes
assert routes["app.tasks.git.*"]["queue"] == "git"
def test_sync_tasks_routed_to_sync_queue(self):
"""Test that sync tasks are routed to the 'sync' queue."""
from app.celery_app import celery_app
routes = celery_app.conf.task_routes
assert "app.tasks.sync.*" in routes
assert routes["app.tasks.sync.*"]["queue"] == "sync"
def test_default_tasks_routed_to_default_queue(self):
"""Test that unmatched tasks are routed to the 'default' queue."""
from app.celery_app import celery_app
routes = celery_app.conf.task_routes
assert "app.tasks.*" in routes
assert routes["app.tasks.*"]["queue"] == "default"
def test_all_queues_are_defined(self):
"""Test that all expected queues are defined in task_queues."""
from app.celery_app import celery_app
queues = celery_app.conf.task_queues
expected_queues = {"agent", "git", "sync", "default"}
assert queues is not None
assert set(queues.keys()) == expected_queues
def test_queue_exchanges_are_configured(self):
"""Test that each queue has its own exchange configured."""
from app.celery_app import celery_app
queues = celery_app.conf.task_queues
for queue_name in ["agent", "git", "sync", "default"]:
assert queue_name in queues
assert queues[queue_name]["exchange"] == queue_name
assert queues[queue_name]["routing_key"] == queue_name
class TestTaskDiscovery:
"""Tests for Celery task auto-discovery."""
def test_task_imports_are_configured(self):
"""Test that task imports are configured for auto-discovery."""
from app.celery_app import celery_app
imports = celery_app.conf.imports
assert imports is not None
assert "app.tasks" in imports
def test_agent_tasks_are_discoverable(self):
"""Test that agent tasks can be discovered and accessed."""
from app.celery_app import celery_app
# Force task registration by importing
import app.tasks.agent # noqa: F401
# Check that agent tasks are registered
registered_tasks = celery_app.tasks
assert "app.tasks.agent.run_agent_step" in registered_tasks
assert "app.tasks.agent.spawn_agent" in registered_tasks
assert "app.tasks.agent.terminate_agent" in registered_tasks
def test_git_tasks_are_discoverable(self):
"""Test that git tasks can be discovered and accessed."""
from app.celery_app import celery_app
# Force task registration by importing
import app.tasks.git # noqa: F401
registered_tasks = celery_app.tasks
assert "app.tasks.git.clone_repository" in registered_tasks
assert "app.tasks.git.commit_changes" in registered_tasks
assert "app.tasks.git.create_branch" in registered_tasks
assert "app.tasks.git.create_pull_request" in registered_tasks
assert "app.tasks.git.push_changes" in registered_tasks
def test_sync_tasks_are_discoverable(self):
"""Test that sync tasks can be discovered and accessed."""
from app.celery_app import celery_app
# Force task registration by importing
import app.tasks.sync # noqa: F401
registered_tasks = celery_app.tasks
assert "app.tasks.sync.sync_issues_incremental" in registered_tasks
assert "app.tasks.sync.sync_issues_full" in registered_tasks
assert "app.tasks.sync.process_webhook_event" in registered_tasks
assert "app.tasks.sync.sync_project_issues" in registered_tasks
assert "app.tasks.sync.push_issue_to_external" in registered_tasks
def test_workflow_tasks_are_discoverable(self):
"""Test that workflow tasks can be discovered and accessed."""
from app.celery_app import celery_app
# Force task registration by importing
import app.tasks.workflow # noqa: F401
registered_tasks = celery_app.tasks
assert "app.tasks.workflow.recover_stale_workflows" in registered_tasks
assert "app.tasks.workflow.execute_workflow_step" in registered_tasks
assert "app.tasks.workflow.handle_approval_response" in registered_tasks
assert "app.tasks.workflow.start_sprint_workflow" in registered_tasks
assert "app.tasks.workflow.start_story_workflow" in registered_tasks
def test_cost_tasks_are_discoverable(self):
"""Test that cost tasks can be discovered and accessed."""
from app.celery_app import celery_app
# Force task registration by importing
import app.tasks.cost # noqa: F401
registered_tasks = celery_app.tasks
assert "app.tasks.cost.aggregate_daily_costs" in registered_tasks
assert "app.tasks.cost.check_budget_thresholds" in registered_tasks
assert "app.tasks.cost.record_llm_usage" in registered_tasks
assert "app.tasks.cost.generate_cost_report" in registered_tasks
assert "app.tasks.cost.reset_daily_budget_counters" in registered_tasks
class TestBeatSchedule:
"""Tests for Celery Beat scheduled tasks configuration."""
def test_beat_schedule_is_configured(self):
"""Test that beat_schedule is configured."""
from app.celery_app import celery_app
assert celery_app.conf.beat_schedule is not None
assert isinstance(celery_app.conf.beat_schedule, dict)
def test_incremental_sync_is_scheduled(self):
"""Test that incremental issue sync is scheduled per ADR-011."""
from app.celery_app import celery_app
schedule = celery_app.conf.beat_schedule
assert "sync-issues-incremental" in schedule
task_config = schedule["sync-issues-incremental"]
assert task_config["task"] == "app.tasks.sync.sync_issues_incremental"
assert task_config["schedule"] == 60.0 # Every 60 seconds
def test_full_sync_is_scheduled(self):
"""Test that full issue sync is scheduled per ADR-011."""
from app.celery_app import celery_app
schedule = celery_app.conf.beat_schedule
assert "sync-issues-full" in schedule
task_config = schedule["sync-issues-full"]
assert task_config["task"] == "app.tasks.sync.sync_issues_full"
assert task_config["schedule"] == 900.0 # Every 15 minutes
def test_stale_workflow_recovery_is_scheduled(self):
"""Test that stale workflow recovery is scheduled per ADR-007."""
from app.celery_app import celery_app
schedule = celery_app.conf.beat_schedule
assert "recover-stale-workflows" in schedule
task_config = schedule["recover-stale-workflows"]
assert task_config["task"] == "app.tasks.workflow.recover_stale_workflows"
assert task_config["schedule"] == 300.0 # Every 5 minutes
def test_daily_cost_aggregation_is_scheduled(self):
"""Test that daily cost aggregation is scheduled per ADR-012."""
from app.celery_app import celery_app
schedule = celery_app.conf.beat_schedule
assert "aggregate-daily-costs" in schedule
task_config = schedule["aggregate-daily-costs"]
assert task_config["task"] == "app.tasks.cost.aggregate_daily_costs"
assert task_config["schedule"] == 3600.0 # Every hour
class TestTaskModuleExports:
"""Tests for the task module __init__.py exports."""
def test_tasks_package_exports_all_modules(self):
"""Test that the tasks package exports all task modules."""
from app import tasks
assert hasattr(tasks, "agent")
assert hasattr(tasks, "git")
assert hasattr(tasks, "sync")
assert hasattr(tasks, "workflow")
assert hasattr(tasks, "cost")
def test_tasks_all_attribute_is_correct(self):
"""Test that __all__ contains all expected module names."""
from app import tasks
expected_modules = ["agent", "git", "sync", "workflow", "cost"]
assert set(tasks.__all__) == set(expected_modules)

View File

@@ -0,0 +1,379 @@
# tests/tasks/test_cost_tasks.py
"""
Tests for cost tracking and budget management tasks.
These tests verify:
- Task signatures are correctly defined
- Tasks are bound (have access to self)
- Tasks return expected structure
- Tasks follow ADR-012 (multi-layered cost tracking)
Note: These tests mock actual execution since they would require
database access and Redis operations in production.
"""
import pytest
from unittest.mock import patch
import uuid
class TestAggregateDailyCostsTask:
"""Tests for the aggregate_daily_costs task."""
def test_aggregate_daily_costs_task_exists(self):
"""Test that aggregate_daily_costs task is registered."""
from app.celery_app import celery_app
import app.tasks.cost # noqa: F401
assert "app.tasks.cost.aggregate_daily_costs" in celery_app.tasks
def test_aggregate_daily_costs_is_bound_task(self):
"""Test that aggregate_daily_costs is a bound task."""
from app.tasks.cost import aggregate_daily_costs
assert aggregate_daily_costs.__bound__ is True
def test_aggregate_daily_costs_has_correct_name(self):
"""Test that aggregate_daily_costs has the correct task name."""
from app.tasks.cost import aggregate_daily_costs
assert aggregate_daily_costs.name == "app.tasks.cost.aggregate_daily_costs"
def test_aggregate_daily_costs_returns_expected_structure(self):
"""Test that aggregate_daily_costs returns expected result."""
from app.tasks.cost import aggregate_daily_costs
result = aggregate_daily_costs()
assert isinstance(result, dict)
assert "status" in result
assert result["status"] == "pending"
class TestCheckBudgetThresholdsTask:
"""Tests for the check_budget_thresholds task."""
def test_check_budget_thresholds_task_exists(self):
"""Test that check_budget_thresholds task is registered."""
from app.celery_app import celery_app
import app.tasks.cost # noqa: F401
assert "app.tasks.cost.check_budget_thresholds" in celery_app.tasks
def test_check_budget_thresholds_is_bound_task(self):
"""Test that check_budget_thresholds is a bound task."""
from app.tasks.cost import check_budget_thresholds
assert check_budget_thresholds.__bound__ is True
def test_check_budget_thresholds_returns_expected_structure(self):
"""Test that check_budget_thresholds returns expected result."""
from app.tasks.cost import check_budget_thresholds
project_id = str(uuid.uuid4())
result = check_budget_thresholds(project_id)
assert isinstance(result, dict)
assert "status" in result
assert "project_id" in result
assert result["project_id"] == project_id
class TestRecordLlmUsageTask:
"""Tests for the record_llm_usage task."""
def test_record_llm_usage_task_exists(self):
"""Test that record_llm_usage task is registered."""
from app.celery_app import celery_app
import app.tasks.cost # noqa: F401
assert "app.tasks.cost.record_llm_usage" in celery_app.tasks
def test_record_llm_usage_is_bound_task(self):
"""Test that record_llm_usage is a bound task."""
from app.tasks.cost import record_llm_usage
assert record_llm_usage.__bound__ is True
def test_record_llm_usage_returns_expected_structure(self):
"""Test that record_llm_usage returns expected result."""
from app.tasks.cost import record_llm_usage
agent_id = str(uuid.uuid4())
project_id = str(uuid.uuid4())
model = "claude-opus-4-5-20251101"
prompt_tokens = 1500
completion_tokens = 500
cost_usd = 0.0825
result = record_llm_usage(
agent_id, project_id, model, prompt_tokens, completion_tokens, cost_usd
)
assert isinstance(result, dict)
assert "status" in result
assert "agent_id" in result
assert "project_id" in result
assert "cost_usd" in result
assert result["agent_id"] == agent_id
assert result["project_id"] == project_id
assert result["cost_usd"] == cost_usd
def test_record_llm_usage_with_different_models(self):
"""Test that record_llm_usage handles different model types."""
from app.tasks.cost import record_llm_usage
agent_id = str(uuid.uuid4())
project_id = str(uuid.uuid4())
models = [
("claude-opus-4-5-20251101", 0.015),
("claude-sonnet-4-20250514", 0.003),
("gpt-4-turbo", 0.01),
("gemini-1.5-pro", 0.007),
]
for model, cost in models:
result = record_llm_usage(
agent_id, project_id, model, 1000, 500, cost
)
assert result["status"] == "pending"
def test_record_llm_usage_with_zero_tokens(self):
"""Test that record_llm_usage handles zero token counts."""
from app.tasks.cost import record_llm_usage
agent_id = str(uuid.uuid4())
project_id = str(uuid.uuid4())
result = record_llm_usage(
agent_id, project_id, "claude-opus-4-5-20251101", 0, 0, 0.0
)
assert result["status"] == "pending"
class TestGenerateCostReportTask:
"""Tests for the generate_cost_report task."""
def test_generate_cost_report_task_exists(self):
"""Test that generate_cost_report task is registered."""
from app.celery_app import celery_app
import app.tasks.cost # noqa: F401
assert "app.tasks.cost.generate_cost_report" in celery_app.tasks
def test_generate_cost_report_is_bound_task(self):
"""Test that generate_cost_report is a bound task."""
from app.tasks.cost import generate_cost_report
assert generate_cost_report.__bound__ is True
def test_generate_cost_report_returns_expected_structure(self):
"""Test that generate_cost_report returns expected result."""
from app.tasks.cost import generate_cost_report
project_id = str(uuid.uuid4())
start_date = "2025-01-01"
end_date = "2025-01-31"
result = generate_cost_report(project_id, start_date, end_date)
assert isinstance(result, dict)
assert "status" in result
assert "project_id" in result
assert "start_date" in result
assert "end_date" in result
assert result["project_id"] == project_id
assert result["start_date"] == start_date
assert result["end_date"] == end_date
def test_generate_cost_report_with_various_date_ranges(self):
"""Test that generate_cost_report handles various date ranges."""
from app.tasks.cost import generate_cost_report
project_id = str(uuid.uuid4())
date_ranges = [
("2025-01-01", "2025-01-01"), # Single day
("2025-01-01", "2025-01-07"), # Week
("2025-01-01", "2025-12-31"), # Full year
]
for start, end in date_ranges:
result = generate_cost_report(project_id, start, end)
assert result["status"] == "pending"
class TestResetDailyBudgetCountersTask:
"""Tests for the reset_daily_budget_counters task."""
def test_reset_daily_budget_counters_task_exists(self):
"""Test that reset_daily_budget_counters task is registered."""
from app.celery_app import celery_app
import app.tasks.cost # noqa: F401
assert "app.tasks.cost.reset_daily_budget_counters" in celery_app.tasks
def test_reset_daily_budget_counters_is_bound_task(self):
"""Test that reset_daily_budget_counters is a bound task."""
from app.tasks.cost import reset_daily_budget_counters
assert reset_daily_budget_counters.__bound__ is True
def test_reset_daily_budget_counters_returns_expected_structure(self):
"""Test that reset_daily_budget_counters returns expected result."""
from app.tasks.cost import reset_daily_budget_counters
result = reset_daily_budget_counters()
assert isinstance(result, dict)
assert "status" in result
assert result["status"] == "pending"
class TestCostTaskRouting:
"""Tests for cost task queue routing."""
def test_cost_tasks_route_to_default_queue(self):
"""Test that cost tasks route to 'default' queue.
Per the routing configuration, cost tasks match 'app.tasks.*'
which routes to the default queue.
"""
from app.celery_app import celery_app
routes = celery_app.conf.task_routes
# Cost tasks match the generic 'app.tasks.*' pattern
assert "app.tasks.*" in routes
assert routes["app.tasks.*"]["queue"] == "default"
def test_all_cost_tasks_match_routing_pattern(self):
"""Test that all cost task names match the routing pattern."""
task_names = [
"app.tasks.cost.aggregate_daily_costs",
"app.tasks.cost.check_budget_thresholds",
"app.tasks.cost.record_llm_usage",
"app.tasks.cost.generate_cost_report",
"app.tasks.cost.reset_daily_budget_counters",
]
for name in task_names:
assert name.startswith("app.tasks.")
class TestCostTaskLogging:
"""Tests for cost task logging behavior."""
def test_aggregate_daily_costs_logs_execution(self):
"""Test that aggregate_daily_costs logs when executed."""
from app.tasks.cost import aggregate_daily_costs
with patch("app.tasks.cost.logger") as mock_logger:
aggregate_daily_costs()
mock_logger.info.assert_called_once()
call_args = mock_logger.info.call_args[0][0]
assert "cost" in call_args.lower() or "aggregat" in call_args.lower()
def test_check_budget_thresholds_logs_execution(self):
"""Test that check_budget_thresholds logs when executed."""
from app.tasks.cost import check_budget_thresholds
project_id = str(uuid.uuid4())
with patch("app.tasks.cost.logger") as mock_logger:
check_budget_thresholds(project_id)
mock_logger.info.assert_called_once()
call_args = mock_logger.info.call_args[0][0]
assert project_id in call_args
def test_record_llm_usage_logs_execution(self):
"""Test that record_llm_usage logs when executed."""
from app.tasks.cost import record_llm_usage
agent_id = str(uuid.uuid4())
project_id = str(uuid.uuid4())
model = "claude-opus-4-5-20251101"
with patch("app.tasks.cost.logger") as mock_logger:
record_llm_usage(agent_id, project_id, model, 100, 50, 0.01)
# Uses debug level, not info
mock_logger.debug.assert_called_once()
call_args = mock_logger.debug.call_args[0][0]
assert model in call_args
def test_generate_cost_report_logs_execution(self):
"""Test that generate_cost_report logs when executed."""
from app.tasks.cost import generate_cost_report
project_id = str(uuid.uuid4())
with patch("app.tasks.cost.logger") as mock_logger:
generate_cost_report(project_id, "2025-01-01", "2025-01-31")
mock_logger.info.assert_called_once()
call_args = mock_logger.info.call_args[0][0]
assert project_id in call_args
def test_reset_daily_budget_counters_logs_execution(self):
"""Test that reset_daily_budget_counters logs when executed."""
from app.tasks.cost import reset_daily_budget_counters
with patch("app.tasks.cost.logger") as mock_logger:
reset_daily_budget_counters()
mock_logger.info.assert_called_once()
call_args = mock_logger.info.call_args[0][0]
assert "reset" in call_args.lower() or "counter" in call_args.lower()
class TestCostTaskSignatures:
"""Tests for cost task signature creation."""
def test_record_llm_usage_signature_creation(self):
"""Test that record_llm_usage signature can be created."""
from app.tasks.cost import record_llm_usage
agent_id = str(uuid.uuid4())
project_id = str(uuid.uuid4())
sig = record_llm_usage.s(
agent_id, project_id, "claude-opus-4-5-20251101", 100, 50, 0.01
)
assert sig is not None
assert len(sig.args) == 6
def test_check_budget_thresholds_signature_creation(self):
"""Test that check_budget_thresholds signature can be created."""
from app.tasks.cost import check_budget_thresholds
project_id = str(uuid.uuid4())
sig = check_budget_thresholds.s(project_id)
assert sig is not None
assert sig.args == (project_id,)
def test_cost_task_chain_creation(self):
"""Test that cost tasks can be chained together."""
from celery import chain
from app.tasks.cost import record_llm_usage, check_budget_thresholds
agent_id = str(uuid.uuid4())
project_id = str(uuid.uuid4())
# Build a chain: record usage, then check thresholds
workflow = chain(
record_llm_usage.s(
agent_id, project_id, "claude-opus-4-5-20251101", 1000, 500, 0.05
),
check_budget_thresholds.s(project_id),
)
assert workflow is not None

View File

@@ -0,0 +1,301 @@
# tests/tasks/test_git_tasks.py
"""
Tests for git operation tasks.
These tests verify:
- Task signatures are correctly defined
- Tasks are bound (have access to self)
- Tasks return expected structure
- Tasks are routed to the 'git' queue
Note: These tests mock actual execution since they would require
Git operations and external APIs in production.
"""
import pytest
from unittest.mock import patch
import uuid
class TestCloneRepositoryTask:
"""Tests for the clone_repository task."""
def test_clone_repository_task_exists(self):
"""Test that clone_repository task is registered."""
from app.celery_app import celery_app
import app.tasks.git # noqa: F401
assert "app.tasks.git.clone_repository" in celery_app.tasks
def test_clone_repository_is_bound_task(self):
"""Test that clone_repository is a bound task."""
from app.tasks.git import clone_repository
assert clone_repository.__bound__ is True
def test_clone_repository_has_correct_name(self):
"""Test that clone_repository has the correct task name."""
from app.tasks.git import clone_repository
assert clone_repository.name == "app.tasks.git.clone_repository"
def test_clone_repository_returns_expected_structure(self):
"""Test that clone_repository returns the expected result structure."""
from app.tasks.git import clone_repository
project_id = str(uuid.uuid4())
repo_url = "https://gitea.example.com/org/repo.git"
branch = "main"
result = clone_repository(project_id, repo_url, branch)
assert isinstance(result, dict)
assert "status" in result
assert "project_id" in result
assert result["project_id"] == project_id
def test_clone_repository_with_default_branch(self):
"""Test that clone_repository uses default branch when not specified."""
from app.tasks.git import clone_repository
project_id = str(uuid.uuid4())
repo_url = "https://github.com/org/repo.git"
# Call without specifying branch (should default to 'main')
result = clone_repository(project_id, repo_url)
assert result["status"] == "pending"
class TestCommitChangesTask:
"""Tests for the commit_changes task."""
def test_commit_changes_task_exists(self):
"""Test that commit_changes task is registered."""
from app.celery_app import celery_app
import app.tasks.git # noqa: F401
assert "app.tasks.git.commit_changes" in celery_app.tasks
def test_commit_changes_is_bound_task(self):
"""Test that commit_changes is a bound task."""
from app.tasks.git import commit_changes
assert commit_changes.__bound__ is True
def test_commit_changes_returns_expected_structure(self):
"""Test that commit_changes returns the expected result structure."""
from app.tasks.git import commit_changes
project_id = str(uuid.uuid4())
message = "feat: Add new feature"
files = ["src/feature.py", "tests/test_feature.py"]
result = commit_changes(project_id, message, files)
assert isinstance(result, dict)
assert "status" in result
assert "project_id" in result
def test_commit_changes_without_files(self):
"""Test that commit_changes handles None files (commit all staged)."""
from app.tasks.git import commit_changes
project_id = str(uuid.uuid4())
message = "chore: Update dependencies"
result = commit_changes(project_id, message, None)
assert result["status"] == "pending"
class TestCreateBranchTask:
"""Tests for the create_branch task."""
def test_create_branch_task_exists(self):
"""Test that create_branch task is registered."""
from app.celery_app import celery_app
import app.tasks.git # noqa: F401
assert "app.tasks.git.create_branch" in celery_app.tasks
def test_create_branch_is_bound_task(self):
"""Test that create_branch is a bound task."""
from app.tasks.git import create_branch
assert create_branch.__bound__ is True
def test_create_branch_returns_expected_structure(self):
"""Test that create_branch returns the expected result structure."""
from app.tasks.git import create_branch
project_id = str(uuid.uuid4())
branch_name = "feature/new-feature"
from_ref = "develop"
result = create_branch(project_id, branch_name, from_ref)
assert isinstance(result, dict)
assert "status" in result
assert "project_id" in result
def test_create_branch_with_default_from_ref(self):
"""Test that create_branch uses default from_ref when not specified."""
from app.tasks.git import create_branch
project_id = str(uuid.uuid4())
branch_name = "feature/123-add-login"
result = create_branch(project_id, branch_name)
assert result["status"] == "pending"
class TestCreatePullRequestTask:
"""Tests for the create_pull_request task."""
def test_create_pull_request_task_exists(self):
"""Test that create_pull_request task is registered."""
from app.celery_app import celery_app
import app.tasks.git # noqa: F401
assert "app.tasks.git.create_pull_request" in celery_app.tasks
def test_create_pull_request_is_bound_task(self):
"""Test that create_pull_request is a bound task."""
from app.tasks.git import create_pull_request
assert create_pull_request.__bound__ is True
def test_create_pull_request_returns_expected_structure(self):
"""Test that create_pull_request returns expected result structure."""
from app.tasks.git import create_pull_request
project_id = str(uuid.uuid4())
title = "feat: Add authentication"
body = "## Summary\n- Added JWT auth\n- Added login endpoint"
head_branch = "feature/auth"
base_branch = "main"
result = create_pull_request(project_id, title, body, head_branch, base_branch)
assert isinstance(result, dict)
assert "status" in result
assert "project_id" in result
def test_create_pull_request_with_default_base(self):
"""Test that create_pull_request uses default base branch."""
from app.tasks.git import create_pull_request
project_id = str(uuid.uuid4())
result = create_pull_request(
project_id, "Fix bug", "Bug fix description", "fix/bug-123"
)
assert result["status"] == "pending"
class TestPushChangesTask:
"""Tests for the push_changes task."""
def test_push_changes_task_exists(self):
"""Test that push_changes task is registered."""
from app.celery_app import celery_app
import app.tasks.git # noqa: F401
assert "app.tasks.git.push_changes" in celery_app.tasks
def test_push_changes_is_bound_task(self):
"""Test that push_changes is a bound task."""
from app.tasks.git import push_changes
assert push_changes.__bound__ is True
def test_push_changes_returns_expected_structure(self):
"""Test that push_changes returns the expected result structure."""
from app.tasks.git import push_changes
project_id = str(uuid.uuid4())
branch = "feature/new-feature"
force = False
result = push_changes(project_id, branch, force)
assert isinstance(result, dict)
assert "status" in result
assert "project_id" in result
def test_push_changes_with_force_option(self):
"""Test that push_changes handles force push option."""
from app.tasks.git import push_changes
project_id = str(uuid.uuid4())
branch = "feature/rebased-branch"
force = True
result = push_changes(project_id, branch, force)
assert result["status"] == "pending"
class TestGitTaskRouting:
"""Tests for git task queue routing."""
def test_git_tasks_should_route_to_git_queue(self):
"""Test that git tasks are configured to route to 'git' queue."""
from app.celery_app import celery_app
routes = celery_app.conf.task_routes
git_route = routes.get("app.tasks.git.*")
assert git_route is not None
assert git_route["queue"] == "git"
def test_all_git_tasks_match_routing_pattern(self):
"""Test that all git task names match the routing pattern."""
from app.tasks import git
task_names = [
"app.tasks.git.clone_repository",
"app.tasks.git.commit_changes",
"app.tasks.git.create_branch",
"app.tasks.git.create_pull_request",
"app.tasks.git.push_changes",
]
for name in task_names:
assert name.startswith("app.tasks.git.")
class TestGitTaskLogging:
"""Tests for git task logging behavior."""
def test_clone_repository_logs_execution(self):
"""Test that clone_repository logs when executed."""
from app.tasks.git import clone_repository
project_id = str(uuid.uuid4())
repo_url = "https://github.com/org/repo.git"
with patch("app.tasks.git.logger") as mock_logger:
clone_repository(project_id, repo_url)
mock_logger.info.assert_called_once()
call_args = mock_logger.info.call_args[0][0]
assert repo_url in call_args
assert project_id in call_args
def test_commit_changes_logs_execution(self):
"""Test that commit_changes logs when executed."""
from app.tasks.git import commit_changes
project_id = str(uuid.uuid4())
message = "test commit"
with patch("app.tasks.git.logger") as mock_logger:
commit_changes(project_id, message)
mock_logger.info.assert_called_once()
call_args = mock_logger.info.call_args[0][0]
assert message in call_args

View File

@@ -0,0 +1,309 @@
# tests/tasks/test_sync_tasks.py
"""
Tests for issue synchronization tasks.
These tests verify:
- Task signatures are correctly defined
- Tasks are bound (have access to self)
- Tasks return expected structure
- Tasks are routed to the 'sync' queue per ADR-011
Note: These tests mock actual execution since they would require
external API calls in production.
"""
import pytest
from unittest.mock import patch
import uuid
class TestSyncIssuesIncrementalTask:
"""Tests for the sync_issues_incremental task."""
def test_sync_issues_incremental_task_exists(self):
"""Test that sync_issues_incremental task is registered."""
from app.celery_app import celery_app
import app.tasks.sync # noqa: F401
assert "app.tasks.sync.sync_issues_incremental" in celery_app.tasks
def test_sync_issues_incremental_is_bound_task(self):
"""Test that sync_issues_incremental is a bound task."""
from app.tasks.sync import sync_issues_incremental
assert sync_issues_incremental.__bound__ is True
def test_sync_issues_incremental_has_correct_name(self):
"""Test that sync_issues_incremental has the correct task name."""
from app.tasks.sync import sync_issues_incremental
assert sync_issues_incremental.name == "app.tasks.sync.sync_issues_incremental"
def test_sync_issues_incremental_returns_expected_structure(self):
"""Test that sync_issues_incremental returns expected result."""
from app.tasks.sync import sync_issues_incremental
result = sync_issues_incremental()
assert isinstance(result, dict)
assert "status" in result
assert "type" in result
assert result["type"] == "incremental"
class TestSyncIssuesFullTask:
"""Tests for the sync_issues_full task."""
def test_sync_issues_full_task_exists(self):
"""Test that sync_issues_full task is registered."""
from app.celery_app import celery_app
import app.tasks.sync # noqa: F401
assert "app.tasks.sync.sync_issues_full" in celery_app.tasks
def test_sync_issues_full_is_bound_task(self):
"""Test that sync_issues_full is a bound task."""
from app.tasks.sync import sync_issues_full
assert sync_issues_full.__bound__ is True
def test_sync_issues_full_has_correct_name(self):
"""Test that sync_issues_full has the correct task name."""
from app.tasks.sync import sync_issues_full
assert sync_issues_full.name == "app.tasks.sync.sync_issues_full"
def test_sync_issues_full_returns_expected_structure(self):
"""Test that sync_issues_full returns expected result."""
from app.tasks.sync import sync_issues_full
result = sync_issues_full()
assert isinstance(result, dict)
assert "status" in result
assert "type" in result
assert result["type"] == "full"
class TestProcessWebhookEventTask:
"""Tests for the process_webhook_event task."""
def test_process_webhook_event_task_exists(self):
"""Test that process_webhook_event task is registered."""
from app.celery_app import celery_app
import app.tasks.sync # noqa: F401
assert "app.tasks.sync.process_webhook_event" in celery_app.tasks
def test_process_webhook_event_is_bound_task(self):
"""Test that process_webhook_event is a bound task."""
from app.tasks.sync import process_webhook_event
assert process_webhook_event.__bound__ is True
def test_process_webhook_event_returns_expected_structure(self):
"""Test that process_webhook_event returns expected result."""
from app.tasks.sync import process_webhook_event
provider = "gitea"
event_type = "issue.created"
payload = {
"action": "opened",
"issue": {"number": 123, "title": "New issue"},
}
result = process_webhook_event(provider, event_type, payload)
assert isinstance(result, dict)
assert "status" in result
assert "provider" in result
assert "event_type" in result
assert result["provider"] == provider
assert result["event_type"] == event_type
def test_process_webhook_event_handles_github_provider(self):
"""Test that process_webhook_event handles GitHub webhooks."""
from app.tasks.sync import process_webhook_event
result = process_webhook_event(
"github", "issues", {"action": "opened", "issue": {"number": 1}}
)
assert result["provider"] == "github"
def test_process_webhook_event_handles_gitlab_provider(self):
"""Test that process_webhook_event handles GitLab webhooks."""
from app.tasks.sync import process_webhook_event
result = process_webhook_event(
"gitlab",
"issue.created",
{"object_kind": "issue", "object_attributes": {"iid": 1}},
)
assert result["provider"] == "gitlab"
class TestSyncProjectIssuesTask:
"""Tests for the sync_project_issues task."""
def test_sync_project_issues_task_exists(self):
"""Test that sync_project_issues task is registered."""
from app.celery_app import celery_app
import app.tasks.sync # noqa: F401
assert "app.tasks.sync.sync_project_issues" in celery_app.tasks
def test_sync_project_issues_is_bound_task(self):
"""Test that sync_project_issues is a bound task."""
from app.tasks.sync import sync_project_issues
assert sync_project_issues.__bound__ is True
def test_sync_project_issues_returns_expected_structure(self):
"""Test that sync_project_issues returns expected result."""
from app.tasks.sync import sync_project_issues
project_id = str(uuid.uuid4())
full = False
result = sync_project_issues(project_id, full)
assert isinstance(result, dict)
assert "status" in result
assert "project_id" in result
assert result["project_id"] == project_id
def test_sync_project_issues_with_full_sync(self):
"""Test that sync_project_issues handles full sync flag."""
from app.tasks.sync import sync_project_issues
project_id = str(uuid.uuid4())
result = sync_project_issues(project_id, full=True)
assert result["status"] == "pending"
class TestPushIssueToExternalTask:
"""Tests for the push_issue_to_external task."""
def test_push_issue_to_external_task_exists(self):
"""Test that push_issue_to_external task is registered."""
from app.celery_app import celery_app
import app.tasks.sync # noqa: F401
assert "app.tasks.sync.push_issue_to_external" in celery_app.tasks
def test_push_issue_to_external_is_bound_task(self):
"""Test that push_issue_to_external is a bound task."""
from app.tasks.sync import push_issue_to_external
assert push_issue_to_external.__bound__ is True
def test_push_issue_to_external_returns_expected_structure(self):
"""Test that push_issue_to_external returns expected result."""
from app.tasks.sync import push_issue_to_external
project_id = str(uuid.uuid4())
issue_id = str(uuid.uuid4())
operation = "create"
result = push_issue_to_external(project_id, issue_id, operation)
assert isinstance(result, dict)
assert "status" in result
assert "issue_id" in result
assert "operation" in result
assert result["issue_id"] == issue_id
assert result["operation"] == operation
def test_push_issue_to_external_update_operation(self):
"""Test that push_issue_to_external handles update operation."""
from app.tasks.sync import push_issue_to_external
project_id = str(uuid.uuid4())
issue_id = str(uuid.uuid4())
result = push_issue_to_external(project_id, issue_id, "update")
assert result["operation"] == "update"
def test_push_issue_to_external_close_operation(self):
"""Test that push_issue_to_external handles close operation."""
from app.tasks.sync import push_issue_to_external
project_id = str(uuid.uuid4())
issue_id = str(uuid.uuid4())
result = push_issue_to_external(project_id, issue_id, "close")
assert result["operation"] == "close"
class TestSyncTaskRouting:
"""Tests for sync task queue routing."""
def test_sync_tasks_should_route_to_sync_queue(self):
"""Test that sync tasks are configured to route to 'sync' queue."""
from app.celery_app import celery_app
routes = celery_app.conf.task_routes
sync_route = routes.get("app.tasks.sync.*")
assert sync_route is not None
assert sync_route["queue"] == "sync"
def test_all_sync_tasks_match_routing_pattern(self):
"""Test that all sync task names match the routing pattern."""
task_names = [
"app.tasks.sync.sync_issues_incremental",
"app.tasks.sync.sync_issues_full",
"app.tasks.sync.process_webhook_event",
"app.tasks.sync.sync_project_issues",
"app.tasks.sync.push_issue_to_external",
]
for name in task_names:
assert name.startswith("app.tasks.sync.")
class TestSyncTaskLogging:
"""Tests for sync task logging behavior."""
def test_sync_issues_incremental_logs_execution(self):
"""Test that sync_issues_incremental logs when executed."""
from app.tasks.sync import sync_issues_incremental
with patch("app.tasks.sync.logger") as mock_logger:
sync_issues_incremental()
mock_logger.info.assert_called_once()
call_args = mock_logger.info.call_args[0][0]
assert "incremental" in call_args.lower()
def test_sync_issues_full_logs_execution(self):
"""Test that sync_issues_full logs when executed."""
from app.tasks.sync import sync_issues_full
with patch("app.tasks.sync.logger") as mock_logger:
sync_issues_full()
mock_logger.info.assert_called_once()
call_args = mock_logger.info.call_args[0][0]
assert "full" in call_args.lower() or "reconciliation" in call_args.lower()
def test_process_webhook_event_logs_execution(self):
"""Test that process_webhook_event logs when executed."""
from app.tasks.sync import process_webhook_event
provider = "gitea"
event_type = "issue.updated"
with patch("app.tasks.sync.logger") as mock_logger:
process_webhook_event(provider, event_type, {})
mock_logger.info.assert_called_once()
call_args = mock_logger.info.call_args[0][0]
assert provider in call_args
assert event_type in call_args

View File

@@ -0,0 +1,350 @@
# 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 pytest
from unittest.mock import patch
import uuid
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."""
from app.celery_app import celery_app
import app.tasks.workflow # noqa: F401
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."""
from app.celery_app import celery_app
import app.tasks.workflow # noqa: F401
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."""
from app.celery_app import celery_app
import app.tasks.workflow # noqa: F401
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."""
from app.celery_app import celery_app
import app.tasks.workflow # noqa: F401
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."""
from app.celery_app import celery_app
import app.tasks.workflow # noqa: F401
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,
execute_workflow_step,
handle_approval_response,
)
project_id = str(uuid.uuid4())
sprint_id = str(uuid.uuid4())
workflow_id = 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