test_narrative_symlog_fast_path.py
file-level
1
files
1
commits
0
hotspots
0
🧊 dead
0
💥 blast risk
| 1 | """muse#53 item 3 -- `muse code narrative` uses the symlog journal as an |
| 2 | index to avoid a full commit-DAG BFS walk, while still reading each |
| 3 | relevant commit's real structured_delta for full narrative fidelity |
| 4 | (impl vs signature distinction, SemVer bumps, breaking-change detection |
| 5 | -- none of which the journal's own coarser classification can produce). |
| 6 | |
| 7 | As with blame's equivalent (muse#53 item 2), output-only assertions |
| 8 | can't distinguish "symlog path used" from "BFS path happened to agree" |
| 9 | -- the pre-existing BFS walk already produces correct narratives on its |
| 10 | own. These tests monkeypatch walk_commits_bfs to prove which mechanism |
| 11 | actually ran. |
| 12 | """ |
| 13 | |
| 14 | from __future__ import annotations |
| 15 | |
| 16 | import json |
| 17 | import pathlib |
| 18 | import textwrap |
| 19 | |
| 20 | import pytest |
| 21 | |
| 22 | from tests.cli_test_helper import CliRunner |
| 23 | |
| 24 | cli = None |
| 25 | runner = CliRunner() |
| 26 | |
| 27 | |
| 28 | @pytest.fixture() |
| 29 | def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path: |
| 30 | monkeypatch.chdir(tmp_path) |
| 31 | monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path)) |
| 32 | r = runner.invoke(cli, ["init", "--domain", "code"]) |
| 33 | assert r.exit_code == 0, r.output |
| 34 | return tmp_path |
| 35 | |
| 36 | |
| 37 | def _write(repo: pathlib.Path, rel: str, content: str) -> None: |
| 38 | (repo / rel).write_text(textwrap.dedent(content)) |
| 39 | |
| 40 | |
| 41 | def _commit(repo: pathlib.Path, msg: str) -> None: |
| 42 | r1 = runner.invoke(cli, ["code", "add", "."]) |
| 43 | assert r1.exit_code == 0, r1.output |
| 44 | r2 = runner.invoke(cli, ["commit", "-m", msg]) |
| 45 | assert r2.exit_code == 0, r2.output |
| 46 | |
| 47 | |
| 48 | CMD = ["code", "narrative"] |
| 49 | |
| 50 | |
| 51 | class TestSymlogFastPathIsActuallyUsed: |
| 52 | def test_narrative_still_works_when_bfs_walk_is_broken( |
| 53 | self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch, |
| 54 | ) -> None: |
| 55 | _write(repo, "billing.py", "def compute_total(items):\n return sum(items)\n") |
| 56 | _commit(repo, "feat: add compute_total") |
| 57 | _write(repo, "billing.py", "def compute_total(items):\n return sum(items) * 2\n") |
| 58 | _commit(repo, "perf: double compute_total") |
| 59 | |
| 60 | import muse.cli.commands.narrative as narrative_mod |
| 61 | |
| 62 | def _broken_bfs(*args: object, **kwargs: object) -> None: |
| 63 | raise AssertionError("walk_commits_bfs must not be called on the symlog fast path") |
| 64 | |
| 65 | monkeypatch.setattr(narrative_mod, "walk_commits_bfs", _broken_bfs) |
| 66 | |
| 67 | r = runner.invoke(cli, CMD + ["billing.py::compute_total", "--json"]) |
| 68 | assert r.exit_code == 0, r.output |
| 69 | data = json.loads(r.output) |
| 70 | assert data["commits_analysed"] >= 2 |
| 71 | |
| 72 | def test_since_flag_falls_back_to_bfs_walk( |
| 73 | self, repo: pathlib.Path, monkeypatch: pytest.MonkeyPatch, |
| 74 | ) -> None: |
| 75 | """--since has no equivalent against the flat journal -- must fall |
| 76 | back to the full BFS walk, same gating as blame's --from.""" |
| 77 | _write(repo, "billing.py", "def compute_total(items):\n return sum(items)\n") |
| 78 | _commit(repo, "feat: add compute_total") |
| 79 | first_head_result = runner.invoke(cli, ["rev-parse", "HEAD", "--json"]) |
| 80 | first_head = json.loads(first_head_result.output)["commit_id"] |
| 81 | _write(repo, "billing.py", "def compute_total(items):\n return sum(items) * 2\n") |
| 82 | _commit(repo, "perf: double compute_total") |
| 83 | |
| 84 | import muse.cli.commands.narrative as narrative_mod |
| 85 | |
| 86 | calls: list[bool] = [] |
| 87 | real_bfs = narrative_mod.walk_commits_bfs |
| 88 | |
| 89 | def _spy_bfs(*args: object, **kwargs: object): |
| 90 | calls.append(True) |
| 91 | return real_bfs(*args, **kwargs) |
| 92 | |
| 93 | monkeypatch.setattr(narrative_mod, "walk_commits_bfs", _spy_bfs) |
| 94 | |
| 95 | r = runner.invoke(cli, CMD + ["billing.py::compute_total", "--since", first_head, "--json"]) |
| 96 | assert r.exit_code == 0, r.output |
| 97 | assert calls, "walk_commits_bfs should have been called for --since" |
| 98 | |
| 99 | |
| 100 | class TestCollectHistoryViaSymlogDirect: |
| 101 | def test_returns_none_when_no_journal_exists(self, repo: pathlib.Path) -> None: |
| 102 | from muse.cli.commands.narrative import _collect_history_via_symlog |
| 103 | |
| 104 | _write(repo, "billing.py", "def compute_total(items):\n return sum(items)\n") |
| 105 | _commit(repo, "feat: add compute_total") |
| 106 | |
| 107 | result = _collect_history_via_symlog(repo, "billing.py::nonexistent", 500) |
| 108 | assert result is None |
| 109 | |
| 110 | def test_impl_and_sig_classification_preserved(self, repo: pathlib.Path) -> None: |
| 111 | """The symlog path must still distinguish impl vs signature changes |
| 112 | -- that fidelity comes from re-reading each commit's structured_delta, |
| 113 | not from the journal's own coarser classification.""" |
| 114 | from muse.cli.commands.narrative import _collect_history_via_symlog |
| 115 | |
| 116 | _write(repo, "billing.py", "def compute_total(items):\n return sum(items)\n") |
| 117 | _commit(repo, "feat: add compute_total") |
| 118 | _write(repo, "billing.py", "def compute_total(items):\n return sum(items) * 2\n") |
| 119 | _commit(repo, "perf: vectorise compute_total body implementation") |
| 120 | _write(repo, "billing.py", 'def compute_total(items, currency="USD"):\n return sum(items) * 2\n') |
| 121 | _commit(repo, "feat: compute_total signature add currency") |
| 122 | |
| 123 | result = _collect_history_via_symlog(repo, "billing.py::compute_total", 500) |
| 124 | assert result is not None |
| 125 | history, commits_analysed, truncated = result |
| 126 | assert history.impl_changes >= 1 |
| 127 | assert history.sig_changes >= 1 |
| 128 | |
| 129 | def test_rename_boundary_crossed_via_born_from(self, repo: pathlib.Path) -> None: |
| 130 | """The journal's born_from chain lets narrative see history across a |
| 131 | rename boundary -- the original fixed-address BFS walk could not.""" |
| 132 | from muse.cli.commands.narrative import _collect_history_via_symlog |
| 133 | |
| 134 | _write(repo, "billing.py", "def compute_total(items):\n return sum(items)\n") |
| 135 | _commit(repo, "feat: add compute_total") |
| 136 | _write(repo, "billing.py", "def compute_invoice_total(items):\n return sum(items)\n") |
| 137 | _commit(repo, "refactor: rename to compute_invoice_total") |
| 138 | |
| 139 | result = _collect_history_via_symlog(repo, "billing.py::compute_invoice_total", 500) |
| 140 | assert result is not None |
| 141 | history, commits_analysed, truncated = result |
| 142 | # The creation event under the OLD address must be visible. |
| 143 | assert history.born_commit is not None |
| 144 | event_types = [e.event_type for e in history.events] |
| 145 | assert "create" in event_types |