gabriel / muse public
test_knowtation_hub_manifest.py python
353 lines 12.7 KB
Raw
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
1 """Tests for Phase 1.8 — Knowtation domain manifest for MuseHub registry.
2
3 Test tiers covered
4 ------------------
5 1. Unit — static manifest.json: file exists, valid JSON, required top-level
6 fields, correct viewer_type, merge_semantics, version, six named dimensions.
7 2. Unit — validate_manifest(): valid manifest passes, missing fields detected,
8 bad dimensions detected, unknown dimension names flagged.
9 3. Unit — manifest_hash(): deterministic across runs, output format, identical
10 manifests produce same hash, different manifests produce different hashes.
11 4. Unit — build_manifest(): required fields present, dimensions include all
12 six, descriptions from static are preserved, schema description used.
13 5. Integration — assert_manifest_in_sync(): static and dynamic dimension names
14 match.
15 6. Performance — manifest_hash() and build_manifest() complete in < 1 second.
16 7. Security — manifest.json contains no credential-looking fields.
17 """
18
19 from __future__ import annotations
20
21 import json
22 import time
23 from typing import Any
24
25 import pytest
26
27 from muse.plugins.knowtation.hub_manifest import (
28 EXPECTED_DIMENSIONS,
29 MANIFEST_FILE,
30 assert_manifest_in_sync,
31 build_manifest,
32 load_manifest,
33 manifest_hash,
34 validate_manifest,
35 )
36
37
38 # ---------------------------------------------------------------------------
39 # Helpers
40 # ---------------------------------------------------------------------------
41
42
43 def _valid_manifest() -> dict[str, Any]:
44 return load_manifest()
45
46
47 # ============================================================================
48 # TestStaticManifestFile
49 # ============================================================================
50
51
52 class TestStaticManifestFile:
53 def test_file_exists(self) -> None:
54 assert MANIFEST_FILE.exists(), f"manifest.json not found at {MANIFEST_FILE}"
55
56 def test_file_is_valid_json(self) -> None:
57 content = MANIFEST_FILE.read_text(encoding="utf-8")
58 parsed = json.loads(content)
59 assert isinstance(parsed, dict)
60
61 def test_slug_is_knowtation(self) -> None:
62 m = load_manifest()
63 assert m["slug"] == "knowtation"
64
65 def test_viewer_type_is_knowledge(self) -> None:
66 m = load_manifest()
67 assert m["viewer_type"] == "knowledge"
68
69 def test_merge_semantics_is_ot(self) -> None:
70 m = load_manifest()
71 assert m["merge_semantics"] == "ot"
72
73 def test_version_present_and_semver(self) -> None:
74 m = load_manifest()
75 v = m["version"]
76 assert isinstance(v, str)
77 parts = v.split(".")
78 assert len(parts) == 3
79 assert all(p.isdigit() for p in parts)
80
81 def test_display_name_present(self) -> None:
82 m = load_manifest()
83 assert isinstance(m.get("display_name"), str)
84 assert len(m["display_name"]) > 0
85
86 def test_description_present(self) -> None:
87 m = load_manifest()
88 assert isinstance(m.get("description"), str)
89 assert len(m["description"]) > 0
90
91 def test_dimensions_is_list(self) -> None:
92 m = load_manifest()
93 assert isinstance(m.get("dimensions"), list)
94
95 def test_exactly_six_dimensions(self) -> None:
96 m = load_manifest()
97 assert len(m["dimensions"]) == 6, (
98 f"Expected 6 dimensions, got {len(m['dimensions'])}"
99 )
100
101 def test_expected_dimension_names_present(self) -> None:
102 m = load_manifest()
103 names = {d["name"] for d in m["dimensions"]}
104 assert names == EXPECTED_DIMENSIONS, (
105 f"Dimension names mismatch: got {names}, expected {EXPECTED_DIMENSIONS}"
106 )
107
108 def test_all_dimensions_have_required_fields(self) -> None:
109 m = load_manifest()
110 required = {"name", "kind", "element_type", "description", "independent_merge"}
111 for i, dim in enumerate(m["dimensions"]):
112 for key in required:
113 assert key in dim, f"dimensions[{i}] missing '{key}'"
114
115 def test_notes_dimension_not_independently_merged(self) -> None:
116 m = load_manifest()
117 notes_dims = [d for d in m["dimensions"] if d["name"] == "notes"]
118 assert notes_dims, "notes dimension missing"
119 assert notes_dims[0]["independent_merge"] is False
120
121 def test_non_notes_dimensions_independently_merged(self) -> None:
122 m = load_manifest()
123 for dim in m["dimensions"]:
124 if dim["name"] != "notes":
125 assert dim["independent_merge"] is True, (
126 f"Dimension '{dim['name']}' should have independent_merge=True"
127 )
128
129 def test_file_is_utf8(self) -> None:
130 MANIFEST_FILE.read_text(encoding="utf-8")
131
132 def test_supported_commands_present(self) -> None:
133 m = load_manifest()
134 cmds = m.get("supported_commands", [])
135 assert isinstance(cmds, list)
136 assert "commit" in cmds
137
138
139 # ============================================================================
140 # TestValidateManifest
141 # ============================================================================
142
143
144 class TestValidateManifest:
145 def test_static_manifest_is_valid(self) -> None:
146 m = load_manifest()
147 errors = validate_manifest(m)
148 assert errors == [], f"Static manifest has validation errors: {errors}"
149
150 def test_missing_required_field_detected(self) -> None:
151 m = dict(load_manifest())
152 del m["viewer_type"]
153 errors = validate_manifest(m)
154 assert any("viewer_type" in e for e in errors)
155
156 def test_missing_description_detected(self) -> None:
157 m = dict(load_manifest())
158 del m["description"]
159 errors = validate_manifest(m)
160 assert any("description" in e for e in errors)
161
162 def test_missing_dimensions_key(self) -> None:
163 m = {k: v for k, v in load_manifest().items() if k != "dimensions"}
164 errors = validate_manifest(m)
165 assert any("dimensions" in e for e in errors)
166
167 def test_empty_dimensions_detected(self) -> None:
168 m = dict(load_manifest())
169 m["dimensions"] = []
170 errors = validate_manifest(m)
171 assert any("dimensions" in e for e in errors)
172
173 def test_dimension_missing_name_detected(self) -> None:
174 m = dict(load_manifest())
175 bad_dim = dict(m["dimensions"][0])
176 del bad_dim["name"]
177 m["dimensions"] = [bad_dim] + m["dimensions"][1:]
178 errors = validate_manifest(m)
179 assert any("name" in e for e in errors)
180
181 def test_returns_list(self) -> None:
182 errors = validate_manifest(load_manifest())
183 assert isinstance(errors, list)
184
185
186 # ============================================================================
187 # TestManifestHash
188 # ============================================================================
189
190
191 class TestManifestHash:
192 def test_hash_format(self) -> None:
193 h = manifest_hash(load_manifest())
194 assert h.startswith("sha256:")
195 assert len(h) == len("sha256:") + 64
196
197 def test_hash_is_deterministic(self) -> None:
198 m = load_manifest()
199 h1 = manifest_hash(m)
200 h2 = manifest_hash(m)
201 assert h1 == h2
202
203 def test_identical_dicts_same_hash(self) -> None:
204 m1 = load_manifest()
205 m2 = load_manifest()
206 assert manifest_hash(m1) == manifest_hash(m2)
207
208 def test_different_dicts_different_hash(self) -> None:
209 m1 = load_manifest()
210 m2 = dict(load_manifest())
211 m2["version"] = "9.9.9"
212 assert manifest_hash(m1) != manifest_hash(m2)
213
214 def test_key_order_does_not_affect_hash(self) -> None:
215 m1 = load_manifest()
216 # Build reversed-key version
217 m2 = dict(reversed(list(m1.items())))
218 assert manifest_hash(m1) == manifest_hash(m2)
219
220 def test_hash_only_hex_after_prefix(self) -> None:
221 h = manifest_hash(load_manifest())
222 hex_part = h[len("sha256:"):]
223 assert all(c in "0123456789abcdef" for c in hex_part)
224
225
226 # ============================================================================
227 # TestBuildManifest
228 # ============================================================================
229
230
231 class TestBuildManifest:
232 def test_build_returns_dict(self) -> None:
233 result = build_manifest()
234 assert isinstance(result, dict)
235
236 def test_build_has_required_fields(self) -> None:
237 result = build_manifest()
238 for key in ("slug", "dimensions", "viewer_type", "merge_semantics", "version"):
239 assert key in result, f"build_manifest() missing '{key}'"
240
241 def test_build_has_six_dimensions(self) -> None:
242 result = build_manifest()
243 assert len(result["dimensions"]) == 6
244
245 def test_build_dimension_names_match_expected(self) -> None:
246 result = build_manifest()
247 names = {d["name"] for d in result["dimensions"]}
248 assert names == EXPECTED_DIMENSIONS
249
250 def test_build_notes_not_independently_merged(self) -> None:
251 result = build_manifest()
252 notes = next(d for d in result["dimensions"] if d["name"] == "notes")
253 assert notes["independent_merge"] is False
254
255 def test_build_preserves_description_from_static(self) -> None:
256 result = build_manifest()
257 static = load_manifest()
258 # Descriptions come from static file — verify they match
259 static_descs = {d["name"]: d["description"] for d in static["dimensions"]}
260 for dim in result["dimensions"]:
261 assert dim["description"] == static_descs.get(dim["name"], ""), (
262 f"Description mismatch for dimension '{dim['name']}'"
263 )
264
265 def test_build_uses_plugin_schema_description(self) -> None:
266 result = build_manifest()
267 # The description in build_manifest comes from schema(); it must be
268 # a non-empty string mentioning the domain.
269 assert isinstance(result.get("description"), str)
270 assert len(result["description"]) > 0
271
272
273 # ============================================================================
274 # TestAssertManifestInSync
275 # ============================================================================
276
277
278 class TestAssertManifestInSync:
279 def test_static_and_dynamic_are_in_sync(self) -> None:
280 # Must not raise — dimension names should match exactly.
281 assert_manifest_in_sync()
282
283 def test_raises_when_static_missing_dimension(self) -> None:
284 from muse.plugins.knowtation import hub_manifest as hm
285 original_load = hm.load_manifest
286
287 def _fake_load() -> dict[str, Any]:
288 m = original_load()
289 m["dimensions"] = [d for d in m["dimensions"] if d["name"] != "links"]
290 return m
291
292 hm.load_manifest = _fake_load
293 try:
294 with pytest.raises(AssertionError, match="out of sync"):
295 hm.assert_manifest_in_sync()
296 finally:
297 hm.load_manifest = original_load
298
299
300 # ============================================================================
301 # TestManifestHashStability (integration)
302 # ============================================================================
303
304
305 class TestManifestHashStability:
306 def test_hash_stable_on_five_consecutive_calls(self) -> None:
307 m = load_manifest()
308 hashes = {manifest_hash(m) for _ in range(5)}
309 assert len(hashes) == 1, "Hash must be identical across all runs"
310
311 def test_build_manifest_hash_stable_on_three_calls(self) -> None:
312 hashes = {manifest_hash(build_manifest()) for _ in range(3)}
313 assert len(hashes) == 1
314
315
316 # ============================================================================
317 # TestPerformance
318 # ============================================================================
319
320
321 class TestPerformance:
322 def test_manifest_hash_under_one_second(self) -> None:
323 m = load_manifest()
324 start = time.monotonic()
325 manifest_hash(m)
326 elapsed = time.monotonic() - start
327 assert elapsed < 1.0, f"manifest_hash took {elapsed:.3f}s"
328
329 def test_build_manifest_under_one_second(self) -> None:
330 start = time.monotonic()
331 build_manifest()
332 elapsed = time.monotonic() - start
333 assert elapsed < 1.0, f"build_manifest took {elapsed:.3f}s"
334
335
336 # ============================================================================
337 # TestSecurity
338 # ============================================================================
339
340
341 class TestSecurity:
342 def test_no_credential_keys(self) -> None:
343 """manifest.json must not contain credential-looking keys."""
344 sensitive = {"password", "token", "secret", "api_key", "private_key"}
345 content = MANIFEST_FILE.read_text(encoding="utf-8").lower()
346 for word in sensitive:
347 assert word not in content, f"manifest.json contains sensitive word '{word}'"
348
349 def test_no_hardcoded_urls_with_credentials(self) -> None:
350 content = MANIFEST_FILE.read_text(encoding="utf-8")
351 assert "://" not in content or (
352 "https://" not in content.replace("https://musehub.ai", "")
353 ), "manifest.json should not contain hardcoded URLs with credentials"
File History 2 commits
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
sha256:d8316ffae901be06347e16ab55be11868eb519dd16ade3e8aa16a99e662f7e62 baseline: rc14 re-baseline after rc3 store corruption recovery Human patch 26 days ago