generated from coulomb/repo-seed
feat(coordination): git-backed DecisionLog event store (WP-0009 T1)
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>
This commit is contained in:
84
tests/test_git_event_store.py
Normal file
84
tests/test_git_event_store.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user