diff --git a/tests/test_cross_process_fold.py b/tests/test_cross_process_fold.py new file mode 100644 index 0000000..e60c944 --- /dev/null +++ b/tests/test_cross_process_fold.py @@ -0,0 +1,83 @@ +"""Cross-process read-your-writes over the git log + fold parity (SHARD-WP-0009 T3). + +The git backend's value over the in-memory double is that the totally ordered log is durable and +shared: a write by one process/handle is immediately visible to another opening the same ref, and +the derived fold is identical to the in-memory fold of the same event sequence (derived = f(log)). +""" + +import os +import subprocess +import sys +import textwrap +from pathlib import Path + +from shard_wiki.coordination import ( + DecisionLog, + EventType, + GitEventStore, + InMemoryEventStore, +) + +_SRC = str(Path(__file__).resolve().parents[1] / "src") + + +def test_new_handle_sees_prior_writes(tmp_path): + repo = tmp_path / "coord" + writer = DecisionLog(GitEventStore(repo)) + writer.append("s", EventType.ALIAS_SET, {"alias": "Home", "target": "shardA:Index"}) + writer.append("s", EventType.BINDING_MADE, {"members": ["a", "b"]}) + # A second, independent handle on the same repo — read-your-writes across handles. + reader = DecisionLog(GitEventStore(repo)) + assert [e.seq for e in reader.events("s")] == [0, 1] + assert reader.fold("s").resolve_alias("Home") == "shardA:Index" + + +def test_append_in_separate_process_is_visible(tmp_path): + repo = tmp_path / "coord" + # Seed from this process so the repo exists. + DecisionLog(GitEventStore(repo)).append( + "s", EventType.ALIAS_SET, {"alias": "A", "target": "x:1"} + ) + child = textwrap.dedent( + f""" + from shard_wiki.coordination import DecisionLog, EventType, GitEventStore + log = DecisionLog(GitEventStore({str(repo)!r})) + log.append("s", EventType.ALIAS_SET, {{"alias": "B", "target": "x:2"}}) + """ + ) + result = subprocess.run( + [sys.executable, "-c", child], + capture_output=True, + text=True, + env={"PYTHONPATH": _SRC, "PATH": os.environ.get("PATH", "")}, + ) + assert result.returncode == 0, result.stderr + # This process, with a fresh handle, sees the child's append in order. + reader = DecisionLog(GitEventStore(repo)) + assert [e.payload["alias"] for e in reader.events("s")] == ["A", "B"] + assert [e.seq for e in reader.events("s")] == [0, 1] + + +def test_cross_process_fold_equals_in_memory_fold(tmp_path): + sequence = [ + (EventType.ALIAS_SET, {"alias": "Home", "target": "shardA:Index"}), + (EventType.BINDING_MADE, {"members": ["a", "b"]}), + (EventType.BINDING_MADE, {"members": ["b", "c"]}), + (EventType.PAGE_FORKED, {"source": "p", "fork": "q"}), + (EventType.ALIAS_SET, {"alias": "Home", "target": "shardB:Main"}), + ] + mem = DecisionLog(InMemoryEventStore()) + for typ, payload in sequence: + mem.append("s", typ, payload) + + repo = tmp_path / "coord" + DecisionLog(GitEventStore(repo)) # init repo + for typ, payload in sequence: + # Each append from a fresh handle to simulate distinct writers over time. + DecisionLog(GitEventStore(repo)).append("s", typ, payload) + + git_state = DecisionLog(GitEventStore(repo)).fold("s") + mem_state = mem.fold("s") + assert git_state.aliases == mem_state.aliases + assert git_state.equivalence_groups == mem_state.equivalence_groups + assert git_state.equivalent_to("a") == frozenset({"a", "b", "c"})