gabriel / musehub public
test_proposal_symbol_delta.py python
456 lines 15.9 KB
Raw
sha256:3ff9c9863a9891bdcde71b4a43228f66d0493e38b7cc1d09fe9eb7de774046b2 feat: add repair-commit wire endpoint (API parity with repa… Opus 4.8 minor ⚠ breaking 1 day ago
1 """TDD: build_proposal_symbol_delta — structured delta algebra → grouped symbol diff.
2
3 Covers the new `musehub.services.proposal_symbol_delta` module.
4
5 Unit tests — pure function, no DB:
6 test_empty_commits_returns_empty_delta
7 test_insert_op_maps_to_added
8 test_delete_op_maps_to_deleted
9 test_replace_op_maps_to_modified
10 test_mutate_op_maps_to_modified
11 test_patch_op_maps_to_modified
12 test_unknown_op_treated_as_modified
13 test_file_level_op_without_double_colon_excluded
14 test_address_parsed_into_file_and_symbol_name
15 test_symbols_grouped_by_file
16 test_total_count_is_sum_of_all_buckets
17 test_deduplication_last_commit_wins
18 net-op reduction:
19 test_net_op_add_then_modify_stays_added
20 test_net_op_add_then_delete_cancels_to_nothing
21 test_net_op_modify_then_delete_is_deleted
22 test_net_op_deleted_then_insert_is_modified
23 test_multiple_commits_multiple_files
24 test_breaking_changes_flagged_on_matching_entry
25 test_by_file_contains_all_buckets_for_a_file
26 test_by_file_sorts_added_before_modified_before_deleted
27 """
28 from __future__ import annotations
29
30 from datetime import datetime, timezone
31
32 import pytest
33
34 from musehub.db.musehub_repo_models import MusehubCommit
35 from musehub.types.json_types import StrDict
36 from musehub.services.proposal_symbol_delta import (
37 ProposalSymbolDelta,
38 SymbolDeltaEntry,
39 build_proposal_symbol_delta,
40 )
41
42
43 # ---------------------------------------------------------------------------
44 # Helpers
45 # ---------------------------------------------------------------------------
46
47 def _commit(
48 *ops: tuple[str, str], # (op_type, address) e.g. ("insert", "src/a.py::Fn")
49 breaking: list[str] | None = None,
50 timestamp: datetime | None = None,
51 ) -> MusehubCommit:
52 """Return a minimal object mimicking MusehubCommit with structured_delta."""
53 child_ops_by_file: dict[str, list[StrDict]] = {}
54 for op_type, address in ops:
55 file_path = address.split("::")[0]
56 child_ops_by_file.setdefault(file_path, []).append(
57 {"op": op_type, "address": address}
58 )
59
60 structured_delta = {
61 "ops": [
62 {"address": file_path, "child_ops": cops}
63 for file_path, cops in child_ops_by_file.items()
64 ]
65 }
66
67 class _FakeCommit:
68 pass
69
70 c = _FakeCommit()
71 c.structured_delta = structured_delta # type: ignore[attr-defined]
72 c.breaking_changes = breaking or [] # type: ignore[attr-defined]
73 c.timestamp = timestamp or datetime.now(timezone.utc) # type: ignore[attr-defined]
74 return c
75
76
77 def _commit_no_delta(breaking: list[str] | None = None) -> MusehubCommit:
78 class _FakeCommit:
79 pass
80 c = _FakeCommit()
81 c.structured_delta = None # type: ignore[attr-defined]
82 c.breaking_changes = breaking or [] # type: ignore[attr-defined]
83 c.timestamp = datetime.now(timezone.utc) # type: ignore[attr-defined]
84 return c
85
86
87 # ---------------------------------------------------------------------------
88 # Empty / null cases
89 # ---------------------------------------------------------------------------
90
91
92 def test_empty_commits_returns_empty_delta() -> None:
93 delta = build_proposal_symbol_delta([])
94 assert delta.added == []
95 assert delta.modified == []
96 assert delta.deleted == []
97 assert delta.by_file == {}
98 assert delta.total == 0
99
100
101 def test_commit_with_no_structured_delta_skipped() -> None:
102 delta = build_proposal_symbol_delta([_commit_no_delta()])
103 assert delta.total == 0
104
105
106 # ---------------------------------------------------------------------------
107 # Op type mapping
108 # ---------------------------------------------------------------------------
109
110
111 def test_insert_op_maps_to_added() -> None:
112 delta = build_proposal_symbol_delta([
113 _commit(("insert", "src/billing.py::compute_total")),
114 ])
115 assert len(delta.added) == 1
116 assert delta.added[0].address == "src/billing.py::compute_total"
117 assert delta.modified == []
118 assert delta.deleted == []
119
120
121 def test_delete_op_maps_to_deleted() -> None:
122 delta = build_proposal_symbol_delta([
123 _commit(("delete", "src/billing.py::old_compute")),
124 ])
125 assert len(delta.deleted) == 1
126 assert delta.deleted[0].address == "src/billing.py::old_compute"
127 assert delta.added == []
128 assert delta.modified == []
129
130
131 def test_replace_op_maps_to_modified() -> None:
132 delta = build_proposal_symbol_delta([
133 _commit(("replace", "src/auth.py::validate_token")),
134 ])
135 assert len(delta.modified) == 1
136 assert delta.modified[0].address == "src/auth.py::validate_token"
137
138
139 def test_mutate_op_maps_to_modified() -> None:
140 delta = build_proposal_symbol_delta([
141 _commit(("mutate", "src/auth.py::Session")),
142 ])
143 assert len(delta.modified) == 1
144 assert delta.modified[0].address == "src/auth.py::Session"
145
146
147 def test_patch_op_maps_to_modified() -> None:
148 delta = build_proposal_symbol_delta([
149 _commit(("patch", "src/core.py::Router")),
150 ])
151 assert len(delta.modified) == 1
152
153
154 def test_unknown_op_treated_as_modified() -> None:
155 delta = build_proposal_symbol_delta([
156 _commit(("update", "src/x.py::Foo")),
157 ])
158 assert len(delta.modified) == 1
159
160
161 # ---------------------------------------------------------------------------
162 # Address parsing
163 # ---------------------------------------------------------------------------
164
165
166 def test_file_level_op_without_double_colon_excluded() -> None:
167 """A child_op whose address has no '::' is not a symbol — skip it."""
168 class _FakeCommit:
169 structured_delta = {
170 "ops": [
171 {
172 "address": "src/billing.py",
173 "child_ops": [
174 {"op": "insert", "address": "src/billing.py"}, # no ::
175 ],
176 }
177 ]
178 }
179 breaking_changes: list[str] = []
180 timestamp = datetime.now(timezone.utc)
181
182 delta = build_proposal_symbol_delta([_FakeCommit()])
183 assert delta.total == 0
184
185
186 def test_address_parsed_into_file_and_symbol_name() -> None:
187 delta = build_proposal_symbol_delta([
188 _commit(("insert", "src/billing.py::compute_total")),
189 ])
190 entry = delta.added[0]
191 assert entry.file_path == "src/billing.py"
192 assert entry.symbol_name == "compute_total"
193 assert entry.address == "src/billing.py::compute_total"
194
195
196 def test_address_with_multiple_colons_handled() -> None:
197 """CSS pseudo-selector addresses like 'file.scss::--merged:hover' split on first '::'."""
198 delta = build_proposal_symbol_delta([
199 _commit(("delete", "src/components/_x.scss::--merged:hover")),
200 ])
201 assert delta.deleted[0].file_path == "src/components/_x.scss"
202 assert delta.deleted[0].symbol_name == "--merged:hover"
203
204
205 # ---------------------------------------------------------------------------
206 # Grouping
207 # ---------------------------------------------------------------------------
208
209
210 def test_symbols_grouped_by_file() -> None:
211 delta = build_proposal_symbol_delta([
212 _commit(
213 ("insert", "src/billing.py::compute_total"),
214 ("replace", "src/auth.py::validate_token"),
215 ("delete", "src/billing.py::old_compute"),
216 ),
217 ])
218 assert "src/billing.py" in delta.by_file
219 assert "src/auth.py" in delta.by_file
220
221 billing = delta.by_file["src/billing.py"]
222 billing_addrs = {e.address for e in billing}
223 assert "src/billing.py::compute_total" in billing_addrs
224 assert "src/billing.py::old_compute" in billing_addrs
225
226 auth = delta.by_file["src/auth.py"]
227 assert auth[0].address == "src/auth.py::validate_token"
228
229
230 def test_by_file_sorts_added_before_modified_before_deleted() -> None:
231 delta = build_proposal_symbol_delta([
232 _commit(
233 ("delete", "src/a.py::removed"),
234 ("replace", "src/a.py::changed"),
235 ("insert", "src/a.py::new_fn"),
236 ),
237 ])
238 entries = delta.by_file["src/a.py"]
239 ops = [e.net_op for e in entries]
240 # added first, then modified, then deleted
241 assert ops.index("added") < ops.index("modified")
242 assert ops.index("modified") < ops.index("deleted")
243
244
245 def test_total_count_is_sum_of_all_buckets() -> None:
246 delta = build_proposal_symbol_delta([
247 _commit(
248 ("insert", "src/a.py::Fn1"),
249 ("insert", "src/a.py::Fn2"),
250 ("replace", "src/b.py::Fn3"),
251 ("delete", "src/c.py::Fn4"),
252 ),
253 ])
254 assert delta.total == 4
255 assert len(delta.added) == 2
256 assert len(delta.modified) == 1
257 assert len(delta.deleted) == 1
258
259
260 # ---------------------------------------------------------------------------
261 # Deduplication
262 # ---------------------------------------------------------------------------
263
264
265 def test_deduplication_last_commit_wins() -> None:
266 """Same symbol in two commits → single entry with the net op."""
267 t1 = datetime(2026, 1, 1, tzinfo=timezone.utc)
268 t2 = datetime(2026, 1, 2, tzinfo=timezone.utc)
269 delta = build_proposal_symbol_delta([
270 _commit(("replace", "src/a.py::Fn"), timestamp=t1),
271 _commit(("replace", "src/a.py::Fn"), timestamp=t2),
272 ])
273 assert delta.total == 1
274 assert len(delta.modified) == 1
275
276
277 # ---------------------------------------------------------------------------
278 # Net-op reduction
279 # ---------------------------------------------------------------------------
280
281
282 def test_net_op_add_then_modify_stays_added() -> None:
283 """insert in c1, replace in c2 → still 'added' (new to this branch)."""
284 t1 = datetime(2026, 1, 1, tzinfo=timezone.utc)
285 t2 = datetime(2026, 1, 2, tzinfo=timezone.utc)
286 delta = build_proposal_symbol_delta([
287 _commit(("insert", "src/a.py::Fn"), timestamp=t1),
288 _commit(("replace", "src/a.py::Fn"), timestamp=t2),
289 ])
290 assert len(delta.added) == 1
291 assert delta.modified == []
292
293
294 def test_net_op_add_then_delete_cancels_to_nothing() -> None:
295 """insert in c1, delete in c2 → net zero, symbol not in any bucket."""
296 t1 = datetime(2026, 1, 1, tzinfo=timezone.utc)
297 t2 = datetime(2026, 1, 2, tzinfo=timezone.utc)
298 delta = build_proposal_symbol_delta([
299 _commit(("insert", "src/a.py::Fn"), timestamp=t1),
300 _commit(("delete", "src/a.py::Fn"), timestamp=t2),
301 ])
302 assert delta.total == 0
303
304
305 def test_net_op_modify_then_delete_is_deleted() -> None:
306 t1 = datetime(2026, 1, 1, tzinfo=timezone.utc)
307 t2 = datetime(2026, 1, 2, tzinfo=timezone.utc)
308 delta = build_proposal_symbol_delta([
309 _commit(("replace", "src/a.py::Fn"), timestamp=t1),
310 _commit(("delete", "src/a.py::Fn"), timestamp=t2),
311 ])
312 assert len(delta.deleted) == 1
313 assert delta.added == []
314 assert delta.modified == []
315
316
317 def test_net_op_deleted_then_insert_is_modified() -> None:
318 """delete in c1, insert in c2 → 'modified' (symbol re-added after removal)."""
319 t1 = datetime(2026, 1, 1, tzinfo=timezone.utc)
320 t2 = datetime(2026, 1, 2, tzinfo=timezone.utc)
321 delta = build_proposal_symbol_delta([
322 _commit(("delete", "src/a.py::Fn"), timestamp=t1),
323 _commit(("insert", "src/a.py::Fn"), timestamp=t2),
324 ])
325 assert len(delta.modified) == 1
326 assert delta.added == []
327 assert delta.deleted == []
328
329
330 # ---------------------------------------------------------------------------
331 # Multi-commit, multi-file
332 # ---------------------------------------------------------------------------
333
334
335 def test_multiple_commits_multiple_files() -> None:
336 t1 = datetime(2026, 1, 1, tzinfo=timezone.utc)
337 t2 = datetime(2026, 1, 2, tzinfo=timezone.utc)
338 t3 = datetime(2026, 1, 3, tzinfo=timezone.utc)
339 delta = build_proposal_symbol_delta([
340 _commit(("insert", "src/a.py::FnA"), ("replace", "src/b.py::FnB"), timestamp=t1),
341 _commit(("insert", "src/c.py::FnC"), timestamp=t2),
342 _commit(("delete", "src/b.py::FnB"), timestamp=t3),
343 ])
344 assert len(delta.added) == 2 # FnA, FnC
345 assert len(delta.deleted) == 1 # FnB (modified then deleted)
346 assert delta.modified == []
347 assert "src/a.py" in delta.by_file
348 assert "src/b.py" in delta.by_file
349 assert "src/c.py" in delta.by_file
350
351
352 # ---------------------------------------------------------------------------
353 # Breaking changes
354 # ---------------------------------------------------------------------------
355
356
357 def test_breaking_changes_flagged_on_matching_entry() -> None:
358 delta = build_proposal_symbol_delta(
359 [_commit(("delete", "src/api.py::public_fn"))],
360 breaking_changes=["src/api.py::public_fn"],
361 )
362 assert delta.deleted[0].is_breaking is True
363
364
365 def test_non_breaking_entry_not_flagged() -> None:
366 delta = build_proposal_symbol_delta(
367 [_commit(("replace", "src/api.py::internal_fn"))],
368 breaking_changes=["src/api.py::public_fn"],
369 )
370 assert delta.modified[0].is_breaking is False
371
372
373 def test_breaking_changes_collected_from_commits() -> None:
374 """Breaking changes embedded on commit records are surfaced."""
375 c = _commit(("delete", "src/api.py::old_fn"), breaking=["src/api.py::old_fn"])
376 delta = build_proposal_symbol_delta([c])
377 assert delta.deleted[0].is_breaking is True
378
379
380 def test_breaking_flag_propagates_through_net_op_reduction() -> None:
381 """A symbol flagged breaking in any commit stays breaking in net result."""
382 t1 = datetime(2026, 1, 1, tzinfo=timezone.utc)
383 t2 = datetime(2026, 1, 2, tzinfo=timezone.utc)
384 c1 = _commit(("replace", "src/a.py::Fn"), breaking=["src/a.py::Fn"], timestamp=t1)
385 c2 = _commit(("replace", "src/a.py::Fn"), timestamp=t2)
386 delta = build_proposal_symbol_delta([c1, c2])
387 assert delta.modified[0].is_breaking is True
388
389
390 # ---------------------------------------------------------------------------
391 # Return type contract
392 # ---------------------------------------------------------------------------
393
394
395 def test_returns_proposal_symbol_delta_instance() -> None:
396 result = build_proposal_symbol_delta([])
397 assert isinstance(result, ProposalSymbolDelta)
398
399
400 def test_entries_are_symbol_delta_entry_instances() -> None:
401 delta = build_proposal_symbol_delta([
402 _commit(("insert", "src/a.py::Fn")),
403 ])
404 assert isinstance(delta.added[0], SymbolDeltaEntry)
405
406
407 # ---------------------------------------------------------------------------
408 # Real delta shape — top-level symbol ops (no parent wrapper)
409 # ---------------------------------------------------------------------------
410
411
412 def test_top_level_symbol_op_no_parent_wrapper() -> None:
413 """Real Muse deltas put symbol ops directly in ops[], not only in child_ops."""
414 class _FakeCommit:
415 structured_delta = {
416 "ops": [
417 {"op": "replace", "address": "src/billing.py::compute_total"},
418 {"op": "insert", "address": "src/billing.py::invoice_line"},
419 {"op": "delete", "address": "src/legacy.py::old_fn"},
420 ]
421 }
422 breaking_changes: list[str] = []
423 timestamp = datetime(2026, 1, 1, tzinfo=timezone.utc)
424
425 delta = build_proposal_symbol_delta([_FakeCommit()])
426 assert len(delta.modified) == 1
427 assert delta.modified[0].address == "src/billing.py::compute_total"
428 assert len(delta.added) == 1
429 assert delta.added[0].address == "src/billing.py::invoice_line"
430 assert len(delta.deleted) == 1
431 assert delta.deleted[0].address == "src/legacy.py::old_fn"
432
433
434 def test_mixed_top_level_and_child_ops() -> None:
435 """Top-level symbol ops and child_ops inside patch ops both processed."""
436 class _FakeCommit:
437 structured_delta = {
438 "ops": [
439 {"op": "replace", "address": "src/auth.py::validate"},
440 {
441 "op": "patch",
442 "address": "src/billing.py",
443 "child_ops": [
444 {"op": "insert", "address": "src/billing.py::new_fn"},
445 ],
446 },
447 ]
448 }
449 breaking_changes: list[str] = []
450 timestamp = datetime(2026, 1, 1, tzinfo=timezone.utc)
451
452 delta = build_proposal_symbol_delta([_FakeCommit()])
453 assert len(delta.modified) == 1
454 assert delta.modified[0].address == "src/auth.py::validate"
455 assert len(delta.added) == 1
456 assert delta.added[0].address == "src/billing.py::new_fn"
File History 1 commit
sha256:3ff9c9863a9891bdcde71b4a43228f66d0493e38b7cc1d09fe9eb7de774046b2 feat: add repair-commit wire endpoint (API parity with repa… Opus 4.8 minor 1 day ago