from __future__ import annotations from collections.abc import Awaitable, Callable import pytest from opensquilla.tools.builtin import filesystem from opensquilla.tools.builtin import patch as patch_tool from opensquilla.tools.registry import get_default_registry from opensquilla.tools.types import ( RetryableToolInputError, SafeToolError, ToolContext, current_tool_context, ) def _original_async(fn: Callable[..., Awaitable[str]]) -> Callable[..., Awaitable[str]]: return fn.__wrapped__.__wrapped__ # type: ignore[attr-defined, no-any-return] @pytest.mark.asyncio async def test_filesystem_write_notifies_bootstrap_or_memory_sources(tmp_path) -> None: memory_calls: list[tuple[str, str]] = [] bootstrap_calls: list[tuple[str, str]] = [] token = current_tool_context.set( ToolContext( is_owner=True, agent_id="main", workspace_dir=str(tmp_path), memory_source_dir=str(tmp_path), on_memory_source_write=lambda agent_id, path: memory_calls.append((agent_id, path)), on_bootstrap_source_write=lambda agent_id, path: bootstrap_calls.append( (agent_id, path) ), ) ) write_file = _original_async(filesystem.write_file) try: await write_file("USER.md", "Name: Alice\n") await write_file("MEMORY.md", "# MEMORY\n") await write_file("memory/USER.md", "not a bootstrap file\n") finally: current_tool_context.reset(token) assert bootstrap_calls == [("main", "USER.md")] assert ("main", "MEMORY.md") in memory_calls assert ("main", "memory/USER.md") in memory_calls @pytest.mark.asyncio async def test_filesystem_write_tools_record_workspace_writes(tmp_path) -> None: target = tmp_path / "src" / "app.py" target.parent.mkdir() target.write_text("old\n", encoding="utf-8") ctx = ToolContext( is_owner=True, workspace_dir=str(tmp_path), file_edit_requires_fresh_read=True, ) token = current_tool_context.set(ctx) read_file = filesystem.read_file write_file = _original_async(filesystem.write_file) edit_file = _original_async(filesystem.edit_file) try: await read_file(str(target)) write_result = await write_file(str(target), "new\n") edit_result = await edit_file(str(target), "new\n", "newer\n") finally: current_tool_context.reset(token) assert "workspace file" in write_result assert "inspect git_diff" in write_result assert "workspace file" in edit_result assert [entry["relative_path"] for entry in ctx.workspace_file_writes] == [ "src/app.py", "src/app.py", ] @pytest.mark.asyncio async def test_edit_file_requires_fresh_full_workspace_read(tmp_path) -> None: target = tmp_path / "src" / "app.py" target.parent.mkdir() target.write_text("value = 1\n", encoding="utf-8") ctx = ToolContext( is_owner=True, workspace_dir=str(tmp_path), file_edit_requires_fresh_read=True, ) token = current_tool_context.set(ctx) read_file = filesystem.read_file edit_file = _original_async(filesystem.edit_file) try: with pytest.raises(RetryableToolInputError) as unread: await edit_file(str(target), "value = 1\n", "value = 2\n") await read_file(str(target)) result = await edit_file(str(target), "value = 1\n", "value = 2\n") finally: current_tool_context.reset(token) assert "must read" in unread.value.user_message assert "Edited" in result assert target.read_text(encoding="utf-8") == "value = 2\n" @pytest.mark.asyncio async def test_fresh_read_guard_records_runtime_event_for_unread_workspace_edit( tmp_path, ) -> None: target = tmp_path / "src" / "app.py" target.parent.mkdir() target.write_text("value = 1\n", encoding="utf-8") events: list[dict[str, object]] = [] ctx = ToolContext( is_owner=True, agent_id="agent-1", session_key="session-1", workspace_dir=str(tmp_path), file_edit_requires_fresh_read=True, on_runtime_event=events.append, ) token = current_tool_context.set(ctx) edit_file = _original_async(filesystem.edit_file) try: with pytest.raises(RetryableToolInputError): await edit_file(str(target), "value = 1\n", "value = 2\n") finally: current_tool_context.reset(token) assert target.read_text(encoding="utf-8") == "value = 1\n" assert events == [ { "feature": "fresh_read_guard", "name": "fresh_read_guard.blocked", "tool": "edit_file", "tool_name": "edit_file", "path": str(target), "resolved_path": str(target.resolve()), "relative_path": "src/app.py", "reason": "missing_fresh_read", "outcome": "blocked", "agent_id": "agent-1", "session_key": "session-1", } ] @pytest.mark.asyncio async def test_edit_file_rejects_stale_workspace_read(tmp_path) -> None: target = tmp_path / "src" / "app.py" target.parent.mkdir() target.write_text("value = 1\n", encoding="utf-8") ctx = ToolContext( is_owner=True, workspace_dir=str(tmp_path), file_edit_requires_fresh_read=True, ) token = current_tool_context.set(ctx) read_file = filesystem.read_file edit_file = _original_async(filesystem.edit_file) try: await read_file(str(target)) target.write_text("value = 2\n", encoding="utf-8") with pytest.raises(RetryableToolInputError) as stale: await edit_file(str(target), "value = 2\n", "value = 3\n") finally: current_tool_context.reset(token) assert "changed since it was read" in stale.value.user_message assert "read_file" in stale.value.user_message assert target.read_text(encoding="utf-8") == "value = 2\n" @pytest.mark.asyncio async def test_fresh_read_guard_records_runtime_event_for_stale_workspace_edit( tmp_path, ) -> None: target = tmp_path / "src" / "app.py" target.parent.mkdir() target.write_text("value = 1\n", encoding="utf-8") events: list[dict[str, object]] = [] ctx = ToolContext( is_owner=True, agent_id="agent-1", session_key="session-1", workspace_dir=str(tmp_path), file_edit_requires_fresh_read=True, on_runtime_event=events.append, ) token = current_tool_context.set(ctx) read_file = filesystem.read_file edit_file = _original_async(filesystem.edit_file) try: await read_file(str(target)) target.write_text("value = 2\n", encoding="utf-8") with pytest.raises(RetryableToolInputError): await edit_file(str(target), "value = 2\n", "value = 3\n") finally: current_tool_context.reset(token) assert len(events) == 1 assert events[0]["feature"] == "fresh_read_guard" assert events[0]["name"] == "fresh_read_guard.blocked" assert events[0]["tool"] == "edit_file" assert events[0]["relative_path"] == "src/app.py" assert events[0]["reason"] == "stale_fresh_read" assert events[0]["outcome"] == "blocked" assert target.read_text(encoding="utf-8") == "value = 2\n" @pytest.mark.asyncio async def test_write_file_existing_workspace_requires_fresh_full_read(tmp_path) -> None: target = tmp_path / "src" / "app.py" target.parent.mkdir() target.write_text("value = 1\n", encoding="utf-8") ctx = ToolContext( is_owner=True, workspace_dir=str(tmp_path), file_edit_requires_fresh_read=True, ) token = current_tool_context.set(ctx) read_file = filesystem.read_file write_file = _original_async(filesystem.write_file) try: with pytest.raises(RetryableToolInputError) as unread: await write_file(str(target), "value = 2\n") await read_file(str(target)) result = await write_file(str(target), "value = 2\n") finally: current_tool_context.reset(token) assert "must read" in unread.value.user_message assert result.startswith("Written ") assert target.read_text(encoding="utf-8") == "value = 2\n" @pytest.mark.asyncio async def test_write_file_allows_new_and_scratch_files_without_workspace_read(tmp_path) -> None: workspace = tmp_path / "workspace" scratch = tmp_path / "scratch" workspace.mkdir() scratch.mkdir() new_target = workspace / "src" / "new.py" scratch_target = scratch / "debug.py" scratch_target.write_text("old\n", encoding="utf-8") ctx = ToolContext( is_owner=True, workspace_dir=str(workspace), scratch_dir=str(scratch), file_edit_requires_fresh_read=True, ) token = current_tool_context.set(ctx) write_file = _original_async(filesystem.write_file) try: new_result = await write_file(str(new_target), "created = True\n") scratch_result = await write_file(str(scratch_target), "debug = True\n") finally: current_tool_context.reset(token) assert new_result.startswith("Written ") assert scratch_result.startswith("Written ") assert new_target.read_text(encoding="utf-8") == "created = True\n" assert scratch_target.read_text(encoding="utf-8") == "debug = True\n" @pytest.mark.asyncio async def test_existing_workspace_edit_does_not_require_read_by_default(tmp_path) -> None: target = tmp_path / "src" / "app.py" target.parent.mkdir() target.write_text("value = 1\n", encoding="utf-8") events: list[dict[str, object]] = [] ctx = ToolContext( is_owner=True, workspace_dir=str(tmp_path), on_runtime_event=events.append, ) token = current_tool_context.set(ctx) edit_file = _original_async(filesystem.edit_file) try: result = await edit_file(str(target), "value = 1\n", "value = 2\n") finally: current_tool_context.reset(token) assert "Edited" in result assert not any( event.get("name") == "fresh_read_guard.blocked" or ( event.get("feature") == "fresh_read_guard" and event.get("outcome") == "blocked" ) for event in events ) assert target.read_text(encoding="utf-8") == "value = 2\n" @pytest.mark.asyncio async def test_filesystem_write_tools_note_docs_and_derived_workspace_writes( tmp_path, ) -> None: docs_target = tmp_path / "docs" / "content" / "manual" / "manual.yml" derived_target = tmp_path / "jq.1.prebuilt" docs_target.parent.mkdir(parents=True) derived_target.write_text("old\n", encoding="utf-8") ctx = ToolContext(is_owner=True, workspace_dir=str(tmp_path)) token = current_tool_context.set(ctx) read_file = filesystem.read_file write_file = _original_async(filesystem.write_file) edit_file = _original_async(filesystem.edit_file) try: docs_result = await write_file(str(docs_target), "docs\n") await read_file(str(derived_target)) derived_result = await edit_file(str(derived_target), "old\n", "new\n") finally: current_tool_context.reset(token) assert "documentation file" in docs_result assert "verify the docs build" in docs_result assert "generated or derived-looking file" in derived_result assert "regenerate/verify" in derived_result @pytest.mark.asyncio async def test_filesystem_write_tools_note_test_workspace_writes(tmp_path) -> None: test_target = tmp_path / "packages" / "core" / "__tests__" / "feature.spec.ts" test_target.parent.mkdir(parents=True) test_target.write_text("old\n", encoding="utf-8") ctx = ToolContext(is_owner=True, workspace_dir=str(tmp_path)) token = current_tool_context.set(ctx) read_file = filesystem.read_file edit_file = _original_async(filesystem.edit_file) try: await read_file(str(test_target)) result = await edit_file(str(test_target), "old\n", "new\n") finally: current_tool_context.reset(token) assert "test file" in result assert "explicitly requested test updates" in result assert "revert it before final" in result @pytest.mark.asyncio async def test_edit_file_missing_old_text_is_model_retriable(tmp_path) -> None: target = tmp_path / "src" / "app.py" target.parent.mkdir() target.write_text("actual = 1\n", encoding="utf-8") token = current_tool_context.set(ToolContext(is_owner=True, workspace_dir=str(tmp_path))) read_file = filesystem.read_file edit_file = _original_async(filesystem.edit_file) try: await read_file(str(target)) with pytest.raises(RetryableToolInputError) as exc_info: await edit_file(str(target), "expected = 1\n", "expected = 2\n") finally: current_tool_context.reset(token) assert "could not find old_text" in exc_info.value.user_message assert "retry with exact text" in exc_info.value.user_message @pytest.mark.asyncio async def test_edit_file_ambiguous_old_text_is_model_retriable(tmp_path) -> None: target = tmp_path / "src" / "app.py" target.parent.mkdir() target.write_text("flag = True\nflag = True\n", encoding="utf-8") token = current_tool_context.set(ToolContext(is_owner=True, workspace_dir=str(tmp_path))) read_file = filesystem.read_file edit_file = _original_async(filesystem.edit_file) try: await read_file(str(target)) with pytest.raises(RetryableToolInputError) as exc_info: await edit_file(str(target), "flag = True\n", "flag = False\n") finally: current_tool_context.reset(token) assert "matches 2 locations" in exc_info.value.user_message assert "unique surrounding context" in exc_info.value.user_message @pytest.mark.asyncio async def test_edit_file_uses_unique_flexible_match_after_exact_miss(tmp_path) -> None: target = tmp_path / "src" / "app.py" target.parent.mkdir() target.write_text( "def run():\n" " if enabled:\n" " return 1\n", encoding="utf-8", ) events: list[dict[str, object]] = [] ctx = ToolContext( is_owner=True, workspace_dir=str(tmp_path), on_runtime_event=events.append, ) token = current_tool_context.set(ctx) read_file = filesystem.read_file edit_file = _original_async(filesystem.edit_file) try: await read_file(str(target)) result = await edit_file( str(target), "if enabled:\n return 1\n", "if enabled:\n return 2\n", ) finally: current_tool_context.reset(token) assert "Edited" in result assert target.read_text(encoding="utf-8") == ( "def run():\n" " if enabled:\n" " return 2\n" ) assert any(event["name"] == "edit_file.flexible_match_used" for event in events) @pytest.mark.asyncio async def test_edit_file_can_disable_flexible_match_recovery(tmp_path) -> None: target = tmp_path / "src" / "app.py" target.parent.mkdir() target.write_text( "def run():\n" " if enabled:\n" " return 1\n", encoding="utf-8", ) events: list[dict[str, object]] = [] ctx = ToolContext( is_owner=True, workspace_dir=str(tmp_path), file_edit_flexible_recovery=False, on_runtime_event=events.append, ) token = current_tool_context.set(ctx) read_file = filesystem.read_file edit_file = _original_async(filesystem.edit_file) try: await read_file(str(target)) with pytest.raises(RetryableToolInputError) as exc_info: await edit_file( str(target), "if enabled:\n return 1\n", "if enabled:\n return 2\n", ) finally: current_tool_context.reset(token) assert "could not find old_text" in exc_info.value.user_message assert target.read_text(encoding="utf-8") == ( "def run():\n" " if enabled:\n" " return 1\n" ) assert any( event["name"] == "edit_file.flexible_match_rejected" and event["reason"] == "disabled" for event in events ) @pytest.mark.asyncio async def test_edit_file_unescapes_old_text_before_flexible_match(tmp_path) -> None: target = tmp_path / "src" / "app.py" target.parent.mkdir() target.write_text("def run():\n return 'old'\n", encoding="utf-8") events: list[dict[str, object]] = [] ctx = ToolContext( is_owner=True, workspace_dir=str(tmp_path), on_runtime_event=events.append, ) token = current_tool_context.set(ctx) read_file = filesystem.read_file edit_file = _original_async(filesystem.edit_file) try: await read_file(str(target)) await edit_file( str(target), "def run():\\n return 'old'\\n", "def run():\n return 'new'\n", ) finally: current_tool_context.reset(token) assert target.read_text(encoding="utf-8") == "def run():\n return 'new'\n" event_names = [event["name"] for event in events] assert "edit_file.unescape_repair_used" in event_names assert "edit_file.flexible_match_used" not in event_names @pytest.mark.asyncio async def test_edit_file_rejects_non_unique_flexible_match(tmp_path) -> None: target = tmp_path / "src" / "app.py" target.parent.mkdir() target.write_text( "if enabled:\n" " return 1\n" "if enabled:\n" " return 1\n", encoding="utf-8", ) events: list[dict[str, object]] = [] ctx = ToolContext( is_owner=True, workspace_dir=str(tmp_path), on_runtime_event=events.append, ) token = current_tool_context.set(ctx) read_file = filesystem.read_file edit_file = _original_async(filesystem.edit_file) try: await read_file(str(target)) with pytest.raises(RetryableToolInputError) as exc_info: await edit_file( str(target), "if enabled:\nreturn 1\n", "if enabled:\nreturn 2\n", ) finally: current_tool_context.reset(token) assert "could not find old_text" in exc_info.value.user_message assert any(event["name"] == "edit_file.flexible_match_rejected" for event in events) @pytest.mark.asyncio async def test_edit_file_accepts_multiple_precise_replacements(tmp_path) -> None: target = tmp_path / "src" / "app.py" target.parent.mkdir() target.write_text( "alpha = 1\n" "keep = True\n" "beta = 2\n", encoding="utf-8", ) ctx = ToolContext(is_owner=True, workspace_dir=str(tmp_path)) token = current_tool_context.set(ctx) read_file = filesystem.read_file edit_file = _original_async(filesystem.edit_file) try: await read_file(str(target)) result = await edit_file( str(target), edits=[ {"old_text": "alpha = 1\n", "new_text": "alpha = 10\n"}, {"oldText": "beta = 2\n", "newText": "beta = 20\n"}, ], ) finally: current_tool_context.reset(token) assert target.read_text(encoding="utf-8") == ( "alpha = 10\n" "keep = True\n" "beta = 20\n" ) assert "applied 2 replacement" in result assert [entry["relative_path"] for entry in ctx.workspace_file_writes] == ["src/app.py"] @pytest.mark.asyncio async def test_edit_file_recovery_guidance_omits_hidden_apply_patch(tmp_path) -> None: target = tmp_path / "src" / "app.py" target.parent.mkdir() target.write_text("value = 1\n", encoding="utf-8") ctx = ToolContext( is_owner=True, workspace_dir=str(tmp_path), allowed_tools={ "exec_command", "read_file", "edit_file", "write_file", "glob_search", "grep_search", "list_dir", "git_status", "git_diff", "retrieve_tool_result", }, ) token = current_tool_context.set(ctx) read_file = filesystem.read_file edit_file = _original_async(filesystem.edit_file) try: await read_file(str(target)) with pytest.raises(RetryableToolInputError) as exc_info: await edit_file(str(target), "missing = True\n", "missing = False\n") finally: current_tool_context.reset(token) assert "could not find old_text" in exc_info.value.user_message assert "apply_patch" not in exc_info.value.user_message assert "smaller exact" in exc_info.value.user_message @pytest.mark.asyncio async def test_write_file_shrink_guard_omits_hidden_apply_patch(tmp_path) -> None: target = tmp_path / "src" / "app.py" target.parent.mkdir() target.write_text("value = 1\n" * 700, encoding="utf-8") ctx = ToolContext( is_owner=True, workspace_dir=str(tmp_path), allowed_tools={ "exec_command", "read_file", "edit_file", "write_file", "glob_search", "grep_search", "list_dir", "git_status", "git_diff", "retrieve_tool_result", }, ) token = current_tool_context.set(ctx) read_file = filesystem.read_file write_file = _original_async(filesystem.write_file) try: await read_file(str(target)) with pytest.raises(SafeToolError) as exc_info: await write_file(str(target), "value = 2\n") finally: current_tool_context.reset(token) assert "write_file refused to overwrite" in exc_info.value.user_message assert "edit_file" in exc_info.value.user_message assert "apply_patch" not in exc_info.value.user_message def test_edit_file_schema_guides_multi_edit_and_numbered_read_output() -> None: registered = get_default_registry().get("edit_file") assert registered is not None spec = registered.spec description = spec.description edits = spec.parameters["edits"] assert spec.required == ["path"] assert "edits[]" in description assert "read_file without offset or limit" in description assert "line-number" in description assert "prefer apply_patch" in description assert edits["type"] == "array" assert "old_text" in edits["items"]["properties"] assert "oldText" in edits["items"]["properties"] def test_coding_tool_descriptions_explain_file_edit_workflow() -> None: registry = get_default_registry() read_file = registry.get("read_file") write_file = registry.get("write_file") apply_patch = registry.get("apply_patch") exec_command = registry.get("exec_command") execute_code = registry.get("execute_code") assert read_file is not None assert write_file is not None assert apply_patch is not None assert exec_command is not None assert execute_code is not None assert "establish fresh edit context" in read_file.spec.description assert "Best for new files and scratch files" in write_file.spec.description assert "Complete file content" in write_file.spec.parameters["content"]["description"] assert "multi-line or larger source edits" in apply_patch.spec.description assert "builds, tests" in exec_command.spec.description assert "Prefer the file editing tools" in execute_code.spec.description @pytest.mark.asyncio async def test_apply_patch_reports_workspace_write_progress(tmp_path) -> None: target = tmp_path / "src" / "app.py" target.parent.mkdir() target.write_text("old\n", encoding="utf-8") ctx = ToolContext(is_owner=True, workspace_dir=str(tmp_path)) token = current_tool_context.set(ctx) apply_patch = _original_async(patch_tool.apply_patch) try: result = await apply_patch( """*** Begin Patch *** Update File: src/app.py @@@ -1,1 +1,1 @@@ -old +new *** End Patch""" ) finally: current_tool_context.reset(token) assert "Applied patch" in result assert "inspect git_diff" in result @pytest.mark.asyncio async def test_patch_notifies_bootstrap_and_memory_sources(tmp_path) -> None: memory_calls: list[tuple[str, str]] = [] bootstrap_calls: list[tuple[str, str]] = [] (tmp_path / "USER.md").write_text("Name:\n", encoding="utf-8") (tmp_path / "memory").mkdir() (tmp_path / "memory" / "2026-05-01.md").write_text("old\n", encoding="utf-8") token = current_tool_context.set( ToolContext( is_owner=True, agent_id="main", workspace_dir=str(tmp_path), memory_source_dir=str(tmp_path), on_memory_source_write=lambda agent_id, path: memory_calls.append((agent_id, path)), on_bootstrap_source_write=lambda agent_id, path: bootstrap_calls.append( (agent_id, path) ), ) ) apply_patch = _original_async(patch_tool.apply_patch) try: await apply_patch( """*** Begin Patch *** Update File: USER.md @@@ -1,1 +1,1 @@@ -Name: +Name: Alice *** Update File: memory/2026-05-01.md @@@ -1,1 +1,1 @@@ -old +new *** End Patch""" ) finally: current_tool_context.reset(token) assert bootstrap_calls == [("main", "USER.md")] assert memory_calls == [("main", "memory/2026-05-01.md")]