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