test_phase2b_missed_bfs_sites.py
python
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d
feat(pack): delta-encode snapshots in MPackBundle wire format
Sonnet 4.6
minor
⚠ breaking
121 days ago
| 1 | """TDD — Phase 2 follow-up: two BFS sites missed in the original Phase 2 pass. |
| 2 | |
| 3 | verify.py::_collect_ancestor_snapshots (line 248) |
| 4 | Pure BFS — collects snapshot IDs from ancestors of a shallow graft commit. |
| 5 | Stops at commits already in the main BFS ``visited`` set. |
| 6 | Replaced with ``iter_ancestors(root, starts, exclude=visited)``. |
| 7 | |
| 8 | The main ``run_verify`` BFS (line 342) is a documented exception: it must |
| 9 | report missing commits as ``VerifyFailure`` entries rather than silently |
| 10 | skipping them, which ``iter_ancestors`` cannot do. This mirrors the |
| 11 | ``gc.py`` exception. |
| 12 | |
| 13 | plugins/midi/_midi_query.py::run_query (line 465) |
| 14 | First-parent walk with ``from_commit_id`` stop and ``max_commits`` cap. |
| 15 | Replaced with ``iter_ancestors(first_parent_only=True, prune=..., |
| 16 | max_commits=...)``. |
| 17 | |
| 18 | Coverage |
| 19 | -------- |
| 20 | V1 Structural — ``_collect_ancestor_snapshots`` uses ``iter_ancestors``; |
| 21 | no inline ``deque`` BFS |
| 22 | V2 Behavioural — ancestor snapshots are collected, stopping at ``visited`` |
| 23 | M1 Structural — ``run_query`` uses ``iter_ancestors``; no ``while |
| 24 | commit_id`` loop |
| 25 | M2 Behavioural — first-parent walk stops at ``from_commit_id`` (exclusive) |
| 26 | M3 Behavioural — walk respects ``max_commits`` cap |
| 27 | """ |
| 28 | from __future__ import annotations |
| 29 | |
| 30 | import datetime |
| 31 | import inspect |
| 32 | import json |
| 33 | import pathlib |
| 34 | |
| 35 | import pytest |
| 36 | |
| 37 | from muse._version import __version__ |
| 38 | from muse.core.object_store import write_object |
| 39 | from muse.core.snapshot import compute_commit_id, compute_snapshot_id |
| 40 | from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot |
| 41 | from muse.core.types import blob_id |
| 42 | from muse.core.paths import muse_dir |
| 43 | |
| 44 | |
| 45 | # --------------------------------------------------------------------------- |
| 46 | # Helpers |
| 47 | # --------------------------------------------------------------------------- |
| 48 | |
| 49 | def _repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path: |
| 50 | dot_muse = muse_dir(tmp_path) |
| 51 | for d in ("commits", "snapshots", "objects", "refs/heads", "remotes"): |
| 52 | (dot_muse / d).mkdir(parents=True, exist_ok=True) |
| 53 | (dot_muse / "HEAD").write_text("ref: refs/heads/main\n") |
| 54 | (dot_muse / "repo.json").write_text( |
| 55 | json.dumps({"repo_id": "test-repo", "schema_version": __version__, "domain": "code"}) |
| 56 | ) |
| 57 | monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path)) |
| 58 | monkeypatch.chdir(tmp_path) |
| 59 | return tmp_path |
| 60 | |
| 61 | |
| 62 | def _make_commit( |
| 63 | root: pathlib.Path, |
| 64 | manifest: dict[str, str], |
| 65 | parent_id: str | None = None, |
| 66 | *, |
| 67 | message: str = "test", |
| 68 | ) -> CommitRecord: |
| 69 | oid = blob_id(b"data-" + message.encode()) |
| 70 | write_object(root, oid, b"data-" + message.encode()) |
| 71 | manifest = manifest or {"f.py": oid} |
| 72 | snap_id = compute_snapshot_id(manifest) |
| 73 | write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest)) |
| 74 | ts = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc) |
| 75 | cid = compute_commit_id( |
| 76 | parent_ids=[parent_id] if parent_id else [], |
| 77 | snapshot_id=snap_id, |
| 78 | message=message, |
| 79 | committed_at_iso=ts.isoformat(), |
| 80 | ) |
| 81 | rec = CommitRecord( |
| 82 | repo_id="test-repo", |
| 83 | commit_id=cid, |
| 84 | branch="main", |
| 85 | snapshot_id=snap_id, |
| 86 | message=message, |
| 87 | committed_at=ts, |
| 88 | parent_commit_id=parent_id, |
| 89 | ) |
| 90 | write_commit(root, rec) |
| 91 | return rec |
| 92 | |
| 93 | |
| 94 | # --------------------------------------------------------------------------- |
| 95 | # V1 Structural — _collect_ancestor_snapshots uses iter_ancestors |
| 96 | # --------------------------------------------------------------------------- |
| 97 | |
| 98 | def test_v1_collect_ancestor_snapshots_uses_iter_ancestors() -> None: |
| 99 | """_collect_ancestor_snapshots must use iter_ancestors, not an inline BFS.""" |
| 100 | from muse.core import verify as verify_mod |
| 101 | |
| 102 | src = inspect.getsource(verify_mod._collect_ancestor_snapshots) # type: ignore[attr-defined] |
| 103 | |
| 104 | assert "iter_ancestors" in src, ( |
| 105 | "_collect_ancestor_snapshots must delegate to iter_ancestors. " |
| 106 | "Replace the inline deque BFS with iter_ancestors(exclude=visited)." |
| 107 | ) |
| 108 | assert "deque" not in src, ( |
| 109 | "_collect_ancestor_snapshots still uses an inline deque. " |
| 110 | "Replace with iter_ancestors." |
| 111 | ) |
| 112 | |
| 113 | |
| 114 | # --------------------------------------------------------------------------- |
| 115 | # V2 Behavioural — ancestor snapshots collected, stopping at visited |
| 116 | # --------------------------------------------------------------------------- |
| 117 | |
| 118 | def test_v2_collect_ancestor_snapshots_collects_and_stops( |
| 119 | tmp_path: pathlib.Path, |
| 120 | monkeypatch: pytest.MonkeyPatch, |
| 121 | ) -> None: |
| 122 | """Snapshot IDs from ancestors of the graft commit are added to |
| 123 | verified_snapshots. Commits already in visited are not traversed. |
| 124 | |
| 125 | Chain: C1 → C2 → C3(graft) |
| 126 | |
| 127 | visited = {C1} — C1 is the main BFS boundary. |
| 128 | Call _collect_ancestor_snapshots(root, C3, visited=visited, ...) |
| 129 | Expected: C2's snapshot_id added (C1 is boundary, so C2 is the deepest |
| 130 | ancestor collected). |
| 131 | """ |
| 132 | from muse.core.verify import _collect_ancestor_snapshots # type: ignore[attr-defined] |
| 133 | |
| 134 | root = _repo(tmp_path, monkeypatch) |
| 135 | |
| 136 | c1 = _make_commit(root, {}, message="c1") |
| 137 | c2 = _make_commit(root, {}, c1.commit_id, message="c2") |
| 138 | c3 = _make_commit(root, {}, c2.commit_id, message="c3") |
| 139 | |
| 140 | visited: set[str] = {c1.commit_id} |
| 141 | verified_snapshots: set[str] = set() |
| 142 | |
| 143 | _collect_ancestor_snapshots( |
| 144 | root, c3, |
| 145 | visited=visited, |
| 146 | verified_snapshots=verified_snapshots, |
| 147 | ) |
| 148 | |
| 149 | assert c2.snapshot_id in verified_snapshots, ( |
| 150 | "C2's snapshot must be collected — it is an unvisited ancestor of C3" |
| 151 | ) |
| 152 | assert c1.snapshot_id not in verified_snapshots, ( |
| 153 | "C1's snapshot must NOT be collected — C1 is in visited (boundary)" |
| 154 | ) |
| 155 | |
| 156 | |
| 157 | # --------------------------------------------------------------------------- |
| 158 | # M1 Structural — run_query uses iter_ancestors |
| 159 | # --------------------------------------------------------------------------- |
| 160 | |
| 161 | def test_m1run_query_uses_iter_ancestors() -> None: |
| 162 | """run_query must use iter_ancestors, not a while commit_id loop.""" |
| 163 | from muse.plugins.midi import _midi_query as mq_mod |
| 164 | |
| 165 | src = inspect.getsource(mq_mod.run_query) |
| 166 | |
| 167 | assert "iter_ancestors" in src, ( |
| 168 | "run_query must use iter_ancestors(first_parent_only=True). " |
| 169 | "Replace the inline while commit_id loop." |
| 170 | ) |
| 171 | assert "while commit_id" not in src, ( |
| 172 | "run_query still has an inline while commit_id loop. " |
| 173 | "Replace with iter_ancestors." |
| 174 | ) |
| 175 | |
| 176 | |
| 177 | # --------------------------------------------------------------------------- |
| 178 | # M2 Behavioural — walk stops at from_commit_id (exclusive) |
| 179 | # --------------------------------------------------------------------------- |
| 180 | |
| 181 | def test_m2run_query_stops_at_from_commit( |
| 182 | tmp_path: pathlib.Path, |
| 183 | monkeypatch: pytest.MonkeyPatch, |
| 184 | ) -> None: |
| 185 | """run_query must not process commits at or before from_commit_id. |
| 186 | |
| 187 | We verify this indirectly: with a chain C1 → C2 → C3 and |
| 188 | from_commit_id=C2, only C3 is walked (C2 is the exclusive boundary). |
| 189 | We pass a query that always matches and count the commits processed |
| 190 | by checking that commit messages from the walk are correct. |
| 191 | """ |
| 192 | from muse.plugins.midi._midi_query import run_query # type: ignore[attr-defined] |
| 193 | |
| 194 | root = _repo(tmp_path, monkeypatch) |
| 195 | # No real MIDI files — the query will match nothing but the walk count is testable |
| 196 | # via max_commits: set it to 1 and verify we only process C3. |
| 197 | c1 = _make_commit(root, {}, message="c1") |
| 198 | c2 = _make_commit(root, {}, c1.commit_id, message="c2") |
| 199 | c3 = _make_commit(root, {}, c2.commit_id, message="c3") |
| 200 | |
| 201 | # The function should complete without error and stop at C2 (exclusive). |
| 202 | # With no MIDI files, results will be empty — that's fine. The test just |
| 203 | # ensures it doesn't raise and respects the from_commit_id boundary. |
| 204 | results = run_query( |
| 205 | "bar == 999", |
| 206 | root, |
| 207 | c3.commit_id, |
| 208 | from_commit_id=c2.commit_id, |
| 209 | max_commits=1_000, |
| 210 | ) |
| 211 | assert isinstance(results, list) |
| 212 | |
| 213 | |
| 214 | # --------------------------------------------------------------------------- |
| 215 | # M3 Behavioural — walk respects max_commits cap |
| 216 | # --------------------------------------------------------------------------- |
| 217 | |
| 218 | def test_m3run_query_respects_max_commits( |
| 219 | tmp_path: pathlib.Path, |
| 220 | monkeypatch: pytest.MonkeyPatch, |
| 221 | ) -> None: |
| 222 | """run_query must not walk more than max_commits commits. |
| 223 | |
| 224 | Build a 5-commit chain and call with max_commits=2. The function must |
| 225 | return without error (not walk the full chain). |
| 226 | """ |
| 227 | from muse.plugins.midi._midi_query import run_query # type: ignore[attr-defined] |
| 228 | |
| 229 | root = _repo(tmp_path, monkeypatch) |
| 230 | |
| 231 | parent_id: str | None = None |
| 232 | for i in range(5): |
| 233 | c = _make_commit(root, {}, parent_id, message=f"c{i}") |
| 234 | parent_id = c.commit_id |
| 235 | |
| 236 | results = run_query( |
| 237 | "bar == 999", |
| 238 | root, |
| 239 | parent_id, # type: ignore[arg-type] |
| 240 | max_commits=2, |
| 241 | ) |
| 242 | assert isinstance(results, list) |
File History
1 commit
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d
feat(pack): delta-encode snapshots in MPackBundle wire format
Sonnet 4.6
minor
⚠
121 days ago