Files
aiturk-hermes-ide/tests/tui_gateway/test_groups_methods.py
T

1014 lines
32 KiB
Python

"""Tests for the gateway-hosted ``groups.*`` JSON-RPC contract."""
from __future__ import annotations
from types import SimpleNamespace
import pytest
import tui_gateway.server as srv
from tui_gateway import methods_groups
@pytest.fixture
def home(tmp_path, monkeypatch):
class DurableRunStore:
durable = True
path = tmp_path / ".hermes"
path.mkdir()
(path / "profiles" / "ops").mkdir(parents=True)
monkeypatch.setenv("HERMES_HOME", str(path))
monkeypatch.setattr(srv, "_run_idempotency_store", DurableRunStore(), raising=False)
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 _server_authority():
from gateway.hosted_rooms import local_authority_gateway_id
return local_authority_gateway_id()
def _create_room():
return _result(
srv._methods["groups.create"](
1,
{
"room_id": "room-1",
"name": "Release room",
"members": [
{
"member_id": "default",
"profile": "default",
"handle": "hermes",
},
{"member_id": "ops", "profile": "ops", "handle": "ops"},
],
"authority_gateway_id": "gateway-a",
},
)
)["room"]
def test_capabilities_are_honest_about_the_driver_boundary(home):
methods_groups.stop_hosted_room_service(timeout=1.0)
result = _result(srv._methods["groups.capabilities"](1, {}))
assert result["protocol_version"] == 2
assert result["driver"] is False
assert result["authority_gateway_id"] == _server_authority()
assert "authority_epoch" in result["features"]
assert "coordinator_fencing" in result["features"]
assert "monotonic_log" in result["features"]
assert "groups.state" in result["methods"]
assert "groups.send" in result["methods"]
assert "groups.send" in srv._LONG_HANDLERS
assert "groups.retry" in result["methods"]
assert "groups.approve" in result["methods"]
advertised = [
str(value).lower() for value in (*result["features"], *result["methods"])
]
assert not any(
token in value
for token in ("attachment", "desktop", "messaging")
for value in advertised
)
assert result["room_link"]["enabled"] is True
def test_capabilities_and_invitation_advertise_scoped_roomlink(home, monkeypatch):
monkeypatch.setenv("API_SERVER_KEY", "gateway-api-key-1234567890")
monkeypatch.setenv("HERMES_PROFILE", "reviewer")
result = _result(srv._methods["groups.capabilities"](1, {}))
assert result["room_link"]["enabled"] is True
assert result["room_link"]["profile"] == "reviewer"
assert result["room_link"]["catalog"]["text"] is True
assert "groups.peer.invite" in result["methods"]
assert "groups.peer.register" in result["methods"]
invitation = _result(
srv._methods["groups.peer.invite"](
2,
{
"room_id": "room-1",
"home_install_id": "install-home",
"authority_gateway_id": "install-home",
"authority_epoch": 1,
"member_id": "member-peer",
"grant_id": "grant-room-1",
},
)
)
assert invitation["target_profile"] == "reviewer"
assert invitation["catalog"] == result["room_link"]["catalog"]
assert "." in invitation["grant"]
from gateway import hosted_rooms
assert hosted_rooms.peer_room_is_reserved(
hosted_rooms.default_db_path(),
room_id="room-1",
target_profile="reviewer",
)
def test_capabilities_disable_roomlink_when_run_replay_is_not_durable(
home, monkeypatch
):
import tui_gateway.methods_groups as groups_methods
class VolatileRunStore:
durable = False
class BoundServer:
_run_idempotency_store = VolatileRunStore()
monkeypatch.setenv("API_SERVER_KEY", "gateway-api-key-1234567890")
monkeypatch.setattr(groups_methods, "_bound_server", BoundServer())
result = _result(srv._methods["groups.capabilities"](1, {}))
assert result["room_link"] == {
"enabled": False,
"reason": "durable_run_storage_required",
}
invitation = srv._methods["groups.peer.invite"](
2,
{
"room_id": "room-volatile",
"home_install_id": "install-home",
"authority_gateway_id": "install-home",
"authority_epoch": 1,
"member_id": "member-peer",
},
)
assert invitation["error"]["code"] == 4120
assert "durable run idempotency" in invitation["error"]["message"]
def test_capabilities_open_shared_durable_run_store_without_test_injection(
home, monkeypatch
):
"""The production dashboard server must not depend on fixture injection."""
monkeypatch.setenv("API_SERVER_KEY", "gateway-api-key-1234567890")
monkeypatch.delattr(srv, "_run_idempotency_store", raising=False)
result = _result(srv._methods["groups.capabilities"](1, {}))
store = srv._run_idempotency_store
try:
assert store.durable is True
assert result["room_link"]["enabled"] is True
finally:
store.close()
def test_app_managed_catalog_and_self_advertised_endpoint_are_consistent(
home, monkeypatch
):
monkeypatch.setenv("API_SERVER_KEY", "gateway-api-key-1234567890")
monkeypatch.setenv("HERMES_DESKTOP", "1")
monkeypatch.setenv("HERMES_ROOM_LINK_URL", "https://peer.example.test/hermes")
capability = _result(srv._methods["groups.capabilities"](1, {}))
invitation = _result(
srv._methods["groups.peer.invite"](
2,
{
"room_id": "room-1",
"home_install_id": "install-home",
"authority_gateway_id": "install-home",
"authority_epoch": 1,
"member_id": "member-peer",
},
)
)
assert capability["persistent_process"] is False
assert capability["room_link"]["catalog"] == invitation["catalog"]
assert capability["room_link"]["endpoint"] == {
"available": True,
"url": "https://peer.example.test/hermes",
"transport_security": "tls",
}
assert invitation["endpoint"] == capability["room_link"]["endpoint"]
def test_launch_profile_is_valid_for_roomlink_invitation(home, monkeypatch):
monkeypatch.setenv("API_SERVER_KEY", "gateway-api-key-1234567890")
monkeypatch.setenv("HERMES_PROFILE", "default")
capability = _result(
srv._methods["groups.capabilities"](1, {"profile": "default"})
)
invitation = _result(
srv._methods["groups.peer.invite"](
2,
{
"room_id": "room-default",
"home_install_id": "install-home",
"authority_gateway_id": "install-home",
"authority_epoch": 1,
"member_id": "member-default",
"profile": "default",
},
)
)
assert capability["room_link"]["enabled"] is True
assert capability["room_link"]["profile"] == "default"
assert invitation["target_profile"] == "default"
def test_roomlink_endpoint_absence_has_machine_reason(home, monkeypatch):
monkeypatch.setenv("API_SERVER_KEY", "gateway-api-key-1234567890")
monkeypatch.delenv("HERMES_ROOM_LINK_URL", raising=False)
result = _result(srv._methods["groups.capabilities"](1, {}))
assert result["room_link"]["endpoint"] == {
"available": False,
"reason": "not_configured",
}
def test_multiplexed_invitation_uses_exact_profile_secret(home, monkeypatch):
from gateway.hosted_room_peer import (
HostedRoomGrantError,
decode_room_grant,
derive_room_grant_secret,
gateway_room_grant_secret,
)
reviewer_home = home / "profiles" / "reviewer"
reviewer_home.mkdir(parents=True)
reviewer_key = "reviewer-api-key-1234567890"
default_key = "default-api-key-1234567890"
(reviewer_home / ".env").write_text(
f"API_SERVER_KEY={reviewer_key}\n", encoding="utf-8"
)
monkeypatch.setenv("API_SERVER_KEY", default_key)
invitation = _result(
srv._methods["groups.peer.invite"](
3,
{
"room_id": "room-1",
"home_install_id": "install-home",
"authority_gateway_id": "gateway-home",
"authority_epoch": 1,
"member_id": "member-reviewer",
"profile": "reviewer",
},
)
)
claims = decode_room_grant(
gateway_room_grant_secret(home),
invitation["grant"],
permission="status",
)
assert claims["target_profile"] == "reviewer"
with pytest.raises(HostedRoomGrantError, match="signature"):
decode_room_grant(
derive_room_grant_secret(default_key),
invitation["grant"],
permission="status",
)
def test_named_profile_needs_no_copied_api_key_for_roomlink(home, monkeypatch):
from gateway.hosted_room_peer import (
HostedRoomGrantError,
decode_room_grant,
derive_room_grant_secret,
gateway_room_grant_secret,
)
reviewer_home = home / "profiles" / "reviewer"
reviewer_home.mkdir(parents=True)
gateway_key = "gateway-api-key-1234567890"
monkeypatch.setenv("API_SERVER_KEY", gateway_key)
invitation = _result(
srv._methods["groups.peer.invite"](
4,
{
"room_id": "room-named-bot",
"home_install_id": "install-home",
"authority_gateway_id": "gateway-home",
"authority_epoch": 1,
"member_id": "member-reviewer",
"profile": "reviewer",
},
)
)
claims = decode_room_grant(
gateway_room_grant_secret(home),
invitation["grant"],
permission="status",
)
assert claims["target_profile"] == "reviewer"
with pytest.raises(HostedRoomGrantError, match="signature"):
decode_room_grant(
derive_room_grant_secret(gateway_key),
invitation["grant"],
permission="status",
)
def test_register_peer_route_probes_scope_and_persists_via_service(home, monkeypatch):
from gateway.hosted_room_peer import catalog_mapping
from gateway.hosted_rooms import local_authority_gateway_id
catalog = catalog_mapping(
installation_id="install-peer",
persistent_process=True,
)
captured = {}
room = _create_room()
class FakeClient:
def __init__(self, *, base_url, api_key, **kwargs):
captured["base_url"] = base_url
captured["api_key"] = api_key
def probe(self, *, grant):
captured["grant"] = grant
return {
"room_id": "room-1",
"home_install_id": local_authority_gateway_id(),
"authority_gateway_id": room["authority_gateway_id"],
"authority_epoch": room["authority_epoch"],
"member_id": "member-peer",
"target_profile": "reviewer",
"catalog": catalog,
}
class FakeService:
db_path = home / "state.db"
def register_peer_route(self, **kwargs):
captured["registered"] = kwargs
monkeypatch.setattr(srv, "get_hosted_room_service", lambda: FakeService())
monkeypatch.setattr(
"tui_gateway.hosted_room_peer_http.PeerRunsHTTPClient",
FakeClient,
)
result = _result(
srv._methods["groups.peer.register"](
3,
{
"room_id": "room-1",
"member_id": "member-peer",
"target_url": "https://peer.example.test",
"target_profile": "reviewer",
"grant": "signed.room.grant",
"catalog": catalog,
},
)
)
assert result["registered"] is True
assert captured["api_key"] == ""
assert captured["registered"]["target_url"] == ("https://peer.example.test")
def test_register_rejects_plaintext_non_loopback(home, monkeypatch):
class FakeService:
db_path = home / "state.db"
monkeypatch.setattr(srv, "get_hosted_room_service", lambda: FakeService())
response = srv._methods["groups.peer.register"](
4,
{
"room_id": "room-1",
"member_id": "member-peer",
"target_url": "http://peer.example.test:8377",
"target_profile": "reviewer",
"grant": "signed.room.grant",
"catalog": {},
},
)
assert response["error"]["code"] == 5120
assert "https outside" in response["error"]["message"]
def test_register_requires_roomlink_protocol_v2(home, monkeypatch):
from gateway.hosted_room_peer import catalog_mapping
class FakeService:
db_path = home / "state.db"
monkeypatch.setattr(srv, "get_hosted_room_service", lambda: FakeService())
response = srv._methods["groups.peer.register"](
5,
{
"room_id": "room-1",
"member_id": "member-peer",
"target_url": "https://peer.example.test",
"target_profile": "reviewer",
"grant": "signed.room.grant",
"catalog": catalog_mapping(
installation_id="install-peer",
protocol_versions=(1,),
persistent_process=True,
),
},
)
assert response["error"]["code"] == 5120
assert "protocol v2" in response["error"]["message"]
def test_create_list_send_and_log_roundtrip(home):
room = _create_room()
assert room["idempotent"] is False
listed = _result(srv._methods["groups.list"](2, {}))
assert [item["room_id"] for item in listed["rooms"]] == ["room-1"]
assert listed["next_offset"] is None
state = _result(srv._methods["groups.state"](3, {"room_id": "room-1"}))
assert state["room"]["authority_gateway_id"] == _server_authority()
assert state["room"]["authority_epoch"] == 1
assert state["room"]["latest_seq"] == 0
sent = _result(
srv._methods["groups.send"](
4,
{
"room_id": "room-1",
"event_id": "event-1",
"actor": {"kind": "user", "id": "desktop-user"},
"payload": {"text": "hello", "thread_id": "thread-1"},
},
)
)
assert sent["accepted"] is True
assert sent["driver_started"] is True
assert sent["event"]["seq"] == 1
assert sent["event"]["kind"] == "message.user"
assert sent["event"]["actor"] == {"kind": "user", "id": "desktop"}
replay = _result(
srv._methods["groups.log"](
5,
{"room_id": "room-1", "since_seq": 0},
)
)
assert replay["latest_seq"] == replay["cursor"] == 1
assert replay["events"][0]["payload"] == {
"text": "hello",
"thread_id": "thread-1",
}
def test_groups_list_returns_bounded_pages(home):
_create_room()
_result(
srv._methods["groups.create"](
2,
{
"room_id": "room-2",
"name": "Second room",
"members": [
{
"member_id": "default",
"profile": "default",
"handle": "hermes",
},
{"member_id": "ops", "profile": "ops", "handle": "ops"},
],
},
)
)
first = _result(srv._methods["groups.list"](3, {"limit": 1}))
second = _result(
srv._methods["groups.list"](
4,
{"limit": 1, "offset": first["next_offset"]},
)
)
assert first["next_offset"] == 1
assert second["next_offset"] == 2
assert {first["rooms"][0]["room_id"], second["rooms"][0]["room_id"]} == {
"room-1",
"room-2",
}
final = _result(srv._methods["groups.list"](5, {"limit": 1, "offset": 2}))
assert final["rooms"] == []
assert final["next_offset"] is None
def test_rpc_retry_is_idempotent_and_conflict_is_visible(home):
_create_room()
params = {
"room_id": "room-1",
"event_id": "event-1",
"actor": {"kind": "user", "id": "desktop-user"},
"payload": {"text": "hello", "thread_id": "thread-1"},
}
first = _result(srv._methods["groups.send"](2, params))
repeated = _result(srv._methods["groups.send"](3, params))
assert first["event"]["seq"] == repeated["event"]["seq"] == 1
assert first["client_event_id"] == repeated["client_event_id"] == "event-1"
assert first["event"]["event_id"].startswith("user:")
assert repeated["event"]["idempotent"] is True
conflict = srv._methods["groups.send"](
4,
{
**params,
"payload": {"text": "different", "thread_id": "thread-1"},
},
)
assert conflict["error"]["code"] == 4111
assert "different content" in conflict["error"]["message"]
def test_foreign_authority_cannot_send_or_disband(home):
from gateway.hosted_rooms import (
claim_authority,
default_db_path,
list_rooms,
read_events,
)
_create_room()
claim_authority(
default_db_path(),
room_id="room-1",
expected_gateway_id=_server_authority(),
expected_epoch=1,
new_gateway_id="foreign-gateway",
event_id="claim-foreign",
)
before = read_events(default_db_path(), room_id="room-1")
sent = srv._methods["groups.send"](
2,
{
"room_id": "room-1",
"event_id": "stale-send",
"payload": {"text": "must not land", "thread_id": "thread-1"},
},
)
disbanded = srv._methods["groups.disband"](3, {"room_id": "room-1"})
assert sent["error"]["code"] == 4111
assert sent["error"]["data"] == {"reason": "authority_conflict"}
assert disbanded["error"]["code"] == 4113
assert disbanded["error"]["data"] == {"reason": "authority_conflict"}
assert read_events(default_db_path(), room_id="room-1") == before
assert list_rooms(default_db_path())[0]["room_id"] == "room-1"
def test_client_event_id_cannot_squat_disband_receipt(home, monkeypatch):
from gateway import hosted_rooms
_create_room()
service = methods_groups.get_hosted_room_service()
assert service is not None
def append_without_starting_work(*, room_id, event_id, payload):
room = hosted_rooms.room_state(service.db_path, room_id=room_id)
return hosted_rooms.append_event(
service.db_path,
room_id=room_id,
event_id=event_id,
kind="message.user",
actor={"kind": "user", "id": "desktop"},
payload=payload,
authority_gateway_id=str(room["authority_gateway_id"]),
authority_epoch=int(room["authority_epoch"]),
)
monkeypatch.setattr(service, "send", append_without_starting_work)
monkeypatch.setattr(service, "stop_room", lambda *_args, **_kwargs: 0)
sent = _result(
srv._methods["groups.send"](
2,
{
"room_id": "room-1",
"event_id": "system:room-disbanded",
"payload": {"text": "still a user message", "thread_id": "thread-1"},
},
)
)
assert sent["client_event_id"] == "system:room-disbanded"
assert sent["event"]["event_id"].startswith("user:")
assert sent["event"]["event_id"] != "system:room-disbanded"
first = _result(srv._methods["groups.disband"](3, {"room_id": "room-1"}))
repeated = _result(srv._methods["groups.disband"](4, {"room_id": "room-1"}))
assert first["tombstone"]["event"]["event_id"] == "system:room-disbanded"
assert repeated["tombstone"]["idempotent"] is True
replay = _result(
srv._methods["groups.log"](
5,
{"room_id": "room-1", "include_disbanded": True},
)
)
kinds = [event["kind"] for event in replay["events"]]
assert kinds[0] == "message.user"
assert kinds[-1] == "room.disbanded"
assert kinds.count("room.disbanded") == 1
def test_send_does_not_trust_client_supplied_actor_identity(home):
_create_room()
sent = _result(
srv._methods["groups.send"](
2,
{
"room_id": "room-1",
"event_id": "event-1",
"actor": {"kind": "user", "id": "spoofed-user"},
"payload": {"text": "hello", "thread_id": "thread-1"},
},
)
)
assert sent["event"]["actor"] == {"kind": "user", "id": "desktop"}
def test_create_ignores_client_supplied_authority_identity(home):
members = [
{"member_id": "default", "profile": "default", "handle": "hermes"},
{"member_id": "ops", "profile": "ops", "handle": "ops"},
]
created = _result(
srv._methods["groups.create"](
1,
{"room_id": "legacy-room", "name": "Legacy", "members": members},
)
)["room"]
retried = _result(
srv._methods["groups.create"](
2,
{
"room_id": "legacy-room",
"name": "Legacy",
"members": members,
"authority_gateway_id": "spoofed-gateway",
},
)
)["room"]
assert created["authority_gateway_id"] == _server_authority()
assert retried["authority_gateway_id"] == _server_authority()
assert retried["idempotent"] is True
def test_legacy_room_adoption_emits_one_lineage_receipt(home):
from gateway.hosted_rooms import create_room, default_db_path
members = [
{"member_id": "default", "profile": "default", "handle": "hermes"},
{"member_id": "ops", "profile": "ops", "handle": "ops"},
]
create_room(
default_db_path(),
room_id="legacy-room",
name="Legacy",
members=members,
authority_gateway_id="legacy",
now=1,
)
adopted = _result(
srv._methods["groups.create"](
2,
{"room_id": "legacy-room", "name": "Legacy", "members": members},
)
)["room"]
state = _result(srv._methods["groups.state"](3, {"room_id": "legacy-room"}))["room"]
assert adopted["adopted"] is True
assert adopted["authority_gateway_id"] == _server_authority()
assert adopted["authority_epoch"] == 2
assert adopted["claim_event"]["payload"] == {
"previous_gateway_id": "legacy",
"authority_gateway_id": _server_authority(),
"authority_epoch": 2,
}
assert state["authority_claim"]["event_id"] == "system:authority-adopted"
assert state["latest_seq"] == 1
@pytest.mark.parametrize(
("method_name", "params"),
[
(
"groups.create",
{
"room_id": "",
"name": "x",
"members": [],
"authority_gateway_id": "gateway-a",
},
),
(
"groups.send",
{
"room_id": "missing",
"event_id": "event-1",
"actor": {"kind": "user", "id": "desktop-user"},
"payload": {},
},
),
("groups.log", {"room_id": "missing", "since_seq": 0}),
],
)
def test_invalid_or_unknown_room_returns_contract_error(home, method_name, params):
result = srv._methods[method_name](1, params)
assert result["error"]["code"] in {4110, 4111, 4112, 5111, 5112}
def test_retry_and_approval_controls_forward_only_exact_local_coordinates(
home, monkeypatch
):
calls = []
identity = SimpleNamespace(
room_id="room-1",
task_id="task-1",
thread_id="thread-1",
turn_id="turn-1",
)
service = SimpleNamespace(
retry_room_task=lambda room_id, task_id: (
calls.append(("retry", room_id, task_id))
or {
"identity": identity,
"status": "queued",
"execution_generation": 1,
"cancel_generation": 0,
}
),
approve_room_task=lambda room_id, **kwargs: (
calls.append(("approve", room_id, kwargs)) or {"resolved": 1}
),
)
monkeypatch.setattr(srv, "get_hosted_room_service", lambda: service)
retried = _result(
srv._methods["groups.retry"](
1,
{"room_id": "room-1", "task_id": "task-1"},
)
)
approved = _result(
srv._methods["groups.approve"](
2,
{
"room_id": "room-1",
"member_id": "ops",
"task_id": "task-1",
"execution_generation": 1,
"request_id": "approval-1",
"choice": "once",
},
)
)
assert retried["task"] == {
"room_id": "room-1",
"task_id": "task-1",
"thread_id": "thread-1",
"turn_id": "turn-1",
"status": "queued",
"execution_generation": 1,
"cancel_generation": 0,
}
assert approved == {"approved": True, "result": {"resolved": 1}}
assert calls == [
("retry", "room-1", "task-1"),
(
"approve",
"room-1",
{
"member_id": "ops",
"task_id": "task-1",
"execution_generation": 1,
"choice": "once",
"request_id": "approval-1",
},
),
]
@pytest.mark.parametrize(
("method_name", "params"),
[
("groups.create", {"room_id": "room-1", "name": "Room", "members": []}),
("groups.send", {"room_id": "room-1", "event_id": "event-1", "payload": {}}),
("groups.disband", {"room_id": "room-1"}),
("groups.stop", {"room_id": "room-1"}),
("groups.retry", {"room_id": "room-1", "task_id": "task-1"}),
(
"groups.approve",
{
"room_id": "room-1",
"member_id": "ops",
"task_id": "task-1",
"execution_generation": 1,
"request_id": "approval-1",
"choice": "once",
},
),
],
)
def test_mutating_controls_fail_closed_without_a_supervised_worker(
home, monkeypatch, method_name, params
):
monkeypatch.setattr(srv, "get_hosted_room_service", lambda: None)
result = srv._methods[method_name](1, params)
assert result["error"]["code"] in {4115, 4123}
def test_disband_tombstones_room(home):
_create_room()
first = _result(srv._methods["groups.disband"](3, {"room_id": "room-1"}))
repeated = _result(srv._methods["groups.disband"](4, {"room_id": "room-1"}))
assert first["tombstone"]["idempotent"] is False
assert repeated["tombstone"]["idempotent"] is True
assert _result(srv._methods["groups.list"](5, {}))["rooms"] == []
deleted = _result(srv._methods["groups.list"](6, {"include_disbanded": True}))[
"rooms"
]
assert deleted[0]["disbanded_at"] == first["tombstone"]["disbanded_at"]
replay = _result(
srv._methods["groups.log"](
7,
{"room_id": "room-1", "include_disbanded": True},
)
)
assert [event["kind"] for event in replay["events"]] == [
"room.stop_requested",
"room.disbanded",
]
def test_pruned_room_send_and_log_report_expired_history(home, monkeypatch):
from gateway import hosted_rooms
members = [
{"member_id": "default", "profile": "default", "handle": "hermes"},
{"member_id": "ops", "profile": "ops", "handle": "ops"},
]
_create_room()
monkeypatch.setattr(hosted_rooms, "MAX_DISBANDED_ROOM_TOMBSTONES", 0)
_result(srv._methods["groups.disband"](2, {"room_id": "room-1"}))
repeated = _result(
srv._methods["groups.disband"](3, {"room_id": "room-1"})
)["tombstone"]
assert repeated["idempotent"] is True
assert repeated["history_expired"] is True
sent = srv._methods["groups.send"](
4,
{
"room_id": "room-1",
"event_id": "stale-send",
"payload": {"text": "stale", "thread_id": "thread-1"},
},
)
logged = srv._methods["groups.log"](
5,
{"room_id": "room-1", "include_disbanded": True},
)
renamed = srv._methods["groups.rename"](
6,
{"room_id": "room-1", "event_id": "stale-rename", "name": "Stale"},
)
assert sent["error"]["code"] == 4111, sent
assert logged["error"]["code"] == 4112, logged
assert renamed["error"]["code"] == 4117, renamed
assert sent["error"]["data"] == {"reason": "room_history_expired"}
assert logged["error"]["data"] == {"reason": "room_history_expired"}
assert renamed["error"]["data"] == {"reason": "room_history_expired"}
assert "permanently retired" in sent["error"]["message"]
recreated = srv._methods["groups.create"](
7,
{"room_id": "room-1", "name": "Replacement", "members": members},
)
assert recreated["error"]["code"] == 4110
created = _result(
srv._methods["groups.create"](
8,
{"room_id": "room-new", "name": "Fresh", "members": members},
)
)
assert created["room"]["room_id"] == "room-new"
def test_disband_stops_and_revokes_before_tombstoning(home, monkeypatch):
_create_room()
calls = []
class FakeService:
db_path = home / "state.db"
def stop_room(self, room_id, **_kwargs):
calls.append(("stop", room_id))
def revoke_room_routes(self, room_id):
calls.append(("revoke", room_id))
monkeypatch.setattr(srv, "get_hosted_room_service", lambda: FakeService())
_result(srv._methods["groups.disband"](9, {"room_id": "room-1"}))
assert calls == [("stop", "room-1"), ("revoke", "room-1")]
assert _result(srv._methods["groups.list"](10, {}))["rooms"] == []
def test_failed_remote_revocation_keeps_room_recoverable(home, monkeypatch):
_create_room()
class FakeService:
db_path = home / "state.db"
def stop_room(self, _room_id, **_kwargs):
return 1
def revoke_room_routes(self, _room_id):
raise RuntimeError("peer is offline")
monkeypatch.setattr(srv, "get_hosted_room_service", lambda: FakeService())
result = srv._methods["groups.disband"](11, {"room_id": "room-1"})
assert result["error"]["code"] == 5114
assert [
room["room_id"]
for room in _result(srv._methods["groups.list"](12, {}))["rooms"]
] == ["room-1"]
def test_disband_does_not_revoke_routes_while_stop_is_unacknowledged(
home, monkeypatch
):
_create_room()
calls = []
class FakeService:
db_path = home / "state.db"
def stop_room(self, _room_id, **kwargs):
calls.append(("stop", kwargs["require_acknowledged"]))
raise RuntimeError("room work is still stopping")
def revoke_room_routes(self, _room_id):
calls.append(("revoke", True))
monkeypatch.setattr(srv, "get_hosted_room_service", lambda: FakeService())
result = srv._methods["groups.disband"](13, {"room_id": "room-1"})
assert result["error"]["code"] == 5114
assert calls == [("stop", True)]
assert [
room["room_id"]
for room in _result(srv._methods["groups.list"](14, {}))["rooms"]
] == ["room-1"]
def test_approve_routes_one_exact_peer_action(home, monkeypatch):
captured = {}
class FakeService:
def approve_room_task(self, room_id, **kwargs):
captured["room_id"] = room_id
captured.update(kwargs)
return {"resolved": 1}
monkeypatch.setattr(srv, "get_hosted_room_service", lambda: FakeService())
result = _result(
srv._methods["groups.approve"](
8,
{
"room_id": "room-1",
"member_id": "member-peer",
"task_id": "task-1",
"execution_generation": 2,
"request_id": "approval-1",
"choice": "once",
},
)
)
assert result == {"approved": True, "result": {"resolved": 1}}
assert captured == {
"room_id": "room-1",
"member_id": "member-peer",
"task_id": "task-1",
"execution_generation": 2,
"request_id": "approval-1",
"choice": "once",
}