gabriel / muse public

test_narrative_symlog_fast_path.py file-level

at sha256:2 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:f feat(branch): reflog copy/rename parity with git, standardize operation… · gabriel · Oct 22, 2026
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