feat: inject sessionKey for remote federation sessions

- Remote sessions now include sessionKey field formatted as 'remoteId:name'
- Local sessions are unchanged (no sessionKey) as they never collide
- Prevents name collisions between local and remote sessions with identical names
- Added tests: test_federation_sessions_local_sessions_have_no_session_key
               test_federation_sessions_remote_sessions_have_session_key
This commit is contained in:
Brian Krabach
2026-04-04 06:36:24 -07:00
parent 28aaf3d484
commit 42142c0954
2 changed files with 132 additions and 2 deletions
+7 -2
View File
@@ -913,9 +913,14 @@ async def federation_sessions(request: Request) -> list[dict]:
] ]
resp.raise_for_status() resp.raise_for_status()
sessions = resp.json() sessions = resp.json()
# Tag each session with deviceName and remoteId # Tag each session with deviceName, remoteId, and unique sessionKey
return [ return [
{**s, "deviceName": remote_name, "remoteId": remote_id} {
**s,
"deviceName": remote_name,
"remoteId": remote_id,
"sessionKey": f"{remote_id}:{s.get('name', '')}",
}
for s in sessions for s in sessions
] ]
except httpx.HTTPStatusError: except httpx.HTTPStatusError:
+125
View File
@@ -1662,6 +1662,131 @@ def test_federation_sessions_remote_id_is_integer_index(
) )
def test_federation_sessions_local_sessions_have_no_session_key(
client, monkeypatch, tmp_path
):
"""GET /api/federation/sessions: local sessions must NOT have a sessionKey field.
Local sessions use name as the unique key — no prefix needed since they
never collide with themselves.
"""
import json
import muxplex.settings as settings_mod
settings_path = tmp_path / "settings.json"
monkeypatch.setattr(settings_mod, "SETTINGS_PATH", settings_path)
# Write settings with no remote instances
settings_path.write_text(
json.dumps({"device_name": "my-local-machine", "remote_instances": []})
)
# Mock local session data
monkeypatch.setattr("muxplex.main.get_session_list", lambda: ["alpha", "beta"])
monkeypatch.setattr(
"muxplex.main.get_snapshots", lambda: {"alpha": "", "beta": ""}
)
response = client.get("/api/federation/sessions")
assert response.status_code == 200
data = response.json()
# Find local sessions (remoteId=None)
local_sessions = [s for s in data if s.get("remoteId") is None]
assert len(local_sessions) == 2, f"Expected 2 local sessions, got: {local_sessions}"
for local in local_sessions:
assert "sessionKey" not in local, (
f"Local session must NOT have sessionKey field, but got: {local}"
)
def test_federation_sessions_remote_sessions_have_session_key(
client, monkeypatch, tmp_path
):
"""GET /api/federation/sessions: remote sessions must have sessionKey = 'remoteId:name'.
The sessionKey format is '{remote_id}:{session_name}' to prevent collisions
between local and remote sessions with identical names.
"""
import json
from unittest.mock import MagicMock
import httpx # noqa: F401
import muxplex.settings as settings_mod
settings_path = tmp_path / "settings.json"
monkeypatch.setattr(settings_mod, "SETTINGS_PATH", settings_path)
# One remote instance
settings_path.write_text(
json.dumps(
{
"device_name": "local-host",
"remote_instances": [
{
"url": "http://spark-2:8088",
"key": "abc123",
"name": "spark-2",
}
],
}
)
)
monkeypatch.setattr("muxplex.main.get_session_list", lambda: [])
monkeypatch.setattr("muxplex.main.get_snapshots", lambda: {})
# Remote returns two sessions
async def mock_get(url, **kwargs):
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.raise_for_status = lambda: None
mock_resp.json = lambda: [
{"name": "work", "snapshot": "", "bell": {}},
{"name": "dev", "snapshot": "", "bell": {}},
]
return mock_resp
mock_client = MagicMock()
mock_client.get = mock_get
monkeypatch.setattr(client.app.state, "federation_client", mock_client)
response = client.get("/api/federation/sessions")
assert response.status_code == 200
data = response.json()
# Find remote sessions (remoteId is not None) that are named sessions (not status entries)
named_remote_sessions = [
s for s in data if s.get("remoteId") is not None and "name" in s
]
assert len(named_remote_sessions) == 2, (
f"Expected 2 named remote sessions, got: {named_remote_sessions}"
)
for session in named_remote_sessions:
assert "sessionKey" in session, (
f"Remote session must have sessionKey field, but got: {session}"
)
expected_key = f"{session['remoteId']}:{session['name']}"
assert session["sessionKey"] == expected_key, (
f"sessionKey must be 'remoteId:name' = {expected_key!r}, "
f"got: {session['sessionKey']!r}"
)
# Verify specific values
work_session = next(s for s in named_remote_sessions if s["name"] == "work")
assert work_session["sessionKey"] == "0:work", (
f"Expected sessionKey '0:work', got: {work_session['sessionKey']!r}"
)
dev_session = next(s for s in named_remote_sessions if s["name"] == "dev")
assert dev_session["sessionKey"] == "0:dev", (
f"Expected sessionKey '0:dev', got: {dev_session['sessionKey']!r}"
)
def test_federation_sessions_includes_remote_failure_status( def test_federation_sessions_includes_remote_failure_status(
client, monkeypatch, tmp_path client, monkeypatch, tmp_path