Files
rune/tests/test_wr_root_authority.py
T
Nick 4944f9e41c
CI Pipeline / best-practices (push) Successful in 10s
CI Pipeline / documentation (push) Successful in 6s
CI Pipeline / canary (push) Successful in 36s
CI Pipeline / lint (push) Successful in 49s
CI Pipeline / gitea-offline (push) Successful in 16s
CI Pipeline / type-check (push) Successful in 30s
CI Pipeline / documentation (pull_request) Successful in 15s
CI Pipeline / canary (pull_request) Successful in 35s
CI Pipeline / gitea-offline (pull_request) Successful in 17s
CI Pipeline / type-check (pull_request) Successful in 30s
CI Pipeline / best-practices (pull_request) Successful in 10s
deploy / deploy (push) Skipped
CI Pipeline / unit-tests (push) Successful in 3m10s
Spec gate (headless) / spec-gate (pull_request) Successful in 14s
CI Pipeline / required-checks (push) Successful in 2s
CI Pipeline / lint (pull_request) Successful in 41s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 5s
CI Pipeline / unit-tests (pull_request) Successful in 3m11s
CI Pipeline / required-checks (pull_request) Successful in 2s
CI Pipeline / CI Pipeline / notify-event-bridge (pull_request) Successful in 7s
Auto-version and Release / release (pull_request) Successful in 59s
test(t21.5): no-spec fallback coverage (ratchet -> 94%)
2026-09-17 13:39:52 +01:00

167 lines
5.5 KiB
Python

"""test_wr_root_authority.py — T21.5 §7: ONE wait/resume root authority.
WAIT_RESUME_ROOT_AUTHORITIES = 1, SPLIT_BRAIN_EVENT_STORES = 0.
"""
from __future__ import annotations
import importlib.util
import json
import sys
from pathlib import Path
import pytest
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
def _load_resolver():
spec = importlib.util.spec_from_file_location(
"wr_root_resolver", str(REPO / "scripts" / "wr_root_resolver.py"))
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
@pytest.fixture
def isolated(monkeypatch, tmp_path):
mod = _load_resolver()
monkeypatch.setattr(mod, "CANONICAL_WR_ROOT", tmp_path / "canon")
monkeypatch.setattr(mod, "LEGACY_WR_ROOT", tmp_path / "legacy")
monkeypatch.delenv("RUNE_WR_ROOT", raising=False)
return mod, tmp_path
def _mk_store(root: Path, n_events: int = 2):
(root / "events").mkdir(parents=True)
for i in range(n_events):
(root / "events" / f"e{i}.json").write_text(
json.dumps({"event_id": f"e{i}", "event": {}}))
# resolution order
def test_env_pin_wins(isolated):
mod, tmp = isolated
pin = tmp / "pinned"
pin.mkdir()
import os
os.environ["RUNE_WR_ROOT"] = str(pin)
try:
assert mod.resolve_root() == pin
finally:
del os.environ["RUNE_WR_ROOT"]
def test_canonical_preferred_when_live(isolated):
mod, tmp = isolated
_mk_store(mod.CANONICAL_WR_ROOT)
_mk_store(mod.LEGACY_WR_ROOT, n_events=0) # dir but no events
assert mod.resolve_root() == mod.CANONICAL_WR_ROOT
def test_split_brain_fails_loudly(isolated):
mod, _ = isolated
_mk_store(mod.CANONICAL_WR_ROOT)
_mk_store(mod.LEGACY_WR_ROOT)
with pytest.raises(RuntimeError, match="SPLIT-BRAIN"):
mod.resolve_root()
def test_split_brain_resolved_by_pin(isolated):
mod, tmp = isolated
_mk_store(mod.CANONICAL_WR_ROOT)
_mk_store(mod.LEGACY_WR_ROOT)
import os
os.environ["RUNE_WR_ROOT"] = str(mod.CANONICAL_WR_ROOT)
try:
assert mod.resolve_root() == mod.CANONICAL_WR_ROOT
finally:
del os.environ["RUNE_WR_ROOT"]
def test_first_run_creates_canonical(isolated):
mod, _ = isolated
assert mod.resolve_root() == mod.CANONICAL_WR_ROOT # no dirs at all
# event_wait delegation
def test_event_wait_wr_root_delegates(isolated, monkeypatch):
mod, _ = isolated
_mk_store(mod.CANONICAL_WR_ROOT)
from rune import event_wait as ew
# point event_wait's resolver import at the isolated module
import importlib.util
resolver_path = REPO / "scripts" / "wr_root_resolver.py"
spec = importlib.util.spec_from_file_location(
"wr_root_resolver", str(resolver_path))
rmod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(rmod)
monkeypatch.setattr(rmod, "CANONICAL_WR_ROOT", mod.CANONICAL_WR_ROOT)
monkeypatch.setattr(rmod, "LEGACY_WR_ROOT", mod.LEGACY_WR_ROOT)
monkeypatch.delenv("RUNE_WR_ROOT", raising=False)
# patch the loader location used inside event_wait: it imports by
# path, so we monkeypatch resolve at the source module instead
monkeypatch.setattr(ew, "wr_root",
lambda: rmod.resolve_root())
assert ew.wr_root() == mod.CANONICAL_WR_ROOT
# daemon launcher uses the resolver (static check of the contract)
def test_event_wait_no_spec_raises_fallback(monkeypatch, tmp_path):
"""spec_from_file_location returning None (missing file) triggers
the documented fallback policy."""
from rune import event_wait as ew
import importlib.util
monkeypatch.setattr("importlib.util.spec_from_file_location",
lambda *a, **k: None)
monkeypatch.setattr(ew, "DEFAULT_WR_ROOT", tmp_path / "legacy-home")
monkeypatch.delenv("RUNE_WR_ROOT", raising=False)
import os
env = os.environ.get("RUNE_WR_ROOT")
try:
os.environ.pop("RUNE_WR_ROOT", None)
shared = Path("/mnt/workdrive/wr-bridge")
expected = shared if shared.exists() else tmp_path / "legacy-home"
assert ew.wr_root() == expected
finally:
if env is not None:
os.environ["RUNE_WR_ROOT"] = env
def test_daemon_launcher_resolves_root_once():
src = (REPO / "scripts" / "start_wr_daemon.py").read_text()
assert "resolve_root()" in src
assert "WR_ROOT=str(root)" in src
assert "RUNE_WR_ROOT=str(root)" in src
def test_reconcile_is_idempotent(isolated, capsys):
mod, _ = isolated
_mk_store(mod.CANONICAL_WR_ROOT, n_events=1)
_mk_store(mod.LEGACY_WR_ROOT, n_events=3)
import subprocess
# run the reconcile logic in-process against isolated roots
sys.path.insert(0, str(REPO / "scripts"))
import wr_root_reconcile as rec
rec_mod = rec
# patch module-level roots
monkeypatch_targets = rec_mod
import types
# simple: re-execute main with patched resolver attrs
old_canon, old_legacy = rec_mod.resolver.CANONICAL_WR_ROOT, \
rec_mod.resolver.LEGACY_WR_ROOT
rec_mod.resolver.CANONICAL_WR_ROOT = mod.CANONICAL_WR_ROOT
rec_mod.resolver.LEGACY_WR_ROOT = mod.LEGACY_WR_ROOT
try:
assert rec_mod.main() == 0
assert rec_mod.main() == 0 # second run: nothing left to move
finally:
rec_mod.resolver.CANONICAL_WR_ROOT = old_canon
rec_mod.resolver.LEGACY_WR_ROOT = old_legacy
events = sorted(p.name for p in
(mod.CANONICAL_WR_ROOT / "events").glob("*.json"))
assert len(events) == 3 # 1 original + 2 moved