test_proposal_symbol_delta.py
file-level
1
files
1
commits
0
hotspots
0
🧊 dead
0
💥 blast risk
| 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" |