"""Phase 00 TDD — SocialPlugin core. RED → GREEN cycle. Run before implementation to confirm failures, then implement until all pass. Coverage matrix: TestSocialPluginShape — protocol satisfaction, registry, domain string TestSocialPluginSchema — DomainSchema structure, dimensions, merge_mode TestSocialPluginInMemory — snapshot pass-through, diff, drift, apply TestSocialPluginMerge — set-union for posts/reactions, profile conflict TestSocialPluginDescribe — schema.description is non-empty string TestSocialPluginDocstrings — module, class, and method docstrings present """ from __future__ import annotations import hashlib import inspect import json import pathlib import sys import time import pytest from muse.domain import ( DriftReport, MergeResult, MuseDomainPlugin, SnapshotManifest, StateDelta, StateSnapshot, StructuredDelta, ) from muse.core.schema import DomainSchema, SetSchema, DimensionSpec from muse.plugins.social.plugin import SocialPlugin from muse.plugins.registry import registered_domains, resolve_plugin_by_domain from muse.core.types import blob_id # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _snap(*path_content_pairs: tuple[str, bytes]) -> SnapshotManifest: """Build a SnapshotManifest from (path, content) pairs.""" files = {path: blob_id(content) for path, content in path_content_pairs} return SnapshotManifest(files=files, domain="social", directories=[]) def _empty_snap() -> SnapshotManifest: return SnapshotManifest(files={}, domain="social", directories=[]) _POST_A = json.dumps({"body": "hello muse", "created_at": "2026-05-01T00:00:00Z"}).encode() _POST_B = json.dumps({"body": "second post", "created_at": "2026-05-01T01:00:00Z"}).encode() _POST_C = json.dumps({"body": "third post", "created_at": "2026-05-01T02:00:00Z"}).encode() _PROFILE_V1 = json.dumps({"handle": "gabriel", "bio": "building the future"}).encode() _PROFILE_V2 = json.dumps({"handle": "gabriel", "bio": "building the singularity"}).encode() _REACTION_A = json.dumps({"emoji": "❤️", "post_id": "sha256:aaa"}).encode() _FOLLOWING = json.dumps({"following": ["alice", "bob"]}).encode() # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @pytest.fixture() def plugin() -> SocialPlugin: return SocialPlugin() # =========================================================================== # Shape — protocol, registry, domain constant # =========================================================================== class TestSocialPluginShape: def test_satisfies_protocol(self, plugin: SocialPlugin) -> None: assert isinstance(plugin, MuseDomainPlugin) def test_has_all_six_methods(self, plugin: SocialPlugin) -> None: for method in ("snapshot", "diff", "merge", "drift", "apply", "schema"): assert callable(getattr(plugin, method, None)), f"missing {method}" def test_domain_constant_is_social(self) -> None: from muse.plugins.social.plugin import _DOMAIN_NAME assert _DOMAIN_NAME == "social" def test_registered_in_registry(self) -> None: assert "social" in registered_domains() def test_resolve_plugin_by_domain_returns_social_plugin(self) -> None: p = resolve_plugin_by_domain("social") assert isinstance(p, SocialPlugin) # =========================================================================== # Schema # =========================================================================== class TestSocialPluginSchema: def test_schema_returns_domain_schema(self, plugin: SocialPlugin) -> None: schema = plugin.schema() assert isinstance(schema, dict) assert "domain" in schema assert "description" in schema assert "top_level" in schema assert "dimensions" in schema assert "merge_mode" in schema def test_schema_domain_is_social(self, plugin: SocialPlugin) -> None: assert plugin.schema()["domain"] == "social" def test_schema_top_level_is_set(self, plugin: SocialPlugin) -> None: top = plugin.schema()["top_level"] assert isinstance(top, dict) assert top["kind"] == "set" def test_schema_merge_mode_three_way(self, plugin: SocialPlugin) -> None: assert plugin.schema()["merge_mode"] == "three_way" def test_schema_has_four_dimensions(self, plugin: SocialPlugin) -> None: assert len(plugin.schema()["dimensions"]) == 4 def test_schema_dimension_names(self, plugin: SocialPlugin) -> None: names = {d["name"] for d in plugin.schema()["dimensions"]} assert names == {"posts", "reactions", "graph", "profile"} def test_posts_dimension_independent(self, plugin: SocialPlugin) -> None: posts_dim = next(d for d in plugin.schema()["dimensions"] if d["name"] == "posts") assert posts_dim["independent_merge"] is True def test_reactions_dimension_independent(self, plugin: SocialPlugin) -> None: d = next(d for d in plugin.schema()["dimensions"] if d["name"] == "reactions") assert d["independent_merge"] is True def test_graph_dimension_independent(self, plugin: SocialPlugin) -> None: d = next(d for d in plugin.schema()["dimensions"] if d["name"] == "graph") assert d["independent_merge"] is True def test_profile_dimension_not_independent(self, plugin: SocialPlugin) -> None: d = next(d for d in plugin.schema()["dimensions"] if d["name"] == "profile") assert d["independent_merge"] is False def test_schema_is_json_serialisable(self, plugin: SocialPlugin) -> None: schema = plugin.schema() assert json.dumps(schema["domain"]) assert json.dumps(schema["merge_mode"]) assert json.dumps(schema["description"]) def test_schema_description_non_empty(self, plugin: SocialPlugin) -> None: assert len(plugin.schema()["description"]) > 20 # =========================================================================== # In-memory snapshot / diff / drift / apply # =========================================================================== class TestSocialPluginInMemory: def test_snapshot_passes_through_manifest(self, plugin: SocialPlugin) -> None: snap = _snap(("posts/abc.json", _POST_A)) result = plugin.snapshot(snap) assert result["files"] == snap["files"] assert result["domain"] == "social" def test_diff_empty_to_empty_has_no_ops(self, plugin: SocialPlugin) -> None: delta = plugin.diff(_empty_snap(), _empty_snap()) assert len(delta["ops"]) == 0 def test_diff_add_post_produces_insert_op(self, plugin: SocialPlugin) -> None: base = _empty_snap() target = _snap(("posts/abc.json", _POST_A)) delta = plugin.diff(base, target) assert len(delta["ops"]) == 1 assert delta["ops"][0]["op"] == "insert" def test_diff_remove_post_produces_delete_op(self, plugin: SocialPlugin) -> None: base = _snap(("posts/abc.json", _POST_A)) target = _empty_snap() delta = plugin.diff(base, target) assert len(delta["ops"]) == 1 assert delta["ops"][0]["op"] == "delete" def test_diff_replace_post_produces_replace_op(self, plugin: SocialPlugin) -> None: base = _snap(("posts/abc.json", _POST_A)) target = _snap(("posts/abc.json", _POST_B)) delta = plugin.diff(base, target) assert len(delta["ops"]) == 1 assert delta["ops"][0]["op"] == "replace" def test_diff_multiple_adds(self, plugin: SocialPlugin) -> None: base = _empty_snap() target = _snap( ("posts/aaa.json", _POST_A), ("posts/bbb.json", _POST_B), ("reactions/r1.json", _REACTION_A), ) delta = plugin.diff(base, target) assert len(delta["ops"]) == 3 def test_drift_no_drift_when_identical(self, plugin: SocialPlugin) -> None: snap = _snap(("posts/abc.json", _POST_A)) report = plugin.drift(snap, snap) assert report.has_drift is False def test_drift_detects_new_post(self, plugin: SocialPlugin) -> None: committed = _snap(("posts/aaa.json", _POST_A)) live = _snap(("posts/aaa.json", _POST_A), ("posts/bbb.json", _POST_B)) report = plugin.drift(committed, live) assert report.has_drift is True def test_drift_detects_deleted_post(self, plugin: SocialPlugin) -> None: committed = _snap(("posts/aaa.json", _POST_A)) live = _empty_snap() report = plugin.drift(committed, live) assert report.has_drift is True def test_drift_report_has_summary(self, plugin: SocialPlugin) -> None: committed = _empty_snap() live = _snap(("posts/aaa.json", _POST_A)) report = plugin.drift(committed, live) assert isinstance(report.summary, str) assert len(report.summary) > 0 def test_apply_returns_live_state_unchanged(self, plugin: SocialPlugin) -> None: snap = _snap(("posts/abc.json", _POST_A)) delta = plugin.diff(_empty_snap(), snap) result = plugin.apply(delta, snap) assert result is snap # =========================================================================== # Merge — set-union for posts/reactions/graph, conflict on profile # =========================================================================== class TestSocialPluginMerge: def test_merge_both_sides_add_different_posts_no_conflict( self, plugin: SocialPlugin ) -> None: base = _empty_snap() left = _snap(("posts/aaa.json", _POST_A)) right = _snap(("posts/bbb.json", _POST_B)) result = plugin.merge(base, left, right) assert result.conflicts == [] assert "posts/aaa.json" in result.merged["files"] assert "posts/bbb.json" in result.merged["files"] def test_merge_both_sides_add_same_post_no_conflict( self, plugin: SocialPlugin ) -> None: base = _empty_snap() left = _snap(("posts/aaa.json", _POST_A)) right = _snap(("posts/aaa.json", _POST_A)) result = plugin.merge(base, left, right) assert result.conflicts == [] assert "posts/aaa.json" in result.merged["files"] def test_merge_only_left_adds_post(self, plugin: SocialPlugin) -> None: base = _empty_snap() left = _snap(("posts/aaa.json", _POST_A)) right = _empty_snap() result = plugin.merge(base, left, right) assert result.conflicts == [] assert "posts/aaa.json" in result.merged["files"] def test_merge_only_right_adds_post(self, plugin: SocialPlugin) -> None: base = _empty_snap() left = _empty_snap() right = _snap(("posts/bbb.json", _POST_B)) result = plugin.merge(base, left, right) assert result.conflicts == [] assert "posts/bbb.json" in result.merged["files"] def test_merge_reactions_set_union(self, plugin: SocialPlugin) -> None: base = _empty_snap() react_b = json.dumps({"emoji": "🔥", "post_id": "sha256:bbb"}).encode() left = _snap(("reactions/r1.json", _REACTION_A)) right = _snap(("reactions/r2.json", react_b)) result = plugin.merge(base, left, right) assert result.conflicts == [] assert len(result.merged["files"]) == 2 def test_merge_concurrent_profile_edits_conflict( self, plugin: SocialPlugin ) -> None: base = _snap(("profile.json", _PROFILE_V1)) left = _snap(("profile.json", _PROFILE_V2)) right_profile = json.dumps({"handle": "gabriel", "bio": "different edit"}).encode() right = _snap(("profile.json", right_profile)) result = plugin.merge(base, left, right) assert "profile.json" in result.conflicts def test_merge_only_left_edits_profile_no_conflict( self, plugin: SocialPlugin ) -> None: base = _snap(("profile.json", _PROFILE_V1)) left = _snap(("profile.json", _PROFILE_V2)) right = _snap(("profile.json", _PROFILE_V1)) result = plugin.merge(base, left, right) assert result.conflicts == [] assert result.merged["files"]["profile.json"] == blob_id(_PROFILE_V2) def test_merge_only_right_edits_profile_no_conflict( self, plugin: SocialPlugin ) -> None: base = _snap(("profile.json", _PROFILE_V1)) left = _snap(("profile.json", _PROFILE_V1)) right = _snap(("profile.json", _PROFILE_V2)) result = plugin.merge(base, left, right) assert result.conflicts == [] assert result.merged["files"]["profile.json"] == blob_id(_PROFILE_V2) def test_merge_graph_following_set_union(self, plugin: SocialPlugin) -> None: # Each follow is a separate content-addressed file — set-union is automatic. follow_alice = json.dumps({"handle": "alice", "since": "2026-05-01T00:00:00Z"}).encode() follow_bob = json.dumps({"handle": "bob", "since": "2026-05-01T00:01:00Z"}).encode() base = _empty_snap() left = _snap(("graph/follows/alice.json", follow_alice)) right = _snap(("graph/follows/bob.json", follow_bob)) result = plugin.merge(base, left, right) assert result.conflicts == [] assert "graph/follows/alice.json" in result.merged["files"] assert "graph/follows/bob.json" in result.merged["files"] def test_merge_returns_merge_result(self, plugin: SocialPlugin) -> None: result = plugin.merge(_empty_snap(), _empty_snap(), _empty_snap()) assert isinstance(result, MergeResult) def test_merge_result_domain_is_social(self, plugin: SocialPlugin) -> None: result = plugin.merge(_empty_snap(), _empty_snap(), _empty_snap()) assert result.merged["domain"] == "social" def test_merge_combined_scenario(self, plugin: SocialPlugin) -> None: # Base: one post + profile base = _snap(("posts/aaa.json", _POST_A), ("profile.json", _PROFILE_V1)) # Left: adds a new post, doesn't touch profile left = _snap( ("posts/aaa.json", _POST_A), ("posts/bbb.json", _POST_B), ("profile.json", _PROFILE_V1), ) # Right: adds a reaction, doesn't touch profile right = _snap( ("posts/aaa.json", _POST_A), ("reactions/r1.json", _REACTION_A), ("profile.json", _PROFILE_V1), ) result = plugin.merge(base, left, right) assert result.conflicts == [] assert "posts/aaa.json" in result.merged["files"] assert "posts/bbb.json" in result.merged["files"] assert "reactions/r1.json" in result.merged["files"] assert "profile.json" in result.merged["files"] # =========================================================================== # Docstrings # =========================================================================== class TestSocialPluginDocstrings: def test_module_has_docstring(self) -> None: import muse.plugins.social.plugin as mod assert mod.__doc__ and len(mod.__doc__.strip()) > 20 def test_class_has_docstring(self, plugin: SocialPlugin) -> None: assert SocialPlugin.__doc__ and len(SocialPlugin.__doc__.strip()) > 20 def test_each_method_has_docstring(self, plugin: SocialPlugin) -> None: for name in ("snapshot", "diff", "merge", "drift", "apply", "schema"): method = getattr(SocialPlugin, name) assert method.__doc__ and len(method.__doc__.strip()) > 10, \ f"{name}() missing docstring"