Files
cleveragents-core/features/steps/cost_tracker_steps.py
T
HAL9000 aee4e25853
CI / build (pull_request) Successful in 44s
CI / lint (pull_request) Successful in 52s
CI / quality (pull_request) Successful in 1m5s
CI / typecheck (pull_request) Successful in 1m19s
CI / helm (pull_request) Successful in 33s
CI / security (pull_request) Successful in 1m28s
CI / push-validation (pull_request) Successful in 26s
CI / unit_tests (pull_request) Successful in 4m42s
CI / integration_tests (pull_request) Successful in 9m38s
CI / docker (pull_request) Successful in 2m10s
CI / coverage (pull_request) Successful in 10m42s
CI / status-check (pull_request) Successful in 4s
fix(budget): fix type safety, formatting, and test correctness in CostTracker
- Convert CostRecord from @dataclass to Pydantic BaseModel (architecture policy)
- Replace declarative_base() + # type: ignore[misc] with DeclarativeBase subclass
- Use Mapped/mapped_column for proper SQLAlchemy 2.0 typed columns
- Remove all cast() calls — types now flow from Mapped[T] annotations
- Change timestamp column from String to DateTime; use datetime.now(UTC)
- Fix wrong expected totals in cost_tracker_service.feature (0.00035 → 0.00075)
- Fix step_check_entries_ids AttributeError: read plan_entries when session_entries absent
- Apply ruff format to both changed files

ISSUES CLOSED: #5248
2026-06-04 09:49:11 -04:00

171 lines
5.9 KiB
Python

"""Step definitions for CostTracker service tests."""
from __future__ import annotations
import tempfile
from pathlib import Path
from behave import given, then, when
from cleveragents.infrastructure.database.cost_tracker import CostRecord, CostTracker
@given("a temporary data directory for cost tracking")
def step_create_temp_dir(context):
"""Create a temporary directory for cost tracking."""
context.temp_dir = tempfile.mkdtemp()
context.data_dir = Path(context.temp_dir)
@given("a CostTracker instance with default model pricing")
def step_create_cost_tracker(context):
"""Create a CostTracker instance with default pricing."""
context.cost_tracker = CostTracker(context.data_dir)
context.recorded_records = []
@when('I record usage for session "{session_id}" and plan "{plan_id}"')
def step_record_usage(context, session_id, plan_id):
"""Record usage for a session and plan."""
for row in context.table:
tokens_in = int(row["tokens_in"])
tokens_out = int(row["tokens_out"])
model = row["model"]
record = context.cost_tracker.record_usage(
session_id=session_id,
plan_id=plan_id,
tokens_in=tokens_in,
tokens_out=tokens_out,
model=model,
)
context.recorded_records.append(record)
@then("the recorded cost should be approximately {cost} USD")
def step_check_recorded_cost(context, cost):
"""Check the recorded cost."""
expected_cost = float(cost)
actual_cost = context.recorded_records[-1].cost_usd
assert abs(actual_cost - expected_cost) < 0.000001, (
f"Expected {expected_cost}, got {actual_cost}"
)
@then('the cost record should have session_id "{session_id}"')
def step_check_session_id(context, session_id):
"""Check the session_id of the recorded cost."""
assert context.recorded_records[-1].session_id == session_id
@then('the cost record should have plan_id "{plan_id}"')
def step_check_plan_id(context, plan_id):
"""Check the plan_id of the recorded cost."""
assert context.recorded_records[-1].plan_id == plan_id
@when('I record multiple usages for session "{session_id}"')
def step_record_multiple_usages_session(context, session_id):
"""Record multiple usages for a session."""
for row in context.table:
plan_id = row["plan_id"]
tokens_in = int(row["tokens_in"])
tokens_out = int(row["tokens_out"])
model = row["model"]
record = context.cost_tracker.record_usage(
session_id=session_id,
plan_id=plan_id,
tokens_in=tokens_in,
tokens_out=tokens_out,
model=model,
)
context.recorded_records.append(record)
@then('the total session cost for "{session_id}" should be approximately {cost} USD')
def step_check_session_cost(context, session_id, cost):
"""Check the total cost for a session."""
expected_cost = float(cost)
actual_cost = context.cost_tracker.get_session_cost(session_id)
assert abs(actual_cost - expected_cost) < 0.000001, (
f"Expected {expected_cost}, got {actual_cost}"
)
@when('I record multiple usages for plan "{plan_id}"')
def step_record_multiple_usages_plan(context, plan_id):
"""Record multiple usages for a plan."""
for row in context.table:
session_id = row["session_id"]
tokens_in = int(row["tokens_in"])
tokens_out = int(row["tokens_out"])
model = row["model"]
record = context.cost_tracker.record_usage(
session_id=session_id,
plan_id=plan_id,
tokens_in=tokens_in,
tokens_out=tokens_out,
model=model,
)
context.recorded_records.append(record)
@then('the total plan cost for "{plan_id}" should be approximately {cost} USD')
def step_check_plan_cost(context, plan_id, cost):
"""Check the total cost for a plan."""
expected_cost = float(cost)
actual_cost = context.cost_tracker.get_plan_cost(plan_id)
assert abs(actual_cost - expected_cost) < 0.000001, (
f"Expected {expected_cost}, got {actual_cost}"
)
@when("I create a new CostTracker instance with the same data directory")
def step_create_new_tracker(context):
"""Create a new CostTracker instance with the same data directory."""
context.cost_tracker = CostTracker(context.data_dir)
@given("a CostTracker instance with custom model pricing")
def step_create_tracker_custom_pricing(context):
"""Create a CostTracker instance with custom pricing."""
pricing = {}
for row in context.table:
model = row["model"]
input_price = float(row["input_price"])
output_price = float(row["output_price"])
pricing[model] = {"input": input_price, "output": output_price}
context.cost_tracker = CostTracker(context.data_dir, model_pricing=pricing)
context.recorded_records = []
@then('I should get {count} entries for session "{session_id}"')
def step_check_session_entries_count(context, count, session_id):
"""Check the number of entries for a session."""
entries = context.cost_tracker.get_session_entries(session_id)
assert len(entries) == int(count), f"Expected {count} entries, got {len(entries)}"
context.session_entries = entries
@then("each entry should have the correct session_id and plan_id")
def step_check_entries_ids(context):
"""Check that each entry has the correct IDs."""
entries = getattr(context, "session_entries", None) or getattr(
context, "plan_entries", []
)
for entry in entries:
assert isinstance(entry, CostRecord)
assert entry.session_id is not None
assert entry.plan_id is not None
@then('I should get {count} entries for plan "{plan_id}"')
def step_check_plan_entries_count(context, count, plan_id):
"""Check the number of entries for a plan."""
entries = context.cost_tracker.get_plan_entries(plan_id)
assert len(entries) == int(count), f"Expected {count} entries, got {len(entries)}"
context.plan_entries = entries