test_commit_workdir_preservation.py
python
sha256:2eaa5d95f9d9383498e76947410a26e5a3ba23d182f339910c424cf88fad412b
fix: try fetch/presign before fetch/mpack to avoid Cloudfla…
Sonnet 4.6
patch
6 days ago
| 1 | """Tests that muse commit does NOT overwrite the working tree. |
| 2 | |
| 3 | Regression suite for the apply_manifest-in-commit bug: |
| 4 | commit.py was calling apply_manifest() after writing the snapshot, which |
| 5 | restored every tracked file from the object store — silently destroying any |
| 6 | unstaged changes that existed on disk at commit time. |
| 7 | |
| 8 | Expected (Git-compatible) behaviour: commit writes to the object store and |
| 9 | advances the branch ref; it never touches files in the working tree. |
| 10 | """ |
| 11 | |
| 12 | from __future__ import annotations |
| 13 | |
| 14 | import pathlib |
| 15 | |
| 16 | import pytest |
| 17 | |
| 18 | from tests.cli_test_helper import CliRunner |
| 19 | |
| 20 | runner = CliRunner() |
| 21 | |
| 22 | |
| 23 | def _run(repo: pathlib.Path, *args: str) -> None: |
| 24 | r = runner.invoke(None, list(args), cwd=repo) |
| 25 | assert r.exit_code == 0, f"muse {' '.join(args)} failed:\n{r.output}" |
| 26 | |
| 27 | |
| 28 | # --------------------------------------------------------------------------- |
| 29 | # Core regression: unstaged changes to a tracked file survive commit |
| 30 | # --------------------------------------------------------------------------- |
| 31 | |
| 32 | |
| 33 | class TestCommitPreservesWorkdir: |
| 34 | def test_unstaged_changes_survive_commit(self, muse_repo: pathlib.Path) -> None: |
| 35 | """Unstaged edits to a tracked file must not be overwritten by commit.""" |
| 36 | f = muse_repo / "song.py" |
| 37 | f.write_text("v1\n") |
| 38 | _run(muse_repo, "code", "add", "song.py") |
| 39 | _run(muse_repo, "commit", "-m", "add song") |
| 40 | |
| 41 | # Make a second change and stage it |
| 42 | f.write_text("v2\n") |
| 43 | _run(muse_repo, "code", "add", "song.py") |
| 44 | |
| 45 | # Make a THIRD change — intentionally NOT staged |
| 46 | f.write_text("v3 — unstaged\n") |
| 47 | |
| 48 | # Commit captures v2 (staged); v3 on disk must survive |
| 49 | _run(muse_repo, "commit", "-m", "commit v2") |
| 50 | |
| 51 | assert f.read_text() == "v3 — unstaged\n", ( |
| 52 | "commit overwrote the working tree with the staged version" |
| 53 | ) |
| 54 | |
| 55 | def test_unstaged_changes_to_multiple_files(self, muse_repo: pathlib.Path) -> None: |
| 56 | """Multiple files with unstaged edits all survive commit.""" |
| 57 | for name in ("a.py", "b.py", "c.py"): |
| 58 | f = muse_repo / name |
| 59 | f.write_text("initial\n") |
| 60 | |
| 61 | _run(muse_repo, "code", "add", ".") |
| 62 | _run(muse_repo, "commit", "-m", "initial") |
| 63 | |
| 64 | # Stage updated versions |
| 65 | for name in ("a.py", "b.py", "c.py"): |
| 66 | (muse_repo / name).write_text("staged\n") |
| 67 | _run(muse_repo, "code", "add", ".") |
| 68 | |
| 69 | # Write unstaged versions |
| 70 | for name in ("a.py", "b.py", "c.py"): |
| 71 | (muse_repo / name).write_text(f"unstaged {name}\n") |
| 72 | |
| 73 | _run(muse_repo, "commit", "-m", "commit staged versions") |
| 74 | |
| 75 | for name in ("a.py", "b.py", "c.py"): |
| 76 | assert (muse_repo / name).read_text() == f"unstaged {name}\n", ( |
| 77 | f"{name} was overwritten by commit" |
| 78 | ) |
| 79 | |
| 80 | def test_untracked_files_survive_commit(self, muse_repo: pathlib.Path) -> None: |
| 81 | """Untracked files must never be touched by commit (was already true).""" |
| 82 | tracked = muse_repo / "tracked.py" |
| 83 | tracked.write_text("tracked\n") |
| 84 | _run(muse_repo, "code", "add", "tracked.py") |
| 85 | _run(muse_repo, "commit", "-m", "add tracked") |
| 86 | |
| 87 | untracked = muse_repo / "notes.txt" |
| 88 | untracked.write_text("my notes\n") |
| 89 | |
| 90 | tracked.write_text("tracked v2\n") |
| 91 | _run(muse_repo, "code", "add", "tracked.py") |
| 92 | _run(muse_repo, "commit", "-m", "update tracked") |
| 93 | |
| 94 | assert untracked.read_text() == "my notes\n" |
| 95 | |
| 96 | def test_new_untracked_file_not_deleted_by_commit(self, muse_repo: pathlib.Path) -> None: |
| 97 | """A new file created after staging must not be deleted by commit.""" |
| 98 | f = muse_repo / "old.py" |
| 99 | f.write_text("old\n") |
| 100 | _run(muse_repo, "code", "add", "old.py") |
| 101 | _run(muse_repo, "commit", "-m", "add old") |
| 102 | |
| 103 | # Stage another file |
| 104 | g = muse_repo / "new_staged.py" |
| 105 | g.write_text("staged\n") |
| 106 | _run(muse_repo, "code", "add", "new_staged.py") |
| 107 | |
| 108 | # Create a brand-new file AFTER staging — should survive commit |
| 109 | extra = muse_repo / "created_after_stage.py" |
| 110 | extra.write_text("i exist\n") |
| 111 | |
| 112 | _run(muse_repo, "commit", "-m", "commit new_staged") |
| 113 | |
| 114 | assert extra.exists(), "commit deleted a file that was never staged" |
| 115 | assert extra.read_text() == "i exist\n" |
| 116 | |
| 117 | def test_staged_deletion_still_removes_file(self, muse_repo: pathlib.Path) -> None: |
| 118 | """muse rm + commit must still delete the file from disk (regression guard).""" |
| 119 | f = muse_repo / "gone.py" |
| 120 | f.write_text("bye\n") |
| 121 | _run(muse_repo, "code", "add", "gone.py") |
| 122 | _run(muse_repo, "commit", "-m", "add gone") |
| 123 | |
| 124 | _run(muse_repo, "rm", "gone.py") |
| 125 | _run(muse_repo, "commit", "-m", "delete gone") |
| 126 | |
| 127 | assert not f.exists(), "staged deletion did not remove the file" |
| 128 | |
| 129 | def test_working_tree_unchanged_after_first_commit(self, muse_repo: pathlib.Path) -> None: |
| 130 | """On the very first commit, files that were staged are still on disk.""" |
| 131 | f = muse_repo / "hello.py" |
| 132 | f.write_text("hello\n") |
| 133 | _run(muse_repo, "code", "add", "hello.py") |
| 134 | _run(muse_repo, "commit", "-m", "first commit") |
| 135 | |
| 136 | assert f.read_text() == "hello\n" |
File History
1 commit
sha256:2eaa5d95f9d9383498e76947410a26e5a3ba23d182f339910c424cf88fad412b
fix: try fetch/presign before fetch/mpack to avoid Cloudfla…
Sonnet 4.6
patch
6 days ago