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>
380 lines
13 KiB
Python
380 lines
13 KiB
Python
# 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 uuid
|
|
from unittest.mock import patch
|
|
|
|
|
|
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."""
|
|
import app.tasks.cost # noqa: F401
|
|
from app.celery_app import celery_app
|
|
|
|
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."""
|
|
import app.tasks.cost # noqa: F401
|
|
from app.celery_app import celery_app
|
|
|
|
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."""
|
|
import app.tasks.cost # noqa: F401
|
|
from app.celery_app import celery_app
|
|
|
|
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."""
|
|
import app.tasks.cost # noqa: F401
|
|
from app.celery_app import celery_app
|
|
|
|
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."""
|
|
import app.tasks.cost # noqa: F401
|
|
from app.celery_app import celery_app
|
|
|
|
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 check_budget_thresholds, record_llm_usage
|
|
|
|
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
|