generated from coulomb/repo-seed
Factor DecisionLog storage behind an EventStore abstraction: InMemoryEventStore stays the default/test double, GitEventStore makes the coordination log git-addressable. Each space is a ref (refs/spaces/<sha1>); append writes an immutable one-blob commit and advances the ref under compare-and-swap, so the commit chain is the per-space total order and a racing appender can never fork the log. Deterministic stable-JSON event serialization. Zero runtime deps (git CLI via subprocess). API and fold unchanged across backends. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
85 lines
3.1 KiB
Python
85 lines
3.1 KiB
Python
"""Tests for the git-backed event store (SHARD-WP-0009 T1).
|
|
|
|
The git backend must satisfy the same EventStore contract as the in-memory one (round-trip,
|
|
ordering, determinism) while making the log git-addressable.
|
|
"""
|
|
|
|
import subprocess
|
|
|
|
import pytest
|
|
|
|
from shard_wiki.coordination import (
|
|
DecisionLog,
|
|
EventType,
|
|
GitEventStore,
|
|
InMemoryEventStore,
|
|
deserialize_event,
|
|
serialize_event,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def git_store(tmp_path):
|
|
return GitEventStore(tmp_path / "coord")
|
|
|
|
|
|
def test_append_git_read_round_trips(git_store):
|
|
log = DecisionLog(git_store)
|
|
ev = log.append("s", EventType.ALIAS_SET, {"alias": "Home", "target": "shardA:Index"})
|
|
(read,) = log.events("s")
|
|
assert read.seq == ev.seq == 0
|
|
assert read.space == "s"
|
|
assert read.type is EventType.ALIAS_SET
|
|
assert read.payload == {"alias": "Home", "target": "shardA:Index"}
|
|
|
|
|
|
def test_ordering_preserved_and_per_space_monotonic(git_store):
|
|
log = DecisionLog(git_store)
|
|
log.append("a", EventType.ALIAS_SET, {"alias": "X", "target": "s:1"})
|
|
log.append("a", EventType.ALIAS_SET, {"alias": "Y", "target": "s:2"})
|
|
log.append("b", EventType.ALIAS_SET, {"alias": "Z", "target": "s:3"})
|
|
assert [e.seq for e in log.events("a")] == [0, 1]
|
|
assert [e.payload["alias"] for e in log.events("a")] == ["X", "Y"]
|
|
assert [e.seq for e in log.events("b")] == [0] # independent ref/ordering
|
|
|
|
|
|
def test_each_append_is_a_git_commit(git_store):
|
|
log = DecisionLog(git_store)
|
|
log.append("s", EventType.BINDING_MADE, {"members": ["a", "b"]})
|
|
log.append("s", EventType.PAGE_FORKED, {"source": "a", "fork": "c"})
|
|
ref = GitEventStore._ref("s")
|
|
count = subprocess.run(
|
|
["git", "-C", str(git_store.repo_path), "rev-list", "--count", ref],
|
|
capture_output=True, text=True, check=True,
|
|
).stdout.strip()
|
|
assert count == "2" # one immutable commit object per append
|
|
|
|
|
|
def test_deterministic_serialization_is_stable_and_sorted():
|
|
log = InMemoryEventStore()
|
|
ev = log.append("s", EventType.ALIAS_SET, {"target": "z", "alias": "a"})
|
|
blob = serialize_event(ev)
|
|
assert serialize_event(ev) == blob # stable across calls
|
|
assert blob.index(b'"alias"') < blob.index(b'"target"') # payload keys sorted, not insertion
|
|
assert deserialize_event(blob).payload == {"alias": "a", "target": "z"}
|
|
|
|
|
|
def test_git_fold_matches_in_memory_fold(git_store):
|
|
events = [
|
|
(EventType.ALIAS_SET, {"alias": "Home", "target": "shardA:Index"}),
|
|
(EventType.BINDING_MADE, {"members": ["a", "b"]}),
|
|
(EventType.BINDING_MADE, {"members": ["b", "c"]}),
|
|
(EventType.ALIAS_SET, {"alias": "Home", "target": "shardB:Main"}),
|
|
]
|
|
mem = DecisionLog(InMemoryEventStore())
|
|
git = DecisionLog(git_store)
|
|
for typ, payload in events:
|
|
mem.append("s", typ, payload)
|
|
git.append("s", typ, payload)
|
|
assert git.fold("s").aliases == mem.fold("s").aliases
|
|
assert git.fold("s").equivalence_groups == mem.fold("s").equivalence_groups
|
|
|
|
|
|
def test_default_decisionlog_is_in_memory():
|
|
assert isinstance(DecisionLog()._store, InMemoryEventStore)
|