gabriel / muse public
test_cmd_domain_info.py python
304 lines 11.9 KB
Raw
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 21 days ago
1 """Comprehensive tests for ``muse domain-info``.
2
3 Audit findings addressed here
4 ------------------------------
5 Security
6 - Format error now goes to stderr (was stdout) — verified below.
7 - ANSI injection in domain name stripped in text mode.
8
9 Agent UX
10 - ``--domain <name>`` — inspect any domain without entering its repo.
11 - ``--capabilities-only`` — lightweight capability check.
12 - ``--all-domains`` — enumerate the registry.
13
14 Docs
15 - Capabilities section added to module docstring.
16
17 Coverage tiers
18 --------------
19 - Unit: _CapabilitiesDict schema, flag registration
20 - Integration: --all-domains JSON/text, --domain flag, --capabilities-only,
21 active-repo mode, JSON output, registered_domains present
22 - Security: ANSI in domain name stripped in text, unknown flag exits non-zero
23 - Stress: 200 sequential --all-domains calls
24 """
25 from __future__ import annotations
26
27 import argparse
28 import json
29 import pathlib
30 from typing import TYPE_CHECKING
31
32 from muse.core.errors import ExitCode
33 from muse.core.paths import muse_dir, repo_json_path
34 from tests.cli_test_helper import CliRunner, InvokeResult
35
36 runner = CliRunner()
37
38
39 # ---------------------------------------------------------------------------
40 # Helpers
41 # ---------------------------------------------------------------------------
42
43 def _make_repo(tmp_path: pathlib.Path, domain: str = "code") -> pathlib.Path:
44 repo = tmp_path / "repo"
45 dot_muse = muse_dir(repo)
46 for sub in ("objects", "commits", "snapshots", "refs/heads"):
47 (dot_muse / sub).mkdir(parents=True)
48 (dot_muse / "HEAD").write_text("ref: refs/heads/main")
49 (dot_muse / "repo.json").write_text(json.dumps({"repo_id": "test-repo", "domain": domain}))
50 return repo
51
52
53 def _di(repo: pathlib.Path | None, *args: str) -> InvokeResult:
54 from muse.cli.app import main as cli
55 env = {"MUSE_REPO_ROOT": str(repo)} if repo is not None else {}
56 return runner.invoke(cli, ["domain-info", *args], env=env)
57
58
59 # ---------------------------------------------------------------------------
60 # Unit
61 # ---------------------------------------------------------------------------
62
63
64 class TestRegisterFlags:
65 def _parse(self, *args: str) -> "argparse.Namespace":
66 import argparse
67 from muse.cli.commands.domain_info import register
68 p = argparse.ArgumentParser()
69 sub = p.add_subparsers()
70 register(sub)
71 return p.parse_args(["domain-info", *args])
72
73 def test_default_json_out_is_false(self) -> None:
74 ns = self._parse()
75 assert ns.json_out is False
76
77 def test_json_flag_sets_json_out(self) -> None:
78 ns = self._parse("--json")
79 assert ns.json_out is True
80
81 def test_j_shorthand_sets_json_out(self) -> None:
82 ns = self._parse("-j")
83 assert ns.json_out is True
84
85
86 class TestUnit:
87 def test_capabilities_dict_fields(self) -> None:
88 from muse.cli.commands.domain_info import _CapabilitiesDict
89 fields = set(_CapabilitiesDict.__annotations__.keys())
90 assert "addressed_merge" in fields
91 assert "crdt" in fields
92 assert "harmony" in fields
93
94
95 # ---------------------------------------------------------------------------
96 # Integration — --all-domains
97 # ---------------------------------------------------------------------------
98
99
100 class TestAllDomains:
101 def test_json_returns_list(self, tmp_path: pathlib.Path) -> None:
102 repo = _make_repo(tmp_path)
103 result = _di(repo, "--all-domains", "--json")
104 assert result.exit_code == 0
105 data = json.loads(result.output)
106 assert "registered_domains" in data
107 assert isinstance(data["registered_domains"], list)
108 assert len(data["registered_domains"]) > 0
109
110 def test_json_shorthand(self, tmp_path: pathlib.Path) -> None:
111 repo = _make_repo(tmp_path)
112 result = _di(repo, "--all-domains", "--json")
113 assert result.exit_code == 0
114 assert "registered_domains" in json.loads(result.output)
115
116 def test_text_one_per_line(self, tmp_path: pathlib.Path) -> None:
117 repo = _make_repo(tmp_path)
118 result = _di(repo, "--all-domains")
119 assert result.exit_code == 0
120 lines = [l for l in result.output.splitlines() if l.strip()]
121 assert len(lines) > 0
122
123 def test_no_repo_required(self, tmp_path: pathlib.Path) -> None:
124 """--all-domains must not require a Muse repository."""
125 result = _di(None, "--all-domains",
126 "--json")
127 assert result.exit_code == 0
128 data = json.loads(result.output)
129 assert "registered_domains" in data
130
131 def test_code_domain_present(self, tmp_path: pathlib.Path) -> None:
132 repo = _make_repo(tmp_path)
133 data = json.loads(_di(repo, "--all-domains", "--json").output)
134 assert "code" in data["registered_domains"]
135
136
137 # ---------------------------------------------------------------------------
138 # Integration — --domain flag (new agent UX)
139 # ---------------------------------------------------------------------------
140
141
142 class TestDomainFlag:
143 def test_inspect_code_domain_without_repo(self, tmp_path: pathlib.Path) -> None:
144 """Agents can inspect a domain without being inside its repo."""
145 result = _di(None, "--domain", "code", "--json")
146 assert result.exit_code == 0
147 data = json.loads(result.output)
148 assert data["domain"] == "code"
149 assert "capabilities" in data
150
151 def test_inspect_code_capabilities_only(self, tmp_path: pathlib.Path) -> None:
152 result = _di(None, "--domain", "code", "--capabilities-only", "--json")
153 assert result.exit_code == 0
154 data = json.loads(result.output)
155 assert "capabilities" in data
156 assert "domain_schema" not in data
157
158 def test_unknown_domain_errors(self, tmp_path: pathlib.Path) -> None:
159 result = _di(None, "--domain", "nonexistent-domain")
160 assert result.exit_code == ExitCode.USER_ERROR
161
162 def test_invalid_domain_name_rejected(self, tmp_path: pathlib.Path) -> None:
163 """Domain names must match the validation regex."""
164 result = _di(None, "--domain", "UPPERCASE")
165 assert result.exit_code == ExitCode.USER_ERROR
166
167 def test_domain_flag_overrides_repo_domain(self, tmp_path: pathlib.Path) -> None:
168 """--domain should be used even when inside a repo with a different domain."""
169 repo = _make_repo(tmp_path, domain="code")
170 result = _di(repo, "--domain", "code", "--json")
171 assert result.exit_code == 0
172 data = json.loads(result.output)
173 assert data["domain"] == "code"
174
175
176 # ---------------------------------------------------------------------------
177 # Integration — --capabilities-only
178 # ---------------------------------------------------------------------------
179
180
181 class TestCapabilitiesOnly:
182 def test_json_has_no_domain_schema_key(self, tmp_path: pathlib.Path) -> None:
183 repo = _make_repo(tmp_path)
184 result = _di(repo, "--capabilities-only", "--json")
185 assert result.exit_code == 0
186 data = json.loads(result.output)
187 assert "domain_schema" not in data
188 assert "capabilities" in data
189 assert "domain" in data
190
191 def test_capabilities_are_booleans(self, tmp_path: pathlib.Path) -> None:
192 repo = _make_repo(tmp_path)
193 data = json.loads(_di(repo, "--capabilities-only", "--json").output)
194 caps = data["capabilities"]
195 for key in ("addressed_merge", "crdt", "harmony"):
196 assert key in caps
197 assert isinstance(caps[key], bool)
198
199 def test_text_format_capabilities_only(self, tmp_path: pathlib.Path) -> None:
200 repo = _make_repo(tmp_path)
201 result = _di(repo, "--capabilities-only")
202 assert result.exit_code == 0
203 assert "Domain:" in result.output
204 assert "Capabilities:" in result.output
205 assert "Plugin:" not in result.output
206
207 def test_domain_flag_and_capabilities_only(self, tmp_path: pathlib.Path) -> None:
208 """Agents use this combo constantly for merge-strategy negotiation."""
209 result = _di(None, "--domain", "code", "--capabilities-only", "--json")
210 assert result.exit_code == 0
211 data = json.loads(result.output)
212 assert data["domain"] == "code"
213 assert "capabilities" in data
214
215
216 # ---------------------------------------------------------------------------
217 # Integration — active-repo mode
218 # ---------------------------------------------------------------------------
219
220
221 class TestActiveRepoMode:
222 def test_json_output_keys(self, tmp_path: pathlib.Path) -> None:
223 repo = _make_repo(tmp_path)
224 result = _di(repo, "--json")
225 assert result.exit_code == 0
226 data = json.loads(result.output)
227 for key in ("domain", "plugin_class", "capabilities", "domain_schema", "registered_domains"):
228 assert key in data, f"missing key: {key}"
229
230 def test_domain_matches_repo(self, tmp_path: pathlib.Path) -> None:
231 repo = _make_repo(tmp_path, domain="code")
232 data = json.loads(_di(repo, "--json").output)
233 assert data["domain"] == "code"
234
235 def test_registered_domains_in_output(self, tmp_path: pathlib.Path) -> None:
236 repo = _make_repo(tmp_path)
237 data = json.loads(_di(repo, "--json").output)
238 assert isinstance(data["registered_domains"], list)
239 assert "code" in data["registered_domains"]
240
241 def test_text_format_shows_domain(self, tmp_path: pathlib.Path) -> None:
242 repo = _make_repo(tmp_path)
243 result = _di(repo)
244 assert result.exit_code == 0
245 assert "Domain:" in result.output
246 assert "Plugin:" in result.output
247 assert "Capabilities:" in result.output
248
249 def test_unknown_domain_in_repo_errors(self, tmp_path: pathlib.Path) -> None:
250 repo = _make_repo(tmp_path, domain="unknown-domain-xyz")
251 result = _di(repo)
252 assert result.exit_code == ExitCode.USER_ERROR
253
254
255 # ---------------------------------------------------------------------------
256 # Security
257 # ---------------------------------------------------------------------------
258
259
260 class TestSecurity:
261 def test_ansi_in_domain_stripped_text(self, tmp_path: pathlib.Path) -> None:
262 """A maliciously crafted repo.json with ANSI in domain is safe in text mode."""
263 repo = _make_repo(tmp_path)
264 (repo_json_path(repo)).write_text(
265 '{"repo_id": "test", "domain": "\\u001b[31mmalicious\\u001b[0m"}'
266 )
267 result = _di(repo)
268 # Command will fail because "malicious" is not a registered domain,
269 # but the important thing is no raw ANSI in output
270 assert "\x1b" not in result.output
271
272 def test_unknown_flag_exits_nonzero(self, tmp_path: pathlib.Path) -> None:
273 repo = _make_repo(tmp_path)
274 result = _di(repo, "--format", "msgpack", "--all-domains")
275 assert result.exit_code != 0
276
277 def test_no_traceback_on_bad_domain(self, tmp_path: pathlib.Path) -> None:
278 result = _di(None, "--domain", "nonexistent-domain")
279 assert "Traceback" not in result.output
280
281 def test_no_traceback_on_unknown_flag(self, tmp_path: pathlib.Path) -> None:
282 repo = _make_repo(tmp_path)
283 result = _di(repo, "--format", "yaml", "--all-domains")
284 assert "Traceback" not in result.output
285
286
287 # ---------------------------------------------------------------------------
288 # Stress
289 # ---------------------------------------------------------------------------
290
291
292 class TestStress:
293 def test_200_all_domains_calls(self, tmp_path: pathlib.Path) -> None:
294 for i in range(200):
295 result = _di(None, "--all-domains", "--json")
296 assert result.exit_code == 0, f"failed at iteration {i}"
297 data = json.loads(result.output)
298 assert "registered_domains" in data
299
300 def test_200_capabilities_only_calls(self, tmp_path: pathlib.Path) -> None:
301 for i in range(200):
302 result = _di(None, "--domain", "code", "--capabilities-only", "--json")
303 assert result.exit_code == 0, f"failed at iteration {i}"
304 assert "capabilities" in json.loads(result.output)
File History 5 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:c10a2ce474b3bb7ff2a3d628e8a3f2e028fd78ca652513496a03a498ae2267b3 chore: sweep all stale DirectoryRenameOp / directory_rename… Sonnet 4.6 minor 23 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