"""TDD — IdentityPlugin.diff(). Covers: - Add identity → InsertOp with address = path - Remove identity → DeleteOp with address = path - Add relationship → InsertOp with address = path - Remove relationship → DeleteOp with address = path - Modify identity (pubkey rotation) → ReplaceOp - No change → empty ops list - Summary field is non-empty for any non-trivial delta - Ops are addressed by canonical path (identities/.json etc.) """ from __future__ import annotations import pytest from muse.domain import SnapshotManifest, StructuredDelta from muse.core.types import Manifest, long_id from muse.plugins.identity.plugin import IdentityPlugin @pytest.fixture def plugin() -> IdentityPlugin: return IdentityPlugin() def snap(*paths: str) -> SnapshotManifest: """Build a minimal snapshot from path → fake-hash pairs.""" return SnapshotManifest( files={p: long_id(f"{abs(hash(p)):064x}") for p in paths}, domain="identity", directories=[], ) def snap_with(files: Manifest) -> SnapshotManifest: return SnapshotManifest(files=files, domain="identity", directories=[]) def op_addresses(delta: "StructuredDelta") -> set[str]: return {op["address"] for op in delta["ops"]} # type: ignore[index] def op_kinds(delta: "StructuredDelta") -> list[str]: return [op["op"] for op in delta["ops"]] # type: ignore[index] # ── identity adds / removes ─────────────────────────────────────────────────── class TestIdentityDiff: def test_no_change_empty_delta(self, plugin: IdentityPlugin) -> None: s = snap("identities/gabriel.json") delta = plugin.diff(s, s) assert delta["ops"] == [] def test_add_identity_produces_insert_op(self, plugin: IdentityPlugin) -> None: base = snap() target = snap("identities/gabriel.json") delta = plugin.diff(base, target) assert any(op["op"] == "insert" for op in delta["ops"]) def test_add_identity_address_is_path(self, plugin: IdentityPlugin) -> None: base = snap() target = snap("identities/gabriel.json") delta = plugin.diff(base, target) assert "identities/gabriel.json" in op_addresses(delta) def test_remove_identity_produces_delete_op(self, plugin: IdentityPlugin) -> None: base = snap("identities/gabriel.json") target = snap() delta = plugin.diff(base, target) assert any(op["op"] == "delete" for op in delta["ops"]) def test_remove_identity_address_is_path(self, plugin: IdentityPlugin) -> None: base = snap("identities/gabriel.json") target = snap() delta = plugin.diff(base, target) assert "identities/gabriel.json" in op_addresses(delta) def test_multiple_adds_produce_multiple_inserts(self, plugin: IdentityPlugin) -> None: base = snap() target = snap("identities/gabriel.json", "identities/alice.json") delta = plugin.diff(base, target) assert op_addresses(delta) == { "identities/gabriel.json", "identities/alice.json" } assert all(op["op"] == "insert" for op in delta["ops"]) def test_modify_identity_produces_replace_op(self, plugin: IdentityPlugin) -> None: base = snap_with({"identities/gabriel.json": long_id("a" * 64)}) target = snap_with({"identities/gabriel.json": long_id("b" * 64)}) delta = plugin.diff(base, target) assert any(op["op"] == "replace" for op in delta["ops"]) assert "identities/gabriel.json" in op_addresses(delta) # ── relationship adds / removes ─────────────────────────────────────────────── class TestRelationshipDiff: def test_add_relationship_produces_insert_op(self, plugin: IdentityPlugin) -> None: base = snap() target = snap("relationships/gabriel--spawns--claude-code.json") delta = plugin.diff(base, target) assert any(op["op"] == "insert" for op in delta["ops"]) assert "relationships/gabriel--spawns--claude-code.json" in op_addresses(delta) def test_remove_relationship_produces_delete_op(self, plugin: IdentityPlugin) -> None: base = snap("relationships/gabriel--spawns--claude-code.json") target = snap() delta = plugin.diff(base, target) assert any(op["op"] == "delete" for op in delta["ops"]) def test_add_multiple_relationships(self, plugin: IdentityPlugin) -> None: base = snap() target = snap( "relationships/gabriel--spawns--claude-code.json", "relationships/gabriel--member_of--musehub-org.json", ) delta = plugin.diff(base, target) assert len(delta["ops"]) == 2 assert all(op["op"] == "insert" for op in delta["ops"]) # ── mixed / compound diffs ──────────────────────────────────────────────────── class TestCompoundDiff: def test_add_identity_and_relationship(self, plugin: IdentityPlugin) -> None: base = snap() target = snap( "identities/gabriel.json", "relationships/gabriel--spawns--claude-code.json", ) delta = plugin.diff(base, target) assert len(delta["ops"]) == 2 def test_remove_and_add_simultaneously(self, plugin: IdentityPlugin) -> None: base = snap("identities/alice.json") target = snap("identities/bob.json") delta = plugin.diff(base, target) kinds = set(op_kinds(delta)) assert "insert" in kinds assert "delete" in kinds def test_summary_non_empty_for_changes(self, plugin: IdentityPlugin) -> None: base = snap() target = snap("identities/gabriel.json") delta = plugin.diff(base, target) assert delta["summary"] def test_summary_empty_for_no_changes(self, plugin: IdentityPlugin) -> None: s = snap("identities/gabriel.json") delta = plugin.diff(s, s) # summary may be empty string or None for no-op assert not delta["summary"] or delta["summary"] in ("", "no changes")