gabriel / muse public
test_cmd_test.py python
304 lines 10.2 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 143 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([], snap_id, "init", committed_at.isoformat())
82 commit = CommitRecord(
83 commit_id=commit_id,
84 repo_id=repo_id,
85 branch="main",
86 snapshot_id=snap_id,
87 message="init",
88 committed_at=committed_at,
89 author="test",
90 )
91 write_commit(tmp_path, commit)
92
93 refs_dir = muse_dir / "refs" / "heads"
94 refs_dir.mkdir(parents=True)
95 (refs_dir / "main").write_text(commit_id)
96 (muse_dir / "HEAD").write_text("ref: refs/heads/main\n")
97
98 return tmp_path
99
100
101 # ---------------------------------------------------------------------------
102 # History mode
103 # ---------------------------------------------------------------------------
104
105
106 class TestHistoryCommand:
107 def test_history_empty(self, repo: pathlib.Path) -> None:
108 """--history with no runs prints a sensible message."""
109 result = runner.invoke(cli, ["code", "test", "--history"], env=_env(repo))
110 assert result.exit_code == 0
111 assert "No test history recorded." in result.output
112
113 def test_history_json(self, repo: pathlib.Path) -> None:
114 """--history --json emits a JSON object."""
115 result = runner.invoke(cli, ["code", "test", "--history", "--json"], env=_env(repo))
116 assert result.exit_code == 0
117 data = json.loads(result.output)
118 assert data["mode"] == "history"
119 assert "history" in data
120
121 def test_flaky_empty(self, repo: pathlib.Path) -> None:
122 """--flaky with no history prints no-flaky message."""
123 result = runner.invoke(cli, ["code", "test", "--flaky"], env=_env(repo))
124 assert result.exit_code == 0
125 assert "No flaky tests found." in result.output
126
127
128 # ---------------------------------------------------------------------------
129 # Dry-run mode
130 # ---------------------------------------------------------------------------
131
132
133 class TestDryRun:
134 def test_dry_run_text(self, repo: pathlib.Path) -> None:
135 """--dry-run --all prints targets without executing pytest."""
136 result = runner.invoke(
137 cli, ["code", "test", "--dry-run", "--all"], env=_env(repo)
138 )
139 assert result.exit_code == 0
140 assert "Would run" in result.output or "pytest" in result.output.lower()
141
142 def test_dry_run_json(self, repo: pathlib.Path) -> None:
143 """--dry-run --json emits valid JSON."""
144 result = runner.invoke(
145 cli, ["code", "test", "--dry-run", "--all", "--json"], env=_env(repo)
146 )
147 assert result.exit_code == 0
148 data = json.loads(result.output)
149 assert data["mode"] == "dry-run"
150
151 def test_dry_run_with_symbol(self, repo: pathlib.Path) -> None:
152 """--dry-run --symbol emits valid JSON with selection."""
153 result = runner.invoke(
154 cli,
155 [
156 "code",
157 "test",
158 "--dry-run",
159 "--symbol",
160 "tests/test_simple.py::test_passes",
161 "--json",
162 ],
163 env=_env(repo),
164 )
165 assert result.exit_code == 0
166 data = json.loads(result.output)
167 assert data["mode"] == "dry-run"
168
169
170 # ---------------------------------------------------------------------------
171 # Execution mode
172 # ---------------------------------------------------------------------------
173
174
175 class TestRunTests:
176 def test_run_specific_file_passes(self, repo: pathlib.Path) -> None:
177 """Running a specific passing test file exits 0."""
178 result = runner.invoke(
179 cli,
180 ["code", "test", str(repo / "tests" / "test_simple.py")],
181 env=_env(repo),
182 )
183 assert result.exit_code == 0
184
185 def test_run_json_mode(self, repo: pathlib.Path) -> None:
186 """--json emits a valid JSON run result."""
187 result = runner.invoke(
188 cli,
189 [
190 "code", "test",
191 str(repo / "tests" / "test_simple.py"),
192 "--json",
193 ],
194 env=_env(repo),
195 )
196 assert result.exit_code == 0
197 # CliRunner combines stdout + stderr; _progress_cb writes dots to stderr
198 # after the JSON block. Extract the JSON object directly.
199 raw = result.output
200 json_start = raw.index("{")
201 json_end = raw.rindex("}") + 1
202 data = json.loads(raw[json_start:json_end])
203 assert data["mode"] == "run"
204 assert "run" in data
205 run_data = data["run"]
206 assert run_data["passed"] >= 1
207 assert run_data["exit_code"] == 0
208
209 def test_failing_test_exits_nonzero(self, repo: pathlib.Path) -> None:
210 """A failing test produces exit_code != 0."""
211 fail_file = repo / "tests" / "test_fail.py"
212 fail_file.write_text(
213 "def test_intentional_fail() -> None:\n assert False\n"
214 )
215 result = runner.invoke(
216 cli,
217 ["code", "test", str(fail_file)],
218 env=_env(repo),
219 )
220 assert result.exit_code != 0
221
222
223 # ---------------------------------------------------------------------------
224 # History persistence
225 # ---------------------------------------------------------------------------
226
227
228 class TestHistoryPersistence:
229 def test_history_saved_after_run(self, repo: pathlib.Path) -> None:
230 """After a successful run, .muse/test_history.msgpack exists."""
231 runner.invoke(
232 cli,
233 ["code", "test", str(repo / "tests" / "test_simple.py")],
234 env=_env(repo),
235 )
236 hist_path = repo / ".muse" / "test_history.msgpack"
237 assert hist_path.exists()
238
239 def test_no_save_skips_history(self, repo: pathlib.Path) -> None:
240 """--no-save prevents history file creation."""
241 runner.invoke(
242 cli,
243 ["code", "test", str(repo / "tests" / "test_simple.py"), "--no-save"],
244 env=_env(repo),
245 )
246 hist_path = repo / ".muse" / "test_history.msgpack"
247 assert not hist_path.exists()
248
249
250 # ---------------------------------------------------------------------------
251 # CI mode
252 # ---------------------------------------------------------------------------
253
254
255 class TestCiMode:
256 def test_ci_with_passing_gate(self, repo: pathlib.Path) -> None:
257 """--ci with an echo gate passes."""
258 toml = (
259 "version = 1\n\n"
260 "[[gate]]\n"
261 'name = "echo"\n'
262 'command = ["echo", "hello"]\n'
263 "timeout_s = 5\n"
264 "required = true\n"
265 )
266 (repo / ".muse" / "ci.toml").write_text(toml)
267 result = runner.invoke(cli, ["code", "test", "--ci"], env=_env(repo))
268 assert result.exit_code == 0
269
270 def test_ci_json_structure(self, repo: pathlib.Path) -> None:
271 """--ci --json emits a valid CI result."""
272 toml = (
273 "version = 1\n\n"
274 "[[gate]]\n"
275 'name = "echo"\n'
276 'command = ["echo", "ok"]\n'
277 "timeout_s = 5\n"
278 "required = true\n"
279 )
280 (repo / ".muse" / "ci.toml").write_text(toml)
281 result = runner.invoke(
282 cli, ["code", "test", "--ci", "--json"], env=_env(repo)
283 )
284 assert result.exit_code == 0
285 data = json.loads(result.output)
286 assert data["mode"] == "ci"
287 assert "ci" in data
288 ci = data["ci"]
289 assert isinstance(ci["passed"], bool)
290 assert isinstance(ci["gates"], list)
291
292 def test_ci_failing_gate_exits_nonzero(self, repo: pathlib.Path) -> None:
293 """--ci with a failing gate exits non-zero."""
294 toml = (
295 "version = 1\n\n"
296 "[[gate]]\n"
297 'name = "fail"\n'
298 f'command = ["{sys.executable}", "-c", "raise SystemExit(1)"]\n'
299 "timeout_s = 5\n"
300 "required = true\n"
301 )
302 (repo / ".muse" / "ci.toml").write_text(toml)
303 result = runner.invoke(cli, ["code", "test", "--ci"], env=_env(repo))
304 assert result.exit_code != 0
File History 2 commits
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 143 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 146 days ago