mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-18 20:14:54 +00:00
Add editing facade (edit) covering the backend-independent edit operations
Introduce EditApi as the facade for editing operations which depend only on the code editor abstraction and thus work with any language backend: create_text_file, replace_content, replace_in_files, the optional line-level operations delete_lines, replace_lines and insert_at_line, and the symbol-level operations replace_symbol_body, insert_after_symbol and insert_before_symbol (moved from LspApi, which retains the LSP-specific rename_symbol and safe_delete_symbol). replace_in_files builds on MultiFileReplacement: a dry run returns a ReplacementPreview, which exposes the occurrences to code and renders the listing of prospective changes; rejections raise a ValueError carrying the listing where applicable. FacadeApi gains a backend-agnostic _create_code_editor; the LSP API's retriever-based factory is renamed to _create_ls_code_editor. The editing tools (file and symbol level) delegate to EditApi via EditApiMixin, retaining only the diagnostics context. The edit facade is always part of the REPL.
This commit is contained in:
1 parent
8ace53c3a1
commit
66ed46bef4
8 files changed
+478
-149
No files matched your search
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
Tests for the editing facade API (backend-independent parts, without a language server).
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from serena.config.serena_config import SerenaConfig
|
||||
from serena.facades.api.edit import EditApi, ReplacementPreview
|
||||
from serena.facades.facade import ApiScope, Facade
|
||||
from serena.project import Project
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def project(tmp_path: Path) -> Project:
|
||||
(tmp_path / "a.py").write_text("x = foo(1)\ny = foo(2)\n", encoding="utf-8")
|
||||
(tmp_path / "b.py").write_text("z = foo(3)\n", encoding="utf-8")
|
||||
return Project.load(str(tmp_path), serena_config=SerenaConfig(gui_log_window=False, web_dashboard=False))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def api(project: Project) -> EditApi:
|
||||
agent = MagicMock()
|
||||
agent.get_active_project_or_raise.return_value = project
|
||||
agent.serena_config.default_max_tool_answer_chars = 10000
|
||||
return EditApi(agent)
|
||||
|
||||
|
||||
def test_facade_exposes_editing_operations(api: EditApi) -> None:
|
||||
default_methods = {
|
||||
"create_text_file",
|
||||
"replace_content",
|
||||
"replace_in_files",
|
||||
"replace_symbol_body",
|
||||
"insert_after_symbol",
|
||||
"insert_before_symbol",
|
||||
}
|
||||
optional_methods = {"delete_lines", "replace_lines", "insert_at_line"} # the line-level operations are optional (as are the tools)
|
||||
|
||||
facade = Facade.from_api(api, ApiScope())
|
||||
assert facade.name == "edit"
|
||||
assert set(facade.enabled_method_names) == default_methods
|
||||
for name in optional_methods:
|
||||
assert facade.get_method(name).info.optional
|
||||
assert all(facade.get_method(name).info.can_edit for name in default_methods | optional_methods)
|
||||
|
||||
|
||||
def test_create_text_file(api: EditApi, project: Project) -> None:
|
||||
result = api.create_text_file("sub/new.txt", "hello\n")
|
||||
assert "new.txt" in result
|
||||
assert (Path(project.project_root) / "sub" / "new.txt").read_text(encoding="utf-8") == "hello\n"
|
||||
|
||||
result = api.create_text_file("sub/new.txt", "changed\n")
|
||||
assert "Overwrote" in result
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
api.create_text_file("../outside.txt", "nope")
|
||||
|
||||
|
||||
def test_replace_in_files_dry_run_returns_inspectable_preview(api: EditApi, project: Project) -> None:
|
||||
preview = api.replace_in_files("foo", "bar", mode="literal", dry_run=True)
|
||||
assert isinstance(preview, ReplacementPreview)
|
||||
|
||||
# the occurrences are accessible from code
|
||||
assert [o.relative_path for o in preview.occurrences] == ["a.py", "a.py", "b.py"]
|
||||
assert preview.affected_files == ["a.py", "b.py"]
|
||||
assert all(o.replacement == "bar" for o in preview.occurrences)
|
||||
|
||||
# the rendering lists the occurrence ids and diffs; nothing was modified
|
||||
rendered = preview.represent()
|
||||
assert "DRY RUN" in rendered
|
||||
assert all(o.occurrence_id in rendered for o in preview.occurrences)
|
||||
assert (Path(project.project_root) / "a.py").read_text(encoding="utf-8") == "x = foo(1)\ny = foo(2)\n"
|
||||
|
||||
|
||||
def test_replace_in_files_guard_failure_includes_preview(api: EditApi) -> None:
|
||||
with pytest.raises(ValueError, match="expected_count=1") as exc_info:
|
||||
api.replace_in_files("foo", "bar", mode="literal", expected_count=1)
|
||||
assert "b.py" in str(exc_info.value) # the listing of prospective changes is included
|
||||
Reference in new issue
Block a user