test_reorder_only_semver_classification.py
file-level
1
files
1
commits
0
hotspots
0
🧊 dead
0
💥 blast risk
| 1 | """muse#91 — semver classifier flags breaking_changes on a reorder-only diff |
| 2 | despite its own "no semantic change" summary. |
| 3 | |
| 4 | Root cause: `build_diff_ops()` (muse/plugins/code/symbol_diff.py) falls back |
| 5 | to a whole-file ReplaceOp labeled "<path> (reformatted -- no semantic |
| 6 | change)" when every symbol's content_id in a modified file is unchanged -- |
| 7 | e.g. a pure reorder of two top-level functions, or a comment-only edit |
| 8 | (muse#92). `_classify_replace()` (muse/core/semver_classifier.py) pattern- |
| 9 | matches `new_summary` for "renamed to"/"signature"/"implementation" but had |
| 10 | no case for this exact fallback text, so it fell through to the conservative |
| 11 | "unrecognised summary" catch-all and was classified breaking/minor -- |
| 12 | directly contradicting the op's own "no semantic change" claim one field |
| 13 | over. |
| 14 | |
| 15 | Fixed by adding a recognized branch for "no semantic change" in |
| 16 | `_classify_replace()`, classifying it `invisible` (no API impact, no bump). |
| 17 | |
| 18 | Reproduces the ticket's exact repro steps through the real CLI: commit a |
| 19 | file with two functions, commit again with ONLY their order swapped, and |
| 20 | assert the resulting commit's structured_delta reports sem_ver_bump="none" |
| 21 | and breaking_changes=[]. |
| 22 | """ |
| 23 | from __future__ import annotations |
| 24 | |
| 25 | import json |
| 26 | import os |
| 27 | import pathlib |
| 28 | |
| 29 | from tests.cli_test_helper import CliRunner, InvokeResult |
| 30 | |
| 31 | runner = CliRunner() |
| 32 | |
| 33 | |
| 34 | def _run(repo: pathlib.Path, *args: str) -> InvokeResult: |
| 35 | saved = os.getcwd() |
| 36 | try: |
| 37 | os.chdir(repo) |
| 38 | return runner.invoke(None, list(args)) |
| 39 | finally: |
| 40 | os.chdir(saved) |
| 41 | |
| 42 | |
| 43 | def test_reorder_only_diff_is_zero_bump_and_not_breaking(tmp_path: pathlib.Path) -> None: |
| 44 | repo = tmp_path |
| 45 | r = _run(repo, "init") |
| 46 | assert r.exit_code == 0, r.stderr |
| 47 | |
| 48 | (repo / "extra.py").write_text( |
| 49 | "def helper_a(x):\n return x\n\n\ndef helper_b(x):\n return x * 2\n" |
| 50 | ) |
| 51 | r = _run(repo, "code", "add", "extra.py") |
| 52 | assert r.exit_code == 0, r.stderr |
| 53 | r = _run(repo, "commit", "-m", "base") |
| 54 | assert r.exit_code == 0, r.stderr |
| 55 | |
| 56 | # Reorder only -- swap the two functions, no other edits. |
| 57 | (repo / "extra.py").write_text( |
| 58 | "def helper_b(x):\n return x * 2\n\n\ndef helper_a(x):\n return x\n" |
| 59 | ) |
| 60 | r = _run(repo, "code", "add", "extra.py") |
| 61 | assert r.exit_code == 0, r.stderr |
| 62 | r = _run(repo, "commit", "-m", "reorder only") |
| 63 | assert r.exit_code == 0, r.stderr |
| 64 | |
| 65 | r = _run(repo, "read-commit", "HEAD", "--json") |
| 66 | assert r.exit_code == 0, r.stderr |
| 67 | delta = json.loads(r.output)["structured_delta"] |
| 68 | |
| 69 | assert delta["sem_ver_bump"] == "none", ( |
| 70 | f"reorder-only diff must not trigger a version bump: {delta}" |
| 71 | ) |
| 72 | assert delta["breaking_changes"] == [], ( |
| 73 | f"reorder-only diff must not be flagged breaking: {delta}" |
| 74 | ) |