from __future__ import annotations import unittest from unittest.mock import patch from src.agent_session import AgentSessionState from src.agent_context_usage import ContextUsageReport, MessageBreakdown from src.agent_types import BudgetConfig from src.token_budget import calculate_token_budget, format_token_budget class TokenBudgetTests(unittest.TestCase): def test_calculate_token_budget_reports_soft_and_hard_limits(self) -> None: session = AgentSessionState.create( ['# System\nYou are helpful.'], 'Inspect the repository and summarize the current implementation status.', user_context={'currentDate': "Today's date is 2026-04-11."}, system_context={'gitStatus': 'Current branch: main'}, ) session.append_assistant('Reading files and checking runtime state.') fake_usage = ContextUsageReport( model='test-model', total_tokens=200, raw_max_tokens=128_000, percentage=0.15, strategy='token_budget', message_count=len(session.messages), categories=(), system_prompt_sections=(), user_context_entries=(), system_context_entries=(), memory_files=(), message_breakdown=MessageBreakdown( user_message_tokens=50, assistant_message_tokens=50, tool_call_tokens=0, tool_result_tokens=0, user_context_tokens=10, tool_calls_by_type=(), ), token_counter_backend='heuristic', token_counter_source='test', token_counter_accurate=False, ) with patch('src.token_budget.collect_context_usage', return_value=fake_usage): snapshot = calculate_token_budget( session=session, model='test-model', budget_config=BudgetConfig(), ) rendered = format_token_budget(snapshot) self.assertGreater(snapshot.projected_input_tokens, 0) self.assertGreater(snapshot.hard_input_limit_tokens, snapshot.soft_input_limit_tokens) self.assertGreater(snapshot.chat_overhead_tokens, 0) self.assertIn('# Token Budget', rendered) self.assertIn('Hard input limit', rendered) self.assertIn('Auto-compact buffer', rendered) def test_calculate_token_budget_honors_explicit_max_input_tokens(self) -> None: session = AgentSessionState.create( ['# System\nYou are helpful.'], 'This prompt is deliberately longer than the tiny configured input budget. ' * 4, ) fake_usage = ContextUsageReport( model='test-model', total_tokens=120, raw_max_tokens=128_000, percentage=0.09, strategy='token_budget', message_count=len(session.messages), categories=(), system_prompt_sections=(), user_context_entries=(), system_context_entries=(), memory_files=(), message_breakdown=MessageBreakdown( user_message_tokens=80, assistant_message_tokens=0, tool_call_tokens=0, tool_result_tokens=0, user_context_tokens=0, tool_calls_by_type=(), ), token_counter_backend='heuristic', token_counter_source='test', token_counter_accurate=False, ) with patch('src.token_budget.collect_context_usage', return_value=fake_usage): snapshot = calculate_token_budget( session=session, model='test-model', budget_config=BudgetConfig(max_input_tokens=20), ) self.assertTrue(snapshot.exceeds_hard_limit) self.assertGreater(snapshot.overflow_tokens, 0) self.assertLessEqual(snapshot.hard_input_limit_tokens, 20)