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