"""TDD tests for CallGraphCache — persistent per-file call-graph cache. Architecture ------------ ``build_forward_graph`` re-reads every Python blob from the object store and re-parses every AST on every CLI invocation. For a 778-file Python codebase that costs ~15 s cold; with a warm cache it should be ~1 s. ``CallGraphCache`` mirrors ``SymbolCache`` exactly, but stores a per-file **subgraph** rather than a symbol tree. The subgraph is the portion of the forward call graph contributed by one source file: ``{caller_address: frozenset[callee_bare_name]}`` This preserves full per-address granularity so the warm path produces an identical ``ForwardGraph`` to the cold path. Key: SHA-256 of file bytes (``object_id`` from the manifest). Value: ``dict[str, frozenset[str]]`` — per-file forward subgraph. File: ``.muse/cache/callgraph.json`` (JSON, atomic write). API: ``load(muse_dir)`` / ``empty()`` / ``get`` / ``put`` / ``prune`` / ``size`` / ``save`` + convenience ``load_callgraph_cache(root)``. Coverage matrix --------------- - Memory operations: get/put/dirty/size/prune/empty class method - Persistence: save creates file; save/load round-trip; no-dirty skip; dirty=False after save; second save is no-op; atomic write - Graceful load: missing file → empty; corrupt → empty; wrong version → empty; invalid entry skipped, valid entries survive - Convenience helper: load_callgraph_cache with and without .muse dir - Integration cold: build_forward_graph with callgraph_cache=None produces correct graph - Integration warm: pre-populated cache skips read_object and ast.parse entirely - Integration save: cache entries populated after build; build does NOT auto-save - Non-Python files: not cached, not added to graph - Syntax errors: parse failure → entry not cached - Correctness: warm graph == cold graph for all addresses - Performance: warm call ≥ 5× faster than cold; < 100 ms for 30 files """ from __future__ import annotations import pathlib import time from unittest.mock import patch import pytest from muse.core.object_store import write_object from muse.core.paths import muse_dir from muse.core.types import Manifest, blob_id # Subgraph type alias: the per-file portion of ForwardGraph _Subgraph = dict[str, frozenset[str]] # --------------------------------------------------------------------------- # Helpers — shared across all test classes # --------------------------------------------------------------------------- def _muse_dir(tmp_path: pathlib.Path) -> pathlib.Path: d = muse_dir(tmp_path) d.mkdir(exist_ok=True) (d / "cache").mkdir(exist_ok=True) return d def _write_py( tmp_path: pathlib.Path, rel_path: str, source: str ) -> tuple[str, bytes]: """Write source to object store; return (object_id, raw_bytes). object_id uses the canonical ``sha256:`` prefix required by the object store's ``validate_object_id`` guard. """ raw = source.encode() oid = blob_id(raw) write_object(tmp_path, oid, raw) return oid, raw def _make_manifest( tmp_path: pathlib.Path, files: dict[str, str] ) -> Manifest: manifest: Manifest = {} for rel_path, source in files.items(): oid, _ = _write_py(tmp_path, rel_path, source) manifest[rel_path] = oid return manifest def _subgraph( file_path: str, caller: str, callees: set[str] ) -> _Subgraph: """Helper to build a minimal per-file subgraph for a single caller.""" return {f"{file_path}::{caller}": frozenset(callees)} # --------------------------------------------------------------------------- # TestCallGraphCacheMemory # --------------------------------------------------------------------------- class TestCallGraphCacheMemory: """In-memory get/put/prune/size/empty operations.""" def test_empty_get_miss(self) -> None: from muse.core.callgraph_cache import CallGraphCache cache = CallGraphCache.empty() assert cache.get("no_such_id") is None def test_put_then_get_hit(self) -> None: from muse.core.callgraph_cache import CallGraphCache cache = CallGraphCache.empty() subgraph: _Subgraph = { "mod.py::caller": frozenset({"compute", "validate"}), "mod.py::leaf": frozenset(), } cache.put("abc123", subgraph) assert cache.get("abc123") == subgraph def test_put_marks_dirty(self) -> None: from muse.core.callgraph_cache import CallGraphCache cache = CallGraphCache.empty() assert not cache._dirty cache.put("id1", {"mod.py::fn": frozenset()}) assert cache._dirty def test_different_ids_independent(self) -> None: from muse.core.callgraph_cache import CallGraphCache cache = CallGraphCache.empty() sg_a: _Subgraph = {"a.py::alpha": frozenset({"beta"})} sg_b: _Subgraph = {"b.py::gamma": frozenset({"delta"})} cache.put("id_a", sg_a) cache.put("id_b", sg_b) assert cache.get("id_a") == sg_a assert cache.get("id_b") == sg_b def test_put_same_id_overwrites(self) -> None: from muse.core.callgraph_cache import CallGraphCache cache = CallGraphCache.empty() cache.put("same", {"f.py::old": frozenset({"x"})}) cache.put("same", {"f.py::new": frozenset({"y"})}) result = cache.get("same") assert "f.py::new" in result assert "f.py::old" not in result assert cache.size == 1 def test_size_starts_zero(self) -> None: from muse.core.callgraph_cache import CallGraphCache assert CallGraphCache.empty().size == 0 def test_size_grows_with_put(self) -> None: from muse.core.callgraph_cache import CallGraphCache cache = CallGraphCache.empty() cache.put("x", {"m.py::f": frozenset()}) cache.put("y", {"m.py::g": frozenset()}) assert cache.size == 2 def test_prune_removes_stale_entries(self) -> None: from muse.core.callgraph_cache import CallGraphCache cache = CallGraphCache.empty() cache.put("keep", {"a.py::f": frozenset()}) cache.put("drop", {"b.py::g": frozenset()}) cache.prune({"keep"}) assert cache.get("keep") is not None assert cache.get("drop") is None def test_prune_marks_dirty_when_stale(self) -> None: from muse.core.callgraph_cache import CallGraphCache cache = CallGraphCache.empty() cache.put("drop", {"a.py::f": frozenset()}) cache._dirty = False cache.prune(set()) assert cache._dirty def test_prune_no_stale_not_dirty(self) -> None: from muse.core.callgraph_cache import CallGraphCache cache = CallGraphCache.empty() cache.put("keep", {"a.py::f": frozenset()}) cache._dirty = False cache.prune({"keep", "other"}) assert not cache._dirty def test_empty_save_is_noop(self, tmp_path: pathlib.Path) -> None: from muse.core.callgraph_cache import CallGraphCache cache = CallGraphCache.empty() cache.put("id", {"m.py::fn": frozenset({"fn"})}) cache.save() # muse_dir is None — must not raise assert not any(tmp_path.rglob("callgraph.json")) def test_empty_subgraph_is_valid(self) -> None: from muse.core.callgraph_cache import CallGraphCache cache = CallGraphCache.empty() cache.put("leaf_fn", {}) result = cache.get("leaf_fn") assert result == {} def test_frozensets_preserved_in_subgraph(self) -> None: from muse.core.callgraph_cache import CallGraphCache cache = CallGraphCache.empty() sg: _Subgraph = {"m.py::fn": frozenset({"a", "b", "c"})} cache.put("id", sg) result = cache.get("id") assert isinstance(result["m.py::fn"], frozenset) assert result["m.py::fn"] == frozenset({"a", "b", "c"}) # --------------------------------------------------------------------------- # TestCallGraphCachePersistence # --------------------------------------------------------------------------- class TestCallGraphCachePersistence: """save() / load() round-trip via .muse/cache/callgraph.json.""" def test_save_creates_file(self, tmp_path: pathlib.Path) -> None: from muse.core.callgraph_cache import CallGraphCache md = _muse_dir(tmp_path) cache = CallGraphCache.load(md) cache.put("id1", {"m.py::fn": frozenset({"compute"})}) cache.save() assert (md / "cache" / "callgraph.json").is_file() def test_save_then_load_round_trip(self, tmp_path: pathlib.Path) -> None: from muse.core.callgraph_cache import CallGraphCache md = _muse_dir(tmp_path) sg: _Subgraph = { "billing.py::compute": frozenset({"validate", "send"}), "billing.py::validate": frozenset(), } oid = "deadbeef" * 8 cache = CallGraphCache.load(md) cache.put(oid, sg) cache.save() loaded = CallGraphCache.load(md) result = loaded.get(oid) assert result is not None assert result == sg def test_round_trip_preserves_frozenset_type(self, tmp_path: pathlib.Path) -> None: from muse.core.callgraph_cache import CallGraphCache md = _muse_dir(tmp_path) cache = CallGraphCache.load(md) cache.put("id", {"m.py::fn": frozenset({"a", "b"})}) cache.save() loaded = CallGraphCache.load(md) result = loaded.get("id") assert isinstance(result["m.py::fn"], frozenset) def test_save_no_dirty_skips_write(self, tmp_path: pathlib.Path) -> None: from muse.core.callgraph_cache import CallGraphCache md = _muse_dir(tmp_path) cache = CallGraphCache.load(md) cache.save() assert not (md / "cache" / "callgraph.json").is_file() def test_save_clears_dirty_flag(self, tmp_path: pathlib.Path) -> None: from muse.core.callgraph_cache import CallGraphCache md = _muse_dir(tmp_path) cache = CallGraphCache.load(md) cache.put("id", {"m.py::fn": frozenset()}) cache.save() assert not cache._dirty def test_second_save_is_noop(self, tmp_path: pathlib.Path) -> None: from muse.core.callgraph_cache import CallGraphCache md = _muse_dir(tmp_path) cache = CallGraphCache.load(md) cache.put("id", {"m.py::fn": frozenset()}) cache.save() mtime1 = (md / "cache" / "callgraph.json").stat().st_mtime_ns cache.save() mtime2 = (md / "cache" / "callgraph.json").stat().st_mtime_ns assert mtime1 == mtime2 def test_multiple_entries_survive_round_trip(self, tmp_path: pathlib.Path) -> None: from muse.core.callgraph_cache import CallGraphCache md = _muse_dir(tmp_path) cache = CallGraphCache.load(md) entries: dict[str, _Subgraph] = { "id_a": {"a.py::alpha": frozenset({"beta"})}, "id_b": {}, "id_c": {"c.py::gamma": frozenset({"delta", "epsilon"})}, } for oid, sg in entries.items(): cache.put(oid, sg) cache.save() loaded = CallGraphCache.load(md) for oid, sg in entries.items(): assert loaded.get(oid) == sg def test_atomic_write_no_tmp_leftover(self, tmp_path: pathlib.Path) -> None: from muse.core.callgraph_cache import CallGraphCache md = _muse_dir(tmp_path) cache = CallGraphCache.load(md) cache.put("id", {"m.py::fn": frozenset()}) cache.save() assert not any((md / "cache").glob("*.tmp")) def test_orphaned_tmp_swept_on_startup(self, tmp_path: pathlib.Path) -> None: """A stale ``.callgraph_*.tmp`` left by a crash is removed by the startup sweep.""" from muse.core.repo import _cleanup_muse_dir_temps md = _muse_dir(tmp_path) orphan = md / "cache" / ".callgraph_abc123.tmp" orphan.write_bytes(b"stale") _cleanup_muse_dir_temps(md) assert not orphan.exists() # --------------------------------------------------------------------------- # TestCallGraphCacheGracefulLoad # --------------------------------------------------------------------------- class TestCallGraphCacheGracefulLoad: """load() never raises — returns empty cache on any error.""" def test_absent_file_returns_empty(self, tmp_path: pathlib.Path) -> None: from muse.core.callgraph_cache import CallGraphCache md = _muse_dir(tmp_path) assert CallGraphCache.load(md).size == 0 def test_corrupt_file_returns_empty(self, tmp_path: pathlib.Path) -> None: from muse.core.callgraph_cache import CallGraphCache md = _muse_dir(tmp_path) (md / "cache").mkdir(parents=True, exist_ok=True) (md / "cache" / "callgraph.json").write_bytes(b"not valid JSON !!!") assert CallGraphCache.load(md).size == 0 def test_wrong_version_returns_empty(self, tmp_path: pathlib.Path) -> None: import json as _json from muse.core.callgraph_cache import CallGraphCache md = _muse_dir(tmp_path) doc = {"version": 999, "entries": {}} (md / "cache").mkdir(parents=True, exist_ok=True) (md / "cache" / "callgraph.json").write_bytes(_json.dumps(doc).encode()) assert CallGraphCache.load(md).size == 0 def test_non_dict_entries_returns_empty(self, tmp_path: pathlib.Path) -> None: import json as _json from muse.core.callgraph_cache import CallGraphCache md = _muse_dir(tmp_path) doc = {"version": 1, "entries": "not a dict"} (md / "cache").mkdir(parents=True, exist_ok=True) (md / "cache" / "callgraph.json").write_bytes(_json.dumps(doc).encode()) assert CallGraphCache.load(md).size == 0 def test_invalid_entry_skipped_valid_survive(self, tmp_path: pathlib.Path) -> None: """A single malformed subgraph entry is skipped; valid ones survive.""" import json as _json from muse.core.callgraph_cache import CallGraphCache, _CACHE_VERSION md = _muse_dir(tmp_path) doc = { "version": _CACHE_VERSION, "entries": { "good_id": {"m.py::fn": ["compute", "validate"]}, # valid "bad_id": {"m.py::fn": "not_a_list"}, # invalid — str not list "empty_id": {}, # valid empty subgraph }, } (md / "cache" / "callgraph.json").write_bytes(_json.dumps(doc).encode()) cache = CallGraphCache.load(md) assert cache.get("good_id") == {"m.py::fn": frozenset({"compute", "validate"})} assert cache.get("bad_id") is None assert cache.get("empty_id") == {} def test_entry_with_non_str_callee_skipped(self, tmp_path: pathlib.Path) -> None: import json as _json from muse.core.callgraph_cache import CallGraphCache, _CACHE_VERSION md = _muse_dir(tmp_path) doc = { "version": _CACHE_VERSION, "entries": { "bad": {"m.py::fn": [123, "valid"]}, # 123 is not str → skip entry }, } (md / "cache").mkdir(parents=True, exist_ok=True) (md / "cache" / "callgraph.json").write_bytes(_json.dumps(doc).encode()) assert CallGraphCache.load(md).get("bad") is None def test_entry_subgraph_not_dict_skipped(self, tmp_path: pathlib.Path) -> None: import json as _json from muse.core.callgraph_cache import CallGraphCache, _CACHE_VERSION md = _muse_dir(tmp_path) doc = { "version": _CACHE_VERSION, "entries": { "bad": "not_a_dict_at_all", # subgraph must be a dict }, } (md / "cache").mkdir(parents=True, exist_ok=True) (md / "cache" / "callgraph.json").write_bytes(_json.dumps(doc).encode()) assert CallGraphCache.load(md).get("bad") is None # --------------------------------------------------------------------------- # TestLoadCallGraphCache — convenience helper # --------------------------------------------------------------------------- class TestLoadCallGraphCache: """load_callgraph_cache(root) convenience loader.""" def test_no_muse_dir_returns_empty(self, tmp_path: pathlib.Path) -> None: from muse.core.callgraph_cache import load_callgraph_cache assert load_callgraph_cache(tmp_path).size == 0 def test_with_muse_dir_loads_existing(self, tmp_path: pathlib.Path) -> None: from muse.core.callgraph_cache import CallGraphCache, load_callgraph_cache md = _muse_dir(tmp_path) seed = CallGraphCache.load(md) seed.put("myid", {"m.py::fn": frozenset({"fn"})}) seed.save() cache = load_callgraph_cache(tmp_path) assert cache.get("myid") == {"m.py::fn": frozenset({"fn"})} def test_with_empty_muse_dir_returns_empty(self, tmp_path: pathlib.Path) -> None: from muse.core.callgraph_cache import load_callgraph_cache _muse_dir(tmp_path) assert load_callgraph_cache(tmp_path).size == 0 # --------------------------------------------------------------------------- # TestBuildForwardGraphWithCache — integration # --------------------------------------------------------------------------- class TestBuildForwardGraphWithCache: """build_forward_graph accepts an optional CallGraphCache and uses it.""" def _repo(self, tmp_path: pathlib.Path) -> pathlib.Path: _muse_dir(tmp_path) return tmp_path def test_cold_cache_none_correct_graph(self, tmp_path: pathlib.Path) -> None: """Without a cache, build_forward_graph still returns the correct graph.""" root = self._repo(tmp_path) src = "def caller():\n callee()\n\ndef callee():\n pass\n" manifest = _make_manifest(root, {"mod.py": src}) from muse.plugins.code._callgraph import build_forward_graph graph = build_forward_graph(root, manifest) caller_addr = next(k for k in graph if "caller" in k) assert "callee" in graph[caller_addr] def test_explicit_cache_hit_skips_read_object(self, tmp_path: pathlib.Path) -> None: """When the cache has an entry for object_id, read_object is not called.""" from muse.core.callgraph_cache import CallGraphCache from muse.plugins.code._callgraph import build_forward_graph root = self._repo(tmp_path) src = "def caller():\n callee()\n" oid, _ = _write_py(root, "mod.py", src) manifest = {"mod.py": oid} # Pre-populate with the known subgraph cache = CallGraphCache.empty() cache.put(oid, {"mod.py::caller": frozenset({"callee"})}) with patch("muse.plugins.code._callgraph.read_object") as mock_read: build_forward_graph(root, manifest, callgraph_cache=cache) mock_read.assert_not_called() def test_cache_miss_calls_read_object(self, tmp_path: pathlib.Path) -> None: """On a cache miss, read_object IS called (the normal cold path).""" from muse.core.callgraph_cache import CallGraphCache from muse.plugins.code._callgraph import build_forward_graph root = self._repo(tmp_path) src = "def caller():\n callee()\n" oid, _ = _write_py(root, "mod.py", src) manifest = {"mod.py": oid} empty_cache = CallGraphCache.empty() with patch( "muse.plugins.code._callgraph.read_object", wraps=__import__( "muse.core.object_store", fromlist=["read_object"] ).read_object, ) as mock_read: build_forward_graph(root, manifest, callgraph_cache=empty_cache) assert mock_read.call_count >= 1 def test_cache_populated_after_build(self, tmp_path: pathlib.Path) -> None: """After build_forward_graph, the cache contains a subgraph for each parsed file.""" from muse.core.callgraph_cache import CallGraphCache from muse.plugins.code._callgraph import build_forward_graph root = self._repo(tmp_path) src = "def caller():\n callee()\n" oid, _ = _write_py(root, "mod.py", src) manifest = {"mod.py": oid} cache = CallGraphCache.empty() build_forward_graph(root, manifest, callgraph_cache=cache) result = cache.get(oid) assert result is not None assert isinstance(result, dict) def test_cache_subgraph_has_correct_callees(self, tmp_path: pathlib.Path) -> None: """The subgraph stored in the cache matches the cold graph output.""" from muse.core.callgraph_cache import CallGraphCache from muse.plugins.code._callgraph import build_forward_graph root = self._repo(tmp_path) src = "def caller():\n callee_a()\n callee_b()\n" oid, _ = _write_py(root, "mod.py", src) manifest = {"mod.py": oid} cache = CallGraphCache.empty() graph = build_forward_graph(root, manifest, callgraph_cache=cache) # The subgraph in the cache must match the graph output for this file subgraph = cache.get(oid) assert subgraph is not None for addr, callees in graph.items(): assert addr in subgraph assert subgraph[addr] == callees def test_warm_graph_equals_cold_graph(self, tmp_path: pathlib.Path) -> None: """Graph built with a warm cache equals graph built cold.""" from muse.core.callgraph_cache import CallGraphCache from muse.plugins.code._callgraph import build_forward_graph root = self._repo(tmp_path) src = "def a():\n b()\n c()\n\ndef b():\n pass\n\ndef c():\n pass\n" oid, _ = _write_py(root, "mod.py", src) manifest = {"mod.py": oid} cold_graph = build_forward_graph(root, manifest) cache = CallGraphCache.empty() build_forward_graph(root, manifest, callgraph_cache=cache) # populates cache warm_graph = build_forward_graph(root, manifest, callgraph_cache=cache) # warm for addr in cold_graph: assert addr in warm_graph assert cold_graph[addr] == warm_graph[addr] def test_non_python_files_not_cached(self, tmp_path: pathlib.Path) -> None: """Non-Python files are not added to the cache or graph.""" from muse.core.callgraph_cache import CallGraphCache from muse.plugins.code._callgraph import build_forward_graph root = self._repo(tmp_path) md_oid, _ = _write_py(root, "README.md", "# My README\n") py_oid, _ = _write_py(root, "mod.py", "def fn():\n pass\n") manifest = {"README.md": md_oid, "mod.py": py_oid} cache = CallGraphCache.empty() build_forward_graph(root, manifest, callgraph_cache=cache) assert cache.get(md_oid) is None # markdown — not cached assert cache.get(py_oid) is not None # Python — cached def test_build_does_not_call_cache_save(self, tmp_path: pathlib.Path) -> None: """build_forward_graph must not call cache.save() — that is the caller's job.""" from muse.core.callgraph_cache import CallGraphCache from muse.plugins.code._callgraph import build_forward_graph root = self._repo(tmp_path) src = "def fn():\n pass\n" oid, _ = _write_py(root, "mod.py", src) manifest = {"mod.py": oid} md = _muse_dir(tmp_path) cache = CallGraphCache.load(md) build_forward_graph(root, manifest, callgraph_cache=cache) # If build_forward_graph called save(), the file would exist now assert not (md / "cache" / "callgraph.json").is_file() def test_syntax_error_file_not_cached(self, tmp_path: pathlib.Path) -> None: """Files with syntax errors are skipped — nothing added to cache.""" from muse.core.callgraph_cache import CallGraphCache from muse.plugins.code._callgraph import build_forward_graph root = self._repo(tmp_path) bad_src = "def broken(:\n pass\n" oid, _ = _write_py(root, "broken.py", bad_src) manifest = {"broken.py": oid} cache = CallGraphCache.empty() build_forward_graph(root, manifest, callgraph_cache=cache) assert cache.get(oid) is None # parse failed → not cached def test_second_call_skips_ast_parse(self, tmp_path: pathlib.Path) -> None: """On the second call with a warm cache, ast.parse is never called.""" from muse.core.callgraph_cache import CallGraphCache from muse.plugins.code._callgraph import build_forward_graph root = self._repo(tmp_path) src = "def caller():\n callee()\n" oid, _ = _write_py(root, "mod.py", src) manifest = {"mod.py": oid} cache = CallGraphCache.empty() build_forward_graph(root, manifest, callgraph_cache=cache) # cold with patch("muse.plugins.code._callgraph.ast") as mock_ast: build_forward_graph(root, manifest, callgraph_cache=cache) # warm mock_ast.parse.assert_not_called() # --------------------------------------------------------------------------- # TestCallGraphCachePerformance # --------------------------------------------------------------------------- class TestCallGraphCachePerformance: """Second call with a warm cache must be substantially faster than cold.""" def _build_repo( self, tmp_path: pathlib.Path, n_files: int = 30 ) -> tuple[pathlib.Path, Manifest]: root = tmp_path _muse_dir(root) manifest: Manifest = {} for i in range(n_files): src = ( f"def fn_{i}():\n helper_{i}()\n util_{i}()\n\n" f"def helper_{i}():\n pass\n\n" f"def util_{i}():\n pass\n" ) oid, _ = _write_py(root, f"mod_{i}.py", src) manifest[f"mod_{i}.py"] = oid return root, manifest def test_warm_cache_at_least_5x_faster(self, tmp_path: pathlib.Path) -> None: from muse.core.callgraph_cache import CallGraphCache from muse.plugins.code._callgraph import build_forward_graph root, manifest = self._build_repo(tmp_path, n_files=30) cache = CallGraphCache.empty() t0 = time.perf_counter() build_forward_graph(root, manifest, callgraph_cache=cache) cold_ms = (time.perf_counter() - t0) * 1000 t1 = time.perf_counter() build_forward_graph(root, manifest, callgraph_cache=cache) warm_ms = (time.perf_counter() - t1) * 1000 assert warm_ms < cold_ms / 5, ( f"Warm ({warm_ms:.1f} ms) should be ≥5× faster than cold ({cold_ms:.1f} ms)" ) def test_warm_cache_under_100ms_for_30_files(self, tmp_path: pathlib.Path) -> None: from muse.core.callgraph_cache import CallGraphCache from muse.plugins.code._callgraph import build_forward_graph root, manifest = self._build_repo(tmp_path, n_files=30) cache = CallGraphCache.empty() build_forward_graph(root, manifest, callgraph_cache=cache) # warm the cache t0 = time.perf_counter() build_forward_graph(root, manifest, callgraph_cache=cache) warm_ms = (time.perf_counter() - t0) * 1000 assert warm_ms < 100, f"Warm call took {warm_ms:.1f} ms — expected < 100 ms" def test_graph_correctness_not_degraded_by_cache(self, tmp_path: pathlib.Path) -> None: """Warm-cache graph is identical to cold-cache graph for all addresses.""" from muse.core.callgraph_cache import CallGraphCache from muse.plugins.code._callgraph import build_forward_graph root, manifest = self._build_repo(tmp_path, n_files=10) cold_graph = build_forward_graph(root, manifest) cache = CallGraphCache.empty() build_forward_graph(root, manifest, callgraph_cache=cache) # warm warm_graph = build_forward_graph(root, manifest, callgraph_cache=cache) for addr in cold_graph: assert addr in warm_graph, f"Address {addr!r} missing from warm graph" assert cold_graph[addr] == warm_graph[addr], ( f"Callee mismatch at {addr!r}: " f"cold={cold_graph[addr]} warm={warm_graph[addr]}" )