from types import SimpleNamespace import pytest from agent.core.agent_loop import Handlers from agent.core.session import Event from agent.core.usage_thresholds import ( USAGE_THRESHOLD_TOOL_NAME, next_usage_warning_threshold, ) class FakeUsageApprovalSession: def __init__(self, *, continuation="continue_agent"): self.pending_approval = { "kind": USAGE_THRESHOLD_TOOL_NAME, "tool_call_id": "usage-threshold-1", "threshold_usd": 5.0, "current_spend_usd": 12.25, "next_threshold_usd": 10.0, "billing_source": "app_telemetry_session", "continuation": continuation, "history_size": 3, "final_response": "done", } self.context_manager = SimpleNamespace(items=[]) self.usage_warning_next_threshold_usd = 5.0 self.events: list[Event] = [] self.turn_count = 0 self.auto_saved = False async def send_event(self, event: Event): self.events.append(event) def increment_turn(self): self.turn_count += 1 async def auto_save_if_needed(self): self.auto_saved = True def test_next_usage_warning_threshold_advances_past_current_spend(): assert next_usage_warning_threshold(4.99, 5.0) == 5.0 assert next_usage_warning_threshold(5.0, 5.0) == 10.0 assert next_usage_warning_threshold(12.25, 5.0) == 20.0 assert next_usage_warning_threshold(40.0, 20.0) == 80.0 @pytest.mark.asyncio async def test_usage_threshold_approval_resumes_agent(monkeypatch): session = FakeUsageApprovalSession(continuation="continue_agent") resumed = False async def fake_run_agent(run_session, text): nonlocal resumed assert run_session is session assert text == "" resumed = True monkeypatch.setattr(Handlers, "run_agent", fake_run_agent) await Handlers.exec_approval( session, [{"tool_call_id": "usage-threshold-1", "approved": True}], ) assert session.pending_approval is None assert resumed is True assert session.usage_warning_next_threshold_usd == 20.0 assert [event.event_type for event in session.events] == [ "tool_state_change", "tool_output", ] assert session.events[-1].data["success"] is True @pytest.mark.asyncio async def test_usage_threshold_approval_completes_finished_turn(monkeypatch): session = FakeUsageApprovalSession(continuation="complete_turn") async def fail_run_agent(*args, **kwargs): raise AssertionError("complete_turn must not call run_agent") monkeypatch.setattr(Handlers, "run_agent", fail_run_agent) await Handlers.exec_approval( session, [{"tool_call_id": "usage-threshold-1", "approved": True}], ) assert session.pending_approval is None assert [event.event_type for event in session.events] == [ "tool_state_change", "tool_output", "turn_complete", ] assert session.events[-1].data == { "history_size": 3, "final_response": "done", } assert session.turn_count == 1 assert session.auto_saved is True @pytest.mark.asyncio async def test_usage_threshold_rejection_stops_turn(monkeypatch): session = FakeUsageApprovalSession() async def fail_run_agent(*args, **kwargs): raise AssertionError("rejection must not call run_agent") monkeypatch.setattr(Handlers, "run_agent", fail_run_agent) await Handlers.exec_approval( session, [{"tool_call_id": "usage-threshold-1", "approved": False}], ) assert session.pending_approval is None assert [event.event_type for event in session.events] == [ "tool_state_change", "tool_output", "interrupted", ] assert session.events[1].data["success"] is False assert session.turn_count == 1 assert session.auto_saved is True @pytest.mark.asyncio async def test_abandon_complete_turn_usage_threshold_completes_prior_turn(): session = FakeUsageApprovalSession(continuation="complete_turn") await Handlers._abandon_pending_approval(session) assert session.pending_approval is None assert [event.event_type for event in session.events] == [ "tool_state_change", "turn_complete", ] assert session.events[-1].data == { "history_size": 3, "final_response": "done", } assert session.turn_count == 1 assert session.auto_saved is True