from pathlib import Path import pytest from gcode.safety import REJECT_SHELL, REJECT_WRITE, SafetyGate, unified_diff from gcode.tools.fs import make_fs_tools from gcode.tools.shell import make_shell_tools def test_unified_diff_contains_change() -> None: diff = unified_diff("a.py", "print(1)\n", "print(2)\n") assert "-print(1)" in diff assert "+print(2)" in diff @pytest.mark.asyncio async def test_unique_replace_and_reject(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.chdir(tmp_path) path = tmp_path / "a.py" path.write_text("foo\nfoo\n", encoding="utf-8") async def deny(kind: str, detail: str) -> bool: return False tools = {t.name: t for t in make_fs_tools(SafetyGate(deny), tmp_path)} out = await tools["replace_in_file"].ainvoke( {"path": "a.py", "old_text": "foo", "new_text": "bar"} ) assert "出现了 2 次" in out assert path.read_text(encoding="utf-8") == "foo\nfoo\n" path.write_text("only-once\n", encoding="utf-8") out = await tools["replace_in_file"].ainvoke( {"path": "a.py", "old_text": "only-once", "new_text": "twice"} ) assert REJECT_WRITE in out assert path.read_text(encoding="utf-8") == "only-once\n" @pytest.mark.asyncio async def test_shell_denied_does_not_run() -> None: async def deny(kind: str, detail: str) -> bool: return False tools = {t.name: t for t in make_shell_tools(SafetyGate(deny))} out = await tools["execute_command"].ainvoke({"command": "echo should-not-run"}) assert REJECT_SHELL in out