gabriel / muse public
test_implicit_edge_cache.py python
685 lines 27.7 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 132 days ago
1 """TDD tests for ImplicitEdgeCache — persistent per-file framework-edge cache.
2
3 Architecture
4 ------------
5 ``build_implicit_edge_graph`` re-reads every Python blob and re-runs all
6 framework plugins (FastAPI, Flask, Celery) on every invocation: ~10 s for
7 the 779-file muse repo. The result is fully determined by the file content
8 — same bytes always produce the same list of ``ImplicitEntryEdge`` objects.
9
10 ``ImplicitEdgeCache`` mirrors ``CallGraphCache`` exactly:
11
12 * Key: SHA-256 of file bytes (``object_id`` from the manifest).
13 * Value: list of ``ImplicitEntryEdge`` dicts for the file
14 (serialised as plain dicts in msgpack; restored as dataclass instances
15 on load so callers always receive ``ImplicitEntryEdge`` objects).
16 * File: ``.muse/cache/implicit_edges.msgpack`` (msgpack, atomic write).
17 * API: ``load(muse_dir)`` / ``empty()`` / ``get`` / ``put`` / ``prune`` /
18 ``size`` / ``save`` + convenience ``load_implicit_edge_cache(root)``.
19
20 Coverage matrix
21 ---------------
22 - Memory operations: get/put/dirty/size/prune/empty
23 - Persistence: save creates file; save/load round-trip with full edge fidelity;
24 no-dirty skip; dirty=False after save; second save is no-op;
25 atomic write (no .tmp leftover)
26 - Graceful load: missing file → empty; corrupt → empty; wrong version → empty;
27 invalid entry skipped, valid survive; non-str field skipped
28 - Convenience helper: load_implicit_edge_cache with and without .muse dir
29 - Integration cold: build_implicit_edge_graph produces correct graph without cache
30 - Integration warm: pre-populated cache skips read_object entirely
31 - Integration save: after build, cache populated; build does NOT auto-save
32 - Non-Python files: not cached, not processed
33 - Correctness: warm graph == cold graph (same edges, same metadata)
34 - Performance: warm call ≥ 5× faster than cold; < 200 ms for 30-file repo
35 """
36
37 from __future__ import annotations
38 from collections.abc import Mapping
39
40 import pathlib
41 import textwrap
42 import time
43 from dataclasses import asdict
44 from unittest.mock import patch
45
46 import msgpack
47 import pytest
48
49 from muse.core.object_store import write_object
50 from muse.core._types import Manifest, blob_id
51 from muse.plugins.code._framework import ImplicitEntryEdge
52
53
54 # ---------------------------------------------------------------------------
55 # Helpers
56 # ---------------------------------------------------------------------------
57
58
59 def _muse_dir(tmp_path: pathlib.Path) -> pathlib.Path:
60 d = tmp_path / ".muse"
61 d.mkdir(exist_ok=True)
62 (d / "cache").mkdir(exist_ok=True)
63 return d
64
65
66 def _write_blob(root: pathlib.Path, source: str) -> str:
67 """Write source bytes to the object store; return canonical object_id."""
68 raw = source.encode()
69 oid = blob_id(raw)
70 write_object(root, oid, raw)
71 return oid
72
73
74 def _make_manifest(root: pathlib.Path, files: Mapping[str, str]) -> Manifest:
75 return {rel: _write_blob(root, src) for rel, src in files.items()}
76
77
78 def _edge(
79 symbol_address: str = "app.py::handle",
80 framework_id: str = "fastapi",
81 kind: str = "http-route",
82 metadata: dict[str, str] | None = None,
83 ) -> ImplicitEntryEdge:
84 return ImplicitEntryEdge(
85 framework_id=framework_id,
86 symbol_address=symbol_address,
87 kind=kind,
88 metadata=metadata or {"method": "GET", "path": "/"},
89 )
90
91
92 # ---------------------------------------------------------------------------
93 # TestImplicitEdgeCacheMemory
94 # ---------------------------------------------------------------------------
95
96
97 class TestImplicitEdgeCacheMemory:
98 """In-memory get/put/prune/size/empty operations."""
99
100 def test_empty_get_miss(self) -> None:
101 from muse.core.implicit_edge_cache import ImplicitEdgeCache
102 assert ImplicitEdgeCache.empty().get("no_such_id") is None
103
104 def test_put_then_get_hit(self) -> None:
105 from muse.core.implicit_edge_cache import ImplicitEdgeCache
106 cache = ImplicitEdgeCache.empty()
107 edges = [_edge("app.py::create"), _edge("app.py::delete", kind="http-route")]
108 cache.put("abc123", edges)
109 result = cache.get("abc123")
110 assert result == edges
111
112 def test_put_marks_dirty(self) -> None:
113 from muse.core.implicit_edge_cache import ImplicitEdgeCache
114 cache = ImplicitEdgeCache.empty()
115 assert not cache._dirty
116 cache.put("id1", [])
117 assert cache._dirty
118
119 def test_different_ids_independent(self) -> None:
120 from muse.core.implicit_edge_cache import ImplicitEdgeCache
121 cache = ImplicitEdgeCache.empty()
122 e1 = [_edge("a.py::fn_a")]
123 e2 = [_edge("b.py::fn_b", framework_id="flask")]
124 cache.put("id_a", e1)
125 cache.put("id_b", e2)
126 assert cache.get("id_a") == e1
127 assert cache.get("id_b") == e2
128
129 def test_put_same_id_overwrites(self) -> None:
130 from muse.core.implicit_edge_cache import ImplicitEdgeCache
131 cache = ImplicitEdgeCache.empty()
132 cache.put("same", [_edge("a.py::old")])
133 cache.put("same", [_edge("a.py::new")])
134 result = cache.get("same")
135 assert result[0].symbol_address == "a.py::new"
136 assert cache.size == 1
137
138 def test_size_starts_zero(self) -> None:
139 from muse.core.implicit_edge_cache import ImplicitEdgeCache
140 assert ImplicitEdgeCache.empty().size == 0
141
142 def test_size_grows_with_put(self) -> None:
143 from muse.core.implicit_edge_cache import ImplicitEdgeCache
144 cache = ImplicitEdgeCache.empty()
145 cache.put("x", [])
146 cache.put("y", [_edge()])
147 assert cache.size == 2
148
149 def test_empty_list_is_valid(self) -> None:
150 """Files with no framework entry-points cache an empty list."""
151 from muse.core.implicit_edge_cache import ImplicitEdgeCache
152 cache = ImplicitEdgeCache.empty()
153 cache.put("plain_file", [])
154 assert cache.get("plain_file") == []
155
156 def test_prune_removes_stale(self) -> None:
157 from muse.core.implicit_edge_cache import ImplicitEdgeCache
158 cache = ImplicitEdgeCache.empty()
159 cache.put("keep", [_edge()])
160 cache.put("drop", [])
161 cache.prune({"keep"})
162 assert cache.get("keep") is not None
163 assert cache.get("drop") is None
164
165 def test_prune_marks_dirty_when_stale(self) -> None:
166 from muse.core.implicit_edge_cache import ImplicitEdgeCache
167 cache = ImplicitEdgeCache.empty()
168 cache.put("drop", [])
169 cache._dirty = False
170 cache.prune(set())
171 assert cache._dirty
172
173 def test_prune_no_stale_not_dirty(self) -> None:
174 from muse.core.implicit_edge_cache import ImplicitEdgeCache
175 cache = ImplicitEdgeCache.empty()
176 cache.put("keep", [])
177 cache._dirty = False
178 cache.prune({"keep"})
179 assert not cache._dirty
180
181 def test_empty_save_is_noop(self, tmp_path: pathlib.Path) -> None:
182 from muse.core.implicit_edge_cache import ImplicitEdgeCache
183 cache = ImplicitEdgeCache.empty()
184 cache.put("id", [_edge()])
185 cache.save() # muse_dir is None — must not raise
186 assert not any(tmp_path.rglob("implicit_edges.msgpack"))
187
188 def test_get_returns_implicit_entry_edge_instances(self) -> None:
189 from muse.core.implicit_edge_cache import ImplicitEdgeCache
190 cache = ImplicitEdgeCache.empty()
191 e = _edge()
192 cache.put("id", [e])
193 result = cache.get("id")
194 assert all(isinstance(r, ImplicitEntryEdge) for r in result)
195
196 def test_metadata_preserved_in_memory(self) -> None:
197 from muse.core.implicit_edge_cache import ImplicitEdgeCache
198 cache = ImplicitEdgeCache.empty()
199 e = _edge(metadata={"method": "POST", "path": "/items/{id}"})
200 cache.put("id", [e])
201 result = cache.get("id")
202 assert result[0].metadata == {"method": "POST", "path": "/items/{id}"}
203
204
205 # ---------------------------------------------------------------------------
206 # TestImplicitEdgeCachePersistence
207 # ---------------------------------------------------------------------------
208
209
210 class TestImplicitEdgeCachePersistence:
211 """save() / load() round-trip via .muse/cache/implicit_edges.msgpack."""
212
213 def test_save_creates_file(self, tmp_path: pathlib.Path) -> None:
214 from muse.core.implicit_edge_cache import ImplicitEdgeCache
215 md = _muse_dir(tmp_path)
216 cache = ImplicitEdgeCache.load(md)
217 cache.put("id1", [_edge()])
218 cache.save()
219 assert (md / "cache" / "implicit_edges.msgpack").is_file()
220
221 def test_save_then_load_round_trip(self, tmp_path: pathlib.Path) -> None:
222 from muse.core.implicit_edge_cache import ImplicitEdgeCache
223 md = _muse_dir(tmp_path)
224 edges = [
225 _edge("routes.py::create_item", kind="http-route", metadata={"method": "POST", "path": "/items"}),
226 _edge("routes.py::list_items", kind="http-route", metadata={"method": "GET", "path": "/items"}),
227 ]
228 oid = "deadbeef" * 8
229
230 cache = ImplicitEdgeCache.load(md)
231 cache.put(oid, edges)
232 cache.save()
233
234 loaded = ImplicitEdgeCache.load(md)
235 result = loaded.get(oid)
236 assert result is not None
237 assert len(result) == 2
238 assert all(isinstance(e, ImplicitEntryEdge) for e in result)
239 assert result == edges
240
241 def test_round_trip_preserves_all_fields(self, tmp_path: pathlib.Path) -> None:
242 from muse.core.implicit_edge_cache import ImplicitEdgeCache
243 md = _muse_dir(tmp_path)
244 e = ImplicitEntryEdge(
245 framework_id="celery",
246 symbol_address="tasks.py::send_email",
247 kind="task",
248 metadata={"queue": "emails"},
249 )
250 cache = ImplicitEdgeCache.load(md)
251 cache.put("id", [e])
252 cache.save()
253
254 loaded = ImplicitEdgeCache.load(md)
255 result = loaded.get("id")[0]
256 assert result.framework_id == "celery"
257 assert result.symbol_address == "tasks.py::send_email"
258 assert result.kind == "task"
259 assert result.metadata == {"queue": "emails"}
260
261 def test_empty_list_round_trips(self, tmp_path: pathlib.Path) -> None:
262 from muse.core.implicit_edge_cache import ImplicitEdgeCache
263 md = _muse_dir(tmp_path)
264 cache = ImplicitEdgeCache.load(md)
265 cache.put("plain", [])
266 cache.save()
267 loaded = ImplicitEdgeCache.load(md)
268 assert loaded.get("plain") == []
269
270 def test_save_no_dirty_skips_write(self, tmp_path: pathlib.Path) -> None:
271 from muse.core.implicit_edge_cache import ImplicitEdgeCache
272 md = _muse_dir(tmp_path)
273 ImplicitEdgeCache.load(md).save()
274 assert not (md / "cache" / "implicit_edges.msgpack").is_file()
275
276 def test_save_clears_dirty_flag(self, tmp_path: pathlib.Path) -> None:
277 from muse.core.implicit_edge_cache import ImplicitEdgeCache
278 md = _muse_dir(tmp_path)
279 cache = ImplicitEdgeCache.load(md)
280 cache.put("id", [])
281 cache.save()
282 assert not cache._dirty
283
284 def test_second_save_is_noop(self, tmp_path: pathlib.Path) -> None:
285 from muse.core.implicit_edge_cache import ImplicitEdgeCache
286 md = _muse_dir(tmp_path)
287 cache = ImplicitEdgeCache.load(md)
288 cache.put("id", [_edge()])
289 cache.save()
290 mtime1 = (md / "cache" / "implicit_edges.msgpack").stat().st_mtime_ns
291 cache.save()
292 mtime2 = (md / "cache" / "implicit_edges.msgpack").stat().st_mtime_ns
293 assert mtime1 == mtime2
294
295 def test_atomic_write_no_tmp_leftover(self, tmp_path: pathlib.Path) -> None:
296 from muse.core.implicit_edge_cache import ImplicitEdgeCache
297 md = _muse_dir(tmp_path)
298 cache = ImplicitEdgeCache.load(md)
299 cache.put("id", [_edge()])
300 cache.save()
301 assert not (md / "cache" / "implicit_edges.msgpack.tmp").exists()
302
303 def test_multiple_entries_survive_round_trip(self, tmp_path: pathlib.Path) -> None:
304 from muse.core.implicit_edge_cache import ImplicitEdgeCache
305 md = _muse_dir(tmp_path)
306 cache = ImplicitEdgeCache.load(md)
307 entries = {
308 "id_a": [_edge("a.py::fn", "fastapi", "http-route", {"method": "GET", "path": "/"})],
309 "id_b": [],
310 "id_c": [_edge("c.py::task", "celery", "task", {"queue": "default"})],
311 }
312 for oid, edges in entries.items():
313 cache.put(oid, edges)
314 cache.save()
315
316 loaded = ImplicitEdgeCache.load(md)
317 for oid, edges in entries.items():
318 assert loaded.get(oid) == edges
319
320
321 # ---------------------------------------------------------------------------
322 # TestImplicitEdgeCacheGracefulLoad
323 # ---------------------------------------------------------------------------
324
325
326 class TestImplicitEdgeCacheGracefulLoad:
327 """load() never raises — returns empty cache on any error."""
328
329 def test_absent_file_returns_empty(self, tmp_path: pathlib.Path) -> None:
330 from muse.core.implicit_edge_cache import ImplicitEdgeCache
331 assert ImplicitEdgeCache.load(_muse_dir(tmp_path)).size == 0
332
333 def test_corrupt_file_returns_empty(self, tmp_path: pathlib.Path) -> None:
334 from muse.core.implicit_edge_cache import ImplicitEdgeCache
335 md = _muse_dir(tmp_path)
336 (md / "cache" / "implicit_edges.msgpack").write_bytes(b"not valid msgpack !!!")
337 assert ImplicitEdgeCache.load(md).size == 0
338
339 def test_wrong_version_returns_empty(self, tmp_path: pathlib.Path) -> None:
340 from muse.core.implicit_edge_cache import ImplicitEdgeCache
341 md = _muse_dir(tmp_path)
342 doc = {"version": 999, "entries": {}}
343 (md / "cache" / "implicit_edges.msgpack").write_bytes(msgpack.packb(doc, use_bin_type=True))
344 assert ImplicitEdgeCache.load(md).size == 0
345
346 def test_non_dict_entries_returns_empty(self, tmp_path: pathlib.Path) -> None:
347 from muse.core.implicit_edge_cache import ImplicitEdgeCache
348 md = _muse_dir(tmp_path)
349 doc = {"version": 1, "entries": "not a dict"}
350 (md / "cache" / "implicit_edges.msgpack").write_bytes(msgpack.packb(doc, use_bin_type=True))
351 assert ImplicitEdgeCache.load(md).size == 0
352
353 def test_invalid_entry_skipped_valid_survive(self, tmp_path: pathlib.Path) -> None:
354 from muse.core.implicit_edge_cache import ImplicitEdgeCache
355 md = _muse_dir(tmp_path)
356 valid_edge = {
357 "framework_id": "fastapi",
358 "symbol_address": "a.py::fn",
359 "kind": "http-route",
360 "metadata": {"method": "GET", "path": "/"},
361 }
362 doc = {
363 "version": 1,
364 "entries": {
365 "good_id": [valid_edge],
366 "bad_id": "not_a_list", # whole entry is invalid
367 "empty_id": [], # valid — no edges
368 },
369 }
370 (md / "cache" / "implicit_edges.msgpack").write_bytes(msgpack.packb(doc, use_bin_type=True))
371 cache = ImplicitEdgeCache.load(md)
372 good = cache.get("good_id")
373 assert good is not None and len(good) == 1
374 assert isinstance(good[0], ImplicitEntryEdge)
375 assert cache.get("bad_id") is None
376 assert cache.get("empty_id") == []
377
378 def test_edge_missing_required_field_skipped(self, tmp_path: pathlib.Path) -> None:
379 from muse.core.implicit_edge_cache import ImplicitEdgeCache
380 md = _muse_dir(tmp_path)
381 bad_edge = {"framework_id": "fastapi", "symbol_address": "a.py::fn"} # missing kind
382 doc = {"version": 1, "entries": {"bad": [bad_edge]}}
383 (md / "cache" / "implicit_edges.msgpack").write_bytes(msgpack.packb(doc, use_bin_type=True))
384 assert ImplicitEdgeCache.load(md).get("bad") is None
385
386 def test_edge_non_str_field_skipped(self, tmp_path: pathlib.Path) -> None:
387 from muse.core.implicit_edge_cache import ImplicitEdgeCache
388 md = _muse_dir(tmp_path)
389 bad_edge = {
390 "framework_id": 123, # must be str
391 "symbol_address": "a.py::fn",
392 "kind": "http-route",
393 "metadata": {},
394 }
395 doc = {"version": 1, "entries": {"bad": [bad_edge]}}
396 (md / "cache" / "implicit_edges.msgpack").write_bytes(msgpack.packb(doc, use_bin_type=True))
397 assert ImplicitEdgeCache.load(md).get("bad") is None
398
399
400 # ---------------------------------------------------------------------------
401 # TestLoadImplicitEdgeCache — convenience helper
402 # ---------------------------------------------------------------------------
403
404
405 class TestLoadImplicitEdgeCache:
406
407 def test_no_muse_dir_returns_empty(self, tmp_path: pathlib.Path) -> None:
408 from muse.core.implicit_edge_cache import load_implicit_edge_cache
409 assert load_implicit_edge_cache(tmp_path).size == 0
410
411 def test_with_muse_dir_loads_existing(self, tmp_path: pathlib.Path) -> None:
412 from muse.core.implicit_edge_cache import ImplicitEdgeCache, load_implicit_edge_cache
413 md = _muse_dir(tmp_path)
414 seed = ImplicitEdgeCache.load(md)
415 seed.put("myid", [_edge("m.py::fn")])
416 seed.save()
417 cache = load_implicit_edge_cache(tmp_path)
418 result = cache.get("myid")
419 assert result is not None and result[0].symbol_address == "m.py::fn"
420
421 def test_with_empty_muse_dir_returns_empty(self, tmp_path: pathlib.Path) -> None:
422 from muse.core.implicit_edge_cache import load_implicit_edge_cache
423 _muse_dir(tmp_path)
424 assert load_implicit_edge_cache(tmp_path).size == 0
425
426
427 # ---------------------------------------------------------------------------
428 # TestBuildImplicitEdgeGraphWithCache — integration
429 # ---------------------------------------------------------------------------
430
431
432 _FASTAPI_SOURCE = textwrap.dedent("""\
433 from fastapi import APIRouter
434 router = APIRouter()
435
436 @router.get("/items")
437 def list_items():
438 return []
439
440 @router.post("/items")
441 def create_item():
442 return {}
443 """)
444
445 _NO_FRAMEWORK_SOURCE = textwrap.dedent("""\
446 def compute(x):
447 return x * 2
448
449 def validate(x):
450 return x > 0
451 """)
452
453
454 class TestBuildImplicitEdgeGraphWithCache:
455 """build_implicit_edge_graph accepts an optional ImplicitEdgeCache."""
456
457 def _repo(self, tmp_path: pathlib.Path) -> pathlib.Path:
458 _muse_dir(tmp_path)
459 return tmp_path
460
461 def test_cold_cache_none_correct_graph(self, tmp_path: pathlib.Path) -> None:
462 """Without a cache, the graph is correct."""
463 root = self._repo(tmp_path)
464 manifest = _make_manifest(root, {"routes.py": _FASTAPI_SOURCE})
465
466 from muse.plugins.code._framework import build_implicit_edge_graph
467 graph = build_implicit_edge_graph(root, manifest)
468 assert any("list_items" in addr or "create_item" in addr for addr in graph)
469
470 def test_explicit_cache_hit_skips_read_object(self, tmp_path: pathlib.Path) -> None:
471 """When the cache has an entry for object_id, read_object is not called."""
472 from muse.core.implicit_edge_cache import ImplicitEdgeCache
473 from muse.plugins.code._framework import build_implicit_edge_graph
474
475 root = self._repo(tmp_path)
476 oid = _write_blob(root, _FASTAPI_SOURCE)
477 manifest = {"routes.py": oid}
478
479 # Pre-populate with a known result
480 cache = ImplicitEdgeCache.empty()
481 cache.put(oid, [_edge("routes.py::list_items")])
482
483 with patch("muse.plugins.code._framework.read_object") as mock_read:
484 build_implicit_edge_graph(root, manifest, implicit_cache=cache)
485 mock_read.assert_not_called()
486
487 def test_cache_miss_calls_read_object(self, tmp_path: pathlib.Path) -> None:
488 """On a cache miss, read_object IS called."""
489 from muse.core.implicit_edge_cache import ImplicitEdgeCache
490 from muse.plugins.code._framework import build_implicit_edge_graph
491
492 root = self._repo(tmp_path)
493 oid = _write_blob(root, _FASTAPI_SOURCE)
494 manifest = {"routes.py": oid}
495
496 cache = ImplicitEdgeCache.empty()
497 with patch(
498 "muse.plugins.code._framework.read_object",
499 wraps=__import__("muse.core.object_store", fromlist=["read_object"]).read_object,
500 ) as mock_read:
501 build_implicit_edge_graph(root, manifest, implicit_cache=cache)
502 assert mock_read.call_count >= 1
503
504 def test_cache_populated_after_build(self, tmp_path: pathlib.Path) -> None:
505 """After build, the cache has an entry for each processed Python file."""
506 from muse.core.implicit_edge_cache import ImplicitEdgeCache
507 from muse.plugins.code._framework import build_implicit_edge_graph
508
509 root = self._repo(tmp_path)
510 oid = _write_blob(root, _FASTAPI_SOURCE)
511 manifest = {"routes.py": oid}
512
513 cache = ImplicitEdgeCache.empty()
514 build_implicit_edge_graph(root, manifest, implicit_cache=cache)
515 assert cache.get(oid) is not None
516
517 def test_no_framework_file_cached_as_empty_list(self, tmp_path: pathlib.Path) -> None:
518 """Plain Python files (no framework) are cached with an empty list."""
519 from muse.core.implicit_edge_cache import ImplicitEdgeCache
520 from muse.plugins.code._framework import build_implicit_edge_graph
521
522 root = self._repo(tmp_path)
523 oid = _write_blob(root, _NO_FRAMEWORK_SOURCE)
524 manifest = {"utils.py": oid}
525
526 cache = ImplicitEdgeCache.empty()
527 build_implicit_edge_graph(root, manifest, implicit_cache=cache)
528 result = cache.get(oid)
529 assert result == []
530
531 def test_warm_graph_equals_cold_graph(self, tmp_path: pathlib.Path) -> None:
532 """Warm-cache graph is identical to the cold-path graph."""
533 from muse.core.implicit_edge_cache import ImplicitEdgeCache
534 from muse.plugins.code._framework import build_implicit_edge_graph
535
536 root = self._repo(tmp_path)
537 manifest = _make_manifest(root, {
538 "routes.py": _FASTAPI_SOURCE,
539 "utils.py": _NO_FRAMEWORK_SOURCE,
540 })
541
542 cold_graph = build_implicit_edge_graph(root, manifest)
543
544 cache = ImplicitEdgeCache.empty()
545 build_implicit_edge_graph(root, manifest, implicit_cache=cache) # populate
546 warm_graph = build_implicit_edge_graph(root, manifest, implicit_cache=cache) # warm
547
548 assert set(cold_graph.keys()) == set(warm_graph.keys())
549 for addr in cold_graph:
550 assert sorted(cold_graph[addr], key=lambda e: e.symbol_address) == \
551 sorted(warm_graph[addr], key=lambda e: e.symbol_address)
552
553 def test_non_python_files_not_cached(self, tmp_path: pathlib.Path) -> None:
554 """Non-Python files are skipped and not cached."""
555 from muse.core.implicit_edge_cache import ImplicitEdgeCache
556 from muse.plugins.code._framework import build_implicit_edge_graph
557
558 root = self._repo(tmp_path)
559 md_oid = _write_blob(root, "# README\n")
560 py_oid = _write_blob(root, _NO_FRAMEWORK_SOURCE)
561 manifest = {"README.md": md_oid, "utils.py": py_oid}
562
563 cache = ImplicitEdgeCache.empty()
564 build_implicit_edge_graph(root, manifest, implicit_cache=cache)
565
566 assert cache.get(md_oid) is None # skipped — not Python
567 assert cache.get(py_oid) is not None # processed
568
569 def test_build_does_not_call_cache_save(self, tmp_path: pathlib.Path) -> None:
570 """build_implicit_edge_graph must not call save() — caller's responsibility."""
571 from muse.core.implicit_edge_cache import ImplicitEdgeCache
572 from muse.plugins.code._framework import build_implicit_edge_graph
573
574 root = self._repo(tmp_path)
575 oid = _write_blob(root, _NO_FRAMEWORK_SOURCE)
576 manifest = {"utils.py": oid}
577
578 md = _muse_dir(tmp_path)
579 cache = ImplicitEdgeCache.load(md)
580 build_implicit_edge_graph(root, manifest, implicit_cache=cache)
581 assert not (md / "cache" / "implicit_edges.msgpack").is_file()
582
583 def test_second_call_skips_read_object(self, tmp_path: pathlib.Path) -> None:
584 """On the second call with a warm cache, read_object is never called."""
585 from muse.core.implicit_edge_cache import ImplicitEdgeCache
586 from muse.plugins.code._framework import build_implicit_edge_graph
587
588 root = self._repo(tmp_path)
589 oid = _write_blob(root, _FASTAPI_SOURCE)
590 manifest = {"routes.py": oid}
591
592 cache = ImplicitEdgeCache.empty()
593 build_implicit_edge_graph(root, manifest, implicit_cache=cache) # cold
594
595 with patch("muse.plugins.code._framework.read_object") as mock_read:
596 build_implicit_edge_graph(root, manifest, implicit_cache=cache) # warm
597 mock_read.assert_not_called()
598
599
600 # ---------------------------------------------------------------------------
601 # TestImplicitEdgeCachePerformance
602 # ---------------------------------------------------------------------------
603
604
605 class TestImplicitEdgeCachePerformance:
606 """Warm cache must be substantially faster than cold for a multi-file repo."""
607
608 def _build_repo(self, tmp_path: pathlib.Path, n_files: int = 30) -> tuple[pathlib.Path, Manifest]:
609 root = tmp_path
610 _muse_dir(root)
611 manifest: Manifest = {}
612 for i in range(n_files):
613 # Alternate between FastAPI files and plain Python files
614 if i % 3 == 0:
615 src = textwrap.dedent(f"""\
616 from fastapi import APIRouter
617 router = APIRouter()
618
619 @router.get("/resource_{i}")
620 def get_{i}():
621 return {{}}
622
623 @router.post("/resource_{i}")
624 def post_{i}():
625 return {{}}
626 """)
627 else:
628 src = f"def helper_{i}(x):\n return x * {i}\n"
629 oid = _write_blob(root, src)
630 manifest[f"mod_{i}.py"] = oid
631 return root, manifest
632
633 def test_warm_cache_at_least_5x_faster(self, tmp_path: pathlib.Path) -> None:
634 from muse.core.implicit_edge_cache import ImplicitEdgeCache
635 from muse.plugins.code._framework import build_implicit_edge_graph
636
637 root, manifest = self._build_repo(tmp_path, n_files=30)
638
639 cache = ImplicitEdgeCache.empty()
640 t0 = time.perf_counter()
641 build_implicit_edge_graph(root, manifest, implicit_cache=cache)
642 cold_ms = (time.perf_counter() - t0) * 1000
643
644 t1 = time.perf_counter()
645 build_implicit_edge_graph(root, manifest, implicit_cache=cache)
646 warm_ms = (time.perf_counter() - t1) * 1000
647
648 assert warm_ms < cold_ms / 5, (
649 f"Warm ({warm_ms:.1f} ms) should be ≥5× faster than cold ({cold_ms:.1f} ms)"
650 )
651
652 def test_warm_under_200ms_for_30_files(self, tmp_path: pathlib.Path) -> None:
653 from muse.core.implicit_edge_cache import ImplicitEdgeCache
654 from muse.plugins.code._framework import build_implicit_edge_graph
655
656 root, manifest = self._build_repo(tmp_path, n_files=30)
657
658 cache = ImplicitEdgeCache.empty()
659 build_implicit_edge_graph(root, manifest, implicit_cache=cache) # warm
660
661 t0 = time.perf_counter()
662 build_implicit_edge_graph(root, manifest, implicit_cache=cache)
663 warm_ms = (time.perf_counter() - t0) * 1000
664
665 assert warm_ms < 200, f"Warm call took {warm_ms:.1f} ms — expected < 200 ms"
666
667 def test_graph_correctness_not_degraded(self, tmp_path: pathlib.Path) -> None:
668 from muse.core.implicit_edge_cache import ImplicitEdgeCache
669 from muse.plugins.code._framework import build_implicit_edge_graph
670
671 root, manifest = self._build_repo(tmp_path, n_files=10)
672
673 cold_graph = build_implicit_edge_graph(root, manifest)
674
675 cache = ImplicitEdgeCache.empty()
676 build_implicit_edge_graph(root, manifest, implicit_cache=cache)
677 warm_graph = build_implicit_edge_graph(root, manifest, implicit_cache=cache)
678
679 assert set(cold_graph.keys()) == set(warm_graph.keys()), (
680 f"Keys differ:\n cold={sorted(cold_graph)}\n warm={sorted(warm_graph)}"
681 )
682 for addr in cold_graph:
683 cold_sorted = sorted(cold_graph[addr], key=lambda e: e.symbol_address)
684 warm_sorted = sorted(warm_graph[addr], key=lambda e: e.symbol_address)
685 assert cold_sorted == warm_sorted
File History 2 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 132 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 138 days ago