Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
"""Tests for the ``groups.replicate`` / ``groups.promote`` / ``groups.demote``
|
||||
JSON-RPC surface — cross-gateway room durability."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
import tui_gateway.server as srv
|
||||
from tui_gateway import methods_groups
|
||||
|
||||
MEMBERS = [{"kind": "bot", "id": "planner"}]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def home(tmp_path, monkeypatch):
|
||||
path = tmp_path / ".hermes"
|
||||
path.mkdir()
|
||||
(path / "profiles" / "ops").mkdir(parents=True)
|
||||
monkeypatch.setenv("HERMES_HOME", str(path))
|
||||
methods_groups.stop_hosted_room_service(timeout=1.0)
|
||||
methods_groups.start_hosted_room_service()
|
||||
yield path
|
||||
methods_groups.stop_hosted_room_service(timeout=1.0)
|
||||
|
||||
|
||||
def _result(envelope):
|
||||
assert "error" not in envelope, envelope
|
||||
return envelope["result"]
|
||||
|
||||
|
||||
def _error(envelope):
|
||||
assert "error" in envelope, envelope
|
||||
return envelope["error"]
|
||||
|
||||
|
||||
def _authority_page(tmp_path, gateway_id="install:" + "a" * 32, n=3):
|
||||
"""Build a real room + log on a SEPARATE 'remote authority' DB and return
|
||||
its replay page, as a replicating client would fetch via groups.log."""
|
||||
from gateway import hosted_rooms as rooms
|
||||
|
||||
db = tmp_path / "remote-authority.db"
|
||||
rooms.create_room(
|
||||
db,
|
||||
room_id="room-1",
|
||||
name="Field Room",
|
||||
members=MEMBERS,
|
||||
authority_gateway_id=gateway_id,
|
||||
)
|
||||
for index in range(n):
|
||||
rooms.append_event(
|
||||
db,
|
||||
room_id="room-1",
|
||||
event_id=f"e{index}",
|
||||
kind="message.user",
|
||||
actor={"kind": "user", "id": "tek"},
|
||||
payload={"text": f"msg {index}"},
|
||||
authority_gateway_id=gateway_id,
|
||||
authority_epoch=1,
|
||||
)
|
||||
return rooms.read_events(db, room_id="room-1", since_seq=0, limit=100)
|
||||
|
||||
|
||||
def test_capabilities_advertise_replication(home):
|
||||
result = _result(srv._methods["groups.capabilities"](1, {}))
|
||||
assert "log_replication" in result["features"]
|
||||
assert "authority_takeover" in result["features"]
|
||||
for name in (
|
||||
"groups.replicate",
|
||||
"groups.replica_state",
|
||||
"groups.promote",
|
||||
"groups.demote",
|
||||
):
|
||||
assert name in result["methods"]
|
||||
assert name in srv._LONG_HANDLERS
|
||||
|
||||
|
||||
def test_replicate_then_state_roundtrip(home, tmp_path):
|
||||
page = _authority_page(tmp_path)
|
||||
result = _result(
|
||||
srv._methods["groups.replicate"](
|
||||
1,
|
||||
{
|
||||
"room_id": "room-1",
|
||||
"room_name": "Field Room",
|
||||
"members": MEMBERS,
|
||||
"page": page,
|
||||
},
|
||||
)
|
||||
)
|
||||
assert result["ingested"] == 3
|
||||
state = _result(srv._methods["groups.replica_state"](2, {"room_id": "room-1"}))
|
||||
assert state["last_seq"] == 3
|
||||
assert state["authority"] == page["authority"]
|
||||
|
||||
|
||||
def test_promote_requires_confirm_and_takes_over(home, tmp_path):
|
||||
page = _authority_page(tmp_path)
|
||||
_result(
|
||||
srv._methods["groups.replicate"](
|
||||
1,
|
||||
{
|
||||
"room_id": "room-1",
|
||||
"room_name": "Field Room",
|
||||
"members": MEMBERS,
|
||||
"page": page,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
refused = _error(srv._methods["groups.promote"](2, {"room_id": "room-1"}))
|
||||
assert refused["code"] == 4118
|
||||
|
||||
promoted = _result(
|
||||
srv._methods["groups.promote"](3, {"room_id": "room-1", "confirm": True})
|
||||
)
|
||||
assert promoted["authority_epoch"] == 2
|
||||
assert promoted["previous_gateway_id"] == page["authority"]["gateway_id"]
|
||||
|
||||
# The room is now hosted locally with full history + claim event.
|
||||
log = _result(srv._methods["groups.log"](4, {"room_id": "room-1"}))
|
||||
kinds = [event["kind"] for event in log["events"]]
|
||||
assert kinds == ["message.user"] * 3 + ["authority.claimed"]
|
||||
assert log["authority"]["epoch"] == 2
|
||||
|
||||
|
||||
def test_demote_fences_local_room_against_newer_epoch(home):
|
||||
from gateway.hosted_rooms import local_authority_gateway_id
|
||||
|
||||
_result(
|
||||
srv._methods["groups.create"](
|
||||
1,
|
||||
{
|
||||
"room_id": "room-1",
|
||||
"name": "Local room",
|
||||
"members": [
|
||||
{
|
||||
"member_id": "default",
|
||||
"profile": "default",
|
||||
"handle": "hermes",
|
||||
},
|
||||
{"member_id": "ops", "profile": "ops", "handle": "ops"},
|
||||
],
|
||||
},
|
||||
)
|
||||
)
|
||||
observed_gateway = "install:" + "b" * 32
|
||||
result = _result(
|
||||
srv._methods["groups.demote"](
|
||||
2,
|
||||
{
|
||||
"room_id": "room-1",
|
||||
"observed_gateway_id": observed_gateway,
|
||||
"observed_epoch": 2,
|
||||
},
|
||||
)
|
||||
)
|
||||
assert result["idempotent"] is False
|
||||
assert result["authority_gateway_id"] == observed_gateway
|
||||
|
||||
# Local sends at the stale authority now fail.
|
||||
envelope = srv._methods["groups.send"](
|
||||
3,
|
||||
{
|
||||
"room_id": "room-1",
|
||||
"event_id": "stale-send",
|
||||
"actor": {"kind": "user", "id": "tek"},
|
||||
"payload": {"text": "should fence"},
|
||||
},
|
||||
)
|
||||
assert "error" in envelope
|
||||
assert local_authority_gateway_id() != observed_gateway
|
||||
|
||||
|
||||
def test_replicate_rejects_gapped_page(home, tmp_path):
|
||||
from gateway import hosted_rooms as rooms
|
||||
|
||||
_authority_page(tmp_path, n=5)
|
||||
db = tmp_path / "remote-authority.db"
|
||||
gapped = rooms.read_events(db, room_id="room-1", since_seq=2, limit=100)
|
||||
envelope = srv._methods["groups.replicate"](
|
||||
1,
|
||||
{
|
||||
"room_id": "room-1",
|
||||
"room_name": "Field Room",
|
||||
"members": MEMBERS,
|
||||
"page": gapped,
|
||||
},
|
||||
)
|
||||
assert _error(envelope)["code"] == 4116
|
||||
Reference in New Issue
Block a user