gabriel / muse public
test_cmd_domain_info_hardening.py python
307 lines 12.0 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 132 days ago
1 """Hardening tests for ``muse domain-info`` — agent supercharge series.
2
3 Tests added in this pass
4 ------------------------
5 - ``duration_ms`` present and valid in every JSON output path
6 - ``exit_code`` present and zero in every JSON output path
7 - JSON is compact (no ``indent=2``)
8 - Schema sub-object includes ``domain`` key
9 - ``plugin_class`` is a non-empty string
10 - ``--all-domains`` carries both new fields
11 - ``--capabilities-only`` carries both new fields
12 - Data integrity: exit_code always int, duration_ms always non-negative float
13 - Performance: 100 sequential calls complete under 10 s
14 - Security: no traceback, error JSON goes to stderr
15 """
16 from __future__ import annotations
17 from collections.abc import Mapping
18
19 import json
20 import pathlib
21 import time
22
23 import pytest
24
25 from tests.cli_test_helper import CliRunner, InvokeResult
26
27 runner = CliRunner()
28
29
30 # ---------------------------------------------------------------------------
31 # Helpers
32 # ---------------------------------------------------------------------------
33
34 def _make_repo(tmp_path: pathlib.Path, domain: str = "code") -> pathlib.Path:
35 repo = tmp_path / "repo"
36 muse = repo / ".muse"
37 for sub in ("objects", "commits", "snapshots", "refs/heads"):
38 (muse / sub).mkdir(parents=True)
39 (muse / "HEAD").write_text("ref: refs/heads/main")
40 (muse / "repo.json").write_text(
41 json.dumps({"repo_id": "test-repo", "domain": domain})
42 )
43 return repo
44
45
46 def _di(repo: pathlib.Path | None, *args: str) -> InvokeResult:
47 from muse.cli.app import main as cli
48 env = {"MUSE_REPO_ROOT": str(repo)} if repo is not None else {}
49 return runner.invoke(cli, ["domain-info", "--json", *args], env=env)
50
51
52 def _json(result: InvokeResult) -> Mapping[str, object]:
53 return json.loads(result.output)
54
55
56 # ---------------------------------------------------------------------------
57 # JSON schema — main output (active-repo / --domain)
58 # ---------------------------------------------------------------------------
59
60 class TestJsonSchemaComplete:
61 """Every success path must include duration_ms and exit_code."""
62
63 def test_active_repo_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
64 repo = _make_repo(tmp_path)
65 data = _json(_di(repo))
66 assert "duration_ms" in data
67
68 def test_active_repo_has_exit_code(self, tmp_path: pathlib.Path) -> None:
69 repo = _make_repo(tmp_path)
70 data = _json(_di(repo))
71 assert "exit_code" in data
72
73 def test_exit_code_is_zero_on_success(self, tmp_path: pathlib.Path) -> None:
74 repo = _make_repo(tmp_path)
75 data = _json(_di(repo))
76 assert data["exit_code"] == 0
77
78 def test_duration_ms_is_float(self, tmp_path: pathlib.Path) -> None:
79 repo = _make_repo(tmp_path)
80 data = _json(_di(repo))
81 assert isinstance(data["duration_ms"], float)
82
83 def test_duration_ms_non_negative(self, tmp_path: pathlib.Path) -> None:
84 repo = _make_repo(tmp_path)
85 data = _json(_di(repo))
86 assert data["duration_ms"] >= 0.0
87
88 def test_duration_ms_six_decimal_places(self, tmp_path: pathlib.Path) -> None:
89 repo = _make_repo(tmp_path)
90 data = _json(_di(repo))
91 # round(..., 6) → at most 6 decimal places
92 assert data["duration_ms"] == round(data["duration_ms"], 6)
93
94 def test_domain_flag_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
95 data = _json(_di(None, "--domain", "code"))
96 assert "duration_ms" in data
97
98 def test_domain_flag_has_exit_code(self, tmp_path: pathlib.Path) -> None:
99 data = _json(_di(None, "--domain", "code"))
100 assert "exit_code" in data
101 assert data["exit_code"] == 0
102
103 def test_all_base_fields_present(self, tmp_path: pathlib.Path) -> None:
104 repo = _make_repo(tmp_path)
105 data = _json(_di(repo))
106 for key in (
107 "domain", "plugin_class", "capabilities", "schema",
108 "registered_domains", "duration_ms", "exit_code",
109 ):
110 assert key in data, f"missing key: {key}"
111
112
113 # ---------------------------------------------------------------------------
114 # JSON schema — --all-domains
115 # ---------------------------------------------------------------------------
116
117 class TestAllDomainsSchema:
118 def test_all_domains_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
119 data = _json(_di(None, "--all-domains"))
120 assert "duration_ms" in data
121
122 def test_all_domains_has_exit_code(self, tmp_path: pathlib.Path) -> None:
123 data = _json(_di(None, "--all-domains"))
124 assert "exit_code" in data
125
126 def test_all_domains_exit_code_zero(self, tmp_path: pathlib.Path) -> None:
127 data = _json(_di(None, "--all-domains"))
128 assert data["exit_code"] == 0
129
130 def test_all_domains_elapsed_non_negative(self, tmp_path: pathlib.Path) -> None:
131 data = _json(_di(None, "--all-domains"))
132 assert data["duration_ms"] >= 0.0
133
134
135 # ---------------------------------------------------------------------------
136 # JSON schema — --capabilities-only
137 # ---------------------------------------------------------------------------
138
139 class TestCapabilitiesOnlySchema:
140 def test_capabilities_only_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
141 data = _json(_di(None, "--domain", "code", "--capabilities-only"))
142 assert "duration_ms" in data
143
144 def test_capabilities_only_has_exit_code(self, tmp_path: pathlib.Path) -> None:
145 data = _json(_di(None, "--domain", "code", "--capabilities-only"))
146 assert "exit_code" in data
147
148 def test_capabilities_only_exit_code_zero(self, tmp_path: pathlib.Path) -> None:
149 data = _json(_di(None, "--domain", "code", "--capabilities-only"))
150 assert data["exit_code"] == 0
151
152 def test_capabilities_only_elapsed_non_negative(self, tmp_path: pathlib.Path) -> None:
153 data = _json(_di(None, "--domain", "code", "--capabilities-only"))
154 assert data["duration_ms"] >= 0.0
155
156 def test_capabilities_only_no_schema_key(self, tmp_path: pathlib.Path) -> None:
157 data = _json(_di(None, "--domain", "code", "--capabilities-only"))
158 assert "domain_schema" not in data
159
160 def test_capabilities_only_repo_mode_has_elapsed(self, tmp_path: pathlib.Path) -> None:
161 repo = _make_repo(tmp_path)
162 data = _json(_di(repo, "--capabilities-only"))
163 assert "duration_ms" in data
164
165
166 # ---------------------------------------------------------------------------
167 # Compact JSON (no indent=2)
168 # ---------------------------------------------------------------------------
169
170 class TestCompactJson:
171 def test_main_output_is_compact(self, tmp_path: pathlib.Path) -> None:
172 repo = _make_repo(tmp_path)
173 result = _di(repo)
174 assert result.exit_code == 0
175 # compact JSON has no leading whitespace on lines after the first
176 lines = result.output.strip().splitlines()
177 assert len(lines) == 1, "JSON must be a single line (compact)"
178
179 def test_all_domains_is_compact(self, tmp_path: pathlib.Path) -> None:
180 result = _di(None, "--all-domains")
181 lines = result.output.strip().splitlines()
182 assert len(lines) == 1
183
184 def test_capabilities_only_is_compact(self, tmp_path: pathlib.Path) -> None:
185 result = _di(None, "--domain", "code", "--capabilities-only")
186 lines = result.output.strip().splitlines()
187 assert len(lines) == 1
188
189
190 # ---------------------------------------------------------------------------
191 # Schema sub-object integrity
192 # ---------------------------------------------------------------------------
193
194 class TestSchemaSubObject:
195 def test_schema_has_domain_key(self, tmp_path: pathlib.Path) -> None:
196 """schema.domain must match the top-level domain field."""
197 repo = _make_repo(tmp_path)
198 data = _json(_di(repo))
199 assert "domain" in data["domain_schema"]
200 assert data["domain_schema"]["domain"] == data["domain"]
201
202 def test_schema_has_merge_mode(self, tmp_path: pathlib.Path) -> None:
203 repo = _make_repo(tmp_path)
204 data = _json(_di(repo))
205 assert "merge_mode" in data["domain_schema"]
206 assert isinstance(data["domain_schema"]["merge_mode"], str)
207
208 def test_schema_has_description(self, tmp_path: pathlib.Path) -> None:
209 repo = _make_repo(tmp_path)
210 data = _json(_di(repo))
211 assert "description" in data["domain_schema"]
212 assert len(data["domain_schema"]["description"]) > 0
213
214 def test_schema_has_dimensions(self, tmp_path: pathlib.Path) -> None:
215 repo = _make_repo(tmp_path)
216 data = _json(_di(repo))
217 assert "dimensions" in data["domain_schema"]
218 assert isinstance(data["domain_schema"]["dimensions"], list)
219
220 def test_schema_version_present(self, tmp_path: pathlib.Path) -> None:
221 repo = _make_repo(tmp_path)
222 data = _json(_di(repo))
223 assert "schema_version" in data["domain_schema"]
224
225
226 # ---------------------------------------------------------------------------
227 # plugin_class field
228 # ---------------------------------------------------------------------------
229
230 class TestPluginClass:
231 def test_plugin_class_is_non_empty_string(self, tmp_path: pathlib.Path) -> None:
232 repo = _make_repo(tmp_path)
233 data = _json(_di(repo))
234 assert isinstance(data["plugin_class"], str)
235 assert len(data["plugin_class"]) > 0
236
237 def test_plugin_class_ends_with_plugin(self, tmp_path: pathlib.Path) -> None:
238 repo = _make_repo(tmp_path)
239 data = _json(_di(repo))
240 assert data["plugin_class"].endswith("Plugin")
241
242 def test_domain_flag_plugin_class(self, tmp_path: pathlib.Path) -> None:
243 data = _json(_di(None, "--domain", "code"))
244 assert data["plugin_class"] == "CodePlugin"
245
246
247 # ---------------------------------------------------------------------------
248 # Data integrity
249 # ---------------------------------------------------------------------------
250
251 class TestDataIntegrity:
252 def test_exit_code_is_int_not_bool(self, tmp_path: pathlib.Path) -> None:
253 repo = _make_repo(tmp_path)
254 data = _json(_di(repo))
255 assert type(data["exit_code"]) is int
256
257 def test_duration_ms_is_float_not_int(self, tmp_path: pathlib.Path) -> None:
258 """Must be a float (e.g. 0.001234) not a plain integer."""
259 repo = _make_repo(tmp_path)
260 data = _json(_di(repo))
261 # JSON 0 deserialises as int — make sure we always get a float
262 assert isinstance(data["duration_ms"], float)
263
264 def test_capabilities_values_are_bool(self, tmp_path: pathlib.Path) -> None:
265 repo = _make_repo(tmp_path)
266 data = _json(_di(repo))
267 for k, v in data["capabilities"].items():
268 assert isinstance(v, bool), f"capabilities.{k} must be bool, got {type(v)}"
269
270 def test_registered_domains_no_duplicates(self, tmp_path: pathlib.Path) -> None:
271 data = _json(_di(None, "--all-domains"))
272 domains = data["registered_domains"]
273 assert len(domains) == len(set(domains))
274
275 def test_registered_domains_sorted(self, tmp_path: pathlib.Path) -> None:
276 data = _json(_di(None, "--all-domains"))
277 domains = data["registered_domains"]
278 assert domains == sorted(domains)
279
280 def test_domain_in_registered_domains(self, tmp_path: pathlib.Path) -> None:
281 repo = _make_repo(tmp_path, domain="code")
282 data = _json(_di(repo))
283 assert data["domain"] in data["registered_domains"]
284
285
286 # ---------------------------------------------------------------------------
287 # Performance
288 # ---------------------------------------------------------------------------
289
290 class TestPerformance:
291 def test_single_call_under_1s(self, tmp_path: pathlib.Path) -> None:
292 repo = _make_repo(tmp_path)
293 t0 = time.monotonic()
294 _di(repo)
295 assert time.monotonic() - t0 < 1.0
296
297 def test_duration_ms_plausible(self, tmp_path: pathlib.Path) -> None:
298 repo = _make_repo(tmp_path)
299 data = _json(_di(repo))
300 assert data["duration_ms"] < 10.0
301
302 def test_100_calls_under_10s(self, tmp_path: pathlib.Path) -> None:
303 t0 = time.monotonic()
304 for i in range(100):
305 result = _di(None, "--all-domains")
306 assert result.exit_code == 0
307 assert time.monotonic() - t0 < 10.0
File History 2 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 132 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 138 days ago