Skip to content

Commit 6d81e26

Browse files
committed
test(scripts): regression tests for tools.json generator bugfixes
- ``test_skips_nested_async_functions`` — pins the bug where ``ast.walk`` recursed into nested ``async def`` closures (e.g. ``register_preview_tool``'s inner ``apply``), surfacing them as duplicate top-level tool entries. - ``test_joins_wrapped_first_sentence`` — pins the bug where the description extractor took only the first line of the docstring, truncating mid-sentence when the first sentence wrapped onto a second line (e.g. ``get_inventory_movements`` shipped with a trailing comma in the original Docker MCP Registry submission). - ``test_trims_to_first_sentence_after_paragraph_join`` — confirms the paragraph-join + sentence-trim path keeps just the first sentence when a docstring opens with two on consecutive lines.
1 parent b8e0b69 commit 6d81e26

1 file changed

Lines changed: 86 additions & 0 deletions

File tree

tests/test_generate_tools_json.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,44 @@ def test_handles_syntax_errors_gracefully(self):
137137
tools = _extract_from_directory(test_dir)
138138
assert len(tools) == 0
139139

140+
def test_skips_nested_async_functions(self):
141+
"""Nested ``async def`` closures must not be mistaken for top-level tools.
142+
143+
Regression: ``register_preview_tool`` wiring uses inner
144+
``async def apply()`` closures to handle the apply step. A previous
145+
implementation walked the AST recursively, which surfaced these
146+
closures as duplicate ``apply`` tool entries (5 of them in
147+
``corrections.py`` alone) in the generated tools.json.
148+
"""
149+
with tempfile.TemporaryDirectory() as tmpdir:
150+
test_dir = Path(tmpdir)
151+
test_file = test_dir / "tool_with_nested.py"
152+
test_file.write_text('''
153+
async def public_tool():
154+
"""Public tool description."""
155+
156+
async def apply():
157+
"""Inner closure that should NOT be extracted."""
158+
pass
159+
160+
async def _private_helper():
161+
"""Inner private — also should NOT be extracted."""
162+
pass
163+
164+
return apply
165+
166+
async def another_public_tool():
167+
"""Another public tool."""
168+
pass
169+
''')
170+
171+
tools = _extract_from_directory(test_dir)
172+
tool_names = [t["name"] for t in tools]
173+
174+
assert tool_names == ["public_tool", "another_public_tool"]
175+
assert "apply" not in tool_names
176+
assert "_private_helper" not in tool_names
177+
140178

141179
class TestExtractDescription:
142180
"""Tests for _extract_description function."""
@@ -193,6 +231,54 @@ async def test_tool():
193231

194232
assert description == "Tool: test_tool"
195233

234+
def test_joins_wrapped_first_sentence(self):
235+
"""A first sentence that wraps onto a second line must not be truncated.
236+
237+
Regression: previously the extractor took only the first line of the
238+
docstring, which produced descriptions ending in a comma when the
239+
first sentence wrapped (e.g. ``get_inventory_movements`` shipped a
240+
truncated description in the Docker MCP Registry submission).
241+
"""
242+
import ast
243+
244+
code = '''
245+
async def wrapped_tool():
246+
"""Get inventory movement history for a SKU — every stock change with dates,
247+
quantities, and what caused each movement.
248+
249+
More details below.
250+
"""
251+
pass
252+
'''
253+
tree = ast.parse(code)
254+
func_node = tree.body[0]
255+
description = _extract_description(func_node)
256+
257+
assert description == (
258+
"Get inventory movement history for a SKU — every stock change "
259+
"with dates, quantities, and what caused each movement."
260+
)
261+
262+
def test_trims_to_first_sentence_after_paragraph_join(self):
263+
"""Once paragraph lines are joined, only the first sentence is kept."""
264+
import ast
265+
266+
code = '''
267+
async def two_sentence_tool():
268+
"""Edit a closed sales order without losing its picked_date and
269+
fulfillment metadata. Reopens the order, applies edits, then closes.
270+
"""
271+
pass
272+
'''
273+
tree = ast.parse(code)
274+
func_node = tree.body[0]
275+
description = _extract_description(func_node)
276+
277+
assert description == (
278+
"Edit a closed sales order without losing its picked_date "
279+
"and fulfillment metadata."
280+
)
281+
196282

197283
class TestValidateTools:
198284
"""Tests for validate_tools function."""

0 commit comments

Comments
 (0)