gabriel / muse public
test_cmd_test.py python
308 lines 10.2 KB
Raw
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor ⚠ breaking 30 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.ids import hash_commit, hash_snapshot
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 = hash_snapshot(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 = hash_commit(
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 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 = dot_muse / "refs" / "heads"
100 refs_dir.mkdir(parents=True)
101 (refs_dir / "main").write_text(commit_id)
102 (dot_muse / "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/cache/test_history.msgpack exists."""
237 runner.invoke(
238 cli,
239 ["code", "test", str(repo / "tests" / "test_simple.py")],
240 env=_env(repo),
241 )
242 assert _test_history_path(repo).exists()
243
244 def test_no_save_skips_history(self, repo: pathlib.Path) -> None:
245 """--no-save prevents history file creation."""
246 runner.invoke(
247 cli,
248 ["code", "test", str(repo / "tests" / "test_simple.py"), "--no-save"],
249 env=_env(repo),
250 )
251 assert not _test_history_path(repo).exists()
252
253
254 # ---------------------------------------------------------------------------
255 # CI mode
256 # ---------------------------------------------------------------------------
257
258
259 class TestCiMode:
260 def test_ci_with_passing_gate(self, repo: pathlib.Path) -> None:
261 """--ci with an echo gate passes."""
262 toml = (
263 "version = 1\n\n"
264 "[[gate]]\n"
265 'name = "echo"\n'
266 'command = ["echo", "hello"]\n'
267 "timeout_s = 5\n"
268 "required = true\n"
269 )
270 (muse_dir(repo) / "ci.toml").write_text(toml)
271 result = runner.invoke(cli, ["code", "test", "--ci"], env=_env(repo))
272 assert result.exit_code == 0
273
274 def test_ci_json_structure(self, repo: pathlib.Path) -> None:
275 """--ci --json emits a valid CI result."""
276 toml = (
277 "version = 1\n\n"
278 "[[gate]]\n"
279 'name = "echo"\n'
280 'command = ["echo", "ok"]\n'
281 "timeout_s = 5\n"
282 "required = true\n"
283 )
284 (muse_dir(repo) / "ci.toml").write_text(toml)
285 result = runner.invoke(
286 cli, ["code", "test", "--ci", "--json"], env=_env(repo)
287 )
288 assert result.exit_code == 0
289 data = json.loads(result.output)
290 assert data["mode"] == "ci"
291 assert "ci" in data
292 ci = data["ci"]
293 assert isinstance(ci["passed"], bool)
294 assert isinstance(ci["gates"], list)
295
296 def test_ci_failing_gate_exits_nonzero(self, repo: pathlib.Path) -> None:
297 """--ci with a failing gate exits non-zero."""
298 toml = (
299 "version = 1\n\n"
300 "[[gate]]\n"
301 'name = "fail"\n'
302 f'command = ["{sys.executable}", "-c", "raise SystemExit(1)"]\n'
303 "timeout_s = 5\n"
304 "required = true\n"
305 )
306 (muse_dir(repo) / "ci.toml").write_text(toml)
307 result = runner.invoke(cli, ["code", "test", "--ci"], env=_env(repo))
308 assert result.exit_code != 0
File History 1 commit
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor 30 days ago