Handle stray artifact files during restore

This commit is contained in:
sladro 2026-04-02 14:07:59 +08:00
parent 1632aefa85
commit ce48dee42c
2 changed files with 76 additions and 3 deletions

View File

@ -13,6 +13,51 @@ from engine.models import BaselineSnapshot, TaskSpec
class ArtifactManager: class ArtifactManager:
task: TaskSpec task: TaskSpec
def _is_excluded(self, relative_path: str) -> bool:
return any(fnmatch(relative_path, exclude) for exclude in self.task.artifacts.exclude)
def _artifact_roots(self) -> list[Path]:
root_dir = self.task.root_dir
roots: list[Path] = []
for pattern in self.task.artifacts.include:
normalized_pattern = pattern.replace("\\", "/")
segments = [segment for segment in normalized_pattern.split("/") if segment and segment != "."]
prefix: list[str] = []
for segment in segments:
if any(char in segment for char in "*?["):
break
prefix.append(segment)
if prefix:
roots.append(root_dir.joinpath(*prefix))
continue
roots.append(root_dir)
minimal_roots: list[Path] = []
for root in sorted(set(roots), key=lambda candidate: (len(candidate.parts), candidate.as_posix())):
if any(existing == root or existing in root.parents for existing in minimal_roots):
continue
minimal_roots.append(root)
return minimal_roots
def _current_paths_within_roots(self) -> list[Path]:
root_dir = self.task.root_dir
discovered: set[Path] = set()
for root in self._artifact_roots():
if not root.exists():
continue
if root.is_file():
relative_path = root.relative_to(root_dir).as_posix()
if not self._is_excluded(relative_path):
discovered.add(root)
continue
for path in root.rglob("*"):
if not path.is_file():
continue
relative_path = path.relative_to(root_dir).as_posix()
if self._is_excluded(relative_path):
continue
discovered.add(path)
return sorted(discovered)
def resolve_paths(self) -> list[Path]: def resolve_paths(self) -> list[Path]:
root_dir = self.task.root_dir root_dir = self.task.root_dir
resolved: set[Path] = set() resolved: set[Path] = set()
@ -21,7 +66,7 @@ class ArtifactManager:
if not path.is_file(): if not path.is_file():
continue continue
relative_path = path.relative_to(root_dir).as_posix() relative_path = path.relative_to(root_dir).as_posix()
if any(fnmatch(relative_path, exclude) for exclude in self.task.artifacts.exclude): if self._is_excluded(relative_path):
continue continue
resolved.add(path) resolved.add(path)
return sorted(resolved) return sorted(resolved)
@ -37,7 +82,7 @@ class ArtifactManager:
return BaselineSnapshot(file_contents=file_contents, file_hashes=file_hashes) return BaselineSnapshot(file_contents=file_contents, file_hashes=file_hashes)
def restore(self, snapshot: BaselineSnapshot) -> None: def restore(self, snapshot: BaselineSnapshot) -> None:
current_paths = set(self.resolve_paths()) current_paths = set(self._current_paths_within_roots())
snapshot_paths = set(snapshot.file_contents) snapshot_paths = set(snapshot.file_contents)
for path in current_paths - snapshot_paths: for path in current_paths - snapshot_paths:
path.unlink() path.unlink()
@ -49,7 +94,7 @@ class ArtifactManager:
def diff_summary(self, snapshot: BaselineSnapshot) -> str: def diff_summary(self, snapshot: BaselineSnapshot) -> str:
lines: list[str] = [] lines: list[str] = []
current_contents: dict[Path, str] = {} current_contents: dict[Path, str] = {}
for path in self.resolve_paths(): for path in self._current_paths_within_roots():
with path.open("r", encoding="utf-8", newline="") as handle: with path.open("r", encoding="utf-8", newline="") as handle:
current_contents[path] = handle.read() current_contents[path] = handle.read()
all_paths = sorted(set(snapshot.file_contents) | set(current_contents)) all_paths = sorted(set(snapshot.file_contents) | set(current_contents))

View File

@ -107,6 +107,34 @@ class ArtifactManagerTest(unittest.TestCase):
self.assertIn("-before", summary) self.assertIn("-before", summary)
self.assertIn("+after", summary) self.assertIn("+after", summary)
def test_diff_summary_and_restore_handle_stray_file_outside_include_glob(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("baseline\n", encoding="utf-8")
manager = ArtifactManager(make_task(root))
snapshot = manager.snapshot()
target.unlink()
stray = artifact_dir / "archive" / "sample.txt"
stray.parent.mkdir()
stray.write_text("renamed\n", encoding="utf-8")
summary = manager.diff_summary(snapshot)
self.assertIn("artifacts/sample.md (before)", summary)
self.assertIn("artifacts/archive/sample.txt (after)", summary)
self.assertIn("-baseline", summary)
self.assertIn("+renamed", summary)
manager.restore(snapshot)
self.assertFalse(stray.exists())
self.assertTrue(target.exists())
self.assertEqual(target.read_text(encoding="utf-8"), "baseline\n")
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()