"""Tests for muse bisect — commit-level and symbol-scoped binary search.""" from __future__ import annotations import datetime import json import pathlib import textwrap import pytest from tests.cli_test_helper import CliRunner from muse.core.commits import CommitDict from muse.domain import InsertOp, PatchOp, ReplaceOp, StructuredDelta from muse.core.paths import ref_path, repo_json_path cli = None # argparse migration — CliRunner ignores this arg runner = CliRunner() # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @pytest.fixture def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path: """Minimal code-domain Muse repo.""" monkeypatch.chdir(tmp_path) monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path)) r = runner.invoke(cli, ["init", "--domain", "code"]) assert r.exit_code == 0, r.output return tmp_path @pytest.fixture def linear_repo(repo: pathlib.Path) -> pathlib.Path: """Repo with 4 commits: c0 (good) → c1 → c2 → c3 (bad). billing.py::compute is added in c0 and modified in c1 and c3. """ work = repo # c0 — genesis (work / "billing.py").write_text(textwrap.dedent("""\ def compute(items): return sum(items) """)) runner.invoke(cli, ["code", "add", "."]) r = runner.invoke(cli, ["commit", "-m", "c0: add compute"]) assert r.exit_code == 0, r.output # c1 — modify compute (work / "billing.py").write_text(textwrap.dedent("""\ def compute(items, discount=0.0): return sum(items) * (1 - discount) """)) runner.invoke(cli, ["code", "add", "."]) r = runner.invoke(cli, ["commit", "-m", "c1: add discount param"]) assert r.exit_code == 0, r.output # c2 — unrelated file (work / "utils.py").write_text("def noop(): pass\n") runner.invoke(cli, ["code", "add", "."]) r = runner.invoke(cli, ["commit", "-m", "c2: add utils"]) assert r.exit_code == 0, r.output # c3 — break compute (work / "billing.py").write_text(textwrap.dedent("""\ def compute(items, discount=0.0, tax=0.0): return sum(items) * (1 - discount) + tax """)) runner.invoke(cli, ["code", "add", "."]) r = runner.invoke(cli, ["commit", "-m", "c3: add tax param (bad)"]) assert r.exit_code == 0, r.output return repo def _commit_ids(repo: pathlib.Path) -> list[str]: """Return all commit IDs oldest-first.""" repo_json = json.loads((repo_json_path(repo)).read_text()) repo_id = repo_json["repo_id"] from muse.core.refs import read_current_branch from muse.core.commits import resolve_commit_ref from muse.plugins.code._query import walk_commits_bfs branch = read_current_branch(repo) head = resolve_commit_ref(repo, branch, None) assert head is not None commits, _ = walk_commits_bfs(repo, head.commit_id) # Return oldest-first return [c.commit_id for c in reversed(commits)] # --------------------------------------------------------------------------- # Core engine tests # --------------------------------------------------------------------------- class TestBisectEngine: """Tests for muse.core.bisect internals.""" def test_commits_touching_symbol_filters_correctly( self, linear_repo: pathlib.Path ) -> None: from muse.core.bisect import _commits_touching_symbol all_ids = _commit_ids(linear_repo) # billing.py::compute was touched in c1 and c3 (not c0 genesis, not c2 utils) touching = _commits_touching_symbol( linear_repo, all_ids, "billing.py::compute" ) assert len(touching) >= 1 # Every returned commit must have a structured_delta op for this symbol from muse.core.commits import read_commit from muse.plugins.code._query import flat_symbol_ops for cid in touching: commit = read_commit(linear_repo, cid) assert commit is not None and commit.structured_delta is not None addrs = [op["address"] for op in flat_symbol_ops(commit.structured_delta["ops"])] assert "billing.py::compute" in addrs def test_commits_touching_symbol_excludes_genesis( self, linear_repo: pathlib.Path ) -> None: from muse.core.bisect import _commits_touching_symbol from muse.core.commits import read_commit all_ids = _commit_ids(linear_repo) # Genesis commit has no structured_delta → must be excluded genesis_id = all_ids[0] genesis = read_commit(linear_repo, genesis_id) assert genesis is not None assert genesis.structured_delta is None or genesis.parent_commit_id is None touching = _commits_touching_symbol( linear_repo, all_ids, "billing.py::compute" ) assert genesis_id not in touching def test_commits_touching_symbol_excludes_unrelated( self, linear_repo: pathlib.Path ) -> None: from muse.core.bisect import _commits_touching_symbol all_ids = _commit_ids(linear_repo) # c2 only added utils.py::noop; must not appear for billing.py::compute touching = _commits_touching_symbol( linear_repo, all_ids, "billing.py::compute" ) touching_set = set(touching) from muse.core.commits import read_commit for cid in all_ids: commit = read_commit(linear_repo, cid) if commit is None or commit.message != "c2: add utils": continue assert cid not in touching_set, "c2 (utils only) must not touch billing.py::compute" def test_addr_matches_exact(self) -> None: from muse.core.bisect import _addr_matches assert _addr_matches("billing.py::Invoice.compute", "billing.py::Invoice.compute") assert not _addr_matches("billing.py::Invoice.apply", "billing.py::Invoice.compute") def test_addr_matches_class_prefix(self) -> None: from muse.core.bisect import _addr_matches # Prefix matching: filtering by class matches all its methods assert _addr_matches("billing.py::Invoice.compute", "billing.py::Invoice") assert _addr_matches("billing.py::Invoice.apply_discount", "billing.py::Invoice") assert not _addr_matches("billing.py::OtherClass.method", "billing.py::Invoice") def test_symbol_ops_in_commit_returns_descriptions( self, linear_repo: pathlib.Path ) -> None: from muse.core.bisect import _symbol_ops_in_commit all_ids = _commit_ids(linear_repo) # Find a commit that touched billing.py::compute from muse.core.commits import read_commit from muse.plugins.code._query import flat_symbol_ops for cid in all_ids: commit = read_commit(linear_repo, cid) if commit is None or commit.structured_delta is None: continue addrs = [op["address"] for op in flat_symbol_ops(commit.structured_delta["ops"])] if "billing.py::compute" in addrs: descriptions = _symbol_ops_in_commit(linear_repo, cid, "billing.py::compute") assert len(descriptions) >= 1 # Each description contains the address assert any("billing.py::compute" in d for d in descriptions) break def test_symbol_ops_in_commit_empty_for_unrelated( self, linear_repo: pathlib.Path ) -> None: from muse.core.bisect import _symbol_ops_in_commit from muse.core.commits import read_commit all_ids = _commit_ids(linear_repo) for cid in all_ids: commit = read_commit(linear_repo, cid) if commit is not None and commit.message == "c2: add utils": descriptions = _symbol_ops_in_commit(linear_repo, cid, "billing.py::compute") assert descriptions == [] break def test_start_bisect_builds_remaining(self, linear_repo: pathlib.Path) -> None: from muse.core.bisect import start_bisect all_ids = _commit_ids(linear_repo) bad_id = all_ids[-1] # c3 good_id = all_ids[0] # c0 result = start_bisect(linear_repo, bad_id, [good_id]) assert not result.done assert result.next_to_test is not None assert result.remaining_count >= 1 def test_start_bisect_with_symbol_filter_reduces_remaining( self, linear_repo: pathlib.Path ) -> None: from muse.core.bisect import start_bisect, _commits_touching_symbol all_ids = _commit_ids(linear_repo) bad_id = all_ids[-1] good_id = all_ids[0] result_plain = start_bisect(linear_repo, bad_id, [good_id]) result_sym = start_bisect(linear_repo, bad_id, [good_id], symbol_filter="billing.py::compute") # Symbol-scoped bisect must have <= remaining commits than unfiltered assert result_sym.remaining_count <= result_plain.remaining_count def test_start_bisect_symbol_filter_persisted( self, linear_repo: pathlib.Path ) -> None: from muse.core.bisect import start_bisect, _load_state all_ids = _commit_ids(linear_repo) start_bisect(linear_repo, all_ids[-1], [all_ids[0]], symbol_filter="billing.py::compute") state = _load_state(linear_repo) assert state is not None assert state.get("symbol_filter") == "billing.py::compute" def test_start_bisect_symbol_filter_populates_symbol_changes( self, linear_repo: pathlib.Path ) -> None: from muse.core.bisect import start_bisect all_ids = _commit_ids(linear_repo) result = start_bisect( linear_repo, all_ids[-1], [all_ids[0]], symbol_filter="billing.py::compute" ) if not result.done and result.next_to_test: # symbol_changes should be populated (non-empty) because next_to_test # touches billing.py::compute (it's in the filtered set) assert isinstance(result.symbol_changes, list) def test_apply_verdict_preserves_symbol_filter( self, linear_repo: pathlib.Path ) -> None: from muse.core.bisect import start_bisect, mark_good, _load_state all_ids = _commit_ids(linear_repo) result = start_bisect( linear_repo, all_ids[-1], [all_ids[0]], symbol_filter="billing.py::compute" ) if result.done or result.next_to_test is None: return # too few commits, skip mark_good(linear_repo, result.next_to_test) state = _load_state(linear_repo) assert state is not None assert state.get("symbol_filter") == "billing.py::compute" def test_bisect_converges_to_first_bad(self, linear_repo: pathlib.Path) -> None: from muse.core.bisect import start_bisect, mark_bad, mark_good all_ids = _commit_ids(linear_repo) bad_id = all_ids[-1] good_id = all_ids[0] result = start_bisect(linear_repo, bad_id, [good_id]) max_steps = 20 # safety ceiling for _ in range(max_steps): if result.done: break assert result.next_to_test is not None # Simplified oracle: all commits after c0 are "bad" in our fixture result = mark_bad(linear_repo, result.next_to_test) assert result.done assert result.first_bad is not None def test_skip_commit_removes_from_remaining(self, linear_repo: pathlib.Path) -> None: from muse.core.bisect import start_bisect, skip_commit, _load_state all_ids = _commit_ids(linear_repo) result = start_bisect(linear_repo, all_ids[-1], [all_ids[0]]) if result.done or result.next_to_test is None: return skip_target = result.next_to_test skip_commit(linear_repo, skip_target) state = _load_state(linear_repo) assert state is not None assert skip_target not in state.get("remaining", []) def test_reset_clears_state(self, linear_repo: pathlib.Path) -> None: from muse.core.bisect import start_bisect, reset_bisect, is_bisect_active all_ids = _commit_ids(linear_repo) start_bisect(linear_repo, all_ids[-1], [all_ids[0]]) assert is_bisect_active(linear_repo) reset_bisect(linear_repo) assert not is_bisect_active(linear_repo) # --------------------------------------------------------------------------- # CLI tests — baseline muse bisect commands # --------------------------------------------------------------------------- class TestBisectCLI: """End-to-end CLI tests for muse bisect.""" def test_bisect_start_exits_zero(self, linear_repo: pathlib.Path) -> None: ids = _commit_ids(linear_repo) r = runner.invoke(cli, ["bisect", "start", "--bad", ids[-1], "--good", ids[0]]) assert r.exit_code == 0, r.output # Clean up runner.invoke(cli, ["bisect", "reset"]) def test_bisect_start_shows_next_to_test(self, linear_repo: pathlib.Path) -> None: ids = _commit_ids(linear_repo) r = runner.invoke(cli, ["bisect", "start", "--bad", ids[-1], "--good", ids[0]]) assert r.exit_code == 0, r.output assert "Next to test" in r.output or "First bad commit" in r.output runner.invoke(cli, ["bisect", "reset"]) def test_bisect_start_requires_good(self, linear_repo: pathlib.Path) -> None: ids = _commit_ids(linear_repo) r = runner.invoke(cli, ["bisect", "start", "--bad", ids[-1]]) assert r.exit_code != 0 def test_bisect_bad_good_cycle(self, linear_repo: pathlib.Path) -> None: ids = _commit_ids(linear_repo) runner.invoke(cli, ["bisect", "start", "--bad", ids[-1], "--good", ids[0]]) # Mark the midpoint as bad, keep narrowing from muse.core.bisect import _load_state state = _load_state(linear_repo) assert state is not None remaining = state.get("remaining", []) if remaining: mid = remaining[len(remaining) // 2] r = runner.invoke(cli, ["bisect", "bad", mid]) assert r.exit_code == 0, r.output runner.invoke(cli, ["bisect", "reset"]) def test_bisect_commands_require_active_session(self, repo: pathlib.Path) -> None: for sub in ("bad", "good", "skip"): r = runner.invoke(cli, ["bisect", sub]) assert r.exit_code != 0 def test_bisect_double_start_fails(self, linear_repo: pathlib.Path) -> None: ids = _commit_ids(linear_repo) runner.invoke(cli, ["bisect", "start", "--bad", ids[-1], "--good", ids[0]]) r = runner.invoke(cli, ["bisect", "start", "--bad", ids[-1], "--good", ids[0]]) assert r.exit_code != 0 runner.invoke(cli, ["bisect", "reset"]) def test_bisect_log_shows_entries(self, linear_repo: pathlib.Path) -> None: ids = _commit_ids(linear_repo) runner.invoke(cli, ["bisect", "start", "--bad", ids[-1], "--good", ids[0]]) r = runner.invoke(cli, ["bisect", "log"]) assert r.exit_code == 0, r.output assert "bad" in r.output or "good" in r.output runner.invoke(cli, ["bisect", "reset"]) def test_bisect_log_empty_without_session(self, repo: pathlib.Path) -> None: r = runner.invoke(cli, ["bisect", "log"]) assert r.exit_code == 0 assert "No bisect log" in r.output or r.output.strip() == "" def test_bisect_reset_exits_zero(self, linear_repo: pathlib.Path) -> None: ids = _commit_ids(linear_repo) runner.invoke(cli, ["bisect", "start", "--bad", ids[-1], "--good", ids[0]]) r = runner.invoke(cli, ["bisect", "reset"]) assert r.exit_code == 0, r.output def test_bisect_reset_clears_active(self, linear_repo: pathlib.Path) -> None: ids = _commit_ids(linear_repo) runner.invoke(cli, ["bisect", "start", "--bad", ids[-1], "--good", ids[0]]) runner.invoke(cli, ["bisect", "reset"]) from muse.core.bisect import is_bisect_active assert not is_bisect_active(linear_repo) def test_bisect_run_command_auto_bisect(self, linear_repo: pathlib.Path) -> None: """Auto-bisect with a command that always says good — terminates cleanly.""" ids = _commit_ids(linear_repo) runner.invoke(cli, ["bisect", "start", "--bad", ids[-1], "--good", ids[0]]) # "exit 0" always → all commits good → bad_id is first bad immediately r = runner.invoke(cli, ["bisect", "run", "true"]) assert r.exit_code == 0, r.output assert "First bad commit" in r.output or "complete" in r.output.lower() runner.invoke(cli, ["bisect", "reset"]) def test_bisect_requires_repo( self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.chdir(tmp_path) r = runner.invoke(cli, ["bisect", "start", "--bad", "abc", "--good", "def"]) assert r.exit_code != 0 # --------------------------------------------------------------------------- # CLI tests — --symbol flag # --------------------------------------------------------------------------- class TestBisectSymbol: """Tests for the --symbol scoping feature on muse bisect start.""" def test_symbol_start_exits_zero(self, linear_repo: pathlib.Path) -> None: ids = _commit_ids(linear_repo) r = runner.invoke(cli, [ "bisect", "start", "--bad", ids[-1], "--good", ids[0], "--symbol", "billing.py::compute", ]) assert r.exit_code == 0, r.output runner.invoke(cli, ["bisect", "reset"]) def test_symbol_start_shows_session_started(self, linear_repo: pathlib.Path) -> None: ids = _commit_ids(linear_repo) r = runner.invoke(cli, [ "bisect", "start", "--bad", ids[-1], "--good", ids[0], "--symbol", "billing.py::compute", ]) assert "Bisect session started" in r.output assert "billing.py::compute" in r.output runner.invoke(cli, ["bisect", "reset"]) def test_symbol_reduces_remaining_vs_plain(self, linear_repo: pathlib.Path) -> None: """Symbol-scoped bisect must have fewer or equal remaining commits.""" ids = _commit_ids(linear_repo) runner.invoke(cli, ["bisect", "start", "--bad", ids[-1], "--good", ids[0]]) from muse.core.bisect import _load_state plain_state = _load_state(linear_repo) plain_remaining = len(plain_state.get("remaining", [])) if plain_state else 0 runner.invoke(cli, ["bisect", "reset"]) runner.invoke(cli, [ "bisect", "start", "--bad", ids[-1], "--good", ids[0], "--symbol", "billing.py::compute", ]) sym_state = _load_state(linear_repo) sym_remaining = len(sym_state.get("remaining", [])) if sym_state else 0 runner.invoke(cli, ["bisect", "reset"]) assert sym_remaining <= plain_remaining def test_symbol_filter_persisted_in_state(self, linear_repo: pathlib.Path) -> None: ids = _commit_ids(linear_repo) runner.invoke(cli, [ "bisect", "start", "--bad", ids[-1], "--good", ids[0], "--symbol", "billing.py::compute", ]) from muse.core.bisect import _load_state state = _load_state(linear_repo) assert state is not None assert state.get("symbol_filter") == "billing.py::compute" runner.invoke(cli, ["bisect", "reset"]) def test_symbol_filter_persisted_after_mark_good(self, linear_repo: pathlib.Path) -> None: """symbol_filter must survive a mark-good verdict.""" ids = _commit_ids(linear_repo) runner.invoke(cli, [ "bisect", "start", "--bad", ids[-1], "--good", ids[0], "--symbol", "billing.py::compute", ]) from muse.core.bisect import _load_state state = _load_state(linear_repo) if state and state.get("remaining"): mid = state["remaining"][len(state["remaining"]) // 2] runner.invoke(cli, ["bisect", "good", mid]) state2 = _load_state(linear_repo) if state2: assert state2.get("symbol_filter") == "billing.py::compute" runner.invoke(cli, ["bisect", "reset"]) def test_symbol_shows_changes_in_output(self, linear_repo: pathlib.Path) -> None: """When next_to_test touches the symbol, output shows symbol changes.""" ids = _commit_ids(linear_repo) r = runner.invoke(cli, [ "bisect", "start", "--bad", ids[-1], "--good", ids[0], "--symbol", "billing.py::compute", ]) assert r.exit_code == 0, r.output # If there are commits to test, the output should mention the symbol # or at least the next-to-test prompt. assert "Next to test" in r.output or "First bad commit" in r.output runner.invoke(cli, ["bisect", "reset"]) def test_symbol_invalid_no_double_colon(self, linear_repo: pathlib.Path) -> None: """--symbol without '::' is an invalid address and must be rejected.""" ids = _commit_ids(linear_repo) r = runner.invoke(cli, [ "bisect", "start", "--bad", ids[-1], "--good", ids[0], "--symbol", "billing.py", ]) assert r.exit_code != 0 def test_symbol_too_long_rejected(self, linear_repo: pathlib.Path) -> None: """--symbol longer than 500 chars must be rejected.""" ids = _commit_ids(linear_repo) long_addr = f"billing.py::{'x' * 500}" r = runner.invoke(cli, [ "bisect", "start", "--bad", ids[-1], "--good", ids[0], "--symbol", long_addr, ]) assert r.exit_code != 0 def test_symbol_nonexistent_warns(self, linear_repo: pathlib.Path) -> None: """--symbol with no matching commits shows a warning, exits zero.""" ids = _commit_ids(linear_repo) r = runner.invoke(cli, [ "bisect", "start", "--bad", ids[-1], "--good", ids[0], "--symbol", "billing.py::nonexistent_symbol_xyz", ]) assert r.exit_code == 0 # Should warn that no commits matched assert "No commits" in r.output or "First bad commit" in r.output runner.invoke(cli, ["bisect", "reset"]) def test_symbol_class_prefix_matches_methods(self, repo: pathlib.Path) -> None: """--symbol billing.py::Invoice matches Invoice.compute and Invoice.apply.""" # Build a repo with a class that has methods. (repo / "billing.py").write_text(textwrap.dedent("""\ class Invoice: def compute(self, items): return sum(items) def apply_discount(self, total, pct): return total * (1 - pct) """)) runner.invoke(cli, ["code", "add", "."]) r = runner.invoke(cli, ["commit", "-m", "add Invoice class"]) assert r.exit_code == 0, r.output (repo / "billing.py").write_text(textwrap.dedent("""\ class Invoice: def compute(self, items, discount=0.0): return sum(items) * (1 - discount) def apply_discount(self, total, pct): return total * (1 - pct) """)) runner.invoke(cli, ["code", "add", "."]) r = runner.invoke(cli, ["commit", "-m", "add discount to compute"]) assert r.exit_code == 0, r.output from muse.core.bisect import _commits_touching_symbol ids = _commit_ids(repo) # Filter by class prefix — should include the commit touching Invoice.compute touching_class = _commits_touching_symbol(repo, ids, "billing.py::Invoice") touching_method = _commits_touching_symbol(repo, ids, "billing.py::Invoice.compute") # Class prefix must be a superset assert set(touching_method) <= set(touching_class) def test_symbol_with_run_command(self, linear_repo: pathlib.Path) -> None: """bisect run with --symbol filter uses symbol-narrowed remaining list.""" ids = _commit_ids(linear_repo) runner.invoke(cli, [ "bisect", "start", "--bad", ids[-1], "--good", ids[0], "--symbol", "billing.py::compute", ]) # Use "true" (always exits 0 = good) to drive the bisect to completion. r = runner.invoke(cli, ["bisect", "run", "true"]) assert r.exit_code == 0, r.output assert "First bad commit" in r.output or "complete" in r.output.lower() runner.invoke(cli, ["bisect", "reset"]) def test_symbol_state_roundtrip(self, linear_repo: pathlib.Path) -> None: """Symbol filter survives save → load roundtrip through TOML.""" from muse.core.bisect import _save_state, _load_state, BisectStateDict state: BisectStateDict = { "bad_id": "ab" * 32, "good_ids": ["cd" * 32], "skipped_ids": [], "remaining": [], "log": [], "symbol_filter": "billing.py::Invoice.compute_total", } _save_state(linear_repo, state) loaded = _load_state(linear_repo) assert loaded is not None assert loaded.get("symbol_filter") == "billing.py::Invoice.compute_total" def test_symbol_filter_special_chars_survive_toml( self, linear_repo: pathlib.Path ) -> None: """Symbol addresses with dots survive TOML serialisation.""" from muse.core.bisect import _save_state, _load_state, BisectStateDict addr = 'billing.py::Invoice.compute_total' state: BisectStateDict = { "bad_id": "ab" * 32, "good_ids": ["cd" * 32], "skipped_ids": [], "remaining": [], "log": [], "symbol_filter": addr, } _save_state(linear_repo, state) loaded = _load_state(linear_repo) assert loaded is not None assert loaded.get("symbol_filter") == addr def test_symbol_filter_with_merge_commit(self, repo: pathlib.Path) -> None: """_commits_touching_symbol finds events on feature-branch commits (parent2).""" # Genesis commit (repo / "core.py").write_text("def bedrock():\n return 42\n") runner.invoke(cli, ["code", "add", "."]) r = runner.invoke(cli, ["commit", "-m", "Add bedrock"]) assert r.exit_code == 0, r.output repo_json = json.loads((repo_json_path(repo)).read_text()) repo_id = repo_json["repo_id"] from muse.core.refs import read_current_branch from muse.core.commits import resolve_commit_ref branch = read_current_branch(repo) head_commit = resolve_commit_ref(repo, branch, None) assert head_commit is not None head_id = head_commit.commit_id now = datetime.datetime(2026, 3, 1, tzinfo=datetime.timezone.utc) from muse.core.ids import hash_commit as _cid feature_snap_id = head_commit.snapshot_id merge_snap_id = head_commit.snapshot_id feature_id = _cid(parent_ids=[head_id], snapshot_id=feature_snap_id, message="Modify bedrock on feature", committed_at_iso=now.isoformat(), author="test") merge_id = _cid(parent_ids=[head_id, feature_id], snapshot_id=merge_snap_id, message="Merge feature", committed_at_iso=now.isoformat(), author="test") bedrock_delta = StructuredDelta( domain="code", ops=[PatchOp( op="patch", address="core.py", child_ops=[ReplaceOp( op="replace", address="core.py::bedrock", old_content_id="a" * 64, new_content_id="b" * 64, old_summary="function bedrock", new_summary="function bedrock (modified)", position=None, )], child_domain="code", child_summary="bedrock modified", )], summary="bedrock modified", ) feature_body: CommitDict = { "commit_id": feature_id, "repo_id": repo_id, "branch": "feat/bedrock", "snapshot_id": head_commit.snapshot_id, "message": "Modify bedrock on feature", "committed_at": now.isoformat(), "parent_commit_id": head_id, "parent2_commit_id": None, "author": "test", "metadata": {}, "structured_delta": bedrock_delta, } merge_body: CommitDict = { "commit_id": merge_id, "repo_id": repo_id, "branch": branch, "snapshot_id": head_commit.snapshot_id, "message": "Merge feature", "committed_at": now.isoformat(), "parent_commit_id": head_id, "parent2_commit_id": feature_id, "author": "test", "metadata": {}, "structured_delta": None, } from muse.core.commits import ( CommitRecord, write_commit, ) write_commit(repo, CommitRecord.from_dict(feature_body)) write_commit(repo, CommitRecord.from_dict(merge_body)) (ref_path(repo, branch)).write_text(merge_id) all_ids = _commit_ids(repo) from muse.core.bisect import _commits_touching_symbol touching = _commits_touching_symbol(repo, all_ids, "core.py::bedrock") assert feature_id in touching, "feature-branch commit must be found by _commits_touching_symbol"