gabriel / muse public
test_phase2b_missed_bfs_sites.py python
242 lines 8.9 KB
Raw
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