From 033bc5042cf24006f18189a9253f131cbf997851 Mon Sep 17 00:00:00 2001 From: forhappy Date: Wed, 14 Oct 2026 21:10:16 -0700 Subject: [PATCH] fix(test): handle packed LFS fault fixtures --- .../docs/architecture/indexed-remote-reads.md | 24 ++++++++ ..._protocol_v2_partial_clone_rustfs_smoke.py | 57 ++++++++++++++----- 2 files changed, 67 insertions(+), 14 deletions(-) diff --git a/crab/docs/architecture/indexed-remote-reads.md b/crab/docs/architecture/indexed-remote-reads.md index 00c2fffc5..2b62023d3 100644 --- a/crab/docs/architecture/indexed-remote-reads.md +++ b/crab/docs/architecture/indexed-remote-reads.md @@ -2154,3 +2154,27 @@ the remote tip, revoke old-tip visibility, install packs into an independent repository and run Git connectivity checks. The existing RustFS ref matrix also exercises native and remote-helper force pushes without fetching first. These tests remain pending until the updated-source CI run completes. + +## Packed-object qualification fault fixture + +The Git-current protocol canary stopped before corruption injection because the +LFS pointer blob was packed, while the fixture opened a loose-object pathname. +The same failure reproduces locally after `git repack -ad`. + +The fixture now exports its small disposable source history and unpacks it into +an empty object database before injection. Healthy packed duplicates cannot +mask the corrupt loose blob. The original object database is moved aside and +restored in `finally`; the success check compares every original object-file +hash, the pointer, source/remote refs and working bytes. The caller deliberately +packs its source first so the previous failure remains a regression case. +The checksum mismatch and successful restored dry run remain required. + +This changes qualification setup only. Full Git-current CI remains required; +a source-layout repair does not establish a product-level compatibility result. + +Local proof with the unchanged #572 release: the repaired packed-source mirror +workflow passes 66 commands and 13 checks; deliberate assertion failure after +the verified checksum rejection restores every original object-file hash and +passes strict Git fsck (34 commands, three checks; cleanup scope only). Seventeen +existing log/source-isolation tests pass. These are selected fixture and mirror +receipts, not a full final-source protocol qualification. diff --git a/crab/scripts/e2e/run_protocol_v2_partial_clone_rustfs_smoke.py b/crab/scripts/e2e/run_protocol_v2_partial_clone_rustfs_smoke.py index cb4cc9044..f6e19ada5 100644 --- a/crab/scripts/e2e/run_protocol_v2_partial_clone_rustfs_smoke.py +++ b/crab/scripts/e2e/run_protocol_v2_partial_clone_rustfs_smoke.py @@ -2835,19 +2835,41 @@ def mirror_lfs_discovery_check(self, source: Path, destination: str) -> None: oid = self.git_value(source, ["rev-parse", "HEAD:hook-lfs.bin"], name="capture LFS pointer Git OID") head = self.git_value(source, ["rev-parse", "HEAD"], name="capture LFS discovery source ref") before = self.git_value(source, ["ls-remote", "--refs", destination], name="capture refs before LFS discovery fault") - path = source / ".git/objects" / oid[:2] / oid[2:] - original = path.read_bytes() + pointer = self.git_value(source, ["cat-file", "blob", oid], name="capture healthy LFS pointer") working_bytes = (source / "hook-lfs.bin").read_bytes() - large_file = self.artifacts / "lfs-discovery-large-blob.bin" - large_file.write_bytes(b"x" * 65536) - large_oid = self.git_value(source, ["hash-object", "-w", str(large_file)], name="create LFS discovery large-header fault") - replacement = (source / ".git/objects" / large_oid[:2] / large_oid[2:]).read_bytes() - command = [str(self.crab_bin), "lfs", "push", "--dry-run", destination] - # Only this disposable fixture's directory entry changes. Restore it - # even if the assertion fails; no source worktree data is rewritten. - path.unlink() + objects = source / ".git/objects" + original_objects = { + str(path.relative_to(objects)): sha256_file(path) + for path in objects.rglob("*") if path.is_file() + } + saved_objects = source / ".git/lfs-discovery-saved-objects" + loose_objects = source / ".git/lfs-discovery-loose-objects" + loose_objects.mkdir() + pack = self.artifacts / "lfs-discovery-source.pack" + self.run_binary( + "export disposable LFS source objects", + [str(self.git_bin), "pack-objects", "--all", "--stdout"], source, pack, + ) + # Unpack into an empty database: Git skips objects already present, and + # a healthy packed duplicate could mask the corrupt loose entry in gix. + self.run_git( + source, ["unpack-objects", "--strict"], + name="prepare isolated loose LFS fault database", + extra_env={"GIT_OBJECT_DIRECTORY": str(loose_objects), "GIT_ALTERNATE_OBJECT_DIRECTORIES": ""}, + input_data=pack.read_bytes(), + ) + make_private_fixture_tree(loose_objects) + objects.rename(saved_objects) try: + loose_objects.rename(objects) + path = objects / oid[:2] / oid[2:] + large_file = self.artifacts / "lfs-discovery-large-blob.bin" + large_file.write_bytes(b"x" * 65536) + large_oid = self.git_value(source, ["hash-object", "-w", str(large_file)], name="create LFS discovery large-header fault") + replacement = (objects / large_oid[:2] / large_oid[2:]).read_bytes() + path.unlink() path.write_bytes(replacement) + command = [str(self.crab_bin), "lfs", "push", "--dry-run", destination] rejected = self.run_cmd("LFS discovery rejects oversized corrupt pointer", command, source, check=False) self.check( "mirror-lfs-discovery-rejects-corrupt-git-pointer", @@ -2857,13 +2879,19 @@ def mirror_lfs_discovery_check(self, source: Path, destination: str) -> None: and self.git_value(source, ["ls-remote", "--refs", destination], name="verify remote refs after LFS discovery refusal") == before, ) finally: - if path.exists(): - path.unlink() - path.write_bytes(original) + # Restore the original object database even when injection or its + # assertion fails; packed/loose layout and original bytes stay intact. + if objects.exists(): + shutil.rmtree(objects) + saved_objects.rename(objects) self.run_cmd("LFS discovery accepts restored Git pointer", command, source) self.check( "mirror-lfs-discovery-restoration-preserves-refs", - path.read_bytes() == original and (source / "hook-lfs.bin").read_bytes() == working_bytes + {str(path.relative_to(objects)): sha256_file(path) + for path in objects.rglob("*") if path.is_file()} == original_objects + and self.git_value(source, ["cat-file", "blob", oid], name="verify restored LFS pointer") == pointer + and self.git_value(source, ["rev-parse", "HEAD"], name="verify restored LFS source ref") == head + and (source / "hook-lfs.bin").read_bytes() == working_bytes and self.git_value(source, ["ls-remote", "--refs", destination], name="verify refs after LFS discovery restoration") == before, ) @@ -2920,6 +2948,7 @@ def mirror_pre_push_batch_checks(self) -> None: ) self.run_git(source, ["add", ".gitattributes", "hook-lfs.bin"]) self.run_git(source, ["commit", "-m", "hook LFS data"]) + self.run_git(source, ["repack", "-ad"], name="pack hook source before LFS discovery fault") self.mirror_lfs_discovery_check(source, destination) self.run_git(source, ["tag", "-a", "hook-v1", "-m", "hook annotated tag"]) selected = self.git_value(source, ["rev-parse", "topic"], name="hook selected commit")