gabriel / muse public
test_cmd_test.py python
310 lines 10.3 KB
Raw
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 21 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.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
File History 4 commits
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 21 days ago
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e fix: rename objects→blobs in push client and all stale test… Sonnet 4.6 patch 22 days ago
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 28 days ago
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor 29 days ago