gabriel / muse public

test_cmd_test.py file-level

at sha256:c · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:c feat(code-test): audit cache internals, add cache_hit/--cache-status, c… · gabriel · Oct 22, 2026
1 """End-to-end CLI tests for ``muse code test``.
2
3 Coverage:
4 - ``muse code test --history`` prints history header (no test runs).
5 - ``muse code test --flaky`` prints "No flaky tests found" when history empty.
6 - ``muse code test --dry-run`` prints targets without running pytest.
7 - ``muse code test --dry-run --json`` emits valid JSON.
8 - ``muse code test --all`` runs full pytest discovery on an explicit file.
9 - ``muse code test <file>`` runs a specific file.
10 - ``muse code test --ci`` executes CI gate suite.
11 - ``muse code test --ci --json`` emits valid JSON CI result.
12 - ``muse code test --json`` emits valid JSON run result.
13 - History is persisted to .muse/cache/test_history.json after a run.
14 - ``--no-save`` does not write history.
15 """
16
17 from __future__ import annotations
18
19 import hashlib
20 import json
21 import pathlib
22 import sys
23
24 import pytest
25
26 from tests.cli_test_helper import CliRunner
27 from muse.core.paths import muse_dir, test_history_path as _test_history_path
28
29 runner = CliRunner()
30 cli = None # argparse migration — CliRunner ignores this arg
31
32
33 def _env(root: pathlib.Path) -> Manifest:
34 return {"MUSE_REPO_ROOT": str(root)}
35
36
37 # ---------------------------------------------------------------------------
38 # Fixtures
39 # ---------------------------------------------------------------------------
40
41
42 @pytest.fixture()
43 def repo(tmp_path: pathlib.Path) -> pathlib.Path:
44 """Create a minimal Muse repo with .muse/ and a passing test file."""
45 import datetime
46
47 from muse.core.commits import (
48 CommitRecord,
49 write_commit,
50 )
51 from muse.core.snapshots import (
52 SnapshotRecord,
53 write_snapshot,
54 )
55 from muse.core.ids import hash_commit, hash_snapshot
56 from muse.core.object_store import write_object
57
58 dot_muse = muse_dir(tmp_path)
59 dot_muse.mkdir()
60
61 repo_id = "test-repo"
62 (dot_muse / "repo.json").write_text(
63 '{"id": "test-repo", "name": "test"}'
64 )
65
66 from muse.core.types import blob_id
67 test_src = b"def test_passes() -> None:\n assert True\n"
68 oid = blob_id(test_src)
69 write_object(tmp_path, oid, test_src)
70
71 tests_dir = tmp_path / "tests"
72 tests_dir.mkdir()
73 (tests_dir / "test_simple.py").write_bytes(test_src)
74
75 manifest: Manifest = {"tests/test_simple.py": oid}
76 snap_id = hash_snapshot(manifest)
77 snap = SnapshotRecord(
78 snapshot_id=snap_id,
79 manifest=manifest,
80 )
81 write_snapshot(tmp_path, snap)
82
83 committed_at = datetime.datetime(2026, 3, 26, 12, 0, 0, tzinfo=datetime.timezone.utc)
84 commit_id = hash_commit(
85 parent_ids=[],
86 snapshot_id=snap_id,
87 message="init",
88 committed_at_iso=committed_at.isoformat(),
89 author="test",
90 )
91 commit = CommitRecord(
92 commit_id=commit_id,
93 branch="main",
94 snapshot_id=snap_id,
95 message="init",
96 committed_at=committed_at,
97 author="test",
98 )
99 write_commit(tmp_path, commit)
100
101 refs_dir = dot_muse / "refs" / "heads"
102 refs_dir.mkdir(parents=True)
103 (refs_dir / "main").write_text(commit_id)
104 (dot_muse / "HEAD").write_text("ref: refs/heads/main\n")
105
106 return tmp_path
107
108
109 # ---------------------------------------------------------------------------
110 # History mode
111 # ---------------------------------------------------------------------------
112
113
114 class TestHistoryCommand:
115 def test_history_empty(self, repo: pathlib.Path) -> None:
116 """--history with no runs prints a sensible message."""
117 result = runner.invoke(cli, ["code", "test", "--history"], env=_env(repo))
118 assert result.exit_code == 0
119 assert "No test history recorded." in result.output
120
121 def test_history_json(self, repo: pathlib.Path) -> None:
122 """--history --json emits a JSON object."""
123 result = runner.invoke(cli, ["code", "test", "--history", "--json"], env=_env(repo))
124 assert result.exit_code == 0
125 data = json.loads(result.output)
126 assert data["mode"] == "history"
127 assert "history" in data
128
129 def test_flaky_empty(self, repo: pathlib.Path) -> None:
130 """--flaky with no history prints no-flaky message."""
131 result = runner.invoke(cli, ["code", "test", "--flaky"], env=_env(repo))
132 assert result.exit_code == 0
133 assert "No flaky tests found." in result.output
134
135
136 # ---------------------------------------------------------------------------
137 # Dry-run mode
138 # ---------------------------------------------------------------------------
139
140
141 class TestDryRun:
142 def test_dry_run_text(self, repo: pathlib.Path) -> None:
143 """--dry-run --all prints targets without executing pytest."""
144 result = runner.invoke(
145 cli, ["code", "test", "--dry-run", "--all"], env=_env(repo)
146 )
147 assert result.exit_code == 0
148 assert "Would run" in result.output or "pytest" in result.output.lower()
149
150 def test_dry_run_json(self, repo: pathlib.Path) -> None:
151 """--dry-run --json emits valid JSON."""
152 result = runner.invoke(
153 cli, ["code", "test", "--dry-run", "--all", "--json"], env=_env(repo)
154 )
155 assert result.exit_code == 0
156 data = json.loads(result.output)
157 assert data["mode"] == "dry-run"
158
159 def test_dry_run_with_symbol(self, repo: pathlib.Path) -> None:
160 """--dry-run --symbol emits valid JSON with selection."""
161 result = runner.invoke(
162 cli,
163 [
164 "code",
165 "test",
166 "--dry-run",
167 "--symbol",
168 "tests/test_simple.py::test_passes",
169 "--json",
170 ],
171 env=_env(repo),
172 )
173 assert result.exit_code == 0
174 data = json.loads(result.output)
175 assert data["mode"] == "dry-run"
176
177
178 # ---------------------------------------------------------------------------
179 # Execution mode
180 # ---------------------------------------------------------------------------
181
182
183 class TestRunTests:
184 def test_run_specific_file_passes(self, repo: pathlib.Path) -> None:
185 """Running a specific passing test file exits 0."""
186 result = runner.invoke(
187 cli,
188 ["code", "test", str(repo / "tests" / "test_simple.py")],
189 env=_env(repo),
190 )
191 assert result.exit_code == 0
192
193 def test_run_json_mode(self, repo: pathlib.Path) -> None:
194 """--json emits a valid JSON run result."""
195 result = runner.invoke(
196 cli,
197 [
198 "code", "test",
199 str(repo / "tests" / "test_simple.py"),
200 "--json",
201 ],
202 env=_env(repo),
203 )
204 assert result.exit_code == 0
205 # CliRunner combines stdout + stderr; _progress_cb writes dots to stderr
206 # after the JSON block. Extract the JSON object directly.
207 raw = result.output
208 json_start = raw.index("{")
209 json_end = raw.rindex("}") + 1
210 data = json.loads(raw[json_start:json_end])
211 assert data["mode"] == "run"
212 assert "run" in data
213 run_data = data["run"]
214 assert run_data["passed"] >= 1
215 assert run_data["exit_code"] == 0
216
217 def test_failing_test_exits_nonzero(self, repo: pathlib.Path) -> None:
218 """A failing test produces exit_code != 0."""
219 fail_file = repo / "tests" / "test_fail.py"
220 fail_file.write_text(
221 "def test_intentional_fail() -> None:\n assert False\n"
222 )
223 result = runner.invoke(
224 cli,
225 ["code", "test", str(fail_file)],
226 env=_env(repo),
227 )
228 assert result.exit_code != 0
229
230
231 # ---------------------------------------------------------------------------
232 # History persistence
233 # ---------------------------------------------------------------------------
234
235
236 class TestHistoryPersistence:
237 def test_history_saved_after_run(self, repo: pathlib.Path) -> None:
238 """After a successful run, .muse/cache/test_history.json exists."""
239 runner.invoke(
240 cli,
241 ["code", "test", str(repo / "tests" / "test_simple.py")],
242 env=_env(repo),
243 )
244 assert _test_history_path(repo).exists()
245
246 def test_no_save_skips_history(self, repo: pathlib.Path) -> None:
247 """--no-save prevents history file creation."""
248 runner.invoke(
249 cli,
250 ["code", "test", str(repo / "tests" / "test_simple.py"), "--no-save"],
251 env=_env(repo),
252 )
253 assert not _test_history_path(repo).exists()
254
255
256 # ---------------------------------------------------------------------------
257 # CI mode
258 # ---------------------------------------------------------------------------
259
260
261 class TestCiMode:
262 def test_ci_with_passing_gate(self, repo: pathlib.Path) -> None:
263 """--ci with an echo gate passes."""
264 toml = (
265 "version = 1\n\n"
266 "[[gate]]\n"
267 'name = "echo"\n'
268 'command = ["echo", "hello"]\n'
269 "timeout_s = 5\n"
270 "required = true\n"
271 )
272 (muse_dir(repo) / "ci.toml").write_text(toml)
273 result = runner.invoke(cli, ["code", "test", "--ci"], env=_env(repo))
274 assert result.exit_code == 0
275
276 def test_ci_json_structure(self, repo: pathlib.Path) -> None:
277 """--ci --json emits a valid CI result."""
278 toml = (
279 "version = 1\n\n"
280 "[[gate]]\n"
281 'name = "echo"\n'
282 'command = ["echo", "ok"]\n'
283 "timeout_s = 5\n"
284 "required = true\n"
285 )
286 (muse_dir(repo) / "ci.toml").write_text(toml)
287 result = runner.invoke(
288 cli, ["code", "test", "--ci", "--json"], env=_env(repo)
289 )
290 assert result.exit_code == 0
291 data = json.loads(result.output)
292 assert data["mode"] == "ci"
293 assert "ci" in data
294 ci = data["ci"]
295 assert isinstance(ci["passed"], bool)
296 assert isinstance(ci["gates"], list)
297
298 def test_ci_failing_gate_exits_nonzero(self, repo: pathlib.Path) -> None:
299 """--ci with a failing gate exits non-zero."""
300 toml = (
301 "version = 1\n\n"
302 "[[gate]]\n"
303 'name = "fail"\n'
304 f'command = ["{sys.executable}", "-c", "raise SystemExit(1)"]\n'
305 "timeout_s = 5\n"
306 "required = true\n"
307 )
308 (muse_dir(repo) / "ci.toml").write_text(toml)
309 result = runner.invoke(cli, ["code", "test", "--ci"], env=_env(repo))
310 assert result.exit_code != 0
311
312
313 # ---------------------------------------------------------------------------
314 # Cache-status mode (muse#49 Phase 4)
315 # ---------------------------------------------------------------------------
316
317
318 class TestCacheStatusCommand:
319 def test_cache_status_text(self, repo: pathlib.Path) -> None:
320 """--cache-status prints a status report and exits 0 without running tests."""
321 result = runner.invoke(cli, ["code", "test", "--cache-status"], env=_env(repo))
322 assert result.exit_code == 0
323 assert "Test-selection cache status" in result.output
324 assert "HEAD snapshot:" in result.output
325
326 def test_cache_status_json(self, repo: pathlib.Path) -> None:
327 """--cache-status --json emits the honest content-addressed schema."""
328 result = runner.invoke(
329 cli, ["code", "test", "--cache-status", "--json"], env=_env(repo)
330 )
331 assert result.exit_code == 0
332 data = json.loads(result.output)
333 assert data["mode"] == "cache-status"
334 status = data["cache_status"]
335 for key in (
336 "cache_hit", "stale", "head_snapshot_id", "semantic_files_total",
337 "files_warm", "files_cold", "symbol_cache_entries",
338 "callgraph_cache_entries", "symbol_cache_age_seconds",
339 "callgraph_cache_age_seconds",
340 ):
341 assert key in status
342
343 def test_cache_status_before_any_run_reports_cold(self, repo: pathlib.Path) -> None:
344 """A committed repo with no prior select_tests call reports cold/stale."""
345 result = runner.invoke(
346 cli, ["code", "test", "--cache-status", "--json"], env=_env(repo)
347 )
348 assert result.exit_code == 0
349 status = json.loads(result.output)["cache_status"]
350 assert status["cache_hit"] is False
351 assert status["stale"] is True
352 assert status["files_cold"] >= 1
353
354 def test_cache_status_after_run_reports_warm(self, repo: pathlib.Path) -> None:
355 """Running the real selection warms the cache; --cache-status reflects it."""
356 first = runner.invoke(
357 cli,
358 ["code", "test", "--dry-run", "--symbol", "tests/test_simple.py::test_passes"],
359 env=_env(repo),
360 )
361 assert first.exit_code == 0, first.output
362
363 result = runner.invoke(
364 cli, ["code", "test", "--cache-status", "--json"], env=_env(repo)
365 )
366 assert result.exit_code == 0
367 status = json.loads(result.output)["cache_status"]
368 assert status["symbol_cache_entries"] > 0
369
370
371 class TestSymbolCachePersistedAfterRealRun:
372 """muse#49 Phase 3 gap fix: previously, a real ``muse code test`` CLI
373 invocation always loaded its own SymbolCache instance to share between
374 changed_symbols_from_diff and select_tests (avoiding a double-load), but
375 never saved it afterward — only CallGraphCache was ever persisted. Every
376 real CLI run therefore silently re-parsed every file's symbol tree from
377 scratch, only ever warming the call-graph cache.
378 """
379
380 def test_symbol_cache_file_exists_after_forced_symbol_run(
381 self, repo: pathlib.Path
382 ) -> None:
383 from muse.core.paths import symbol_cache_path
384
385 assert not symbol_cache_path(repo).exists()
386
387 result = runner.invoke(
388 cli,
389 ["code", "test", "--dry-run", "--symbol", "tests/test_simple.py::test_passes"],
390 env=_env(repo),
391 )
392 assert result.exit_code == 0, result.output
393 assert symbol_cache_path(repo).exists()
394
395 def test_symbol_cache_file_exists_after_default_diff_run(
396 self, repo: pathlib.Path
397 ) -> None:
398 """Covers the default (working-tree-diff) selection path too."""
399 from muse.core.paths import symbol_cache_path
400
401 (repo / "tests" / "test_simple.py").write_text(
402 "def test_passes() -> None:\n assert True\n\n"
403 "def test_new() -> None:\n assert 1 == 1\n"
404 )
405 result = runner.invoke(cli, ["code", "test", "--dry-run"], env=_env(repo))
406 assert result.exit_code == 0, result.output
407 assert symbol_cache_path(repo).exists()