"""Data-integrity stress tests for the entire muse merge code path. Root cause of the data-loss incident ------------------------------------- ``muse merge`` silently defaulted every unreadable snapshot to ``{}`` via ``get_head_snapshot_manifest(...) or {}``. When a snapshot file was missing or in the wrong format (e.g. a plain JSON blob without the muse object header), all three manifests (base/ours/theirs) resolved to ``{}``. This caused: 1. ``apply_merge({}, {}, {}, …)`` → ``{}`` 2. ``compute_snapshot_id({})`` → SHA-256 of ``b""`` = ``e3b0c44…`` 3. ``_restore_from_manifest(root, {})`` → ``apply_manifest(root, {})`` → ALL tracked files deleted. The fix is dual-layered: * **merge.py**: every snapshot read is now a hard fail — ``None`` returns abort the merge with an error before any manifest is applied to the tree. * **merge.py**: before ``_restore_from_manifest`` is called, the merged manifest is validated; applying an empty result to a non-empty working tree is rejected. Test categories --------------- I Sentinel-value unit tests (document the dangerous constants). II Store-read-failure guard tests (missing/corrupt snapshot → abort). III Empty-manifest guard (merged result empty despite non-empty inputs → abort). IV Working-tree integrity (full CLI round-trip — count files, verify content). V apply_manifest safety (workdir.py layer). VI The exact regression scenario (format-migration topology). VII Stress tests (100-file repos, repeated merges, diamond DAGs). """ from __future__ import annotations import datetime import json import pathlib import pytest from tests.cli_test_helper import CliRunner from muse.core.types import Manifest, blob_id, fake_id from muse.core.object_store import object_path from muse.core.paths import commits_dir, muse_dir, ref_path, snapshots_dir type _EnvMap = dict[str, str] runner = CliRunner() cli = None # CliRunner ignores this positional arg # sentinel ID produced by hash_snapshot({}) — empty manifest = data-loss indicator from muse.core.ids import hash_snapshot as _hash_snapshot_fn _SHA256_EMPTY = _hash_snapshot_fn({}) # --------------------------------------------------------------------------- # Repo helpers (mirror test_stress_merge_regression.py conventions) # --------------------------------------------------------------------------- def _h(label: str) -> str: """Stable fake content hash for a label string.""" return fake_id(label) def _env(root: pathlib.Path) -> _EnvMap: return {"MUSE_REPO_ROOT": str(root)} def _run(root: pathlib.Path, *args: str) -> tuple[int, str]: """Run a muse CLI command, auto-injecting ``--force`` for merge calls.""" final_args = list(args) if final_args and final_args[0] == "merge" and "--force" not in final_args: final_args.insert(1, "--force") result = runner.invoke(cli, final_args, env=_env(root), catch_exceptions=False) return result.exit_code, result.output def _run_unchecked(root: pathlib.Path, *args: str) -> tuple[int, str]: final_args = list(args) if final_args and final_args[0] == "merge" and "--force" not in final_args: final_args.insert(1, "--force") result = runner.invoke(cli, final_args, env=_env(root)) return result.exit_code, result.output def _write_object(root: pathlib.Path, content: bytes) -> str: from muse.core.object_store import object_path, write_object oid = blob_id(content) write_object(root, oid, content) return oid def _write_file(root: pathlib.Path, content: str) -> str: return _write_object(root, content.encode()) def _init_repo(tmp_path: pathlib.Path, domain: str = "code") -> tuple[pathlib.Path, str]: """Initialise a minimal repo and return (root, repo_id).""" dot_muse = muse_dir(tmp_path) dot_muse.mkdir() repo_id = fake_id("repo") (dot_muse / "repo.json").write_text(json.dumps({ "repo_id": repo_id, "domain": domain, "default_branch": "main", "created_at": "2025-01-01T00:00:00+00:00", }), encoding="utf-8") (dot_muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8") (dot_muse / "refs" / "heads").mkdir(parents=True) (dot_muse / "snapshots").mkdir() (dot_muse / "commits").mkdir() (dot_muse / "objects").mkdir() return tmp_path, repo_id def _make_commit( root: pathlib.Path, repo_id: str, branch: str = "main", message: str = "test", manifest: Manifest | None = None, parent_commit_id: str | None = None, parent2_commit_id: str | None = None, ) -> str: """Write a snapshot + commit record, advance the branch ref, return commit_id.""" from muse.core.ids import hash_commit as compute_commit_id, hash_snapshot as compute_snapshot_id from muse.core.commits import ( CommitRecord, write_commit, ) from muse.core.snapshots import ( SnapshotRecord, write_snapshot, ) ref_file = ref_path(root, branch) if parent_commit_id is None and ref_file.exists(): parent_commit_id = ref_file.read_text().strip() or None m = manifest or {} snap_id = compute_snapshot_id(m) committed_at = datetime.datetime.now(datetime.timezone.utc) parent_ids: list[str] = [] if parent_commit_id: parent_ids.append(parent_commit_id) if parent2_commit_id: parent_ids.append(parent2_commit_id) commit_id = compute_commit_id( parent_ids=parent_ids, snapshot_id=snap_id, message=message, committed_at_iso=committed_at.isoformat(), ) write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=m)) write_commit(root, CommitRecord( commit_id=commit_id, branch=branch, snapshot_id=snap_id, message=message, committed_at=committed_at, parent_commit_id=parent_commit_id, parent2_commit_id=parent2_commit_id, )) ref_file.parent.mkdir(parents=True, exist_ok=True) ref_file.write_text(commit_id, encoding="utf-8") return commit_id def _ref(root: pathlib.Path, branch: str) -> str: return (ref_path(root, branch)).read_text(encoding="utf-8").strip() def _head_manifest(root: pathlib.Path, branch: str) -> _EnvMap: """Return the snapshot manifest for *branch* HEAD.""" from muse.core.commits import read_commit from muse.core.snapshots import read_snapshot commit = read_commit(root, _ref(root, branch)) assert commit is not None, f"No commit on branch {branch}" snap = read_snapshot(root, commit.snapshot_id) assert snap is not None, f"No snapshot for {commit.snapshot_id[:8]}" return snap.manifest # --------------------------------------------------------------------------- # Category I — Sentinel-value unit tests # --------------------------------------------------------------------------- class TestSentinelValuesI: """Document the dangerous constants that signal a broken merge.""" def test_I1_compute_snapshot_id_empty_dict_produces_known_sha256(self) -> None: """I1: compute_snapshot_id({}) == _SHA256_EMPTY — the data-loss sentinel. If this value ever appears as a committed snapshot_id, every tracked file was deleted. This test documents the constant so future readers know exactly what to look for. """ from muse.core.ids import hash_snapshot as compute_snapshot_id assert compute_snapshot_id({}) == _SHA256_EMPTY def test_I2_apply_merge_with_all_empty_inputs_returns_empty(self) -> None: """I2: apply_merge({}, {}, {}, ∅, ∅, ∅) → {} — documents the dangerous passthrough.""" from muse.core.merge_engine import apply_merge result = apply_merge({}, {}, {}, set(), set(), set()) assert result == {} def test_I3_apply_manifest_with_empty_target_raises_when_prev_non_empty( self, tmp_path: pathlib.Path ) -> None: """I3: apply_manifest(root, prev, {}) raises ValueError when prev is non-empty. The guard prevents callers from accidentally deleting all tracked files when an unintentionally empty target manifest is passed. """ from muse.core.workdir import apply_manifest root, repo_id = _init_repo(tmp_path) prev: dict[str, str] = {} for i in range(5): content = f"file_{i} = True\n".encode() oid = blob_id(content) obj_file = object_path(root, oid) obj_file.parent.mkdir(parents=True, exist_ok=True) obj_file.write_bytes(content) (root / f"file_{i}.py").write_bytes(content) prev[f"file_{i}.py"] = oid with pytest.raises(ValueError, match="empty target_manifest"): apply_manifest(root, prev, {}) def test_I4_diff_snapshots_both_empty_returns_empty_set(self) -> None: """I4: diff_snapshots({}, {}) == set() — no phantom changes.""" from muse.core.merge_engine import diff_snapshots assert diff_snapshots({}, {}) == set() def test_I5_detect_conflicts_both_empty_returns_empty(self) -> None: """I5: detect_conflicts(set(), set(), {}, {}) == set().""" from muse.core.merge_engine import detect_conflicts assert detect_conflicts(set(), set(), {}, {}) == set() # --------------------------------------------------------------------------- # Category II — Store-read-failure guard tests # --------------------------------------------------------------------------- class TestStoreReadFailureGuardII: """merge.py must abort with an error when any required snapshot is unreadable. None of these cases should silently fall back to {} and proceed to delete files. """ def _setup_two_branch_repo( self, tmp_path: pathlib.Path ) -> tuple[pathlib.Path, str, str, str, str]: """Set up a simple diverged repo: main and feat both have commits. Returns (root, repo_id, main_commit_id, feat_commit_id, base_commit_id). """ root, repo_id = _init_repo(tmp_path) f0 = _write_file(root, "base.py = True\n") base_c = _make_commit(root, repo_id, "main", "base", {"base.py": f0}) (ref_path(root, "feat")).write_text(base_c) f1 = _write_file(root, "main_only.py = True\n") main_c = _make_commit(root, repo_id, "main", "main change", {"base.py": f0, "main_only.py": f1}) f2 = _write_file(root, "feat_only.py = True\n") feat_c = _make_commit(root, repo_id, "feat", "feat change", {"base.py": f0, "feat_only.py": f2}) return root, repo_id, main_c, feat_c, base_c def test_II1_missing_ours_snapshot_aborts_merge(self, tmp_path: pathlib.Path) -> None: """II1: if ours (main) snapshot file is deleted, merge must abort — not delete all files.""" root, repo_id, main_c, feat_c, base_c = self._setup_two_branch_repo(tmp_path) from muse.core.commits import read_commit commit = read_commit(root, main_c) assert commit is not None snap_path = object_path(root, commit.snapshot_id) snap_path.unlink() # Delete the snapshot file # Attempt the merge — must fail, not succeed with empty snapshot. code, out = _run_unchecked(root, "merge", "feat") assert code != 0, ( f"REGRESSION: merge succeeded despite ours snapshot being missing. " f"Expected abort to prevent data loss.\nOutput: {out}" ) # Verify main HEAD has NOT advanced past main_c. assert _ref(root, "main") == main_c, ( "REGRESSION: main HEAD advanced after a merge that should have aborted." ) def test_II2_missing_theirs_snapshot_aborts_merge(self, tmp_path: pathlib.Path) -> None: """II2: if theirs (feat) snapshot file is deleted, merge must abort.""" root, repo_id, main_c, feat_c, base_c = self._setup_two_branch_repo(tmp_path) from muse.core.commits import read_commit commit = read_commit(root, feat_c) assert commit is not None snap_path = object_path(root, commit.snapshot_id) snap_path.unlink() code, out = _run_unchecked(root, "merge", "feat") assert code != 0, ( f"REGRESSION: merge succeeded despite theirs snapshot being missing.\nOutput: {out}" ) assert _ref(root, "main") == main_c def test_II3_corrupt_ours_snapshot_aborts_merge(self, tmp_path: pathlib.Path) -> None: """II3: corrupt ours snapshot (binary garbage, invalid JSON) must abort merge.""" root, repo_id, main_c, feat_c, base_c = self._setup_two_branch_repo(tmp_path) from muse.core.commits import read_commit commit = read_commit(root, main_c) assert commit is not None snap_path = object_path(root, commit.snapshot_id) snap_path.write_bytes(b"\xff\xfe invalid msgpack garbage") code, out = _run_unchecked(root, "merge", "feat") assert code != 0, ( f"REGRESSION: merge succeeded with corrupt ours snapshot.\nOutput: {out}" ) assert _ref(root, "main") == main_c def test_II4_corrupt_theirs_snapshot_aborts_merge(self, tmp_path: pathlib.Path) -> None: """II4: corrupt theirs snapshot must abort merge.""" root, repo_id, main_c, feat_c, base_c = self._setup_two_branch_repo(tmp_path) from muse.core.commits import read_commit commit = read_commit(root, feat_c) assert commit is not None snap_path = object_path(root, commit.snapshot_id) snap_path.write_bytes(b"\x00\x01\x02 also garbage") code, out = _run_unchecked(root, "merge", "feat") assert code != 0, ( f"REGRESSION: merge succeeded with corrupt theirs snapshot.\nOutput: {out}" ) assert _ref(root, "main") == main_c def test_II5_missing_base_snapshot_aborts_merge(self, tmp_path: pathlib.Path) -> None: """II5: if the merge-base snapshot is missing, merge must abort — not treat base as {}.""" root, repo_id, main_c, feat_c, base_c = self._setup_two_branch_repo(tmp_path) # Delete the base snapshot. from muse.core.commits import read_commit base_commit = read_commit(root, base_c) assert base_commit is not None snap_path = object_path(root, base_commit.snapshot_id) snap_path.unlink() code, out = _run_unchecked(root, "merge", "feat") assert code != 0, ( "REGRESSION: merge succeeded with missing base snapshot — " "treating base as {} inflates change-sets and may corrupt the merge.\n" f"Output: {out}" ) assert _ref(root, "main") == main_c def test_II6_missing_ours_commit_file_aborts_merge(self, tmp_path: pathlib.Path) -> None: """II6: if the ours commit file is deleted, merge must abort.""" root, repo_id, main_c, feat_c, base_c = self._setup_two_branch_repo(tmp_path) cp = object_path(root, main_c) cp.unlink() code, out = _run_unchecked(root, "merge", "feat") assert code != 0, ( f"REGRESSION: merge succeeded with missing ours commit file.\nOutput: {out}" ) def test_II7_fast_forward_missing_theirs_snapshot_aborts(self, tmp_path: pathlib.Path) -> None: """II7: fast-forward merge with missing theirs snapshot must abort — not delete all files. Before the fix: ff_manifest defaults to {}, _restore_from_manifest({}) deletes everything. After the fix: abort with an error before touching the working tree. """ root, repo_id = _init_repo(tmp_path) # Write real files to the working tree. f0 = _write_file(root, "keeper.py = 42\n") (root / "keeper.py").write_bytes(b"keeper.py = 42\n") base_c = _make_commit(root, repo_id, "main", "base", {"keeper.py": f0}) (ref_path(root, "feat")).write_text(base_c) f1 = _write_file(root, "new.py = True\n") feat_c = _make_commit(root, repo_id, "feat", "feat commit", {"keeper.py": f0, "new.py": f1}) # Delete feat's snapshot — main is behind feat (fast-forward case). from muse.core.commits import read_commit feat_commit = read_commit(root, feat_c) assert feat_commit is not None object_path(root, feat_commit.snapshot_id).unlink() code, out = _run_unchecked(root, "merge", "feat") assert code != 0, ( "REGRESSION: fast-forward merge succeeded despite theirs snapshot missing. " f"This would have applied apply_manifest({{}}) and deleted keeper.py.\nOutput: {out}" ) # The critical assertion: keeper.py must still exist. assert (root / "keeper.py").exists(), ( "DATA LOSS: keeper.py was deleted when theirs snapshot was missing " "during fast-forward. The guard must abort BEFORE apply_manifest." ) def test_II8_headerless_json_snapshot_treated_as_corrupt( self, tmp_path: pathlib.Path ) -> None: """II8: snapshot without muse object header is treated as corrupt → merge aborts. Simulates a snapshot file written as plain JSON bytes (no 'snapshot \0' header). The muse object reader cannot parse it, causing read_snapshot to return None. Merge must abort rather than proceeding with an empty manifest. """ root, repo_id, main_c, feat_c, base_c = self._setup_two_branch_repo(tmp_path) from muse.core.commits import read_commit commit = read_commit(root, main_c) assert commit is not None snap_path = object_path(root, commit.snapshot_id) # Overwrite the object with the equivalent JSON without the muse object header. # This simulates an old-format file that cannot be parsed as a muse object. old_json = json.dumps({"snapshot_id": commit.snapshot_id, "manifest": {}}).encode() snap_path.write_bytes(old_json) code, out = _run_unchecked(root, "merge", "feat") assert code != 0, ( "REGRESSION: merge succeeded when ours snapshot was in JSON (old format). " "This is the exact scenario that caused the data-loss incident.\n" f"Expected: abort. Got: {out}" ) assert _ref(root, "main") == main_c def test_II9_missing_ours_snapshot_does_not_delete_working_tree( self, tmp_path: pathlib.Path ) -> None: """II9: working-tree files must survive when ours snapshot is unreadable. Belt-and-suspenders: even if the merge somehow proceeds, it must not apply an empty manifest to the working tree. """ root, repo_id = _init_repo(tmp_path) # Write 10 files to the working tree. manifest: Manifest = {} for i in range(10): content = f"module_{i} = True\n".encode() oid = _write_object(root, content) (root / f"module_{i}.py").write_bytes(content) manifest[f"module_{i}.py"] = oid base_c = _make_commit(root, repo_id, "main", "base", manifest) (ref_path(root, "feat")).write_text(base_c) extra = _write_file(root, "extra.py = True\n") feat_c = _make_commit(root, repo_id, "feat", "feat", {**manifest, "extra.py": extra}) # Advance main past base so this is a three-way merge. bump = _write_file(root, "bump.py = True\n") _make_commit(root, repo_id, "main", "main advance", {**manifest, "bump.py": bump}) # Delete main's latest snapshot. from muse.core.commits import read_commit main_commit = read_commit(root, _ref(root, "main")) assert main_commit is not None object_path(root, main_commit.snapshot_id).unlink() _run_unchecked(root, "merge", "feat") # All 10 original files must still exist. for i in range(10): assert (root / f"module_{i}.py").exists(), ( f"DATA LOSS: module_{i}.py deleted when merge should have aborted " "due to unreadable ours snapshot." ) # --------------------------------------------------------------------------- # Category III — Empty-manifest guard tests # --------------------------------------------------------------------------- class TestEmptyManifestGuardIII: """The merged result must never be applied to the working tree if it is suspiciously empty given the inputs. """ def test_III1_merge_result_snapshot_id_is_never_sha256_empty( self, tmp_path: pathlib.Path ) -> None: """III1: a successful merge must never produce a commit with snapshot_id == SHA-256(""). e3b0c44… is the fingerprint of an empty snapshot; if it ever appears in the commit graph, the merge engine produced an empty manifest and deleted all tracked files. """ root, repo_id = _init_repo(tmp_path) f0 = _write_file(root, "a.py = 0\n") base_c = _make_commit(root, repo_id, "main", "base", {"a.py": f0}) (ref_path(root, "feat")).write_text(base_c) f1 = _write_file(root, "a.py = 1\n") _make_commit(root, repo_id, "main", "ours", {"a.py": f1}) f2 = _write_file(root, "b.py = True\n") _make_commit(root, repo_id, "feat", "theirs", {"a.py": f0, "b.py": f2}) code, out = _run(root, "merge", "feat") assert code == 0, out from muse.core.commits import read_commit commit = read_commit(root, _ref(root, "main")) assert commit is not None assert commit.snapshot_id != _SHA256_EMPTY, ( f"REGRESSION: merge commit has snapshot_id == SHA-256('') == {_SHA256_EMPTY[:16]}…\n" "This means the merged manifest was empty and all tracked files were deleted.\n" "This is the data-loss sentinel produced by compute_snapshot_id({})." ) def test_III2_merged_manifest_has_at_least_as_many_files_as_base( self, tmp_path: pathlib.Path ) -> None: """III2: clean merge → merged manifest >= base file count. When neither side deletes a file, the merged manifest must have AT LEAST as many entries as the base. Fewer entries means files were silently dropped. """ root, repo_id = _init_repo(tmp_path) base_manifest = {f"file_{i}.py": _write_file(root, f"x_{i} = {i}\n") for i in range(20)} base_c = _make_commit(root, repo_id, "main", "base", base_manifest) (ref_path(root, "feat")).write_text(base_c) # ours: modify file_0.py only. ours_manifest = {**base_manifest, "file_0.py": _write_file(root, "x_0 = 'ours'\n")} _make_commit(root, repo_id, "main", "ours", ours_manifest) # theirs: modify file_1.py only. theirs_manifest = {**base_manifest, "file_1.py": _write_file(root, "x_1 = 'theirs'\n")} _make_commit(root, repo_id, "feat", "theirs", theirs_manifest) code, out = _run(root, "merge", "feat") assert code == 0, out merged = _head_manifest(root, "main") assert len(merged) >= len(base_manifest), ( f"REGRESSION: merged manifest has {len(merged)} files but base had " f"{len(base_manifest)}. Files were silently dropped." ) def test_III3_merged_manifest_contains_all_base_files_when_no_deletions( self, tmp_path: pathlib.Path ) -> None: """III3: no-deletion merge — every base file must appear in merged.""" root, repo_id = _init_repo(tmp_path) base_manifest = {f"mod_{i}.py": _write_file(root, f"MOD_{i} = True\n") for i in range(15)} base_c = _make_commit(root, repo_id, "main", "base", base_manifest) (ref_path(root, "feat")).write_text(base_c) new_main = _write_file(root, "new_main.py = True\n") _make_commit(root, repo_id, "main", "ours", {**base_manifest, "new_main.py": new_main}) new_feat = _write_file(root, "new_feat.py = True\n") _make_commit(root, repo_id, "feat", "theirs", {**base_manifest, "new_feat.py": new_feat}) code, out = _run(root, "merge", "feat") assert code == 0, out merged = _head_manifest(root, "main") for path in base_manifest: assert path in merged, ( f"REGRESSION: base file '{path}' is missing from merged manifest. " "Files are being silently dropped." ) # --------------------------------------------------------------------------- # Category IV — Working-tree integrity (full CLI round-trips) # --------------------------------------------------------------------------- class TestWorkingTreeIntegrityIV: """Full CLI merges must leave the working tree in a coherent state.""" def test_IV1_three_way_merge_working_tree_matches_snapshot( self, tmp_path: pathlib.Path ) -> None: """IV1: after a clean merge, working tree files match the merged snapshot.""" root, repo_id = _init_repo(tmp_path) content_a = b"A = 1\n" content_b = b"B = 2\n" a_id = _write_object(root, content_a) b_id = _write_object(root, content_b) base_c = _make_commit(root, repo_id, "main", "base", {"a.py": a_id}) (ref_path(root, "feat")).write_text(base_c) a2_content = b"A = 'ours'\n" a2_id = _write_object(root, a2_content) _make_commit(root, repo_id, "main", "ours", {"a.py": a2_id}) _make_commit(root, repo_id, "feat", "theirs", {"a.py": a_id, "b.py": b_id}) code, out = _run(root, "merge", "feat") assert code == 0, out # After merge: working tree should have a.py (ours version) and b.py (theirs). merged = _head_manifest(root, "main") assert "b.py" in merged, "theirs-only b.py missing from merged snapshot" assert merged.get("a.py") == a2_id, "ours change to a.py not preserved in merged snapshot" def test_IV2_fast_forward_file_count_preserved(self, tmp_path: pathlib.Path) -> None: """IV2: fast-forward merge preserves ALL files from the target branch.""" root, repo_id = _init_repo(tmp_path) # Write 25 files. manifest: Manifest = {} for i in range(25): oid = _write_file(root, f"x_{i} = {i}\n") manifest[f"file_{i:02d}.py"] = oid base_c = _make_commit(root, repo_id, "main", "base", {"start.py": _write_file(root, "x=0\n")}) (ref_path(root, "feat")).write_text(base_c) _make_commit(root, repo_id, "feat", "feat: 25 files", manifest) code, _out = _run(root, "merge", "feat") assert code == 0 merged = _head_manifest(root, "main") for path in manifest: assert path in merged, f"DATA LOSS: {path} missing after fast-forward merge" def test_IV3_three_way_merge_both_sides_preserved(self, tmp_path: pathlib.Path) -> None: """IV3: ours-only AND theirs-only files both present in merged result.""" root, repo_id = _init_repo(tmp_path) base_id = _write_file(root, "base = True\n") base_c = _make_commit(root, repo_id, "main", "base", {"base.py": base_id}) (ref_path(root, "feat")).write_text(base_c) ours_id = _write_file(root, "ours = True\n") _make_commit(root, repo_id, "main", "ours", {"base.py": base_id, "ours_only.py": ours_id}) theirs_id = _write_file(root, "theirs = True\n") _make_commit(root, repo_id, "feat", "theirs", {"base.py": base_id, "theirs_only.py": theirs_id}) code, out = _run(root, "merge", "feat") assert code == 0, out merged = _head_manifest(root, "main") assert "ours_only.py" in merged, "ours-only file was dropped in merge" assert "theirs_only.py" in merged, "theirs-only file was dropped in merge" assert "base.py" in merged, "base file was dropped in merge" def test_IV4_merge_commit_snapshot_not_empty(self, tmp_path: pathlib.Path) -> None: """IV4: merge commit snapshot_id must never be SHA-256 of empty bytes.""" root, repo_id = _init_repo(tmp_path) f0 = _write_file(root, "a = 0\n") f1 = _write_file(root, "a = 1\n") f2 = _write_file(root, "b = True\n") base_c = _make_commit(root, repo_id, "main", "base", {"a.py": f0}) (ref_path(root, "feat")).write_text(base_c) _make_commit(root, repo_id, "main", "ours", {"a.py": f1}) _make_commit(root, repo_id, "feat", "theirs", {"a.py": f0, "b.py": f2}) code, out = _run(root, "merge", "feat") assert code == 0, out from muse.core.commits import read_commit mc = read_commit(root, _ref(root, "main")) assert mc is not None assert mc.snapshot_id != _SHA256_EMPTY, ( "DATA LOSS: merge commit snapshot_id is SHA-256 of empty bytes. " "The merged manifest was empty — all files were or would be deleted." ) def test_IV5_merge_commit_has_two_parents(self, tmp_path: pathlib.Path) -> None: """IV5: three-way merge commit must record both parent commit IDs.""" root, repo_id = _init_repo(tmp_path) f0 = _write_file(root, "a = 0\n") base_c = _make_commit(root, repo_id, "main", "base", {"a.py": f0}) (ref_path(root, "feat")).write_text(base_c) f1 = _write_file(root, "a = 1\n") _make_commit(root, repo_id, "main", "ours", {"a.py": f1}) f2 = _write_file(root, "b = True\n") _make_commit(root, repo_id, "feat", "theirs", {"a.py": f0, "b.py": f2}) _run(root, "merge", "feat") from muse.core.commits import read_commit mc = read_commit(root, _ref(root, "main")) assert mc is not None assert mc.parent2_commit_id is not None, ( "Three-way merge commit missing second parent — history will appear linear." ) def test_IV6_strategy_ours_does_not_delete_theirs_only_files( self, tmp_path: pathlib.Path ) -> None: """IV6: --strategy=ours must not delete theirs-only files from merged manifest.""" root, repo_id = _init_repo(tmp_path) f0 = _write_file(root, "shared = 0\n") base_c = _make_commit(root, repo_id, "main", "base", {"shared.py": f0}) (ref_path(root, "feat")).write_text(base_c) f_ours = _write_file(root, "shared = 'ours'\n") theirs_only = _write_file(root, "new_feat = True\n") _make_commit(root, repo_id, "main", "ours change", {"shared.py": f_ours}) f_theirs = _write_file(root, "shared = 'theirs'\n") _make_commit(root, repo_id, "feat", "theirs", {"shared.py": f_theirs, "new_feat.py": theirs_only}) code, out = _run(root, "merge", "--strategy", "ours", "feat") assert code == 0, out merged = _head_manifest(root, "main") assert merged.get("shared.py") == f_ours, "strategy=ours must keep ours version of conflict" assert "new_feat.py" in merged, ( "REGRESSION: --strategy=ours deleted theirs-only new_feat.py. " "Non-conflicting theirs additions must still appear in merged." ) def test_IV7_strategy_theirs_does_not_delete_ours_only_files( self, tmp_path: pathlib.Path ) -> None: """IV7: --strategy=theirs must not delete ours-only files from merged manifest.""" root, repo_id = _init_repo(tmp_path) f0 = _write_file(root, "shared = 0\n") base_c = _make_commit(root, repo_id, "main", "base", {"shared.py": f0}) (ref_path(root, "feat")).write_text(base_c) ours_only = _write_file(root, "ours_new = True\n") f_ours = _write_file(root, "shared = 'ours'\n") _make_commit(root, repo_id, "main", "ours", {"shared.py": f_ours, "ours_new.py": ours_only}) f_theirs = _write_file(root, "shared = 'theirs'\n") _make_commit(root, repo_id, "feat", "theirs", {"shared.py": f_theirs}) code, out = _run(root, "merge", "--strategy", "theirs", "feat") assert code == 0, out merged = _head_manifest(root, "main") assert merged.get("shared.py") == f_theirs, "strategy=theirs must keep theirs version" assert "ours_new.py" in merged, ( "REGRESSION: --strategy=theirs deleted ours-only ours_new.py. " "Non-conflicting ours additions must still appear in merged." ) # --------------------------------------------------------------------------- # Category V — apply_manifest safety # --------------------------------------------------------------------------- class TestApplyManifestSafetyV: """apply_manifest layer must be precise and not corrupt the working tree.""" def test_V1_apply_manifest_writes_target_files(self, tmp_path: pathlib.Path) -> None: """V1: apply_manifest restores files from the object store correctly.""" from muse.core.workdir import apply_manifest root, _ = _init_repo(tmp_path) content = b"HELLO = True\n" oid = _write_object(root, content) apply_manifest(root, {}, {"hello.py": oid}) assert (root / "hello.py").read_bytes() == content def test_V2_apply_manifest_removes_files_not_in_target(self, tmp_path: pathlib.Path) -> None: """V2: apply_manifest removes tracked files absent from target.""" from muse.core.workdir import apply_manifest root, _ = _init_repo(tmp_path) content = b"OLD = True\n" oid = _write_object(root, content) (root / "old.py").write_bytes(content) new_content = b"NEW = True\n" new_oid = _write_object(root, new_content) apply_manifest(root, {"old.py": oid}, {"new.py": new_oid}) assert not (root / "old.py").exists(), "apply_manifest should remove tracked files not in target" assert (root / "new.py").read_bytes() == new_content def test_V3_apply_manifest_does_not_delete_muse_dir(self, tmp_path: pathlib.Path) -> None: """V3: apply_manifest must never delete .muse/ regardless of target.""" from muse.core.workdir import apply_manifest root, _ = _init_repo(tmp_path) assert (muse_dir(root)).exists() try: apply_manifest(root, {}, {}) except (ValueError, SystemExit): pass # Guard fired correctly. assert (muse_dir(root)).exists(), ".muse/ was deleted by apply_manifest — critical failure" def test_V4_apply_manifest_does_not_follow_symlinks(self, tmp_path: pathlib.Path) -> None: """V4: symlinked files outside the repo are not deleted by apply_manifest.""" from muse.core.workdir import apply_manifest repo_dir = tmp_path / "myrepo" repo_dir.mkdir() root, _ = _init_repo(repo_dir) external = tmp_path / "external_file.txt" external.write_bytes(b"I am external") link = root / "link_to_external.py" link.symlink_to(external) try: apply_manifest(root, {}, {}) except (ValueError, SystemExit): pass # Guard fired — acceptable. assert external.exists(), ( "apply_manifest followed a symlink outside the repo and deleted the target." ) # --------------------------------------------------------------------------- # Category VI — The exact regression scenario # --------------------------------------------------------------------------- class TestFormatMigrationRegressionVI: """Reproduce the exact scenario that caused the data-loss incident. When a snapshot object is unreadable (missing muse object header, wrong format, or corrupt), the old code returned None and defaulted to {}. All manifests resolved to {}, leading to an empty merge and file deletion. """ def test_VI1_regression_snapshot_id_never_equals_sha256_empty_after_merge( self, tmp_path: pathlib.Path ) -> None: """VI1: the data-loss sentinel must never appear in the commit graph. Walk every commit in the graph after a merge and assert that no snapshot_id equals e3b0c44… (SHA-256 of empty bytes). """ root, repo_id = _init_repo(tmp_path) # Build a real diverged graph with many files. base_manifest = {f"src_{i}.py": _write_file(root, f"v = {i}\n") for i in range(10)} base_c = _make_commit(root, repo_id, "main", "base", base_manifest) (ref_path(root, "feat")).write_text(base_c) ours_manifest = {**base_manifest, "ours.py": _write_file(root, "OURS = True\n")} _make_commit(root, repo_id, "main", "ours", ours_manifest) theirs_manifest = {**base_manifest, "theirs.py": _write_file(root, "THEIRS = True\n")} _make_commit(root, repo_id, "feat", "theirs", theirs_manifest) code, out = _run(root, "merge", "feat") assert code == 0, out # Walk the entire commit graph and check every snapshot_id. from muse.core.commits import read_commit visited: set[str] = set() queue = [_ref(root, "main")] while queue: cid = queue.pop() if cid in visited: continue visited.add(cid) commit = read_commit(root, cid) if commit is None: continue assert commit.snapshot_id != _SHA256_EMPTY, ( f"REGRESSION: commit {cid[:8]} has snapshot_id == SHA-256('') — " "the data-loss sentinel. This commit has an empty manifest." ) if commit.parent_commit_id: queue.append(commit.parent_commit_id) if commit.parent2_commit_id: queue.append(commit.parent2_commit_id) def test_VI2_merge_after_simulated_format_migration_aborts_not_deletes( self, tmp_path: pathlib.Path ) -> None: """VI2: when the ours snapshot is in a corrupt/headerless format, merge must abort. Simulates the data-loss scenario: a snapshot file written as plain JSON (no muse object header) is unreadable, ours snapshot returns None. The merge must abort rather than silently empty the working tree. """ root, repo_id = _init_repo(tmp_path) # Create 20 files in the working tree. manifest: Manifest = {} for i in range(20): content = f"module_{i} = True\n".encode() oid = _write_object(root, content) (root / f"module_{i}.py").write_bytes(content) manifest[f"module_{i}.py"] = oid base_c = _make_commit(root, repo_id, "main", "base", manifest) (ref_path(root, "feat")).write_text(base_c) extra = _write_file(root, "extra = True\n") _make_commit(root, repo_id, "feat", "feat adds extra", {**manifest, "extra.py": extra}) bump = _write_file(root, "bump = True\n") main_c = _make_commit(root, repo_id, "main", "main adds bump", {**manifest, "bump.py": bump}) # Simulate format migration: overwrite ours snapshot with old JSON bytes. from muse.core.commits import read_commit main_commit = read_commit(root, main_c) assert main_commit is not None snap_path = object_path(root, main_commit.snapshot_id) # Write JSON without the muse object header — parser will fail on this. old_json_bytes = json.dumps({ "snapshot_id": main_commit.snapshot_id, "manifest": {k: v for k, v in {**manifest, "bump.py": bump}.items()}, }).encode() snap_path.write_bytes(old_json_bytes) code, _out = _run_unchecked(root, "merge", "feat") # Either the merge aborts (code != 0) OR it succeeds with correct content. # What is NEVER acceptable: merging with an empty manifest that deletes files. if code == 0: merged = _head_manifest(root, "main") assert merged != {}, ( "DATA LOSS: merge succeeded with an empty manifest. " "All files were deleted because ours snapshot was unreadable (headerless JSON)." ) # Must have non-trivially many files. assert len(merged) >= len(manifest), ( f"DATA LOSS: merged has only {len(merged)} files, expected ≥ {len(manifest)}." ) # If code != 0: correct behaviour (abort). # Critical: the working-tree files must still exist. for i in range(20): assert (root / f"module_{i}.py").exists(), ( f"DATA LOSS: module_{i}.py was deleted when merge aborted due to unreadable snapshot." ) def test_VI3_merge_commit_snapshot_id_matches_actual_files( self, tmp_path: pathlib.Path ) -> None: """VI3: compute_snapshot_id of the merged manifest must equal the stored snapshot_id.""" from muse.core.ids import hash_snapshot as compute_snapshot_id from muse.core.commits import read_commit from muse.core.snapshots import read_snapshot root, repo_id = _init_repo(tmp_path) f0 = _write_file(root, "a = 0\n") f1 = _write_file(root, "a = 1\n") f2 = _write_file(root, "b = True\n") base_c = _make_commit(root, repo_id, "main", "base", {"a.py": f0}) (ref_path(root, "feat")).write_text(base_c) _make_commit(root, repo_id, "main", "ours", {"a.py": f1}) _make_commit(root, repo_id, "feat", "theirs", {"a.py": f0, "b.py": f2}) code, out = _run(root, "merge", "feat") assert code == 0, out commit = read_commit(root, _ref(root, "main")) assert commit is not None snap = read_snapshot(root, commit.snapshot_id) assert snap is not None recomputed = compute_snapshot_id(snap.manifest) assert recomputed == commit.snapshot_id, ( "snapshot_id in the commit record doesn't match " "compute_snapshot_id(snapshot.manifest). The snapshot is corrupt." ) # --------------------------------------------------------------------------- # Category VII — Stress tests # --------------------------------------------------------------------------- class TestStressVII: """Extreme stress tests: large file counts, repeated merges, complex topologies.""" def test_VII1_100_file_clean_merge_all_files_preserved(self, tmp_path: pathlib.Path) -> None: """VII1: merge with 100 theirs-only file additions — none may be dropped.""" root, repo_id = _init_repo(tmp_path) base_id = _write_file(root, "base = True\n") base_c = _make_commit(root, repo_id, "main", "base", {"base.py": base_id}) (ref_path(root, "feat")).write_text(base_c) # ours: minor bump to base.py. bumped = _write_file(root, "base = 2\n") _make_commit(root, repo_id, "main", "ours: bump", {"base.py": bumped}) # theirs: 100 new files. theirs_manifest: Manifest = {"base.py": base_id} for i in range(100): oid = _write_file(root, f"mod_{i:03d} = True\n") theirs_manifest[f"mod_{i:03d}.py"] = oid _make_commit(root, repo_id, "feat", "theirs: 100 mods", theirs_manifest) code, out = _run(root, "merge", "feat") assert code == 0, out merged = _head_manifest(root, "main") dropped = [f"mod_{i:03d}.py" for i in range(100) if f"mod_{i:03d}.py" not in merged] assert not dropped, ( f"DATA LOSS: {len(dropped)} of 100 theirs-only files dropped after merge: " f"{dropped[:5]}{'...' if len(dropped) > 5 else ''}" ) def test_VII2_repeated_merges_file_count_never_decreases( self, tmp_path: pathlib.Path ) -> None: """VII2: five sequential branch merges — total file count must be monotonically non-decreasing. Each wave: - Branches from the current main HEAD (inheriting all previously merged files). - Adds 5 unique files ON TOP of the current main state. - Main is bumped with 1 unique file (true 3-way merge). - After merge: main must have all prior files + 5 wave files + 1 bump. Expected final count: 1 (base) + 5 waves × 5 files + 5 bumps = 31. """ root, repo_id = _init_repo(tmp_path) base_id = _write_file(root, "base = True\n") _make_commit(root, repo_id, "main", "base", {"base.py": base_id}) prev_count = 1 for wave in range(5): branch = f"wave_{wave}" # Branch from the current main HEAD — inherits all previously merged files. (ref_path(root, branch)).write_text(_ref(root, "main")) # Wave branch: current main state + 5 new unique files. wave_manifest = dict(_head_manifest(root, "main")) for j in range(5): oid = _write_file(root, f"wave_{wave}_file_{j} = True\n") wave_manifest[f"w{wave}_{j}.py"] = oid _make_commit(root, repo_id, branch, f"wave {wave} adds 5 files", wave_manifest) # Advance main with 1 unique file so this is a true 3-way merge. bump_manifest = dict(_head_manifest(root, "main")) bump_id = _write_file(root, f"main_bump_{wave} = True\n") bump_manifest[f"main_bump_{wave}.py"] = bump_id _make_commit(root, repo_id, "main", f"main bump {wave}", bump_manifest) code, out = _run(root, "merge", branch) assert code == 0, f"Wave {wave} merge failed: {out}" current_count = len(_head_manifest(root, "main")) assert current_count >= prev_count, ( f"DATA LOSS after wave {wave}: file count decreased " f"from {prev_count} to {current_count}." ) prev_count = current_count # 1 base + 5 waves × 5 files + 5 bumps = 31. assert prev_count >= 31, ( f"Expected at least 31 files after 5 waves, got {prev_count}." ) def test_VII3_diamond_topology_no_files_lost(self, tmp_path: pathlib.Path) -> None: """VII3: diamond merge topology — LCA is correctly found, no data lost. Topology: C0 (base: 10 files) / \\ C1 C2 (ours adds 5) (theirs adds 5 different) \\ / merge → must have all 20 files """ root, repo_id = _init_repo(tmp_path) base_manifest = {f"base_{i}.py": _write_file(root, f"base_{i} = True\n") for i in range(10)} base_c = _make_commit(root, repo_id, "main", "C0", base_manifest) (ref_path(root, "feat")).write_text(base_c) # C1: ours adds 5 files. c1_manifest = {**base_manifest} for i in range(5): c1_manifest[f"ours_{i}.py"] = _write_file(root, f"ours_{i} = True\n") _make_commit(root, repo_id, "main", "C1: ours adds 5", c1_manifest) # C2: theirs adds 5 different files. c2_manifest = {**base_manifest} for i in range(5): c2_manifest[f"theirs_{i}.py"] = _write_file(root, f"theirs_{i} = True\n") _make_commit(root, repo_id, "feat", "C2: theirs adds 5", c2_manifest) code, out = _run(root, "merge", "feat") assert code == 0, out merged = _head_manifest(root, "main") assert len(merged) == 20, ( f"DATA LOSS: expected 20 files after diamond merge, got {len(merged)}. " f"Missing: {sorted(set(list(c1_manifest) + list(c2_manifest)) - set(merged))}" ) def test_VII4_merge_with_deep_history_correct_lca(self, tmp_path: pathlib.Path) -> None: """VII4: 50-commit deep history — LCA found correctly, no files lost.""" root, repo_id = _init_repo(tmp_path) # Build 50 commits on main. f0 = _write_file(root, "anchor = 0\n") current_manifest: Manifest = {"anchor.py": f0} base_c = _make_commit(root, repo_id, "main", "C0", current_manifest) for depth in range(49): fi = _write_file(root, f"depth_{depth} = True\n") current_manifest = {**current_manifest, f"depth_{depth}.py": fi} _make_commit(root, repo_id, "main", f"C{depth + 1}", current_manifest) # Branch at the VERY END. tip_c = _ref(root, "main") (ref_path(root, "feat")).write_text(tip_c) # Advance main by 1. main_extra = _write_file(root, "main_extra = True\n") main_manifest = {**current_manifest, "main_extra.py": main_extra} _make_commit(root, repo_id, "main", "main advance", main_manifest) # Advance feat by 1. feat_extra = _write_file(root, "feat_extra = True\n") feat_manifest = {**current_manifest, "feat_extra.py": feat_extra} _make_commit(root, repo_id, "feat", "feat advance", feat_manifest) code, out = _run(root, "merge", "feat") assert code == 0, out merged = _head_manifest(root, "main") assert "main_extra.py" in merged, "ours-only main_extra.py lost in deep-history merge" assert "feat_extra.py" in merged, "theirs-only feat_extra.py lost in deep-history merge" # All 49 depth files must still be present. for depth in range(49): assert f"depth_{depth}.py" in merged, f"depth_{depth}.py lost in deep-history merge" def test_VII5_stress_strategy_ours_100_theirs_files_all_preserved( self, tmp_path: pathlib.Path ) -> None: """VII5: --strategy=ours with 100 theirs-only additions — all must appear in merged. The old strategy=ours bug took the entire ours manifest verbatim, discarding all theirs-only changes. This test ensures 100 theirs-only files survive even when strategy=ours is used to resolve conflicts. """ root, repo_id = _init_repo(tmp_path) shared_content = _write_file(root, "shared = 'base'\n") base_c = _make_commit(root, repo_id, "main", "base", {"shared.py": shared_content}) (ref_path(root, "feat")).write_text(base_c) ours_shared = _write_file(root, "shared = 'ours'\n") _make_commit(root, repo_id, "main", "ours: modify shared", {"shared.py": ours_shared}) # theirs: conflict on shared.py + 100 theirs-only additions. theirs_shared = _write_file(root, "shared = 'theirs'\n") theirs_manifest: Manifest = {"shared.py": theirs_shared} for i in range(100): oid = _write_file(root, f"extra_{i} = True\n") theirs_manifest[f"extra_{i:03d}.py"] = oid _make_commit(root, repo_id, "feat", "theirs: conflict + 100 extras", theirs_manifest) code, out = _run(root, "merge", "--strategy", "ours", "feat") assert code == 0, out merged = _head_manifest(root, "main") dropped = [f"extra_{i:03d}.py" for i in range(100) if f"extra_{i:03d}.py" not in merged] assert not dropped, ( f"REGRESSION: --strategy=ours dropped {len(dropped)} theirs-only files: " f"{dropped[:5]}{'...' if len(dropped) > 5 else ''}" ) assert merged.get("shared.py") == ours_shared, "--strategy=ours must keep ours version of conflict" def test_VII6_interleaved_add_delete_all_correct(self, tmp_path: pathlib.Path) -> None: """VII6: interleaved adds and deletes on both sides — final manifest exactly correct.""" root, repo_id = _init_repo(tmp_path) # Base: files 0-19. base_manifest = {f"f{i:02d}.py": _write_file(root, f"f{i} = {i}\n") for i in range(20)} base_c = _make_commit(root, repo_id, "main", "base", base_manifest) (ref_path(root, "feat")).write_text(base_c) # ours: delete even files (0,2,4…18), keep odd, add ours_new. ours_manifest = {k: v for k, v in base_manifest.items() if int(k[1:3]) % 2 == 1} ours_manifest["ours_new.py"] = _write_file(root, "OURS_NEW = True\n") _make_commit(root, repo_id, "main", "ours: delete evens, add ours_new", ours_manifest) # theirs: delete files 0-9, keep 10-19, add theirs_new. theirs_manifest = {k: v for k, v in base_manifest.items() if int(k[1:3]) >= 10} theirs_manifest["theirs_new.py"] = _write_file(root, "THEIRS_NEW = True\n") _make_commit(root, repo_id, "feat", "theirs: delete f00-f09, add theirs_new", theirs_manifest) code, out = _run(root, "merge", "feat") assert code == 0, out merged = _head_manifest(root, "main") # Files deleted by ours (evens 0-18): ours deleted, theirs may have kept some. # The three-way merge rule: if ours deleted and theirs didn't change → keep deleted. # Files deleted by theirs (0-9): theirs deleted, ours may have kept some. # ours_new and theirs_new must both be present. assert "ours_new.py" in merged, "ours_new.py was lost in interleaved merge" assert "theirs_new.py" in merged, "theirs_new.py was lost in interleaved merge" # No extra phantom files. for path in merged: assert path in ours_manifest or path in theirs_manifest or path in base_manifest or \ path in ("ours_new.py", "theirs_new.py"), ( f"Phantom file {path!r} in merged manifest — not from any input" ) def test_VII7_merge_output_is_deterministic(self, tmp_path: pathlib.Path) -> None: """VII7: merging the same two branches twice produces the same commit_id. Because commit_id is computed from (parent_ids, snapshot_id, message, timestamp), two runs with the same timestamp must produce the same commit_id. This tests that the merge is truly deterministic. """ from muse.core.merge_engine import apply_merge, detect_conflicts, diff_snapshots from muse.core.ids import hash_snapshot as compute_snapshot_id # Build a deterministic merge scenario at the pure-function level. base = {"a.py": _h("a-base"), "b.py": _h("b-base")} ours = {"a.py": _h("a-ours"), "b.py": _h("b-base")} theirs = {"a.py": _h("a-base"), "b.py": _h("b-theirs"), "c.py": _h("c-new")} ours_changed = diff_snapshots(base, ours) theirs_changed = diff_snapshots(base, theirs) conflicts = detect_conflicts(ours_changed, theirs_changed, ours, theirs) merged1 = apply_merge(base, ours, theirs, ours_changed, theirs_changed, conflicts) merged2 = apply_merge(base, ours, theirs, ours_changed, theirs_changed, conflicts) assert merged1 == merged2, "apply_merge is not deterministic" assert compute_snapshot_id(merged1) == compute_snapshot_id(merged2), ( "compute_snapshot_id is not deterministic" ) def test_VII8_dry_run_never_modifies_any_commit(self, tmp_path: pathlib.Path) -> None: """VII8: --dry-run must not write any commit or advance any branch ref.""" root, repo_id = _init_repo(tmp_path) f0 = _write_file(root, "a = 0\n") base_c = _make_commit(root, repo_id, "main", "base", {"a.py": f0}) (ref_path(root, "feat")).write_text(base_c) f1 = _write_file(root, "a = 1\n") main_c = _make_commit(root, repo_id, "main", "ours", {"a.py": f1}) f2 = _write_file(root, "b = True\n") _make_commit(root, repo_id, "feat", "theirs", {"a.py": f0, "b.py": f2}) objects_dir = muse_dir(root) / "objects" / "sha256" object_count_before = len([p for p in objects_dir.rglob("*") if p.is_file()]) code, _out = _run(root, "merge", "--dry-run", "feat") assert code == 0 object_count_after = len([p for p in objects_dir.rglob("*") if p.is_file()]) assert _ref(root, "main") == main_c, "--dry-run must not advance main HEAD" assert object_count_after == object_count_before, "--dry-run must not write any objects" def test_VII9_no_ff_with_100_files_all_preserved(self, tmp_path: pathlib.Path) -> None: """VII9: --no-ff with 100-file fast-forward-eligible merge — all files preserved.""" root, repo_id = _init_repo(tmp_path) base_id = _write_file(root, "anchor = True\n") base_c = _make_commit(root, repo_id, "main", "base", {"anchor.py": base_id}) (ref_path(root, "feat")).write_text(base_c) large_manifest: Manifest = {"anchor.py": base_id} for i in range(100): oid = _write_file(root, f"big_{i:03d} = True\n") large_manifest[f"big_{i:03d}.py"] = oid _make_commit(root, repo_id, "feat", "feat: 100 files", large_manifest) # --no-ff forces a three-way merge commit even though this is fast-forwardable. code, out = _run(root, "merge", "--no-ff", "feat") assert code == 0, out merged = _head_manifest(root, "main") for i in range(100): assert f"big_{i:03d}.py" in merged, f"big_{i:03d}.py missing after --no-ff merge" # Must have created a real merge commit (two parents). from muse.core.commits import read_commit mc = read_commit(root, _ref(root, "main")) assert mc is not None assert mc.parent2_commit_id is not None, "--no-ff must produce a merge commit with 2 parents"