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