"""Tests for Phase 1.8 — Knowtation domain manifest for MuseHub registry. Test tiers covered ------------------ 1. Unit — static manifest.json: file exists, valid JSON, required top-level fields, correct viewer_type, merge_semantics, version, six named dimensions. 2. Unit — validate_manifest(): valid manifest passes, missing fields detected, bad dimensions detected, unknown dimension names flagged. 3. Unit — manifest_hash(): deterministic across runs, output format, identical manifests produce same hash, different manifests produce different hashes. 4. Unit — build_manifest(): required fields present, dimensions include all six, descriptions from static are preserved, schema description used. 5. Integration — assert_manifest_in_sync(): static and dynamic dimension names match. 6. Performance — manifest_hash() and build_manifest() complete in < 1 second. 7. Security — manifest.json contains no credential-looking fields. """ from __future__ import annotations import json import time from typing import Any import pytest from muse.plugins.knowtation.hub_manifest import ( EXPECTED_DIMENSIONS, MANIFEST_FILE, assert_manifest_in_sync, build_manifest, load_manifest, manifest_hash, validate_manifest, ) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _valid_manifest() -> dict[str, Any]: return load_manifest() # ============================================================================ # TestStaticManifestFile # ============================================================================ class TestStaticManifestFile: def test_file_exists(self) -> None: assert MANIFEST_FILE.exists(), f"manifest.json not found at {MANIFEST_FILE}" def test_file_is_valid_json(self) -> None: content = MANIFEST_FILE.read_text(encoding="utf-8") parsed = json.loads(content) assert isinstance(parsed, dict) def test_slug_is_knowtation(self) -> None: m = load_manifest() assert m["slug"] == "knowtation" def test_viewer_type_is_knowledge(self) -> None: m = load_manifest() assert m["viewer_type"] == "knowledge" def test_merge_semantics_is_ot(self) -> None: m = load_manifest() assert m["merge_semantics"] == "ot" def test_version_present_and_semver(self) -> None: m = load_manifest() v = m["version"] assert isinstance(v, str) parts = v.split(".") assert len(parts) == 3 assert all(p.isdigit() for p in parts) def test_display_name_present(self) -> None: m = load_manifest() assert isinstance(m.get("display_name"), str) assert len(m["display_name"]) > 0 def test_description_present(self) -> None: m = load_manifest() assert isinstance(m.get("description"), str) assert len(m["description"]) > 0 def test_dimensions_is_list(self) -> None: m = load_manifest() assert isinstance(m.get("dimensions"), list) def test_exactly_six_dimensions(self) -> None: m = load_manifest() assert len(m["dimensions"]) == 6, ( f"Expected 6 dimensions, got {len(m['dimensions'])}" ) def test_expected_dimension_names_present(self) -> None: m = load_manifest() names = {d["name"] for d in m["dimensions"]} assert names == EXPECTED_DIMENSIONS, ( f"Dimension names mismatch: got {names}, expected {EXPECTED_DIMENSIONS}" ) def test_all_dimensions_have_required_fields(self) -> None: m = load_manifest() required = {"name", "kind", "element_type", "description", "independent_merge"} for i, dim in enumerate(m["dimensions"]): for key in required: assert key in dim, f"dimensions[{i}] missing '{key}'" def test_notes_dimension_not_independently_merged(self) -> None: m = load_manifest() notes_dims = [d for d in m["dimensions"] if d["name"] == "notes"] assert notes_dims, "notes dimension missing" assert notes_dims[0]["independent_merge"] is False def test_non_notes_dimensions_independently_merged(self) -> None: m = load_manifest() for dim in m["dimensions"]: if dim["name"] != "notes": assert dim["independent_merge"] is True, ( f"Dimension '{dim['name']}' should have independent_merge=True" ) def test_file_is_utf8(self) -> None: MANIFEST_FILE.read_text(encoding="utf-8") def test_supported_commands_present(self) -> None: m = load_manifest() cmds = m.get("supported_commands", []) assert isinstance(cmds, list) assert "commit" in cmds # ============================================================================ # TestValidateManifest # ============================================================================ class TestValidateManifest: def test_static_manifest_is_valid(self) -> None: m = load_manifest() errors = validate_manifest(m) assert errors == [], f"Static manifest has validation errors: {errors}" def test_missing_required_field_detected(self) -> None: m = dict(load_manifest()) del m["viewer_type"] errors = validate_manifest(m) assert any("viewer_type" in e for e in errors) def test_missing_description_detected(self) -> None: m = dict(load_manifest()) del m["description"] errors = validate_manifest(m) assert any("description" in e for e in errors) def test_missing_dimensions_key(self) -> None: m = {k: v for k, v in load_manifest().items() if k != "dimensions"} errors = validate_manifest(m) assert any("dimensions" in e for e in errors) def test_empty_dimensions_detected(self) -> None: m = dict(load_manifest()) m["dimensions"] = [] errors = validate_manifest(m) assert any("dimensions" in e for e in errors) def test_dimension_missing_name_detected(self) -> None: m = dict(load_manifest()) bad_dim = dict(m["dimensions"][0]) del bad_dim["name"] m["dimensions"] = [bad_dim] + m["dimensions"][1:] errors = validate_manifest(m) assert any("name" in e for e in errors) def test_returns_list(self) -> None: errors = validate_manifest(load_manifest()) assert isinstance(errors, list) # ============================================================================ # TestManifestHash # ============================================================================ class TestManifestHash: def test_hash_format(self) -> None: h = manifest_hash(load_manifest()) assert h.startswith("sha256:") assert len(h) == len("sha256:") + 64 def test_hash_is_deterministic(self) -> None: m = load_manifest() h1 = manifest_hash(m) h2 = manifest_hash(m) assert h1 == h2 def test_identical_dicts_same_hash(self) -> None: m1 = load_manifest() m2 = load_manifest() assert manifest_hash(m1) == manifest_hash(m2) def test_different_dicts_different_hash(self) -> None: m1 = load_manifest() m2 = dict(load_manifest()) m2["version"] = "9.9.9" assert manifest_hash(m1) != manifest_hash(m2) def test_key_order_does_not_affect_hash(self) -> None: m1 = load_manifest() # Build reversed-key version m2 = dict(reversed(list(m1.items()))) assert manifest_hash(m1) == manifest_hash(m2) def test_hash_only_hex_after_prefix(self) -> None: h = manifest_hash(load_manifest()) hex_part = h[len("sha256:"):] assert all(c in "0123456789abcdef" for c in hex_part) # ============================================================================ # TestBuildManifest # ============================================================================ class TestBuildManifest: def test_build_returns_dict(self) -> None: result = build_manifest() assert isinstance(result, dict) def test_build_has_required_fields(self) -> None: result = build_manifest() for key in ("slug", "dimensions", "viewer_type", "merge_semantics", "version"): assert key in result, f"build_manifest() missing '{key}'" def test_build_has_six_dimensions(self) -> None: result = build_manifest() assert len(result["dimensions"]) == 6 def test_build_dimension_names_match_expected(self) -> None: result = build_manifest() names = {d["name"] for d in result["dimensions"]} assert names == EXPECTED_DIMENSIONS def test_build_notes_not_independently_merged(self) -> None: result = build_manifest() notes = next(d for d in result["dimensions"] if d["name"] == "notes") assert notes["independent_merge"] is False def test_build_preserves_description_from_static(self) -> None: result = build_manifest() static = load_manifest() # Descriptions come from static file — verify they match static_descs = {d["name"]: d["description"] for d in static["dimensions"]} for dim in result["dimensions"]: assert dim["description"] == static_descs.get(dim["name"], ""), ( f"Description mismatch for dimension '{dim['name']}'" ) def test_build_uses_plugin_schema_description(self) -> None: result = build_manifest() # The description in build_manifest comes from schema(); it must be # a non-empty string mentioning the domain. assert isinstance(result.get("description"), str) assert len(result["description"]) > 0 # ============================================================================ # TestAssertManifestInSync # ============================================================================ class TestAssertManifestInSync: def test_static_and_dynamic_are_in_sync(self) -> None: # Must not raise — dimension names should match exactly. assert_manifest_in_sync() def test_raises_when_static_missing_dimension(self) -> None: from muse.plugins.knowtation import hub_manifest as hm original_load = hm.load_manifest def _fake_load() -> dict[str, Any]: m = original_load() m["dimensions"] = [d for d in m["dimensions"] if d["name"] != "links"] return m hm.load_manifest = _fake_load try: with pytest.raises(AssertionError, match="out of sync"): hm.assert_manifest_in_sync() finally: hm.load_manifest = original_load # ============================================================================ # TestManifestHashStability (integration) # ============================================================================ class TestManifestHashStability: def test_hash_stable_on_five_consecutive_calls(self) -> None: m = load_manifest() hashes = {manifest_hash(m) for _ in range(5)} assert len(hashes) == 1, "Hash must be identical across all runs" def test_build_manifest_hash_stable_on_three_calls(self) -> None: hashes = {manifest_hash(build_manifest()) for _ in range(3)} assert len(hashes) == 1 # ============================================================================ # TestPerformance # ============================================================================ class TestPerformance: def test_manifest_hash_under_one_second(self) -> None: m = load_manifest() start = time.monotonic() manifest_hash(m) elapsed = time.monotonic() - start assert elapsed < 1.0, f"manifest_hash took {elapsed:.3f}s" def test_build_manifest_under_one_second(self) -> None: start = time.monotonic() build_manifest() elapsed = time.monotonic() - start assert elapsed < 1.0, f"build_manifest took {elapsed:.3f}s" # ============================================================================ # TestSecurity # ============================================================================ class TestSecurity: def test_no_credential_keys(self) -> None: """manifest.json must not contain credential-looking keys.""" sensitive = {"password", "token", "secret", "api_key", "private_key"} content = MANIFEST_FILE.read_text(encoding="utf-8").lower() for word in sensitive: assert word not in content, f"manifest.json contains sensitive word '{word}'" def test_no_hardcoded_urls_with_credentials(self) -> None: content = MANIFEST_FILE.read_text(encoding="utf-8") assert "://" not in content or ( "https://" not in content.replace("https://musehub.ai", "") ), "manifest.json should not contain hardcoded URLs with credentials"