mirror of
https://github.com/NCBM/plyngent.git
synced 2026-07-23 05:55:16 +08:00
core/tools: soft-confirm file overwrite only, not partial edits
This commit is contained in:
@@ -110,36 +110,37 @@ def _shell_or_dash_c_reason(argv: Sequence[str], *, via: str) -> str | None:
|
|||||||
|
|
||||||
|
|
||||||
def _write_file_reason(args: Mapping[str, object]) -> str | None:
|
def _write_file_reason(args: Mapping[str, object]) -> str | None:
|
||||||
|
"""Confirm only when write_file would replace an existing file."""
|
||||||
path = args.get("path")
|
path = args.get("path")
|
||||||
if not isinstance(path, str) or not path:
|
if not isinstance(path, str) or not path:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
target = resolve_path(path)
|
target = resolve_path(path)
|
||||||
except WorkspaceError:
|
except WorkspaceError:
|
||||||
return f"write file {path!r}"
|
# Path policy will fail later; no soft-confirm without a resolved target.
|
||||||
return f"write file {path!r} ({target})"
|
|
||||||
|
|
||||||
|
|
||||||
def _edit_replace_reason(args: Mapping[str, object]) -> str | None:
|
|
||||||
path = args.get("path")
|
|
||||||
if not isinstance(path, str) or not path:
|
|
||||||
return None
|
return None
|
||||||
return f"edit (replace) in {path!r}"
|
if target.is_file():
|
||||||
|
return f"overwrite existing file {path!r} ({target})"
|
||||||
|
# New file or directory path: no total-overwrite risk for soft-confirm.
|
||||||
def _edit_lineno_reason(args: Mapping[str, object]) -> str | None:
|
|
||||||
path = args.get("path")
|
|
||||||
if not isinstance(path, str) or not path:
|
|
||||||
return None
|
return None
|
||||||
start = args.get("start_line")
|
|
||||||
end = args.get("end_line")
|
|
||||||
return f"edit lines {start}-{end} in {path!r}"
|
|
||||||
|
|
||||||
|
|
||||||
def _copy_path_reason(args: Mapping[str, object]) -> str | None:
|
def _copy_path_reason(args: Mapping[str, object]) -> str | None:
|
||||||
|
"""Confirm copy only when it would replace an existing destination path."""
|
||||||
src = args.get("src")
|
src = args.get("src")
|
||||||
dst = args.get("dst")
|
dst = args.get("dst")
|
||||||
return f"copy {src!r} → {dst!r}"
|
overwrite = bool(args.get("overwrite", False))
|
||||||
|
if not overwrite:
|
||||||
|
return None
|
||||||
|
if not isinstance(dst, str) or not dst:
|
||||||
|
return f"copy {src!r} → {dst!r} (overwrite)"
|
||||||
|
try:
|
||||||
|
target = resolve_path(dst)
|
||||||
|
except WorkspaceError:
|
||||||
|
return None
|
||||||
|
if target.exists():
|
||||||
|
return f"copy {src!r} → overwrite existing {dst!r} ({target})"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _move_path_reason(args: Mapping[str, object]) -> str | None:
|
def _move_path_reason(args: Mapping[str, object]) -> str | None:
|
||||||
@@ -222,10 +223,6 @@ def classify_danger(name: str, args: Mapping[str, object]) -> str | None: # noq
|
|||||||
return _copy_path_reason(args)
|
return _copy_path_reason(args)
|
||||||
if name == "write_file":
|
if name == "write_file":
|
||||||
return _write_file_reason(args)
|
return _write_file_reason(args)
|
||||||
if name == "edit_replace":
|
|
||||||
return _edit_replace_reason(args)
|
|
||||||
if name == "edit_lineno":
|
|
||||||
return _edit_lineno_reason(args)
|
|
||||||
if name == "run_command":
|
if name == "run_command":
|
||||||
return _run_command_reason(args)
|
return _run_command_reason(args)
|
||||||
if name == "run_command_batch":
|
if name == "run_command_batch":
|
||||||
|
|||||||
@@ -12,18 +12,32 @@ def test_classify_delete_and_move() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_classify_copy() -> None:
|
def test_classify_copy() -> None:
|
||||||
assert "copy" in (classify_danger("copy_path", {"src": "a", "dst": "b"}) or "")
|
# Without overwrite flag, copy is not soft-confirmed.
|
||||||
|
assert classify_danger("copy_path", {"src": "a", "dst": "b"}) is None
|
||||||
|
assert classify_danger(
|
||||||
|
"copy_path", {"src": "a", "dst": "b", "overwrite": False}
|
||||||
|
) is None
|
||||||
|
|
||||||
|
|
||||||
def test_classify_write_and_edits(tmp_path: Path) -> None:
|
def test_classify_write_overwrite_only(tmp_path: Path) -> None:
|
||||||
from plyngent.tools.workspace import clear_workspace_root, set_workspace_root
|
from plyngent.tools.workspace import clear_workspace_root, set_workspace_root
|
||||||
|
|
||||||
set_workspace_root(tmp_path)
|
set_workspace_root(tmp_path)
|
||||||
try:
|
try:
|
||||||
|
# New file: no soft-confirm
|
||||||
|
assert classify_danger("write_file", {"path": "new.txt"}) is None
|
||||||
|
# Partial edits: never soft-confirm
|
||||||
|
assert classify_danger("edit_replace", {"path": "x.txt"}) is None
|
||||||
|
assert classify_danger("edit_lineno", {"path": "x.txt", "start_line": 1, "end_line": 2}) is None
|
||||||
|
# Existing file write: confirm total overwrite
|
||||||
|
(tmp_path / "x.txt").write_text("old", encoding="utf-8")
|
||||||
reason = classify_danger("write_file", {"path": "x.txt"})
|
reason = classify_danger("write_file", {"path": "x.txt"})
|
||||||
assert reason is not None and "write file" in reason
|
assert reason is not None and "overwrite" in reason
|
||||||
assert "edit" in (classify_danger("edit_replace", {"path": "x.txt"}) or "")
|
(tmp_path / "dst.txt").write_text("d", encoding="utf-8")
|
||||||
assert "lines" in (classify_danger("edit_lineno", {"path": "x.txt", "start_line": 1, "end_line": 2}) or "")
|
(tmp_path / "src.txt").write_text("s", encoding="utf-8")
|
||||||
|
assert classify_danger("copy_path", {"src": "src.txt", "dst": "dst.txt", "overwrite": False}) is None
|
||||||
|
creason = classify_danger("copy_path", {"src": "src.txt", "dst": "dst.txt", "overwrite": True})
|
||||||
|
assert creason is not None and "overwrite" in creason
|
||||||
finally:
|
finally:
|
||||||
clear_workspace_root()
|
clear_workspace_root()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user