"""TDD tests for muse#49 Phase 2 — cache_hit/cache_misses on SelectionResult. ``select_tests`` now threads a mutable ``cache_stats`` dict through ``symbols_for_snapshot`` and ``build_forward_graph`` (both accept an optional ``stats=`` kwarg that increments hit/miss counters in place). The final ``SelectionResult`` exposes ``cache_hit`` (True only when zero misses occurred across both caches) and ``cache_misses`` (the raw count). These tests prove the *mechanism* — that the reported values trace back to real cache misses in the underlying cache objects — rather than asserting only on final selection output, which would pass trivially even if the stats wiring were a no-op (test-selection correctness is independent of cache warmth; only performance differs). """ from __future__ import annotations import logging import pathlib import pytest from muse.core.callgraph_cache import CallGraphCache from muse.core.object_store import write_object from muse.core.paths import muse_dir from muse.core.symbol_cache import SymbolCache from muse.core.test_selection import ChangedSymbol, select_tests from muse.core.types import Manifest, blob_id def _make_repo(tmp_path: pathlib.Path) -> tuple[pathlib.Path, Manifest]: dot_muse = muse_dir(tmp_path) dot_muse.mkdir(exist_ok=True) prod_src = b"""\ def compute(x: int) -> int: return x * 2 """ test_src = b"""\ from prod import compute def test_compute() -> None: assert compute(2) == 4 """ prod_oid = blob_id(prod_src) test_oid = blob_id(test_src) write_object(tmp_path, prod_oid, prod_src) write_object(tmp_path, test_oid, test_src) (tmp_path / "prod.py").write_bytes(prod_src) tests_dir = tmp_path / "tests" tests_dir.mkdir(exist_ok=True) (tests_dir / "test_prod.py").write_bytes(test_src) manifest: Manifest = { "prod.py": prod_oid, "tests/test_prod.py": test_oid, } return tmp_path, manifest class TestColdCacheReportsMiss: def test_first_run_cache_hit_false_with_misses(self, tmp_path: pathlib.Path) -> None: """A fully cold run (empty caches) must report cache_hit=False and cache_misses > 0 — proving the counters actually observe the misses that occur in symbols_for_snapshot/build_forward_graph, not a hardcoded value.""" root, manifest = _make_repo(tmp_path) changed = [ChangedSymbol(address="prod.py::compute", change_kind="modified")] result = select_tests( root, changed, manifest, cache=SymbolCache.empty(), callgraph_cache=CallGraphCache.empty(), ) assert result["cache_hit"] is False assert result["cache_misses"] > 0 def test_cold_run_emits_warning( self, tmp_path: pathlib.Path, caplog: pytest.LogCaptureFixture ) -> None: """CT_03: the cold-rebuild path must emit a visible warning log line, not fail silently with no feedback to the user.""" root, manifest = _make_repo(tmp_path) changed = [ChangedSymbol(address="prod.py::compute", change_kind="modified")] with caplog.at_level(logging.WARNING, logger="muse.core.test_selection"): select_tests( root, changed, manifest, cache=SymbolCache.empty(), callgraph_cache=CallGraphCache.empty(), ) assert any("cache cold" in rec.message for rec in caplog.records), ( "expected a 'cache cold' warning log on a cold-cache run" ) class TestWarmCacheReportsHit: def test_second_run_cache_hit_true_zero_misses(self, tmp_path: pathlib.Path) -> None: """Proves the mechanism directly: after warming both caches via a first select_tests call, a second call against the SAME manifest must report zero misses — i.e. the stats dict is actually wired to the real cache .get() calls, not independently computed.""" root, manifest = _make_repo(tmp_path) changed = [ChangedSymbol(address="prod.py::compute", change_kind="modified")] sym_cache = SymbolCache.empty() cg_cache = CallGraphCache.empty() first = select_tests(root, changed, manifest, cache=sym_cache, callgraph_cache=cg_cache) assert first["cache_hit"] is False # sanity: first run was indeed cold second = select_tests(root, changed, manifest, cache=sym_cache, callgraph_cache=cg_cache) assert second["cache_hit"] is True assert second["cache_misses"] == 0 def test_warm_run_emits_no_cold_warning( self, tmp_path: pathlib.Path, caplog: pytest.LogCaptureFixture ) -> None: root, manifest = _make_repo(tmp_path) changed = [ChangedSymbol(address="prod.py::compute", change_kind="modified")] sym_cache = SymbolCache.empty() cg_cache = CallGraphCache.empty() select_tests(root, changed, manifest, cache=sym_cache, callgraph_cache=cg_cache) caplog.clear() with caplog.at_level(logging.WARNING, logger="muse.core.test_selection"): select_tests(root, changed, manifest, cache=sym_cache, callgraph_cache=cg_cache) assert not any("cache cold" in rec.message for rec in caplog.records) class TestEmptyChangedAddressesShortCircuit: def test_no_changed_symbols_reports_hit_true(self, tmp_path: pathlib.Path) -> None: """The early-return path (no changed symbols) never touches either cache, so it must report a trivial cache_hit=True, cache_misses=0 — it would be misleading to claim a miss when no lookup happened.""" root, manifest = _make_repo(tmp_path) result = select_tests(root, [], manifest) assert result["cache_hit"] is True assert result["cache_misses"] == 0