"""Tests for tools.tool_output_limits. Covers: 1. Default values when no config is provided. 2. Config override picks up user-supplied max_bytes / max_lines / max_line_length. 3. Malformed values (None, negative, wrong type) fall back to defaults rather than raising. 4. Integration: the helpers return what the terminal_tool and file_operations call paths will actually consume. Port-tracking: anomalyco/opencode PR #23770 (feat(truncate): allow configuring tool output truncation limits). """ from __future__ import annotations from unittest.mock import patch import pytest from tools import tool_output_limits as tol class TestDefaults: def test_defaults_match_previous_hardcoded_values(self): assert tol.DEFAULT_MAX_BYTES == 50_000 assert tol.DEFAULT_MAX_LINES == 2000 assert tol.DEFAULT_MAX_LINE_LENGTH == 2000 def test_get_limits_returns_defaults_when_config_missing(self): with patch("hermes_cli.config.load_config", return_value={}): limits = tol.get_tool_output_limits() assert limits == { "max_bytes": tol.DEFAULT_MAX_BYTES, "max_lines": tol.DEFAULT_MAX_LINES, "max_line_length": tol.DEFAULT_MAX_LINE_LENGTH, } def test_get_limits_returns_defaults_when_config_not_a_dict(self): # load_config should always return a dict but be defensive anyway. with patch("hermes_cli.config.load_config", return_value="not a dict"): limits = tol.get_tool_output_limits() assert limits["max_bytes"] == tol.DEFAULT_MAX_BYTES def test_get_limits_returns_defaults_when_load_config_raises(self): def _boom(): raise RuntimeError("boom") with patch("hermes_cli.config.load_config", side_effect=_boom): limits = tol.get_tool_output_limits() assert limits["max_lines"] == tol.DEFAULT_MAX_LINES class TestOverrides: def test_user_config_overrides_all_three(self): cfg = { "tool_output": { "max_bytes": 100_000, "max_lines": 5000, "max_line_length": 4096, } } with patch("hermes_cli.config.load_config", return_value=cfg): limits = tol.get_tool_output_limits() assert limits == { "max_bytes": 100_000, "max_lines": 5000, "max_line_length": 4096, } def test_partial_override_preserves_other_defaults(self): cfg = {"tool_output": {"max_bytes": 200_000}} with patch("hermes_cli.config.load_config", return_value=cfg): limits = tol.get_tool_output_limits() assert limits["max_bytes"] == 200_000 assert limits["max_lines"] == tol.DEFAULT_MAX_LINES assert limits["max_line_length"] == tol.DEFAULT_MAX_LINE_LENGTH def test_section_not_a_dict_falls_back(self): cfg = {"tool_output": "nonsense"} with patch("hermes_cli.config.load_config", return_value=cfg): limits = tol.get_tool_output_limits() assert limits["max_bytes"] == tol.DEFAULT_MAX_BYTES class TestCoercion: @pytest.mark.parametrize("bad", [None, "not a number", -1, 0, [], {}]) def test_invalid_values_fall_back_to_defaults(self, bad): cfg = {"tool_output": {"max_bytes": bad, "max_lines": bad, "max_line_length": bad}} with patch("hermes_cli.config.load_config", return_value=cfg): limits = tol.get_tool_output_limits() assert limits["max_bytes"] == tol.DEFAULT_MAX_BYTES assert limits["max_lines"] == tol.DEFAULT_MAX_LINES assert limits["max_line_length"] == tol.DEFAULT_MAX_LINE_LENGTH def test_string_integer_is_coerced(self): cfg = {"tool_output": {"max_bytes": "75000"}} with patch("hermes_cli.config.load_config", return_value=cfg): limits = tol.get_tool_output_limits() assert limits["max_bytes"] == 75_000 class TestShortcuts: def test_individual_accessors_delegate_to_get_tool_output_limits(self): cfg = { "tool_output": { "max_bytes": 111, "max_lines": 222, "max_line_length": 333, } } with patch("hermes_cli.config.load_config", return_value=cfg): assert tol.get_max_bytes() == 111 assert tol.get_max_lines() == 222 assert tol.get_max_line_length() == 333 class TestDefaultConfigHasSection: """The DEFAULT_CONFIG in hermes_cli.config must expose tool_output so that ``hermes setup`` and default installs stay in sync with the helpers here.""" def test_default_config_contains_tool_output_section(self): from hermes_cli.config import DEFAULT_CONFIG assert "tool_output" in DEFAULT_CONFIG section = DEFAULT_CONFIG["tool_output"] assert isinstance(section, dict) assert section["max_bytes"] == tol.DEFAULT_MAX_BYTES assert section["max_lines"] == tol.DEFAULT_MAX_LINES assert section["max_line_length"] == tol.DEFAULT_MAX_LINE_LENGTH class TestIntegrationReadPagination: """normalize_read_pagination uses get_max_lines() — verify the plumbing.""" def test_pagination_limit_clamped_by_config_value(self): from tools.file_operations import normalize_read_pagination cfg = {"tool_output": {"max_lines": 50}} with patch("hermes_cli.config.load_config", return_value=cfg): offset, limit = normalize_read_pagination(offset=1, limit=1000) # limit should have been clamped to 50 (the configured max_lines) assert limit == 50 assert offset == 1 def test_pagination_default_when_config_missing(self): from tools.file_operations import normalize_read_pagination with patch("hermes_cli.config.load_config", return_value={}): offset, limit = normalize_read_pagination(offset=10, limit=100000) # Clamped to default MAX_LINES (2000). assert limit == tol.DEFAULT_MAX_LINES assert offset == 10