gabriel / muse public
test_cmd_test.py python
309 lines 10.3 KB
Raw
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor ⚠ breaking 121 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/cache/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 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.store import (
48 CommitRecord,
49 SnapshotRecord,
50 write_commit,
51 write_snapshot,
52 )
53 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
54 from muse.core.object_store import write_object
55
56 dot_muse = muse_dir(tmp_path)
57 dot_muse.mkdir()
58
59 repo_id = "test-repo"
60 (dot_muse / "repo.json").write_text(
61 '{"id": "test-repo", "name": "test"}'
62 )
63
64 from muse.core.types import blob_id
65 test_src = b"def test_passes() -> None:\n assert True\n"
66 oid = blob_id(test_src)
67 write_object(tmp_path, oid, test_src)
68
69 tests_dir = tmp_path / "tests"
70 tests_dir.mkdir()
71 (tests_dir / "test_simple.py").write_bytes(test_src)
72
73 manifest: Manifest = {"tests/test_simple.py": oid}
74 snap_id = compute_snapshot_id(manifest)
75 snap = SnapshotRecord(
76 snapshot_id=snap_id,
77 manifest=manifest,
78 )
79 write_snapshot(tmp_path, snap)
80
81 committed_at = datetime.datetime(2026, 3, 26, 12, 0, 0, tzinfo=datetime.timezone.utc)
82 commit_id = compute_commit_id(
83 parent_ids=[],
84 snapshot_id=snap_id,
85 message="init",
86 committed_at_iso=committed_at.isoformat(),
87 author="test",
88 )
89 commit = CommitRecord(
90 commit_id=commit_id,
91 repo_id="test-repo",
92 branch="main",
93 snapshot_id=snap_id,
94 message="init",
95 committed_at=committed_at,
96 author="test",
97 )
98 write_commit(tmp_path, commit)
99
100 refs_dir = dot_muse / "refs" / "heads"
101 refs_dir.mkdir(parents=True)
102 (refs_dir / "main").write_text(commit_id)
103 (dot_muse / "HEAD").write_text("ref: refs/heads/main\n")
104
105 return tmp_path
106
107
108 # ---------------------------------------------------------------------------
109 # History mode
110 # ---------------------------------------------------------------------------
111
112
113 class TestHistoryCommand:
114 def test_history_empty(self, repo: pathlib.Path) -> None:
115 """--history with no runs prints a sensible message."""
116 result = runner.invoke(cli, ["code", "test", "--history"], env=_env(repo))
117 assert result.exit_code == 0
118 assert "No test history recorded." in result.output
119
120 def test_history_json(self, repo: pathlib.Path) -> None:
121 """--history --json emits a JSON object."""
122 result = runner.invoke(cli, ["code", "test", "--history", "--json"], env=_env(repo))
123 assert result.exit_code == 0
124 data = json.loads(result.output)
125 assert data["mode"] == "history"
126 assert "history" in data
127
128 def test_flaky_empty(self, repo: pathlib.Path) -> None:
129 """--flaky with no history prints no-flaky message."""
130 result = runner.invoke(cli, ["code", "test", "--flaky"], env=_env(repo))
131 assert result.exit_code == 0
132 assert "No flaky tests found." in result.output
133
134
135 # ---------------------------------------------------------------------------
136 # Dry-run mode
137 # ---------------------------------------------------------------------------
138
139
140 class TestDryRun:
141 def test_dry_run_text(self, repo: pathlib.Path) -> None:
142 """--dry-run --all prints targets without executing pytest."""
143 result = runner.invoke(
144 cli, ["code", "test", "--dry-run", "--all"], env=_env(repo)
145 )
146 assert result.exit_code == 0
147 assert "Would run" in result.output or "pytest" in result.output.lower()
148
149 def test_dry_run_json(self, repo: pathlib.Path) -> None:
150 """--dry-run --json emits valid JSON."""
151 result = runner.invoke(
152 cli, ["code", "test", "--dry-run", "--all", "--json"], env=_env(repo)
153 )
154 assert result.exit_code == 0
155 data = json.loads(result.output)
156 assert data["mode"] == "dry-run"
157
158 def test_dry_run_with_symbol(self, repo: pathlib.Path) -> None:
159 """--dry-run --symbol emits valid JSON with selection."""
160 result = runner.invoke(
161 cli,
162 [
163 "code",
164 "test",
165 "--dry-run",
166 "--symbol",
167 "tests/test_simple.py::test_passes",
168 "--json",
169 ],
170 env=_env(repo),
171 )
172 assert result.exit_code == 0
173 data = json.loads(result.output)
174 assert data["mode"] == "dry-run"
175
176
177 # ---------------------------------------------------------------------------
178 # Execution mode
179 # ---------------------------------------------------------------------------
180
181
182 class TestRunTests:
183 def test_run_specific_file_passes(self, repo: pathlib.Path) -> None:
184 """Running a specific passing test file exits 0."""
185 result = runner.invoke(
186 cli,
187 ["code", "test", str(repo / "tests" / "test_simple.py")],
188 env=_env(repo),
189 )
190 assert result.exit_code == 0
191
192 def test_run_json_mode(self, repo: pathlib.Path) -> None:
193 """--json emits a valid JSON run result."""
194 result = runner.invoke(
195 cli,
196 [
197 "code", "test",
198 str(repo / "tests" / "test_simple.py"),
199 "--json",
200 ],
201 env=_env(repo),
202 )
203 assert result.exit_code == 0
204 # CliRunner combines stdout + stderr; _progress_cb writes dots to stderr
205 # after the JSON block. Extract the JSON object directly.
206 raw = result.output
207 json_start = raw.index("{")
208 json_end = raw.rindex("}") + 1
209 data = json.loads(raw[json_start:json_end])
210 assert data["mode"] == "run"
211 assert "run" in data
212 run_data = data["run"]
213 assert run_data["passed"] >= 1
214 assert run_data["exit_code"] == 0
215
216 def test_failing_test_exits_nonzero(self, repo: pathlib.Path) -> None:
217 """A failing test produces exit_code != 0."""
218 fail_file = repo / "tests" / "test_fail.py"
219 fail_file.write_text(
220 "def test_intentional_fail() -> None:\n assert False\n"
221 )
222 result = runner.invoke(
223 cli,
224 ["code", "test", str(fail_file)],
225 env=_env(repo),
226 )
227 assert result.exit_code != 0
228
229
230 # ---------------------------------------------------------------------------
231 # History persistence
232 # ---------------------------------------------------------------------------
233
234
235 class TestHistoryPersistence:
236 def test_history_saved_after_run(self, repo: pathlib.Path) -> None:
237 """After a successful run, .muse/cache/test_history.msgpack exists."""
238 runner.invoke(
239 cli,
240 ["code", "test", str(repo / "tests" / "test_simple.py")],
241 env=_env(repo),
242 )
243 assert _test_history_path(repo).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 assert not _test_history_path(repo).exists()
253
254
255 # ---------------------------------------------------------------------------
256 # CI mode
257 # ---------------------------------------------------------------------------
258
259
260 class TestCiMode:
261 def test_ci_with_passing_gate(self, repo: pathlib.Path) -> None:
262 """--ci with an echo gate passes."""
263 toml = (
264 "version = 1\n\n"
265 "[[gate]]\n"
266 'name = "echo"\n'
267 'command = ["echo", "hello"]\n'
268 "timeout_s = 5\n"
269 "required = true\n"
270 )
271 (muse_dir(repo) / "ci.toml").write_text(toml)
272 result = runner.invoke(cli, ["code", "test", "--ci"], env=_env(repo))
273 assert result.exit_code == 0
274
275 def test_ci_json_structure(self, repo: pathlib.Path) -> None:
276 """--ci --json emits a valid CI result."""
277 toml = (
278 "version = 1\n\n"
279 "[[gate]]\n"
280 'name = "echo"\n'
281 'command = ["echo", "ok"]\n'
282 "timeout_s = 5\n"
283 "required = true\n"
284 )
285 (muse_dir(repo) / "ci.toml").write_text(toml)
286 result = runner.invoke(
287 cli, ["code", "test", "--ci", "--json"], env=_env(repo)
288 )
289 assert result.exit_code == 0
290 data = json.loads(result.output)
291 assert data["mode"] == "ci"
292 assert "ci" in data
293 ci = data["ci"]
294 assert isinstance(ci["passed"], bool)
295 assert isinstance(ci["gates"], list)
296
297 def test_ci_failing_gate_exits_nonzero(self, repo: pathlib.Path) -> None:
298 """--ci with a failing gate exits non-zero."""
299 toml = (
300 "version = 1\n\n"
301 "[[gate]]\n"
302 'name = "fail"\n'
303 f'command = ["{sys.executable}", "-c", "raise SystemExit(1)"]\n'
304 "timeout_s = 5\n"
305 "required = true\n"
306 )
307 (muse_dir(repo) / "ci.toml").write_text(toml)
308 result = runner.invoke(cli, ["code", "test", "--ci"], env=_env(repo))
309 assert result.exit_code != 0
File History 1 commit
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor 121 days ago