test(hosted-rooms): pin the coordination store off the master state.db; label sidecars

Invariant test for the #103489 salvage: a profile gateway and the root
gateway resolve the same hosted-room coordination file at the install
root, and that file is never the root SessionDB (`state.db`). Every
profile gateway starts the hosted-room worker unconditionally, so the
old mapping made each profile process a long-lived writer on the master
session store — the simultaneous-restart corruption in #102120 and the
main offender in the #103339 / #103490 fleet reports.

Also: WAL-fallback labels say `shared-state.db`, and test fakes take the
store path from `default_db_path()` instead of hardcoding `state.db`.

Reported-by: #102120 #103339 (@RChina) #103490
Refs #102120 #103339 #103490
This commit is contained in:
Teknium
2026-09-11 02:10:27 -07:00
parent cfeccbdf34
commit d41e906598
4 changed files with 32 additions and 8 deletions
+1 -1
View File
@@ -114,7 +114,7 @@ class HostedRoomPolicyCheckpoint:
self.db_path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(self.db_path, timeout=10)
conn.row_factory = sqlite3.Row
apply_wal_with_fallback(conn, db_label="state.db (room policy checkpoint)")
apply_wal_with_fallback(conn, db_label="shared-state.db (room policy checkpoint)")
return conn
@staticmethod
+1 -1
View File
@@ -423,7 +423,7 @@ def local_authority_gateway_id() -> str:
_connect = partial(
connect, db_label="state.db (hosted_rooms)", ready=_schema_is_current,
connect, db_label="shared-state.db (hosted_rooms)", ready=_schema_is_current,
initialize=lambda conn: _initialize_schema(conn), lock_retries=_JOURNAL_MODE_LOCK_RETRIES)
+23
View File
@@ -1339,3 +1339,26 @@ def test_room_log_page_bound_counts_bytes_not_characters(tmp_path, monkeypatch):
assert [event["seq"] for event in page["events"]] == [1]
assert page_bytes(page) <= budget
assert page["has_more"] is True
def test_default_db_path_never_names_the_master_session_store(tmp_path, monkeypatch):
"""Hosted-room coordination lives beside, never inside, the master ``state.db``.
Every profile gateway starts the hosted-room worker, so a store resolved to the
root session DB made every profile process a long-lived writer on state.db —
the multi-profile restart corruption in #102120 / #103339 / #103490. The store
is shared across profiles (one file at the install root) but is not the
SessionDB file the root gateway owns.
"""
root = tmp_path / ".hermes"
profile_home = root / "profiles" / "bot1"
profile_home.mkdir(parents=True)
monkeypatch.setenv("HERMES_HOME", str(profile_home))
from_profile = rooms.default_db_path()
monkeypatch.setenv("HERMES_HOME", str(root))
from_root = rooms.default_db_path()
assert from_profile == from_root, "one coordination file per install"
assert from_root.parent == root
assert from_root.name != "state.db"
+7 -6
View File
@@ -6,6 +6,7 @@ from types import SimpleNamespace
import pytest
from gateway.hosted_rooms import default_db_path as hosted_rooms_default_db_path
import tui_gateway.server as srv
from tui_gateway import methods_groups
@@ -346,7 +347,7 @@ def test_register_peer_route_probes_scope_and_persists_via_service(home, monkeyp
}
class FakeService:
db_path = home / "state.db"
db_path = hosted_rooms_default_db_path()
def register_peer_route(self, **kwargs):
captured["registered"] = kwargs
@@ -376,7 +377,7 @@ def test_register_peer_route_probes_scope_and_persists_via_service(home, monkeyp
def test_register_rejects_plaintext_non_loopback(home, monkeypatch):
class FakeService:
db_path = home / "state.db"
db_path = hosted_rooms_default_db_path()
monkeypatch.setattr(srv, "get_hosted_room_service", lambda: FakeService())
response = srv._methods["groups.peer.register"](
@@ -398,7 +399,7 @@ def test_register_requires_roomlink_protocol_v2(home, monkeypatch):
from gateway.hosted_room_peer import catalog_mapping
class FakeService:
db_path = home / "state.db"
db_path = hosted_rooms_default_db_path()
monkeypatch.setattr(srv, "get_hosted_room_service", lambda: FakeService())
response = srv._methods["groups.peer.register"](
@@ -914,7 +915,7 @@ def test_disband_stops_and_revokes_before_tombstoning(home, monkeypatch):
calls = []
class FakeService:
db_path = home / "state.db"
db_path = hosted_rooms_default_db_path()
def stop_room(self, room_id, **_kwargs):
calls.append(("stop", room_id))
@@ -933,7 +934,7 @@ def test_failed_remote_revocation_keeps_room_recoverable(home, monkeypatch):
_create_room()
class FakeService:
db_path = home / "state.db"
db_path = hosted_rooms_default_db_path()
def stop_room(self, _room_id, **_kwargs):
return 1
@@ -958,7 +959,7 @@ def test_disband_does_not_revoke_routes_while_stop_is_unacknowledged(
calls = []
class FakeService:
db_path = home / "state.db"
db_path = hosted_rooms_default_db_path()
def stop_room(self, _room_id, **kwargs):
calls.append(("stop", kwargs["require_acknowledged"]))