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