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