From 3172ce264b8b54083b73461c08e87bb888d34a2a Mon Sep 17 00:00:00 2001 From: sladro Date: Thu, 2 Apr 2026 11:52:10 +0800 Subject: [PATCH] fix: preserve artifact line endings and restore deletions --- engine/artifact_manager.py | 17 +++++++++--- tests/test_artifact_manager.py | 51 ++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/engine/artifact_manager.py b/engine/artifact_manager.py index bcfe4c7..7034458 100644 --- a/engine/artifact_manager.py +++ b/engine/artifact_manager.py @@ -1,10 +1,10 @@ from __future__ import annotations from dataclasses import dataclass +from difflib import unified_diff from fnmatch import fnmatch from hashlib import sha256 from pathlib import Path -from difflib import unified_diff from engine.models import BaselineSnapshot, TaskSpec @@ -30,19 +30,28 @@ class ArtifactManager: file_contents: dict[Path, str] = {} file_hashes: dict[Path, str] = {} for path in self.resolve_paths(): - content = path.read_text(encoding="utf-8") + with path.open("r", encoding="utf-8", newline="") as handle: + content = handle.read() file_contents[path] = content file_hashes[path] = sha256(content.encode("utf-8")).hexdigest() return BaselineSnapshot(file_contents=file_contents, file_hashes=file_hashes) def restore(self, snapshot: BaselineSnapshot) -> None: + current_paths = set(self.resolve_paths()) + snapshot_paths = set(snapshot.file_contents) + for path in current_paths - snapshot_paths: + path.unlink() for path, content in snapshot.file_contents.items(): path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(content, encoding="utf-8") + with path.open("w", encoding="utf-8", newline="") as handle: + handle.write(content) def diff_summary(self, snapshot: BaselineSnapshot) -> str: lines: list[str] = [] - current_contents = {path: path.read_text(encoding="utf-8") for path in self.resolve_paths()} + current_contents: dict[Path, str] = {} + for path in self.resolve_paths(): + with path.open("r", encoding="utf-8", newline="") as handle: + current_contents[path] = handle.read() all_paths = sorted(set(snapshot.file_contents) | set(current_contents)) for path in all_paths: before = snapshot.file_contents.get(path, "") diff --git a/tests/test_artifact_manager.py b/tests/test_artifact_manager.py index 0eca28a..a7ca848 100644 --- a/tests/test_artifact_manager.py +++ b/tests/test_artifact_manager.py @@ -1,4 +1,5 @@ from pathlib import Path +from hashlib import sha256 import tempfile import unittest @@ -42,6 +43,56 @@ class ArtifactManagerTest(unittest.TestCase): manager.restore(snapshot) self.assertEqual(target.read_text(encoding="utf-8"), "hello\n") + def test_restore_removes_newly_created_included_artifact(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + artifact_dir = root / "artifacts" + artifact_dir.mkdir() + target = artifact_dir / "sample.md" + target.write_text("hello\n", encoding="utf-8") + manager = ArtifactManager(make_task(root)) + snapshot = manager.snapshot() + extra = artifact_dir / "new.md" + extra.write_text("new\n", encoding="utf-8") + manager.restore(snapshot) + self.assertFalse(extra.exists()) + self.assertEqual(target.read_text(encoding="utf-8"), "hello\n") + + def test_snapshot_and_restore_preserve_crlf_content(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + artifact_dir = root / "artifacts" + artifact_dir.mkdir() + target = artifact_dir / "sample.md" + original = "line1\r\nline2\r\n" + with target.open("w", encoding="utf-8", newline="") as handle: + handle.write(original) + manager = ArtifactManager(make_task(root)) + snapshot = manager.snapshot() + self.assertEqual(snapshot.file_contents[target], original) + self.assertEqual( + snapshot.file_hashes[target], + sha256(original.encode("utf-8")).hexdigest(), + ) + with target.open("w", encoding="utf-8", newline="") as handle: + handle.write("changed\r\n") + manager.restore(snapshot) + with target.open("r", encoding="utf-8", newline="") as handle: + restored = handle.read() + self.assertEqual(restored, original) + + def test_resolve_paths_is_deterministic_and_respects_excludes(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + artifact_dir = root / "artifacts" + artifact_dir.mkdir() + (artifact_dir / "b.md").write_text("b\n", encoding="utf-8") + (artifact_dir / "ignore.md").write_text("ignore\n", encoding="utf-8") + (artifact_dir / "a.md").write_text("a\n", encoding="utf-8") + manager = ArtifactManager(make_task(root)) + paths = manager.resolve_paths() + self.assertEqual(paths, [artifact_dir / "a.md", artifact_dir / "b.md"]) + def test_diff_summary_contains_changed_line(self) -> None: with tempfile.TemporaryDirectory() as tmp: root = Path(tmp)