Skip to content

Commit 14c08f8

Browse files
raifdmuellerclaude
andauthored
fix: CLI rejects negative --max-depth and --limit values (#248, #249) (#253)
- structure --max-depth now validates for non-negative values - search --limit/--max-results now validates for non-negative values - Both use click.BadParameter with clear error messages - Follows existing pattern from sections-at-level (Issue #199) - Bump version to 0.4.22 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 952dafd commit 14c08f8

File tree

5 files changed

+195
-3
lines changed

5 files changed

+195
-3
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "dacli"
3-
version = "0.4.23"
3+
version = "0.4.24"
44
description = "Documentation Access CLI - Navigate and query large documentation projects"
55
readme = "README.md"
66
license = { text = "MIT" }

src/dacli/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44
through hierarchical, content-aware access via the Model Context Protocol (MCP).
55
"""
66

7-
__version__ = "0.4.23"
7+
8+
__version__ = "0.4.24"

src/dacli/cli.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,13 @@ def cli(
389389
@pass_context
390390
def structure(ctx: CliContext, max_depth: int | None):
391391
"""Get the hierarchical document structure."""
392+
# Validate max_depth is non-negative (Issue #248)
393+
if max_depth is not None and max_depth < 0:
394+
raise click.BadParameter(
395+
f"max-depth must be non-negative, got {max_depth}. "
396+
"Use 0 for root level only, or omit for full depth.",
397+
param_hint="'--max-depth'",
398+
)
392399
result = ctx.index.get_structure(max_depth)
393400
click.echo(format_output(ctx, result))
394401

@@ -516,6 +523,13 @@ def sections_at_level(ctx: CliContext, level: int):
516523
@pass_context
517524
def search(ctx: CliContext, query: str, scope: str | None, max_results: int):
518525
"""Search for content in the documentation."""
526+
# Validate max_results is non-negative (Issue #249)
527+
if max_results < 0:
528+
raise click.BadParameter(
529+
f"limit must be non-negative, got {max_results}. "
530+
"Use 0 for no results, or a positive number to limit results.",
531+
param_hint="'--limit'",
532+
)
519533
# Validate query is not empty
520534
if not query or not query.strip():
521535
click.echo("Error: Search query cannot be empty", err=True)
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
"""Tests for Issues #248 and #249: CLI should reject negative --max-depth and --limit.
2+
3+
Issue #248: `dacli structure --max-depth -1` accepts negative values without error.
4+
Issue #249: `dacli search "test" --limit -5` accepts negative values without error.
5+
6+
Both should reject negative values with a clear error message,
7+
following the same pattern as sections-at-level (Issue #199).
8+
"""
9+
10+
from pathlib import Path
11+
12+
import pytest
13+
from click.testing import CliRunner
14+
15+
from dacli.cli import cli
16+
17+
18+
@pytest.fixture
19+
def temp_doc_dir(tmp_path: Path) -> Path:
20+
"""Create a temporary directory with test documents."""
21+
doc_file = tmp_path / "test.adoc"
22+
doc_file.write_text(
23+
"""= Test Document
24+
25+
== Introduction
26+
27+
Some introductory content.
28+
29+
== Details
30+
31+
More detailed content here.
32+
""",
33+
encoding="utf-8",
34+
)
35+
return tmp_path
36+
37+
38+
class TestStructureNegativeMaxDepth:
39+
"""Issue #248: structure --max-depth should reject negative values."""
40+
41+
def test_negative_max_depth_rejected(self, temp_doc_dir: Path):
42+
"""--max-depth -1 should be rejected with clear error."""
43+
runner = CliRunner()
44+
result = runner.invoke(
45+
cli,
46+
["--docs-root", str(temp_doc_dir), "structure", "--max-depth", "-1"],
47+
)
48+
49+
assert result.exit_code != 0
50+
assert "max-depth must be non-negative" in result.output
51+
52+
def test_negative_max_depth_includes_value(self, temp_doc_dir: Path):
53+
"""Error message should include the actual negative value."""
54+
runner = CliRunner()
55+
result = runner.invoke(
56+
cli,
57+
["--docs-root", str(temp_doc_dir), "structure", "--max-depth", "-5"],
58+
)
59+
60+
assert result.exit_code != 0
61+
assert "got -5" in result.output
62+
63+
def test_max_depth_zero_works(self, temp_doc_dir: Path):
64+
"""--max-depth 0 should work (returns only root level)."""
65+
runner = CliRunner()
66+
result = runner.invoke(
67+
cli,
68+
["--docs-root", str(temp_doc_dir), "structure", "--max-depth", "0"],
69+
)
70+
71+
assert result.exit_code == 0
72+
73+
def test_max_depth_positive_works(self, temp_doc_dir: Path):
74+
"""--max-depth 2 should work normally."""
75+
runner = CliRunner()
76+
result = runner.invoke(
77+
cli,
78+
["--docs-root", str(temp_doc_dir), "structure", "--max-depth", "2"],
79+
)
80+
81+
assert result.exit_code == 0
82+
83+
def test_max_depth_none_works(self, temp_doc_dir: Path):
84+
"""Omitting --max-depth should work (returns full structure)."""
85+
runner = CliRunner()
86+
result = runner.invoke(
87+
cli,
88+
["--docs-root", str(temp_doc_dir), "structure"],
89+
)
90+
91+
assert result.exit_code == 0
92+
93+
def test_str_alias_negative_max_depth_rejected(self, temp_doc_dir: Path):
94+
"""The 'str' alias should also reject negative --max-depth."""
95+
runner = CliRunner()
96+
result = runner.invoke(
97+
cli,
98+
["--docs-root", str(temp_doc_dir), "str", "--max-depth", "-1"],
99+
)
100+
101+
assert result.exit_code != 0
102+
assert "max-depth must be non-negative" in result.output
103+
104+
105+
class TestSearchNegativeLimit:
106+
"""Issue #249: search --limit should reject negative values."""
107+
108+
def test_negative_limit_rejected(self, temp_doc_dir: Path):
109+
"""--limit -5 should be rejected with clear error."""
110+
runner = CliRunner()
111+
result = runner.invoke(
112+
cli,
113+
["--docs-root", str(temp_doc_dir), "search", "test", "--limit", "-5"],
114+
)
115+
116+
assert result.exit_code != 0
117+
assert "limit must be non-negative" in result.output
118+
119+
def test_negative_limit_includes_value(self, temp_doc_dir: Path):
120+
"""Error message should include the actual negative value."""
121+
runner = CliRunner()
122+
result = runner.invoke(
123+
cli,
124+
["--docs-root", str(temp_doc_dir), "search", "test", "--limit", "-1"],
125+
)
126+
127+
assert result.exit_code != 0
128+
assert "got -1" in result.output
129+
130+
def test_negative_max_results_rejected(self, temp_doc_dir: Path):
131+
"""--max-results -3 should also be rejected (alias for --limit)."""
132+
runner = CliRunner()
133+
result = runner.invoke(
134+
cli,
135+
[
136+
"--docs-root",
137+
str(temp_doc_dir),
138+
"search",
139+
"test",
140+
"--max-results",
141+
"-3",
142+
],
143+
)
144+
145+
assert result.exit_code != 0
146+
assert "limit must be non-negative" in result.output
147+
148+
def test_limit_zero_works(self, temp_doc_dir: Path):
149+
"""--limit 0 should work (returns no results)."""
150+
runner = CliRunner()
151+
result = runner.invoke(
152+
cli,
153+
["--docs-root", str(temp_doc_dir), "search", "test", "--limit", "0"],
154+
)
155+
156+
assert result.exit_code == 0
157+
158+
def test_limit_positive_works(self, temp_doc_dir: Path):
159+
"""--limit 5 should work normally."""
160+
runner = CliRunner()
161+
result = runner.invoke(
162+
cli,
163+
["--docs-root", str(temp_doc_dir), "search", "test", "--limit", "5"],
164+
)
165+
166+
assert result.exit_code == 0
167+
168+
def test_search_alias_negative_limit_rejected(self, temp_doc_dir: Path):
169+
"""The 's' alias should also reject negative --limit."""
170+
runner = CliRunner()
171+
result = runner.invoke(
172+
cli,
173+
["--docs-root", str(temp_doc_dir), "s", "test", "--limit", "-5"],
174+
)
175+
176+
assert result.exit_code != 0
177+
assert "limit must be non-negative" in result.output

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)