diff --git a/backend/packages/harness/deerflow/sandbox/tools.py b/backend/packages/harness/deerflow/sandbox/tools.py index af08f5402..6f86629f7 100644 --- a/backend/packages/harness/deerflow/sandbox/tools.py +++ b/backend/packages/harness/deerflow/sandbox/tools.py @@ -2268,7 +2268,9 @@ def str_replace_tool( with get_file_operation_lock(sandbox, path): content = sandbox.read_file(path) if not content: - return "OK" + if not old_str: + return "OK" + return f"Error: String to replace not found in file: {requested_path}" if old_str not in content: return f"Error: String to replace not found in file: {requested_path}" if replace_all: diff --git a/backend/tests/test_str_replace_empty_file.py b/backend/tests/test_str_replace_empty_file.py new file mode 100644 index 000000000..a68068d0e --- /dev/null +++ b/backend/tests/test_str_replace_empty_file.py @@ -0,0 +1,54 @@ +"""str_replace tool behaviour on empty files. + +An empty file used to short-circuit to ``"OK"`` regardless of ``old_str``, +so a real substring replacement silently "succeeded" without changing anything +and without telling the model the target was missing. The fix only returns +``"OK"`` on an empty file when ``old_str`` is itself empty (a no-op edit); +a non-empty ``old_str`` now reports the string was not found. +""" + +from pathlib import Path +from types import SimpleNamespace + +from deerflow.sandbox.local.local_sandbox import LocalSandbox +from deerflow.sandbox.tools import str_replace_tool + + +def _local_runtime(tmp_path: Path) -> SimpleNamespace: + for sub in ("workspace", "uploads", "outputs"): + (tmp_path / sub).mkdir(parents=True, exist_ok=True) + thread_data = { + "workspace_path": str(tmp_path / "workspace"), + "uploads_path": str(tmp_path / "uploads"), + "outputs_path": str(tmp_path / "outputs"), + } + return SimpleNamespace( + state={"sandbox": {"sandbox_id": "local:t1"}, "thread_data": thread_data}, + context={"thread_id": "t1"}, + ) + + +def _str_replace(tmp_path, monkeypatch, *, old_str: str, new_str: str = "x") -> str: + runtime = _local_runtime(tmp_path) + (tmp_path / "outputs" / "empty.txt").write_text("", encoding="utf-8") + monkeypatch.setattr("deerflow.sandbox.tools.ensure_sandbox_initialized", lambda runtime: LocalSandbox("t1")) + monkeypatch.setattr("deerflow.sandbox.tools.ensure_thread_directories_exist", lambda runtime: None) + return str_replace_tool.func( + runtime=runtime, + description="replace in empty file", + path="/mnt/user-data/outputs/empty.txt", + old_str=old_str, + new_str=new_str, + ) + + +def test_empty_file_with_non_empty_old_str_reports_not_found(tmp_path, monkeypatch) -> None: + result = _str_replace(tmp_path, monkeypatch, old_str="something") + assert result.startswith("Error: String to replace not found in file") + assert "empty.txt" in result + + +def test_empty_file_with_empty_old_str_returns_ok(tmp_path, monkeypatch) -> None: + # An empty old_str is a no-op edit and remains a benign "OK" on an empty file. + result = _str_replace(tmp_path, monkeypatch, old_str="") + assert result == "OK"