forked from cardosofelipe/fast-next-template
Infrastructure: - Add Redis and Celery workers to all docker-compose files - Fix celery migration race condition in entrypoint.sh - Add healthchecks and resource limits to dev compose - Update .env.template with Redis/Celery variables Backend Models & Schemas: - Rename Sprint.completed_points to velocity (per requirements) - Add AgentInstance.name as required field - Rename Issue external tracker fields for consistency - Add IssueSource and TrackerType enums - Add Project.default_tracker_type field Backend Fixes: - Add Celery retry configuration with exponential backoff - Remove unused sequence counter from EventBus - Add mypy overrides for test dependencies - Fix test file using wrong schema (UserUpdate -> dict) Frontend Fixes: - Fix memory leak in useProjectEvents (proper cleanup) - Fix race condition with stale closure in reconnection - Sync TokenWithUser type with regenerated API client - Fix expires_in null handling in useAuth - Clean up unused imports in prototype pages - Add ESLint relaxed rules for prototype files CI/CD: - Add E2E testing stage with Testcontainers - Add security scanning with Trivy and pip-audit - Add dependency caching for faster builds Tests: - Update all tests to use renamed fields (velocity, name, etc.) - Fix 14 schema test failures - All 1500 tests pass with 91% coverage 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
315 lines
12 KiB
Python
315 lines
12 KiB
Python
# 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
|
|
"""
|
|
|
|
|
|
|
|
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."""
|
|
# Force task registration by importing
|
|
import app.tasks.agent # noqa: F401
|
|
from app.celery_app import celery_app
|
|
|
|
# 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."""
|
|
# Force task registration by importing
|
|
import app.tasks.git # noqa: F401
|
|
from app.celery_app import celery_app
|
|
|
|
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."""
|
|
# Force task registration by importing
|
|
import app.tasks.sync # noqa: F401
|
|
from app.celery_app import celery_app
|
|
|
|
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."""
|
|
# Force task registration by importing
|
|
import app.tasks.workflow # noqa: F401
|
|
from app.celery_app import celery_app
|
|
|
|
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."""
|
|
# Force task registration by importing
|
|
import app.tasks.cost # noqa: F401
|
|
from app.celery_app import celery_app
|
|
|
|
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)
|