gabriel / muse public
test_implicit_edge_cache.py python
705 lines 28.6 KB
Raw
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 22 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 JSON; restored as dataclass instances
15 on load so callers always receive ``ImplicitEntryEdge`` objects).
16 * File: ``.muse/cache/implicit_edges.json`` (JSON, 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 pytest
47
48 from muse.core.object_store import write_object
49 from muse.core.types import Manifest, blob_id
50 from muse.core.paths import muse_dir
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 = muse_dir(tmp_path)
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.json"))
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.json."""
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.json").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.json").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.json").stat().st_mtime_ns
291 cache.save()
292 mtime2 = (md / "cache" / "implicit_edges.json").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 any((md / "cache").glob("*.tmp"))
302
303 def test_orphaned_tmp_swept_on_startup(self, tmp_path: pathlib.Path) -> None:
304 """A stale ``.implicit_edges_*.tmp`` left by a crash is removed by the startup sweep."""
305 from muse.core.repo import _cleanup_muse_dir_temps
306 md = _muse_dir(tmp_path)
307 orphan = md / "cache" / ".implicit_edges_abc123.tmp"
308 orphan.write_bytes(b"stale")
309 _cleanup_muse_dir_temps(md)
310 assert not orphan.exists()
311
312 def test_multiple_entries_survive_round_trip(self, tmp_path: pathlib.Path) -> None:
313 from muse.core.implicit_edge_cache import ImplicitEdgeCache
314 md = _muse_dir(tmp_path)
315 cache = ImplicitEdgeCache.load(md)
316 entries = {
317 "id_a": [_edge("a.py::fn", "fastapi", "http-route", {"method": "GET", "path": "/"})],
318 "id_b": [],
319 "id_c": [_edge("c.py::task", "celery", "task", {"queue": "default"})],
320 }
321 for oid, edges in entries.items():
322 cache.put(oid, edges)
323 cache.save()
324
325 loaded = ImplicitEdgeCache.load(md)
326 for oid, edges in entries.items():
327 assert loaded.get(oid) == edges
328
329
330 # ---------------------------------------------------------------------------
331 # TestImplicitEdgeCacheGracefulLoad
332 # ---------------------------------------------------------------------------
333
334
335 class TestImplicitEdgeCacheGracefulLoad:
336 """load() never raises — returns empty cache on any error."""
337
338 def test_absent_file_returns_empty(self, tmp_path: pathlib.Path) -> None:
339 from muse.core.implicit_edge_cache import ImplicitEdgeCache
340 assert ImplicitEdgeCache.load(_muse_dir(tmp_path)).size == 0
341
342 def test_corrupt_file_returns_empty(self, tmp_path: pathlib.Path) -> None:
343 from muse.core.implicit_edge_cache import ImplicitEdgeCache
344 md = _muse_dir(tmp_path)
345 (md / "cache").mkdir(parents=True, exist_ok=True)
346 (md / "cache" / "implicit_edges.json").write_bytes(b"not valid JSON !!!")
347 assert ImplicitEdgeCache.load(md).size == 0
348
349 def test_wrong_version_returns_empty(self, tmp_path: pathlib.Path) -> None:
350 import json as _json
351 from muse.core.implicit_edge_cache import ImplicitEdgeCache
352 md = _muse_dir(tmp_path)
353 doc = {"version": 999, "entries": {}}
354 (md / "cache").mkdir(parents=True, exist_ok=True)
355 (md / "cache" / "implicit_edges.json").write_bytes(_json.dumps(doc).encode())
356 assert ImplicitEdgeCache.load(md).size == 0
357
358 def test_non_dict_entries_returns_empty(self, tmp_path: pathlib.Path) -> None:
359 import json as _json
360 from muse.core.implicit_edge_cache import ImplicitEdgeCache
361 md = _muse_dir(tmp_path)
362 doc = {"version": 1, "entries": "not a dict"}
363 (md / "cache").mkdir(parents=True, exist_ok=True)
364 (md / "cache" / "implicit_edges.json").write_bytes(_json.dumps(doc).encode())
365 assert ImplicitEdgeCache.load(md).size == 0
366
367 def test_invalid_entry_skipped_valid_survive(self, tmp_path: pathlib.Path) -> None:
368 import json as _json
369 from muse.core.implicit_edge_cache import ImplicitEdgeCache, _CACHE_VERSION
370 md = _muse_dir(tmp_path)
371 valid_edge = {
372 "framework_id": "fastapi",
373 "symbol_address": "a.py::fn",
374 "kind": "http-route",
375 "metadata": {"method": "GET", "path": "/"},
376 }
377 doc = {
378 "version": _CACHE_VERSION,
379 "entries": {
380 "good_id": [valid_edge],
381 "bad_id": "not_a_list", # whole entry is invalid
382 "empty_id": [], # valid — no edges
383 },
384 }
385 (md / "cache").mkdir(parents=True, exist_ok=True)
386 (md / "cache" / "implicit_edges.json").write_bytes(_json.dumps(doc).encode())
387 cache = ImplicitEdgeCache.load(md)
388 good = cache.get("good_id")
389 assert good is not None and len(good) == 1
390 assert isinstance(good[0], ImplicitEntryEdge)
391 assert cache.get("bad_id") is None
392 assert cache.get("empty_id") == []
393
394 def test_edge_missing_required_field_skipped(self, tmp_path: pathlib.Path) -> None:
395 import json as _json
396 from muse.core.implicit_edge_cache import ImplicitEdgeCache, _CACHE_VERSION
397 md = _muse_dir(tmp_path)
398 bad_edge = {"framework_id": "fastapi", "symbol_address": "a.py::fn"} # missing kind
399 doc = {"version": _CACHE_VERSION, "entries": {"bad": [bad_edge]}}
400 (md / "cache").mkdir(parents=True, exist_ok=True)
401 (md / "cache" / "implicit_edges.json").write_bytes(_json.dumps(doc).encode())
402 assert ImplicitEdgeCache.load(md).get("bad") is None
403
404 def test_edge_non_str_field_skipped(self, tmp_path: pathlib.Path) -> None:
405 import json as _json
406 from muse.core.implicit_edge_cache import ImplicitEdgeCache, _CACHE_VERSION
407 md = _muse_dir(tmp_path)
408 bad_edge = {
409 "framework_id": 123, # must be str
410 "symbol_address": "a.py::fn",
411 "kind": "http-route",
412 "metadata": {},
413 }
414 doc = {"version": _CACHE_VERSION, "entries": {"bad": [bad_edge]}}
415 (md / "cache").mkdir(parents=True, exist_ok=True)
416 (md / "cache" / "implicit_edges.json").write_bytes(_json.dumps(doc).encode())
417 assert ImplicitEdgeCache.load(md).get("bad") is None
418
419
420 # ---------------------------------------------------------------------------
421 # TestLoadImplicitEdgeCache — convenience helper
422 # ---------------------------------------------------------------------------
423
424
425 class TestLoadImplicitEdgeCache:
426
427 def test_no_muse_dir_returns_empty(self, tmp_path: pathlib.Path) -> None:
428 from muse.core.implicit_edge_cache import load_implicit_edge_cache
429 assert load_implicit_edge_cache(tmp_path).size == 0
430
431 def test_with_muse_dir_loads_existing(self, tmp_path: pathlib.Path) -> None:
432 from muse.core.implicit_edge_cache import ImplicitEdgeCache, load_implicit_edge_cache
433 md = _muse_dir(tmp_path)
434 seed = ImplicitEdgeCache.load(md)
435 seed.put("myid", [_edge("m.py::fn")])
436 seed.save()
437 cache = load_implicit_edge_cache(tmp_path)
438 result = cache.get("myid")
439 assert result is not None and result[0].symbol_address == "m.py::fn"
440
441 def test_with_empty_muse_dir_returns_empty(self, tmp_path: pathlib.Path) -> None:
442 from muse.core.implicit_edge_cache import load_implicit_edge_cache
443 _muse_dir(tmp_path)
444 assert load_implicit_edge_cache(tmp_path).size == 0
445
446
447 # ---------------------------------------------------------------------------
448 # TestBuildImplicitEdgeGraphWithCache — integration
449 # ---------------------------------------------------------------------------
450
451
452 _FASTAPI_SOURCE = textwrap.dedent("""\
453 from fastapi import APIRouter
454 router = APIRouter()
455
456 @router.get("/items")
457 def list_items():
458 return []
459
460 @router.post("/items")
461 def create_item():
462 return {}
463 """)
464
465 _NO_FRAMEWORK_SOURCE = textwrap.dedent("""\
466 def compute(x):
467 return x * 2
468
469 def validate(x):
470 return x > 0
471 """)
472
473
474 class TestBuildImplicitEdgeGraphWithCache:
475 """build_implicit_edge_graph accepts an optional ImplicitEdgeCache."""
476
477 def _repo(self, tmp_path: pathlib.Path) -> pathlib.Path:
478 _muse_dir(tmp_path)
479 return tmp_path
480
481 def test_cold_cache_none_correct_graph(self, tmp_path: pathlib.Path) -> None:
482 """Without a cache, the graph is correct."""
483 root = self._repo(tmp_path)
484 manifest = _make_manifest(root, {"routes.py": _FASTAPI_SOURCE})
485
486 from muse.plugins.code._framework import build_implicit_edge_graph
487 graph = build_implicit_edge_graph(root, manifest)
488 assert any("list_items" in addr or "create_item" in addr for addr in graph)
489
490 def test_explicit_cache_hit_skips_read_object(self, tmp_path: pathlib.Path) -> None:
491 """When the cache has an entry for object_id, read_object is not called."""
492 from muse.core.implicit_edge_cache import ImplicitEdgeCache
493 from muse.plugins.code._framework import build_implicit_edge_graph
494
495 root = self._repo(tmp_path)
496 oid = _write_blob(root, _FASTAPI_SOURCE)
497 manifest = {"routes.py": oid}
498
499 # Pre-populate with a known result
500 cache = ImplicitEdgeCache.empty()
501 cache.put(oid, [_edge("routes.py::list_items")])
502
503 with patch("muse.plugins.code._framework.read_object") as mock_read:
504 build_implicit_edge_graph(root, manifest, implicit_cache=cache)
505 mock_read.assert_not_called()
506
507 def test_cache_miss_calls_read_object(self, tmp_path: pathlib.Path) -> None:
508 """On a cache miss, read_object IS called."""
509 from muse.core.implicit_edge_cache import ImplicitEdgeCache
510 from muse.plugins.code._framework import build_implicit_edge_graph
511
512 root = self._repo(tmp_path)
513 oid = _write_blob(root, _FASTAPI_SOURCE)
514 manifest = {"routes.py": oid}
515
516 cache = ImplicitEdgeCache.empty()
517 with patch(
518 "muse.plugins.code._framework.read_object",
519 wraps=__import__("muse.core.object_store", fromlist=["read_object"]).read_object,
520 ) as mock_read:
521 build_implicit_edge_graph(root, manifest, implicit_cache=cache)
522 assert mock_read.call_count >= 1
523
524 def test_cache_populated_after_build(self, tmp_path: pathlib.Path) -> None:
525 """After build, the cache has an entry for each processed Python file."""
526 from muse.core.implicit_edge_cache import ImplicitEdgeCache
527 from muse.plugins.code._framework import build_implicit_edge_graph
528
529 root = self._repo(tmp_path)
530 oid = _write_blob(root, _FASTAPI_SOURCE)
531 manifest = {"routes.py": oid}
532
533 cache = ImplicitEdgeCache.empty()
534 build_implicit_edge_graph(root, manifest, implicit_cache=cache)
535 assert cache.get(oid) is not None
536
537 def test_no_framework_file_cached_as_empty_list(self, tmp_path: pathlib.Path) -> None:
538 """Plain Python files (no framework) are cached with an empty list."""
539 from muse.core.implicit_edge_cache import ImplicitEdgeCache
540 from muse.plugins.code._framework import build_implicit_edge_graph
541
542 root = self._repo(tmp_path)
543 oid = _write_blob(root, _NO_FRAMEWORK_SOURCE)
544 manifest = {"utils.py": oid}
545
546 cache = ImplicitEdgeCache.empty()
547 build_implicit_edge_graph(root, manifest, implicit_cache=cache)
548 result = cache.get(oid)
549 assert result == []
550
551 def test_warm_graph_equals_cold_graph(self, tmp_path: pathlib.Path) -> None:
552 """Warm-cache graph is identical to the cold-path graph."""
553 from muse.core.implicit_edge_cache import ImplicitEdgeCache
554 from muse.plugins.code._framework import build_implicit_edge_graph
555
556 root = self._repo(tmp_path)
557 manifest = _make_manifest(root, {
558 "routes.py": _FASTAPI_SOURCE,
559 "utils.py": _NO_FRAMEWORK_SOURCE,
560 })
561
562 cold_graph = build_implicit_edge_graph(root, manifest)
563
564 cache = ImplicitEdgeCache.empty()
565 build_implicit_edge_graph(root, manifest, implicit_cache=cache) # populate
566 warm_graph = build_implicit_edge_graph(root, manifest, implicit_cache=cache) # warm
567
568 assert set(cold_graph.keys()) == set(warm_graph.keys())
569 for addr in cold_graph:
570 assert sorted(cold_graph[addr], key=lambda e: e.symbol_address) == \
571 sorted(warm_graph[addr], key=lambda e: e.symbol_address)
572
573 def test_non_python_files_not_cached(self, tmp_path: pathlib.Path) -> None:
574 """Non-Python files are skipped and not cached."""
575 from muse.core.implicit_edge_cache import ImplicitEdgeCache
576 from muse.plugins.code._framework import build_implicit_edge_graph
577
578 root = self._repo(tmp_path)
579 md_oid = _write_blob(root, "# README\n")
580 py_oid = _write_blob(root, _NO_FRAMEWORK_SOURCE)
581 manifest = {"README.md": md_oid, "utils.py": py_oid}
582
583 cache = ImplicitEdgeCache.empty()
584 build_implicit_edge_graph(root, manifest, implicit_cache=cache)
585
586 assert cache.get(md_oid) is None # skipped — not Python
587 assert cache.get(py_oid) is not None # processed
588
589 def test_build_does_not_call_cache_save(self, tmp_path: pathlib.Path) -> None:
590 """build_implicit_edge_graph must not call save() — caller's responsibility."""
591 from muse.core.implicit_edge_cache import ImplicitEdgeCache
592 from muse.plugins.code._framework import build_implicit_edge_graph
593
594 root = self._repo(tmp_path)
595 oid = _write_blob(root, _NO_FRAMEWORK_SOURCE)
596 manifest = {"utils.py": oid}
597
598 md = _muse_dir(tmp_path)
599 cache = ImplicitEdgeCache.load(md)
600 build_implicit_edge_graph(root, manifest, implicit_cache=cache)
601 assert not (md / "cache" / "implicit_edges.json").is_file()
602
603 def test_second_call_skips_read_object(self, tmp_path: pathlib.Path) -> None:
604 """On the second call with a warm cache, read_object is never called."""
605 from muse.core.implicit_edge_cache import ImplicitEdgeCache
606 from muse.plugins.code._framework import build_implicit_edge_graph
607
608 root = self._repo(tmp_path)
609 oid = _write_blob(root, _FASTAPI_SOURCE)
610 manifest = {"routes.py": oid}
611
612 cache = ImplicitEdgeCache.empty()
613 build_implicit_edge_graph(root, manifest, implicit_cache=cache) # cold
614
615 with patch("muse.plugins.code._framework.read_object") as mock_read:
616 build_implicit_edge_graph(root, manifest, implicit_cache=cache) # warm
617 mock_read.assert_not_called()
618
619
620 # ---------------------------------------------------------------------------
621 # TestImplicitEdgeCachePerformance
622 # ---------------------------------------------------------------------------
623
624
625 class TestImplicitEdgeCachePerformance:
626 """Warm cache must be substantially faster than cold for a multi-file repo."""
627
628 def _build_repo(self, tmp_path: pathlib.Path, n_files: int = 30) -> tuple[pathlib.Path, Manifest]:
629 root = tmp_path
630 _muse_dir(root)
631 manifest: Manifest = {}
632 for i in range(n_files):
633 # Alternate between FastAPI files and plain Python files
634 if i % 3 == 0:
635 src = textwrap.dedent(f"""\
636 from fastapi import APIRouter
637 router = APIRouter()
638
639 @router.get("/resource_{i}")
640 def get_{i}():
641 return {{}}
642
643 @router.post("/resource_{i}")
644 def post_{i}():
645 return {{}}
646 """)
647 else:
648 src = f"def helper_{i}(x):\n return x * {i}\n"
649 oid = _write_blob(root, src)
650 manifest[f"mod_{i}.py"] = oid
651 return root, manifest
652
653 def test_warm_cache_at_least_5x_faster(self, tmp_path: pathlib.Path) -> None:
654 from muse.core.implicit_edge_cache import ImplicitEdgeCache
655 from muse.plugins.code._framework import build_implicit_edge_graph
656
657 root, manifest = self._build_repo(tmp_path, n_files=30)
658
659 cache = ImplicitEdgeCache.empty()
660 t0 = time.perf_counter()
661 build_implicit_edge_graph(root, manifest, implicit_cache=cache)
662 cold_ms = (time.perf_counter() - t0) * 1000
663
664 t1 = time.perf_counter()
665 build_implicit_edge_graph(root, manifest, implicit_cache=cache)
666 warm_ms = (time.perf_counter() - t1) * 1000
667
668 assert warm_ms < cold_ms / 5, (
669 f"Warm ({warm_ms:.1f} ms) should be ≥5× faster than cold ({cold_ms:.1f} ms)"
670 )
671
672 def test_warm_under_200ms_for_30_files(self, tmp_path: pathlib.Path) -> None:
673 from muse.core.implicit_edge_cache import ImplicitEdgeCache
674 from muse.plugins.code._framework import build_implicit_edge_graph
675
676 root, manifest = self._build_repo(tmp_path, n_files=30)
677
678 cache = ImplicitEdgeCache.empty()
679 build_implicit_edge_graph(root, manifest, implicit_cache=cache) # warm
680
681 t0 = time.perf_counter()
682 build_implicit_edge_graph(root, manifest, implicit_cache=cache)
683 warm_ms = (time.perf_counter() - t0) * 1000
684
685 assert warm_ms < 200, f"Warm call took {warm_ms:.1f} ms — expected < 200 ms"
686
687 def test_graph_correctness_not_degraded(self, tmp_path: pathlib.Path) -> None:
688 from muse.core.implicit_edge_cache import ImplicitEdgeCache
689 from muse.plugins.code._framework import build_implicit_edge_graph
690
691 root, manifest = self._build_repo(tmp_path, n_files=10)
692
693 cold_graph = build_implicit_edge_graph(root, manifest)
694
695 cache = ImplicitEdgeCache.empty()
696 build_implicit_edge_graph(root, manifest, implicit_cache=cache)
697 warm_graph = build_implicit_edge_graph(root, manifest, implicit_cache=cache)
698
699 assert set(cold_graph.keys()) == set(warm_graph.keys()), (
700 f"Keys differ:\n cold={sorted(cold_graph)}\n warm={sorted(warm_graph)}"
701 )
702 for addr in cold_graph:
703 cold_sorted = sorted(cold_graph[addr], key=lambda e: e.symbol_address)
704 warm_sorted = sorted(warm_graph[addr], key=lambda e: e.symbol_address)
705 assert cold_sorted == warm_sorted
File History 4 commits
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 22 days ago
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e fix: rename objects→blobs in push client and all stale test… Sonnet 4.6 patch 23 days ago
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 29 days ago
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor 30 days ago