Change-detectors, tautologies, source-reading tests, redundant duplicates, mock-echo tests and dead/unrunnable tests. Per-test rationale in the lane ledger (category + reason for every removal).
181 lines
7.2 KiB
Python
181 lines
7.2 KiB
Python
"""Tests for :func:`hermes_cli.config.resolve_turn_limit` and the
|
|
``TURN_LIMIT_UNLIMITED`` sentinel.
|
|
|
|
Covers the full spelling table (int, float, numeric string, ``"none"``,
|
|
``"unlimited"``, ``"infinite"``, ``"-1"``, ``"0"``, YAML ``None``, bool,
|
|
garbage) and the config→env bridge in the gateway and TUI resolvers.
|
|
"""
|
|
import os
|
|
import pytest
|
|
|
|
from hermes_cli.config import resolve_turn_limit, TURN_LIMIT_UNLIMITED
|
|
|
|
|
|
class TestNumericValues:
|
|
def test_int_passthrough(self):
|
|
assert resolve_turn_limit(90) == 90
|
|
assert resolve_turn_limit(120) == 120
|
|
assert resolve_turn_limit(1) == 1
|
|
|
|
def test_float_truncated(self):
|
|
assert resolve_turn_limit(3.7) == 3
|
|
assert resolve_turn_limit(3.0) == 3
|
|
assert resolve_turn_limit(100.9) == 100
|
|
|
|
def test_numeric_string(self):
|
|
assert resolve_turn_limit("120") == 120
|
|
assert resolve_turn_limit("3") == 3
|
|
assert resolve_turn_limit("3.7") == 3 # float string → int
|
|
|
|
def test_negative_int_is_unlimited(self):
|
|
assert resolve_turn_limit(-5) == TURN_LIMIT_UNLIMITED
|
|
|
|
|
|
|
|
class TestUnlimitedSpellings:
|
|
@pytest.mark.parametrize("spelling", [
|
|
"none", "None", "NONE",
|
|
"unlimited",
|
|
"infinite",
|
|
"∞",
|
|
"-1", "0",
|
|
])
|
|
def test_string_spellings_resolve_to_sentinel(self, spelling):
|
|
assert resolve_turn_limit(spelling) == TURN_LIMIT_UNLIMITED
|
|
|
|
@pytest.mark.parametrize("spelling", [" none ", " unlimited ", "\tinfinite\t"])
|
|
def test_whitespace_tolerant(self, spelling):
|
|
assert resolve_turn_limit(spelling) == TURN_LIMIT_UNLIMITED
|
|
|
|
def test_zero_int_is_unlimited(self):
|
|
assert resolve_turn_limit(0) == TURN_LIMIT_UNLIMITED
|
|
|
|
def test_zero_float_is_unlimited(self):
|
|
assert resolve_turn_limit(0.0) == TURN_LIMIT_UNLIMITED
|
|
|
|
|
|
class TestAbsentAndDefault:
|
|
def test_none_returns_default(self):
|
|
# Default is now unlimited (max_turns caused more problems than it solved).
|
|
assert resolve_turn_limit(None) == TURN_LIMIT_UNLIMITED
|
|
|
|
def test_none_custom_default(self):
|
|
assert resolve_turn_limit(None, default=500) == 500
|
|
|
|
def test_empty_string_returns_default(self):
|
|
assert resolve_turn_limit("") == TURN_LIMIT_UNLIMITED
|
|
|
|
def test_whitespace_only_returns_default(self):
|
|
assert resolve_turn_limit(" ") == TURN_LIMIT_UNLIMITED
|
|
|
|
|
|
|
|
class TestInvalidInputs:
|
|
def test_bool_rejected(self):
|
|
# bool is an int subclass — must not silently become 1/0
|
|
assert resolve_turn_limit(True) == TURN_LIMIT_UNLIMITED
|
|
assert resolve_turn_limit(False) == TURN_LIMIT_UNLIMITED
|
|
|
|
def test_garbage_string_returns_default(self):
|
|
assert resolve_turn_limit("garbage") == TURN_LIMIT_UNLIMITED
|
|
assert resolve_turn_limit("not_a_number") == TURN_LIMIT_UNLIMITED
|
|
|
|
def test_list_returns_default(self):
|
|
assert resolve_turn_limit([]) == TURN_LIMIT_UNLIMITED
|
|
assert resolve_turn_limit([90]) == TURN_LIMIT_UNLIMITED
|
|
|
|
def test_dict_returns_default(self):
|
|
assert resolve_turn_limit({}) == TURN_LIMIT_UNLIMITED
|
|
assert resolve_turn_limit({"max_turns": 90}) == TURN_LIMIT_UNLIMITED
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestGatewayBridgeNullHandling:
|
|
"""Verify that _bridge_max_turns_from_config does NOT serialize Python None
|
|
(from YAML ``null`` or bare ``key:``) into the env var as the string
|
|
"None", which would resolve to unlimited instead of the default."""
|
|
|
|
def test_none_value_not_bridged(self, monkeypatch, tmp_path):
|
|
"""YAML ``max_turns: null`` should not set HERMES_MAX_ITERATIONS."""
|
|
cfg_file = tmp_path / "config.yaml"
|
|
cfg_file.write_text("agent:\n max_turns: null\n", encoding="utf-8")
|
|
monkeypatch.setenv("HERMES_MAX_ITERATIONS", "stale-120")
|
|
# Import here to avoid module-level gateway dependency
|
|
from gateway.run import _bridge_max_turns_from_config
|
|
_bridge_max_turns_from_config(tmp_path)
|
|
# The stale env var should be cleared so downstream resolver
|
|
# applies the default rather than using the stale value.
|
|
assert "HERMES_MAX_ITERATIONS" not in os.environ
|
|
|
|
def test_string_none_bridged_correctly(self, monkeypatch, tmp_path):
|
|
"""YAML ``max_turns: "none"`` should bridge as the string "none"."""
|
|
cfg_file = tmp_path / "config.yaml"
|
|
cfg_file.write_text('agent:\n max_turns: "none"\n', encoding="utf-8")
|
|
monkeypatch.delenv("HERMES_MAX_ITERATIONS", raising=False)
|
|
from gateway.run import _bridge_max_turns_from_config
|
|
_bridge_max_turns_from_config(tmp_path)
|
|
assert os.environ.get("HERMES_MAX_ITERATIONS") == "none"
|
|
|
|
def test_int_bridged_correctly(self, monkeypatch, tmp_path):
|
|
"""YAML ``max_turns: 120`` should bridge as "120"."""
|
|
cfg_file = tmp_path / "config.yaml"
|
|
cfg_file.write_text("agent:\n max_turns: 120\n", encoding="utf-8")
|
|
monkeypatch.delenv("HERMES_MAX_ITERATIONS", raising=False)
|
|
from gateway.run import _bridge_max_turns_from_config
|
|
_bridge_max_turns_from_config(tmp_path)
|
|
assert os.environ.get("HERMES_MAX_ITERATIONS") == "120"
|
|
|
|
def test_bare_key_treated_as_null(self, monkeypatch, tmp_path):
|
|
"""YAML ``max_turns:`` (bare key, no value) parses as Python None."""
|
|
cfg_file = tmp_path / "config.yaml"
|
|
cfg_file.write_text("agent:\n max_turns:\n", encoding="utf-8")
|
|
monkeypatch.setenv("HERMES_MAX_ITERATIONS", "stale-90")
|
|
from gateway.run import _bridge_max_turns_from_config
|
|
_bridge_max_turns_from_config(tmp_path)
|
|
assert "HERMES_MAX_ITERATIONS" not in os.environ
|
|
|
|
|
|
class TestTUIResolver:
|
|
"""Verify that _cfg_max_turns in tui_gateway/server.py routes through
|
|
resolve_turn_limit instead of bare int()."""
|
|
|
|
def test_string_none_resolves_to_unlimited(self):
|
|
from tui_gateway.server import _cfg_max_turns
|
|
cfg = {"agent": {"max_turns": "none"}}
|
|
assert _cfg_max_turns(cfg, default=25) == TURN_LIMIT_UNLIMITED
|
|
|
|
|
|
|
|
def test_int_passthrough(self):
|
|
from tui_gateway.server import _cfg_max_turns
|
|
cfg = {"agent": {"max_turns": 50}}
|
|
assert _cfg_max_turns(cfg, default=25) == 50
|
|
|
|
def test_none_falls_through_to_default(self):
|
|
from tui_gateway.server import _cfg_max_turns
|
|
cfg = {"agent": {}}
|
|
assert _cfg_max_turns(cfg, default=25) == 25
|
|
|
|
def test_env_var_override(self, monkeypatch):
|
|
from tui_gateway.server import _cfg_max_turns
|
|
monkeypatch.setenv("HERMES_TUI_MAX_TURNS", "none")
|
|
cfg = {"agent": {"max_turns": 50}}
|
|
assert _cfg_max_turns(cfg, default=25) == TURN_LIMIT_UNLIMITED
|
|
|
|
def test_env_var_zero_is_unlimited(self, monkeypatch):
|
|
"""Old code swallowed 0 via `int(0) > 0` → False. Now it routes
|
|
through resolve_turn_limit which treats 0 as unlimited."""
|
|
from tui_gateway.server import _cfg_max_turns
|
|
monkeypatch.setenv("HERMES_TUI_MAX_TURNS", "0")
|
|
cfg = {"agent": {"max_turns": 50}}
|
|
assert _cfg_max_turns(cfg, default=25) == TURN_LIMIT_UNLIMITED
|
|
|
|
def test_root_level_max_turns(self):
|
|
"""Legacy root-level max_turns still works."""
|
|
from tui_gateway.server import _cfg_max_turns
|
|
cfg = {"max_turns": "none"}
|
|
assert _cfg_max_turns(cfg, default=25) == TURN_LIMIT_UNLIMITED
|