/ tests / tools / test_tool_output_limits.py
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