test(tools): add unit tests for budget_config module
Cover default constants, BudgetConfig defaults, frozen immutability, custom construction, and the resolve_threshold() priority chain (pinned > tool_overrides > registry > default). 20 tests total. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
176
tests/tools/test_budget_config.py
Normal file
176
tests/tools/test_budget_config.py
Normal file
@@ -0,0 +1,176 @@
|
||||
"""Unit tests for tools/budget_config.py.
|
||||
|
||||
Covers default values, resolve_threshold() priority chain
|
||||
(pinned > tool_overrides > registry > default), immutability,
|
||||
and the PINNED_THRESHOLDS escape-hatch for read_file.
|
||||
"""
|
||||
|
||||
import dataclasses
|
||||
import math
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from tools.budget_config import (
|
||||
DEFAULT_BUDGET,
|
||||
DEFAULT_PREVIEW_SIZE_CHARS,
|
||||
DEFAULT_RESULT_SIZE_CHARS,
|
||||
DEFAULT_TURN_BUDGET_CHARS,
|
||||
PINNED_THRESHOLDS,
|
||||
BudgetConfig,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Module-level constants
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestModuleConstants:
|
||||
"""Verify documented default values haven't drifted."""
|
||||
|
||||
def test_default_result_size(self):
|
||||
assert DEFAULT_RESULT_SIZE_CHARS == 100_000
|
||||
|
||||
def test_default_turn_budget(self):
|
||||
assert DEFAULT_TURN_BUDGET_CHARS == 200_000
|
||||
|
||||
def test_default_preview_size(self):
|
||||
assert DEFAULT_PREVIEW_SIZE_CHARS == 1_500
|
||||
|
||||
|
||||
class TestPinnedThresholds:
|
||||
"""PINNED_THRESHOLDS – tools whose values must never be overridden."""
|
||||
|
||||
def test_read_file_is_inf(self):
|
||||
assert PINNED_THRESHOLDS["read_file"] == float("inf")
|
||||
assert math.isinf(PINNED_THRESHOLDS["read_file"])
|
||||
|
||||
def test_pinned_is_not_empty(self):
|
||||
assert len(PINNED_THRESHOLDS) >= 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# BudgetConfig defaults
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestBudgetConfigDefaults:
|
||||
"""BudgetConfig() should match the module-level defaults exactly."""
|
||||
|
||||
def test_default_result_size(self):
|
||||
cfg = BudgetConfig()
|
||||
assert cfg.default_result_size == DEFAULT_RESULT_SIZE_CHARS
|
||||
|
||||
def test_default_turn_budget(self):
|
||||
cfg = BudgetConfig()
|
||||
assert cfg.turn_budget == DEFAULT_TURN_BUDGET_CHARS
|
||||
|
||||
def test_default_preview_size(self):
|
||||
cfg = BudgetConfig()
|
||||
assert cfg.preview_size == DEFAULT_PREVIEW_SIZE_CHARS
|
||||
|
||||
def test_default_tool_overrides_empty(self):
|
||||
cfg = BudgetConfig()
|
||||
assert cfg.tool_overrides == {}
|
||||
|
||||
def test_default_budget_singleton_matches(self):
|
||||
"""DEFAULT_BUDGET should equal a freshly constructed BudgetConfig."""
|
||||
assert DEFAULT_BUDGET == BudgetConfig()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Immutability (frozen=True)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestBudgetConfigFrozen:
|
||||
"""Frozen dataclass must reject attribute mutation."""
|
||||
|
||||
def test_cannot_set_default_result_size(self):
|
||||
cfg = BudgetConfig()
|
||||
with pytest.raises(dataclasses.FrozenInstanceError):
|
||||
cfg.default_result_size = 999
|
||||
|
||||
def test_cannot_set_turn_budget(self):
|
||||
cfg = BudgetConfig()
|
||||
with pytest.raises(dataclasses.FrozenInstanceError):
|
||||
cfg.turn_budget = 999
|
||||
|
||||
def test_cannot_set_preview_size(self):
|
||||
cfg = BudgetConfig()
|
||||
with pytest.raises(dataclasses.FrozenInstanceError):
|
||||
cfg.preview_size = 999
|
||||
|
||||
def test_cannot_set_tool_overrides(self):
|
||||
cfg = BudgetConfig()
|
||||
with pytest.raises(dataclasses.FrozenInstanceError):
|
||||
cfg.tool_overrides = {"foo": 1}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Custom construction
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestBudgetConfigCustom:
|
||||
"""BudgetConfig can be created with non-default values."""
|
||||
|
||||
def test_custom_values(self):
|
||||
cfg = BudgetConfig(
|
||||
default_result_size=50_000,
|
||||
turn_budget=100_000,
|
||||
preview_size=500,
|
||||
tool_overrides={"my_tool": 42},
|
||||
)
|
||||
assert cfg.default_result_size == 50_000
|
||||
assert cfg.turn_budget == 100_000
|
||||
assert cfg.preview_size == 500
|
||||
assert cfg.tool_overrides == {"my_tool": 42}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# resolve_threshold() priority chain
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestResolveThreshold:
|
||||
"""Priority: pinned > tool_overrides > registry > default."""
|
||||
|
||||
def test_pinned_wins_over_override(self):
|
||||
"""Even if tool_overrides contains read_file, pinned value wins."""
|
||||
cfg = BudgetConfig(tool_overrides={"read_file": 1})
|
||||
result = cfg.resolve_threshold("read_file")
|
||||
assert result == float("inf")
|
||||
|
||||
def test_tool_override_wins_over_default(self):
|
||||
"""tool_overrides should be returned before falling back to registry."""
|
||||
cfg = BudgetConfig(tool_overrides={"my_tool": 42})
|
||||
result = cfg.resolve_threshold("my_tool")
|
||||
assert result == 42
|
||||
|
||||
@patch("tools.registry.registry")
|
||||
def test_falls_back_to_registry(self, mock_registry):
|
||||
"""When not pinned and not in overrides, delegate to registry."""
|
||||
mock_registry.get_max_result_size.return_value = 77_777
|
||||
cfg = BudgetConfig()
|
||||
result = cfg.resolve_threshold("some_tool")
|
||||
mock_registry.get_max_result_size.assert_called_once_with(
|
||||
"some_tool", default=DEFAULT_RESULT_SIZE_CHARS
|
||||
)
|
||||
assert result == 77_777
|
||||
|
||||
@patch("tools.registry.registry")
|
||||
def test_registry_receives_custom_default(self, mock_registry):
|
||||
"""Custom default_result_size flows through to registry call."""
|
||||
mock_registry.get_max_result_size.return_value = 50_000
|
||||
cfg = BudgetConfig(default_result_size=50_000)
|
||||
cfg.resolve_threshold("unknown_tool")
|
||||
mock_registry.get_max_result_size.assert_called_once_with(
|
||||
"unknown_tool", default=50_000
|
||||
)
|
||||
|
||||
def test_pinned_read_file_returns_inf(self):
|
||||
"""Canonical case: read_file must always return inf."""
|
||||
cfg = BudgetConfig()
|
||||
assert cfg.resolve_threshold("read_file") == float("inf")
|
||||
Reference in New Issue
Block a user