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