hermes-agent/tests/tools/test_budget_config.py
luyao618 50ad66aee6 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>
2026-04-11 02:58:48 -07:00

176 lines
5.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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")