gabriel / muse public
test_cmd_test.py python
310 lines 10.3 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
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/test_history.msgpack 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
28 runner = CliRunner()
29 cli = None # argparse migration — CliRunner ignores this arg
30
31
32 def _env(root: pathlib.Path) -> Manifest:
33 return {"MUSE_REPO_ROOT": str(root)}
34
35
36 # ---------------------------------------------------------------------------
37 # Fixtures
38 # ---------------------------------------------------------------------------
39
40
41 @pytest.fixture()
42 def repo(tmp_path: pathlib.Path) -> pathlib.Path:
43 """Create a minimal Muse repo with .muse/ and a passing test file."""
44 import datetime
45
46 from muse.core.store import (
47 CommitRecord,
48 SnapshotRecord,
49 write_commit,
50 write_snapshot,
51 )
52 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
53 from muse.core.object_store import write_object
54
55 muse_dir = tmp_path / ".muse"
56 muse_dir.mkdir()
57
58 repo_id = "test-repo"
59 (muse_dir / "repo.json").write_text(
60 '{"id": "test-repo", "name": "test"}'
61 )
62
63 from muse.core._types import blob_id
64 test_src = b"def test_passes() -> None:\n assert True\n"
65 oid = blob_id(test_src)
66 write_object(tmp_path, oid, test_src)
67
68 tests_dir = tmp_path / "tests"
69 tests_dir.mkdir()
70 (tests_dir / "test_simple.py").write_bytes(test_src)
71
72 manifest: Manifest = {"tests/test_simple.py": oid}
73 snap_id = compute_snapshot_id(manifest)
74 snap = SnapshotRecord(
75 snapshot_id=snap_id,
76 manifest=manifest,
77 )
78 write_snapshot(tmp_path, snap)
79
80 committed_at = datetime.datetime(2026, 3, 26, 12, 0, 0, tzinfo=datetime.timezone.utc)
81 commit_id = compute_commit_id(
82 repo_id=repo_id,
83 parent_ids=[],
84 snapshot_id=snap_id,
85 message="init",
86 committed_at_iso=committed_at.isoformat(),
87 author="test",)
88 commit = CommitRecord(
89 commit_id=commit_id,
90 repo_id=repo_id,
91 created_on_branch="main",
92 snapshot_id=snap_id,
93 message="init",
94 committed_at=committed_at,
95 author="test",
96 )
97 write_commit(tmp_path, commit)
98
99 refs_dir = muse_dir / "refs" / "heads"
100 refs_dir.mkdir(parents=True)
101 (refs_dir / "main").write_text(commit_id)
102 (muse_dir / "HEAD").write_text("ref: refs/heads/main\n")
103
104 return tmp_path
105
106
107 # ---------------------------------------------------------------------------
108 # History mode
109 # ---------------------------------------------------------------------------
110
111
112 class TestHistoryCommand:
113 def test_history_empty(self, repo: pathlib.Path) -> None:
114 """--history with no runs prints a sensible message."""
115 result = runner.invoke(cli, ["code", "test", "--history"], env=_env(repo))
116 assert result.exit_code == 0
117 assert "No test history recorded." in result.output
118
119 def test_history_json(self, repo: pathlib.Path) -> None:
120 """--history --json emits a JSON object."""
121 result = runner.invoke(cli, ["code", "test", "--history", "--json"], env=_env(repo))
122 assert result.exit_code == 0
123 data = json.loads(result.output)
124 assert data["mode"] == "history"
125 assert "history" in data
126
127 def test_flaky_empty(self, repo: pathlib.Path) -> None:
128 """--flaky with no history prints no-flaky message."""
129 result = runner.invoke(cli, ["code", "test", "--flaky"], env=_env(repo))
130 assert result.exit_code == 0
131 assert "No flaky tests found." in result.output
132
133
134 # ---------------------------------------------------------------------------
135 # Dry-run mode
136 # ---------------------------------------------------------------------------
137
138
139 class TestDryRun:
140 def test_dry_run_text(self, repo: pathlib.Path) -> None:
141 """--dry-run --all prints targets without executing pytest."""
142 result = runner.invoke(
143 cli, ["code", "test", "--dry-run", "--all"], env=_env(repo)
144 )
145 assert result.exit_code == 0
146 assert "Would run" in result.output or "pytest" in result.output.lower()
147
148 def test_dry_run_json(self, repo: pathlib.Path) -> None:
149 """--dry-run --json emits valid JSON."""
150 result = runner.invoke(
151 cli, ["code", "test", "--dry-run", "--all", "--json"], env=_env(repo)
152 )
153 assert result.exit_code == 0
154 data = json.loads(result.output)
155 assert data["mode"] == "dry-run"
156
157 def test_dry_run_with_symbol(self, repo: pathlib.Path) -> None:
158 """--dry-run --symbol emits valid JSON with selection."""
159 result = runner.invoke(
160 cli,
161 [
162 "code",
163 "test",
164 "--dry-run",
165 "--symbol",
166 "tests/test_simple.py::test_passes",
167 "--json",
168 ],
169 env=_env(repo),
170 )
171 assert result.exit_code == 0
172 data = json.loads(result.output)
173 assert data["mode"] == "dry-run"
174
175
176 # ---------------------------------------------------------------------------
177 # Execution mode
178 # ---------------------------------------------------------------------------
179
180
181 class TestRunTests:
182 def test_run_specific_file_passes(self, repo: pathlib.Path) -> None:
183 """Running a specific passing test file exits 0."""
184 result = runner.invoke(
185 cli,
186 ["code", "test", str(repo / "tests" / "test_simple.py")],
187 env=_env(repo),
188 )
189 assert result.exit_code == 0
190
191 def test_run_json_mode(self, repo: pathlib.Path) -> None:
192 """--json emits a valid JSON run result."""
193 result = runner.invoke(
194 cli,
195 [
196 "code", "test",
197 str(repo / "tests" / "test_simple.py"),
198 "--json",
199 ],
200 env=_env(repo),
201 )
202 assert result.exit_code == 0
203 # CliRunner combines stdout + stderr; _progress_cb writes dots to stderr
204 # after the JSON block. Extract the JSON object directly.
205 raw = result.output
206 json_start = raw.index("{")
207 json_end = raw.rindex("}") + 1
208 data = json.loads(raw[json_start:json_end])
209 assert data["mode"] == "run"
210 assert "run" in data
211 run_data = data["run"]
212 assert run_data["passed"] >= 1
213 assert run_data["exit_code"] == 0
214
215 def test_failing_test_exits_nonzero(self, repo: pathlib.Path) -> None:
216 """A failing test produces exit_code != 0."""
217 fail_file = repo / "tests" / "test_fail.py"
218 fail_file.write_text(
219 "def test_intentional_fail() -> None:\n assert False\n"
220 )
221 result = runner.invoke(
222 cli,
223 ["code", "test", str(fail_file)],
224 env=_env(repo),
225 )
226 assert result.exit_code != 0
227
228
229 # ---------------------------------------------------------------------------
230 # History persistence
231 # ---------------------------------------------------------------------------
232
233
234 class TestHistoryPersistence:
235 def test_history_saved_after_run(self, repo: pathlib.Path) -> None:
236 """After a successful run, .muse/test_history.msgpack exists."""
237 runner.invoke(
238 cli,
239 ["code", "test", str(repo / "tests" / "test_simple.py")],
240 env=_env(repo),
241 )
242 hist_path = repo / ".muse" / "test_history.msgpack"
243 assert hist_path.exists()
244
245 def test_no_save_skips_history(self, repo: pathlib.Path) -> None:
246 """--no-save prevents history file creation."""
247 runner.invoke(
248 cli,
249 ["code", "test", str(repo / "tests" / "test_simple.py"), "--no-save"],
250 env=_env(repo),
251 )
252 hist_path = repo / ".muse" / "test_history.msgpack"
253 assert not hist_path.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 (repo / ".muse" / "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 (repo / ".muse" / "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 (repo / ".muse" / "ci.toml").write_text(toml)
309 result = runner.invoke(cli, ["code", "test", "--ci"], env=_env(repo))
310 assert result.exit_code != 0
File History 3 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 137 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 140 days ago