test_identity_domain_diff.py
python
sha256:06dba78c2a78e251b580422dd1fd547f3c8357ff18f7709a860873b2d24dbbbf
chore: bump version to 0.2.0rc14
Sonnet 4.6
patch
16 hours ago
| 1 | """TDD — IdentityPlugin.diff(). |
| 2 | |
| 3 | Covers: |
| 4 | - Add identity → InsertOp with address = path |
| 5 | - Remove identity → DeleteOp with address = path |
| 6 | - Add relationship → InsertOp with address = path |
| 7 | - Remove relationship → DeleteOp with address = path |
| 8 | - Modify identity (pubkey rotation) → ReplaceOp |
| 9 | - No change → empty ops list |
| 10 | - Summary field is non-empty for any non-trivial delta |
| 11 | - Ops are addressed by canonical path (identities/<handle>.json etc.) |
| 12 | """ |
| 13 | from __future__ import annotations |
| 14 | |
| 15 | import pytest |
| 16 | |
| 17 | from muse.domain import SnapshotManifest, StructuredDelta |
| 18 | from muse.core.types import Manifest, long_id |
| 19 | from muse.plugins.identity.plugin import IdentityPlugin |
| 20 | |
| 21 | |
| 22 | @pytest.fixture |
| 23 | def plugin() -> IdentityPlugin: |
| 24 | return IdentityPlugin() |
| 25 | |
| 26 | |
| 27 | def snap(*paths: str) -> SnapshotManifest: |
| 28 | """Build a minimal snapshot from path → fake-hash pairs.""" |
| 29 | return SnapshotManifest( |
| 30 | files={p: long_id(f"{abs(hash(p)):064x}") for p in paths}, |
| 31 | domain="identity", |
| 32 | directories=[], |
| 33 | ) |
| 34 | |
| 35 | |
| 36 | def snap_with(files: Manifest) -> SnapshotManifest: |
| 37 | return SnapshotManifest(files=files, domain="identity", directories=[]) |
| 38 | |
| 39 | |
| 40 | def op_addresses(delta: "StructuredDelta") -> set[str]: |
| 41 | return {op["address"] for op in delta["ops"]} # type: ignore[index] |
| 42 | |
| 43 | |
| 44 | def op_kinds(delta: "StructuredDelta") -> list[str]: |
| 45 | return [op["op"] for op in delta["ops"]] # type: ignore[index] |
| 46 | |
| 47 | |
| 48 | # ── identity adds / removes ─────────────────────────────────────────────────── |
| 49 | |
| 50 | class TestIdentityDiff: |
| 51 | def test_no_change_empty_delta(self, plugin: IdentityPlugin) -> None: |
| 52 | s = snap("identities/gabriel.json") |
| 53 | delta = plugin.diff(s, s) |
| 54 | assert delta["ops"] == [] |
| 55 | |
| 56 | def test_add_identity_produces_insert_op(self, plugin: IdentityPlugin) -> None: |
| 57 | base = snap() |
| 58 | target = snap("identities/gabriel.json") |
| 59 | delta = plugin.diff(base, target) |
| 60 | assert any(op["op"] == "insert" for op in delta["ops"]) |
| 61 | |
| 62 | def test_add_identity_address_is_path(self, plugin: IdentityPlugin) -> None: |
| 63 | base = snap() |
| 64 | target = snap("identities/gabriel.json") |
| 65 | delta = plugin.diff(base, target) |
| 66 | assert "identities/gabriel.json" in op_addresses(delta) |
| 67 | |
| 68 | def test_remove_identity_produces_delete_op(self, plugin: IdentityPlugin) -> None: |
| 69 | base = snap("identities/gabriel.json") |
| 70 | target = snap() |
| 71 | delta = plugin.diff(base, target) |
| 72 | assert any(op["op"] == "delete" for op in delta["ops"]) |
| 73 | |
| 74 | def test_remove_identity_address_is_path(self, plugin: IdentityPlugin) -> None: |
| 75 | base = snap("identities/gabriel.json") |
| 76 | target = snap() |
| 77 | delta = plugin.diff(base, target) |
| 78 | assert "identities/gabriel.json" in op_addresses(delta) |
| 79 | |
| 80 | def test_multiple_adds_produce_multiple_inserts(self, plugin: IdentityPlugin) -> None: |
| 81 | base = snap() |
| 82 | target = snap("identities/gabriel.json", "identities/alice.json") |
| 83 | delta = plugin.diff(base, target) |
| 84 | assert op_addresses(delta) == { |
| 85 | "identities/gabriel.json", "identities/alice.json" |
| 86 | } |
| 87 | assert all(op["op"] == "insert" for op in delta["ops"]) |
| 88 | |
| 89 | def test_modify_identity_produces_replace_op(self, plugin: IdentityPlugin) -> None: |
| 90 | base = snap_with({"identities/gabriel.json": long_id("a" * 64)}) |
| 91 | target = snap_with({"identities/gabriel.json": long_id("b" * 64)}) |
| 92 | delta = plugin.diff(base, target) |
| 93 | assert any(op["op"] == "replace" for op in delta["ops"]) |
| 94 | assert "identities/gabriel.json" in op_addresses(delta) |
| 95 | |
| 96 | |
| 97 | # ── relationship adds / removes ─────────────────────────────────────────────── |
| 98 | |
| 99 | class TestRelationshipDiff: |
| 100 | def test_add_relationship_produces_insert_op(self, plugin: IdentityPlugin) -> None: |
| 101 | base = snap() |
| 102 | target = snap("relationships/gabriel--spawns--claude-code.json") |
| 103 | delta = plugin.diff(base, target) |
| 104 | assert any(op["op"] == "insert" for op in delta["ops"]) |
| 105 | assert "relationships/gabriel--spawns--claude-code.json" in op_addresses(delta) |
| 106 | |
| 107 | def test_remove_relationship_produces_delete_op(self, plugin: IdentityPlugin) -> None: |
| 108 | base = snap("relationships/gabriel--spawns--claude-code.json") |
| 109 | target = snap() |
| 110 | delta = plugin.diff(base, target) |
| 111 | assert any(op["op"] == "delete" for op in delta["ops"]) |
| 112 | |
| 113 | def test_add_multiple_relationships(self, plugin: IdentityPlugin) -> None: |
| 114 | base = snap() |
| 115 | target = snap( |
| 116 | "relationships/gabriel--spawns--claude-code.json", |
| 117 | "relationships/gabriel--member_of--musehub-org.json", |
| 118 | ) |
| 119 | delta = plugin.diff(base, target) |
| 120 | assert len(delta["ops"]) == 2 |
| 121 | assert all(op["op"] == "insert" for op in delta["ops"]) |
| 122 | |
| 123 | |
| 124 | # ── mixed / compound diffs ──────────────────────────────────────────────────── |
| 125 | |
| 126 | class TestCompoundDiff: |
| 127 | def test_add_identity_and_relationship(self, plugin: IdentityPlugin) -> None: |
| 128 | base = snap() |
| 129 | target = snap( |
| 130 | "identities/gabriel.json", |
| 131 | "relationships/gabriel--spawns--claude-code.json", |
| 132 | ) |
| 133 | delta = plugin.diff(base, target) |
| 134 | assert len(delta["ops"]) == 2 |
| 135 | |
| 136 | def test_remove_and_add_simultaneously(self, plugin: IdentityPlugin) -> None: |
| 137 | base = snap("identities/alice.json") |
| 138 | target = snap("identities/bob.json") |
| 139 | delta = plugin.diff(base, target) |
| 140 | kinds = set(op_kinds(delta)) |
| 141 | assert "insert" in kinds |
| 142 | assert "delete" in kinds |
| 143 | |
| 144 | def test_summary_non_empty_for_changes(self, plugin: IdentityPlugin) -> None: |
| 145 | base = snap() |
| 146 | target = snap("identities/gabriel.json") |
| 147 | delta = plugin.diff(base, target) |
| 148 | assert delta["summary"] |
| 149 | |
| 150 | def test_summary_empty_for_no_changes(self, plugin: IdentityPlugin) -> None: |
| 151 | s = snap("identities/gabriel.json") |
| 152 | delta = plugin.diff(s, s) |
| 153 | # summary may be empty string or None for no-op |
| 154 | assert not delta["summary"] or delta["summary"] in ("", "no changes") |
File History
1 commit
sha256:06dba78c2a78e251b580422dd1fd547f3c8357ff18f7709a860873b2d24dbbbf
chore: bump version to 0.2.0rc14
Sonnet 4.6
patch
16 hours ago