drydock-cli 3.0.99__tar.gz → 3.0.102__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {drydock_cli-3.0.99/drydock_cli.egg-info → drydock_cli-3.0.102}/PKG-INFO +1 -1
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/tools/__init__.py +33 -7
- {drydock_cli-3.0.99 → drydock_cli-3.0.102/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock_cli.egg-info/SOURCES.txt +3 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/pyproject.toml +1 -1
- drydock_cli-3.0.102/tests/test_bash_stop_partial.py +50 -0
- drydock_cli-3.0.102/tests/test_edit_replace_all.py +37 -0
- drydock_cli-3.0.102/tests/test_glob_edit_edges.py +28 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/LICENSE +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/NOTICE +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/README.md +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/advisor.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/agent.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/cci.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/cli.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/config.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/detect.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/extract.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/guards.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/poam.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/providers.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/skills.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/stig.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/tui/app.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock/web.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/setup.cfg +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_advisor.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_bash_background.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_bash_binary_output.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_bash_sanitize.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_bash_shell.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_bash_stdin.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_bash_timeout_param.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_cci.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_config.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_graphify_example.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_grep_and_read_robust.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_poam.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_tool_arg_coercion.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_viewimage.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_write_content_coerce.py +0 -0
- {drydock_cli-3.0.99 → drydock_cli-3.0.102}/tests/test_xccdf.py +0 -0
|
@@ -192,13 +192,16 @@ SCHEMAS = [
|
|
|
192
192
|
},
|
|
193
193
|
{
|
|
194
194
|
"name": "Edit",
|
|
195
|
-
"description": "Replace exact text in a file. old_string must match exactly."
|
|
195
|
+
"description": "Replace exact text in a file. old_string must match exactly. "
|
|
196
|
+
"By default old_string must be unique; pass replace_all:true "
|
|
197
|
+
"to replace every occurrence.",
|
|
196
198
|
"input_schema": {
|
|
197
199
|
"type": "object",
|
|
198
200
|
"properties": {
|
|
199
201
|
"file_path": {"type": "string"},
|
|
200
202
|
"old_string": {"type": "string", "description": "Exact text to find"},
|
|
201
203
|
"new_string": {"type": "string", "description": "Replacement text"},
|
|
204
|
+
"replace_all": {"type": "boolean", "description": "Replace ALL occurrences (default false)"},
|
|
202
205
|
},
|
|
203
206
|
"required": ["file_path", "old_string", "new_string"],
|
|
204
207
|
},
|
|
@@ -830,6 +833,11 @@ def tool_edit(params: dict, config: dict) -> str:
|
|
|
830
833
|
# crashing with a TypeError in the substring search / replace.
|
|
831
834
|
old = _as_text(params.get("old_string", ""))
|
|
832
835
|
new = _as_text(params.get("new_string", ""))
|
|
836
|
+
if old == "":
|
|
837
|
+
# An empty old_string matches between every char (count = len+1) → the
|
|
838
|
+
# generic "found N times" is baffling. Say what's actually wrong.
|
|
839
|
+
return ("Error: old_string is empty. Edit replaces existing text — give the "
|
|
840
|
+
"exact text to find. To create a file or add to the end, use Write.")
|
|
833
841
|
if has_conflict_markers(new):
|
|
834
842
|
return conflict_marker_refusal(fp)
|
|
835
843
|
# If a directory was passed, try to infer the intended file (avoids a
|
|
@@ -875,9 +883,15 @@ def tool_edit(params: dict, config: dict) -> str:
|
|
|
875
883
|
f"to copy the exact text (including indentation)."
|
|
876
884
|
)
|
|
877
885
|
count = content.count(old)
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
886
|
+
# replace_all (models expect it, like the standard Edit tool): replace
|
|
887
|
+
# EVERY occurrence. Without it, multiple matches are ambiguous → error.
|
|
888
|
+
replace_all = bool(params.get("replace_all", False))
|
|
889
|
+
if count > 1 and not replace_all:
|
|
890
|
+
return (
|
|
891
|
+
f"Error: old_string found {count} times in {fp}. Add more context "
|
|
892
|
+
f"to make it unique, or pass replace_all: true to replace all."
|
|
893
|
+
)
|
|
894
|
+
updated = content.replace(old, new) if replace_all else content.replace(old, new, 1)
|
|
881
895
|
if updated == content:
|
|
882
896
|
# No-op edit (old_string == new_string, or the replacement changes
|
|
883
897
|
# nothing). Reporting "Edited" here is a false success that invites
|
|
@@ -893,6 +907,8 @@ def tool_edit(params: dict, config: dict) -> str:
|
|
|
893
907
|
Path(fp).write_text(updated, encoding="utf-8")
|
|
894
908
|
_record_undo(config, fp, content)
|
|
895
909
|
result = f"Edited {fp}: replaced {len(old)} chars with {len(new)} chars"
|
|
910
|
+
if replace_all and count > 1:
|
|
911
|
+
result += f" ({count} occurrences)"
|
|
896
912
|
# Advisory post-edit syntax check.
|
|
897
913
|
warn = python_syntax_warning(fp, updated)
|
|
898
914
|
if warn:
|
|
@@ -1051,6 +1067,14 @@ def tool_bash(params: dict, config: dict) -> str:
|
|
|
1051
1067
|
if cancel is not None and cancel.is_set():
|
|
1052
1068
|
kill_process_group(proc)
|
|
1053
1069
|
proc.wait()
|
|
1070
|
+
partial = _sanitize_bash_output(
|
|
1071
|
+
_collapse_repeated_lines("".join(list(chunks)))
|
|
1072
|
+
).rstrip()
|
|
1073
|
+
if partial:
|
|
1074
|
+
tail = partial[-_PARTIAL_TAIL:]
|
|
1075
|
+
if len(partial) > _PARTIAL_TAIL:
|
|
1076
|
+
tail = "[... earlier output truncated ...]\n" + tail
|
|
1077
|
+
return f"[stopped by user]\n\n--- output before stop ---\n{tail}"
|
|
1054
1078
|
return "[stopped by user]"
|
|
1055
1079
|
# The SHELL has exited but the pipe is still open → the command
|
|
1056
1080
|
# backgrounded a child (`cmd &`, a server the task wants to keep
|
|
@@ -1119,8 +1143,10 @@ def tool_bash(params: dict, config: dict) -> str:
|
|
|
1119
1143
|
|
|
1120
1144
|
|
|
1121
1145
|
def tool_glob(params: dict, config: dict) -> str:
|
|
1122
|
-
pattern = params
|
|
1123
|
-
|
|
1146
|
+
pattern = _as_str_arg(params.get("pattern"))
|
|
1147
|
+
if not pattern:
|
|
1148
|
+
return "Error: Glob needs a 'pattern' (e.g. '**/*.py')."
|
|
1149
|
+
base = _as_str_arg(params.get("path")) or config.get("cwd") or "."
|
|
1124
1150
|
try:
|
|
1125
1151
|
matches = sorted(_glob.glob(os.path.join(base, pattern), recursive=True))
|
|
1126
1152
|
if not matches:
|
|
@@ -1135,7 +1161,7 @@ def tool_glob(params: dict, config: dict) -> str:
|
|
|
1135
1161
|
def tool_grep(params: dict, config: dict) -> str:
|
|
1136
1162
|
pattern = _as_str_arg(params.get("pattern"))
|
|
1137
1163
|
path = _as_str_arg(params.get("path")) or config.get("cwd") or "."
|
|
1138
|
-
include = params.get("include", "")
|
|
1164
|
+
include = _as_str_arg(params.get("include", ""))
|
|
1139
1165
|
try:
|
|
1140
1166
|
cmd = ["grep", "-rn", "--color=never"]
|
|
1141
1167
|
if include:
|
|
@@ -57,6 +57,7 @@ tests/test_bash_safety.py
|
|
|
57
57
|
tests/test_bash_sanitize.py
|
|
58
58
|
tests/test_bash_shell.py
|
|
59
59
|
tests/test_bash_stdin.py
|
|
60
|
+
tests/test_bash_stop_partial.py
|
|
60
61
|
tests/test_bash_timeout_network.py
|
|
61
62
|
tests/test_bash_timeout_param.py
|
|
62
63
|
tests/test_cci.py
|
|
@@ -69,11 +70,13 @@ tests/test_context_limit_config.py
|
|
|
69
70
|
tests/test_detect.py
|
|
70
71
|
tests/test_dispatch.py
|
|
71
72
|
tests/test_e2e_connected.py
|
|
73
|
+
tests/test_edit_replace_all.py
|
|
72
74
|
tests/test_empty_response.py
|
|
73
75
|
tests/test_extract.py
|
|
74
76
|
tests/test_failure_loop.py
|
|
75
77
|
tests/test_first_run_setup.py
|
|
76
78
|
tests/test_gittools.py
|
|
79
|
+
tests/test_glob_edit_edges.py
|
|
77
80
|
tests/test_graphify_example.py
|
|
78
81
|
tests/test_graphrag.py
|
|
79
82
|
tests/test_grep_and_read_robust.py
|
|
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|
|
7
7
|
# PyPI distribution name is drydock-cli (the established install name, continued
|
|
8
8
|
# from the retired v2 fork); the import package + CLI command stay `drydock`.
|
|
9
9
|
name = "drydock-cli"
|
|
10
|
-
version = "3.0.
|
|
10
|
+
version = "3.0.102"
|
|
11
11
|
description = "Drydock — a local, provider-agnostic terminal coding agent for local LLMs"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.11"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""tool_bash must preserve partial output when the user presses STOP.
|
|
2
|
+
Without this fix, cancel.is_set() triggered a bare "[stopped by user]" return
|
|
3
|
+
that discarded all chunks accumulated before the stop — losing useful diagnostics
|
|
4
|
+
(test-run results, build output, etc.) that the model needs to continue.
|
|
5
|
+
|
|
6
|
+
Mirrors the timeout-partial-output behaviour already in test_bash_background.py.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
from drydock.tools import tool_bash
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_stop_preserves_partial_output():
|
|
17
|
+
"""Partial output printed before the cancel Event fires must appear in the
|
|
18
|
+
tool result alongside the '[stopped by user]' marker."""
|
|
19
|
+
cancel = threading.Event()
|
|
20
|
+
|
|
21
|
+
# Fire cancel after a short delay so the command has time to emit output.
|
|
22
|
+
def _fire():
|
|
23
|
+
time.sleep(0.5)
|
|
24
|
+
cancel.set()
|
|
25
|
+
|
|
26
|
+
threading.Thread(target=_fire, daemon=True).start()
|
|
27
|
+
out = tool_bash(
|
|
28
|
+
{"command": "echo PARTIAL_LINE; echo second_line; sleep 30", "timeout": 20},
|
|
29
|
+
{"cwd": "/tmp", "_cancel": cancel},
|
|
30
|
+
)
|
|
31
|
+
assert "stopped by user" in out
|
|
32
|
+
assert "PARTIAL_LINE" in out, f"partial output missing from stop result: {out!r}"
|
|
33
|
+
assert "output before stop" in out
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_stop_with_no_output_returns_plain_marker():
|
|
37
|
+
"""If the command produces no output before the stop, the result is just
|
|
38
|
+
'[stopped by user]' without a spurious empty block."""
|
|
39
|
+
cancel = threading.Event()
|
|
40
|
+
|
|
41
|
+
def _fire():
|
|
42
|
+
time.sleep(0.05)
|
|
43
|
+
cancel.set()
|
|
44
|
+
|
|
45
|
+
threading.Thread(target=_fire, daemon=True).start()
|
|
46
|
+
out = tool_bash(
|
|
47
|
+
{"command": "sleep 30", "timeout": 20},
|
|
48
|
+
{"cwd": "/tmp", "_cancel": cancel},
|
|
49
|
+
)
|
|
50
|
+
assert out == "[stopped by user]"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""The Edit tool honors replace_all (models expect it, like the standard Edit
|
|
2
|
+
tool). Without it, multiple matches are an error; with it, every occurrence is
|
|
3
|
+
replaced. Found by probing Edit edge cases."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from drydock.tools import tool_edit
|
|
7
|
+
from drydock import tool_registry
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _edit(tmp_path, setup, **kw):
|
|
11
|
+
f = tmp_path / "f.txt"; f.write_text(setup)
|
|
12
|
+
out = tool_edit({"file_path": str(f), **kw}, {"cwd": str(tmp_path)})
|
|
13
|
+
return out, f.read_text()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_replace_all_replaces_every_occurrence(tmp_path):
|
|
17
|
+
out, text = _edit(tmp_path, "a a a\n", old_string="a", new_string="b", replace_all=True)
|
|
18
|
+
assert "Error" not in out and text == "b b b\n" and "3 occurrences" in out
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_default_still_errors_on_multiple(tmp_path):
|
|
22
|
+
out, text = _edit(tmp_path, "a a a\n", old_string="a", new_string="b")
|
|
23
|
+
assert "found 3 times" in out and text == "a a a\n" # unchanged
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_replace_all_single_occurrence(tmp_path):
|
|
27
|
+
out, text = _edit(tmp_path, "just one\n", old_string="one", new_string="two", replace_all=True)
|
|
28
|
+
assert text == "just two\n"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_replace_all_no_match_errors(tmp_path):
|
|
32
|
+
out, text = _edit(tmp_path, "hello\n", old_string="zzz", new_string="b", replace_all=True)
|
|
33
|
+
assert "not found" in out and text == "hello\n"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_replace_all_in_schema():
|
|
37
|
+
assert "replace_all" in tool_registry.get("Edit").schema["input_schema"]["properties"]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""More edge cases from probing: Glob must not crash on a missing/list pattern,
|
|
2
|
+
and an empty Edit old_string should say what's wrong (not the baffling generic
|
|
3
|
+
'found N times')."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from drydock.tools import tool_glob, tool_edit
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_glob_missing_pattern_is_error_not_crash():
|
|
10
|
+
out = tool_glob({}, {"cwd": "/tmp"})
|
|
11
|
+
assert out.startswith("Error:") and "pattern" in out
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_glob_list_pattern_unwrapped(tmp_path):
|
|
15
|
+
(tmp_path / "a.py").write_text("x")
|
|
16
|
+
out = tool_glob({"pattern": ["*.py"], "path": str(tmp_path)}, {"cwd": str(tmp_path)})
|
|
17
|
+
assert "a.py" in out
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_glob_no_matches(tmp_path):
|
|
21
|
+
out = tool_glob({"pattern": "*.nope", "path": str(tmp_path)}, {"cwd": str(tmp_path)})
|
|
22
|
+
assert out.strip() == "(no matches)"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_edit_empty_old_string_clear_error(tmp_path):
|
|
26
|
+
f = tmp_path / "e.txt"; f.write_text("hello\n")
|
|
27
|
+
out = tool_edit({"file_path": str(f), "old_string": "", "new_string": "X"}, {"cwd": str(tmp_path)})
|
|
28
|
+
assert "empty" in out.lower() and f.read_text() == "hello\n"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|