test_tool_output_limits.py
1 """Tests for tools.tool_output_limits. 2 3 Covers: 4 1. Default values when no config is provided. 5 2. Config override picks up user-supplied max_bytes / max_lines / 6 max_line_length. 7 3. Malformed values (None, negative, wrong type) fall back to defaults 8 rather than raising. 9 4. Integration: the helpers return what the terminal_tool and 10 file_operations call paths will actually consume. 11 12 Port-tracking: anomalyco/opencode PR #23770 13 (feat(truncate): allow configuring tool output truncation limits). 14 """ 15 16 from __future__ import annotations 17 18 from unittest.mock import patch 19 20 import pytest 21 22 from tools import tool_output_limits as tol 23 24 25 class TestDefaults: 26 def test_defaults_match_previous_hardcoded_values(self): 27 assert tol.DEFAULT_MAX_BYTES == 50_000 28 assert tol.DEFAULT_MAX_LINES == 2000 29 assert tol.DEFAULT_MAX_LINE_LENGTH == 2000 30 31 def test_get_limits_returns_defaults_when_config_missing(self): 32 with patch("hermes_cli.config.load_config", return_value={}): 33 limits = tol.get_tool_output_limits() 34 assert limits == { 35 "max_bytes": tol.DEFAULT_MAX_BYTES, 36 "max_lines": tol.DEFAULT_MAX_LINES, 37 "max_line_length": tol.DEFAULT_MAX_LINE_LENGTH, 38 } 39 40 def test_get_limits_returns_defaults_when_config_not_a_dict(self): 41 # load_config should always return a dict but be defensive anyway. 42 with patch("hermes_cli.config.load_config", return_value="not a dict"): 43 limits = tol.get_tool_output_limits() 44 assert limits["max_bytes"] == tol.DEFAULT_MAX_BYTES 45 46 def test_get_limits_returns_defaults_when_load_config_raises(self): 47 def _boom(): 48 raise RuntimeError("boom") 49 50 with patch("hermes_cli.config.load_config", side_effect=_boom): 51 limits = tol.get_tool_output_limits() 52 assert limits["max_lines"] == tol.DEFAULT_MAX_LINES 53 54 55 class TestOverrides: 56 def test_user_config_overrides_all_three(self): 57 cfg = { 58 "tool_output": { 59 "max_bytes": 100_000, 60 "max_lines": 5000, 61 "max_line_length": 4096, 62 } 63 } 64 with patch("hermes_cli.config.load_config", return_value=cfg): 65 limits = tol.get_tool_output_limits() 66 assert limits == { 67 "max_bytes": 100_000, 68 "max_lines": 5000, 69 "max_line_length": 4096, 70 } 71 72 def test_partial_override_preserves_other_defaults(self): 73 cfg = {"tool_output": {"max_bytes": 200_000}} 74 with patch("hermes_cli.config.load_config", return_value=cfg): 75 limits = tol.get_tool_output_limits() 76 assert limits["max_bytes"] == 200_000 77 assert limits["max_lines"] == tol.DEFAULT_MAX_LINES 78 assert limits["max_line_length"] == tol.DEFAULT_MAX_LINE_LENGTH 79 80 def test_section_not_a_dict_falls_back(self): 81 cfg = {"tool_output": "nonsense"} 82 with patch("hermes_cli.config.load_config", return_value=cfg): 83 limits = tol.get_tool_output_limits() 84 assert limits["max_bytes"] == tol.DEFAULT_MAX_BYTES 85 86 87 class TestCoercion: 88 @pytest.mark.parametrize("bad", [None, "not a number", -1, 0, [], {}]) 89 def test_invalid_values_fall_back_to_defaults(self, bad): 90 cfg = {"tool_output": {"max_bytes": bad, "max_lines": bad, "max_line_length": bad}} 91 with patch("hermes_cli.config.load_config", return_value=cfg): 92 limits = tol.get_tool_output_limits() 93 assert limits["max_bytes"] == tol.DEFAULT_MAX_BYTES 94 assert limits["max_lines"] == tol.DEFAULT_MAX_LINES 95 assert limits["max_line_length"] == tol.DEFAULT_MAX_LINE_LENGTH 96 97 def test_string_integer_is_coerced(self): 98 cfg = {"tool_output": {"max_bytes": "75000"}} 99 with patch("hermes_cli.config.load_config", return_value=cfg): 100 limits = tol.get_tool_output_limits() 101 assert limits["max_bytes"] == 75_000 102 103 104 class TestShortcuts: 105 def test_individual_accessors_delegate_to_get_tool_output_limits(self): 106 cfg = { 107 "tool_output": { 108 "max_bytes": 111, 109 "max_lines": 222, 110 "max_line_length": 333, 111 } 112 } 113 with patch("hermes_cli.config.load_config", return_value=cfg): 114 assert tol.get_max_bytes() == 111 115 assert tol.get_max_lines() == 222 116 assert tol.get_max_line_length() == 333 117 118 119 class TestDefaultConfigHasSection: 120 """The DEFAULT_CONFIG in hermes_cli.config must expose tool_output so 121 that ``hermes setup`` and default installs stay in sync with the 122 helpers here.""" 123 124 def test_default_config_contains_tool_output_section(self): 125 from hermes_cli.config import DEFAULT_CONFIG 126 assert "tool_output" in DEFAULT_CONFIG 127 section = DEFAULT_CONFIG["tool_output"] 128 assert isinstance(section, dict) 129 assert section["max_bytes"] == tol.DEFAULT_MAX_BYTES 130 assert section["max_lines"] == tol.DEFAULT_MAX_LINES 131 assert section["max_line_length"] == tol.DEFAULT_MAX_LINE_LENGTH 132 133 134 class TestIntegrationReadPagination: 135 """normalize_read_pagination uses get_max_lines() — verify the plumbing.""" 136 137 def test_pagination_limit_clamped_by_config_value(self): 138 from tools.file_operations import normalize_read_pagination 139 cfg = {"tool_output": {"max_lines": 50}} 140 with patch("hermes_cli.config.load_config", return_value=cfg): 141 offset, limit = normalize_read_pagination(offset=1, limit=1000) 142 # limit should have been clamped to 50 (the configured max_lines) 143 assert limit == 50 144 assert offset == 1 145 146 def test_pagination_default_when_config_missing(self): 147 from tools.file_operations import normalize_read_pagination 148 with patch("hermes_cli.config.load_config", return_value={}): 149 offset, limit = normalize_read_pagination(offset=10, limit=100000) 150 # Clamped to default MAX_LINES (2000). 151 assert limit == tol.DEFAULT_MAX_LINES 152 assert offset == 10