test: use real collaboration visibility state (#43229)
This commit is contained in:
@@ -682,12 +682,11 @@ class TestWorkflowCollaborationService:
|
||||
)
|
||||
|
||||
def test_relay_collaboration_event_graph_view_state_updates_without_broadcast(
|
||||
self, service: tuple[WorkflowCollaborationService, Mock, Mock]
|
||||
self, real_service: ServiceFixture
|
||||
) -> None:
|
||||
collaboration_service, repository, socketio = service
|
||||
repository.get_sid_mapping.return_value = {"workflow_id": "wf-1", "user_id": "u-1"}
|
||||
repository.get_current_leader.return_value = "sid-other"
|
||||
repository.update_session_graph_active.return_value = True
|
||||
collaboration_service, repository, socketio = real_service
|
||||
seed_session(repository, "sid-1")
|
||||
repository.set_leader("wf-1", "sid-other")
|
||||
payload = {
|
||||
"type": "graph_view_state",
|
||||
"data": {"graphActive": False, "sequence": 3},
|
||||
@@ -698,15 +697,17 @@ class TestWorkflowCollaborationService:
|
||||
result = collaboration_service.relay_collaboration_event("sid-1", payload)
|
||||
|
||||
assert result == ({"msg": "graph_view_state_updated"}, 200)
|
||||
repository.update_session_graph_active.assert_called_once_with("wf-1", "sid-1", False, 3)
|
||||
session_info = repository.get_session_info("wf-1", "sid-1")
|
||||
assert session_info is not None
|
||||
assert session_info["graph_active"] is False
|
||||
socketio.emit.assert_not_called()
|
||||
repository.set_leader.assert_not_called()
|
||||
assert repository.get_current_leader("wf-1") == "sid-other"
|
||||
|
||||
def test_graph_view_state_serializes_visibility_write_with_leader_demotion(
|
||||
self, service: tuple[WorkflowCollaborationService, Mock, Mock]
|
||||
self, real_service: ServiceFixture
|
||||
) -> None:
|
||||
collaboration_service, repository, _socketio = service
|
||||
repository.get_sid_mapping.return_value = {"workflow_id": "wf-1", "user_id": "u-1"}
|
||||
collaboration_service, repository, _socketio = real_service
|
||||
seed_session(repository, "sid-1")
|
||||
events: list[str] = []
|
||||
|
||||
@contextmanager
|
||||
@@ -715,8 +716,12 @@ class TestWorkflowCollaborationService:
|
||||
yield
|
||||
events.append("lock_exit")
|
||||
|
||||
repository.graph_view_state_lock.return_value = graph_view_lock()
|
||||
repository.update_session_graph_active.side_effect = lambda *_args: events.append("visibility_write") or True
|
||||
update_visibility = repository.update_session_graph_active
|
||||
|
||||
def record_visibility(workflow_id: str, sid: str, active: bool, sequence: int) -> bool:
|
||||
events.append("visibility_write")
|
||||
return update_visibility(workflow_id, sid, active, sequence)
|
||||
|
||||
payload = {
|
||||
"type": "graph_view_state",
|
||||
"data": {"graphActive": False, "sequence": 4},
|
||||
@@ -724,6 +729,8 @@ class TestWorkflowCollaborationService:
|
||||
}
|
||||
|
||||
with (
|
||||
patch.object(repository, "graph_view_state_lock", return_value=graph_view_lock()),
|
||||
patch.object(repository, "update_session_graph_active", side_effect=record_visibility),
|
||||
patch.object(collaboration_service, "refresh_session_state"),
|
||||
patch.object(
|
||||
collaboration_service,
|
||||
@@ -737,11 +744,11 @@ class TestWorkflowCollaborationService:
|
||||
assert events == ["lock_enter", "visibility_write", "leader_demotion", "lock_exit"]
|
||||
|
||||
def test_relay_collaboration_event_graph_view_state_ignores_stale_update_without_demotion(
|
||||
self, service: tuple[WorkflowCollaborationService, Mock, Mock]
|
||||
self, real_service: ServiceFixture
|
||||
) -> None:
|
||||
collaboration_service, repository, _socketio = service
|
||||
repository.get_sid_mapping.return_value = {"workflow_id": "wf-1", "user_id": "u-1"}
|
||||
repository.update_session_graph_active.return_value = False
|
||||
collaboration_service, repository, _socketio = real_service
|
||||
seed_session(repository, "sid-1")
|
||||
assert repository.update_session_graph_active("wf-1", "sid-1", True, 3)
|
||||
payload = {
|
||||
"type": "graph_view_state",
|
||||
"data": {"graphActive": False, "sequence": 2},
|
||||
@@ -755,34 +762,38 @@ class TestWorkflowCollaborationService:
|
||||
result = collaboration_service.relay_collaboration_event("sid-1", payload)
|
||||
|
||||
assert result == ({"msg": "graph_view_state_ignored"}, 200)
|
||||
repository.update_session_graph_active.assert_called_once_with("wf-1", "sid-1", False, 2)
|
||||
session_info = repository.get_session_info("wf-1", "sid-1")
|
||||
assert session_info is not None
|
||||
assert session_info["graph_active"] is True
|
||||
demote_leader.assert_not_called()
|
||||
|
||||
def test_relay_collaboration_event_graph_view_state_demotes_hidden_leader(
|
||||
self, service: tuple[WorkflowCollaborationService, Mock, Mock]
|
||||
self, real_service: ServiceFixture
|
||||
) -> None:
|
||||
collaboration_service, repository, _socketio = service
|
||||
repository.get_sid_mapping.return_value = {"workflow_id": "wf-1", "user_id": "u-1"}
|
||||
repository.get_current_leader.return_value = "sid-1"
|
||||
repository.update_session_graph_active.return_value = True
|
||||
repository.list_sessions.return_value = [
|
||||
{
|
||||
"user_id": "u-1",
|
||||
"username": "A",
|
||||
"avatar": None,
|
||||
"sid": "sid-1",
|
||||
"connected_at": 1,
|
||||
"graph_active": False,
|
||||
},
|
||||
{
|
||||
"user_id": "u-2",
|
||||
"username": "B",
|
||||
"avatar": None,
|
||||
"sid": "sid-2",
|
||||
"connected_at": 2,
|
||||
"graph_active": True,
|
||||
},
|
||||
]
|
||||
collaboration_service, repository, _socketio = real_service
|
||||
seed_session(repository, "sid-1")
|
||||
repository.set_leader("wf-1", "sid-1")
|
||||
seed_sessions(
|
||||
repository,
|
||||
[
|
||||
{
|
||||
"user_id": "u-1",
|
||||
"username": "A",
|
||||
"avatar": None,
|
||||
"sid": "sid-1",
|
||||
"connected_at": 1,
|
||||
"graph_active": False,
|
||||
},
|
||||
{
|
||||
"user_id": "u-2",
|
||||
"username": "B",
|
||||
"avatar": None,
|
||||
"sid": "sid-2",
|
||||
"connected_at": 2,
|
||||
"graph_active": True,
|
||||
},
|
||||
],
|
||||
)
|
||||
payload = {
|
||||
"type": "graph_view_state",
|
||||
"data": {"graphActive": False, "sequence": 3},
|
||||
@@ -797,27 +808,31 @@ class TestWorkflowCollaborationService:
|
||||
result = collaboration_service.relay_collaboration_event("sid-1", payload)
|
||||
|
||||
assert result == ({"msg": "graph_view_state_updated"}, 200)
|
||||
repository.update_session_graph_active.assert_called_once_with("wf-1", "sid-1", False, 3)
|
||||
repository.set_leader.assert_called_once_with("wf-1", "sid-2")
|
||||
session_info = repository.get_session_info("wf-1", "sid-1")
|
||||
assert session_info is not None
|
||||
assert session_info["graph_active"] is False
|
||||
assert repository.get_current_leader("wf-1") == "sid-2"
|
||||
broadcast_leader_change.assert_called_once_with("wf-1", "sid-2")
|
||||
|
||||
def test_relay_collaboration_event_graph_view_state_keeps_leader_when_all_hidden(
|
||||
self, service: tuple[WorkflowCollaborationService, Mock, Mock]
|
||||
self, real_service: ServiceFixture
|
||||
) -> None:
|
||||
collaboration_service, repository, _socketio = service
|
||||
repository.get_sid_mapping.return_value = {"workflow_id": "wf-1", "user_id": "u-1"}
|
||||
repository.get_current_leader.return_value = "sid-1"
|
||||
repository.update_session_graph_active.return_value = True
|
||||
repository.list_sessions.return_value = [
|
||||
{
|
||||
"user_id": "u-1",
|
||||
"username": "A",
|
||||
"avatar": None,
|
||||
"sid": "sid-1",
|
||||
"connected_at": 1,
|
||||
"graph_active": False,
|
||||
},
|
||||
]
|
||||
collaboration_service, repository, _socketio = real_service
|
||||
seed_session(repository, "sid-1")
|
||||
repository.set_leader("wf-1", "sid-1")
|
||||
seed_sessions(
|
||||
repository,
|
||||
[
|
||||
{
|
||||
"user_id": "u-1",
|
||||
"username": "A",
|
||||
"avatar": None,
|
||||
"sid": "sid-1",
|
||||
"connected_at": 1,
|
||||
"graph_active": False,
|
||||
},
|
||||
],
|
||||
)
|
||||
payload = {
|
||||
"type": "graph_view_state",
|
||||
"data": {"graphActive": False, "sequence": 3},
|
||||
@@ -832,17 +847,15 @@ class TestWorkflowCollaborationService:
|
||||
result = collaboration_service.relay_collaboration_event("sid-1", payload)
|
||||
|
||||
assert result == ({"msg": "graph_view_state_updated"}, 200)
|
||||
repository.set_leader.assert_not_called()
|
||||
repository.delete_leader.assert_not_called()
|
||||
assert repository.get_current_leader("wf-1") == "sid-1"
|
||||
broadcast_leader_change.assert_not_called()
|
||||
|
||||
def test_relay_collaboration_event_graph_view_state_non_leader_no_demotion(
|
||||
self, service: tuple[WorkflowCollaborationService, Mock, Mock]
|
||||
self, real_service: ServiceFixture
|
||||
) -> None:
|
||||
collaboration_service, repository, _socketio = service
|
||||
repository.get_sid_mapping.return_value = {"workflow_id": "wf-1", "user_id": "u-1"}
|
||||
repository.get_current_leader.return_value = "sid-leader"
|
||||
repository.update_session_graph_active.return_value = True
|
||||
collaboration_service, repository, _socketio = real_service
|
||||
seed_session(repository, "sid-1")
|
||||
repository.set_leader("wf-1", "sid-leader")
|
||||
payload = {
|
||||
"type": "graph_view_state",
|
||||
"data": {"graphActive": False, "sequence": 3},
|
||||
@@ -856,35 +869,37 @@ class TestWorkflowCollaborationService:
|
||||
result = collaboration_service.relay_collaboration_event("sid-1", payload)
|
||||
|
||||
assert result == ({"msg": "graph_view_state_updated"}, 200)
|
||||
repository.update_session_graph_active.assert_called_once_with("wf-1", "sid-1", False, 3)
|
||||
repository.set_leader.assert_not_called()
|
||||
session_info = repository.get_session_info("wf-1", "sid-1")
|
||||
assert session_info is not None
|
||||
assert session_info["graph_active"] is False
|
||||
assert repository.get_current_leader("wf-1") == "sid-leader"
|
||||
broadcast_leader_change.assert_not_called()
|
||||
repository.list_sessions.assert_not_called()
|
||||
|
||||
def test_relay_collaboration_event_sync_request_reelects_active_leader(
|
||||
self, service: tuple[WorkflowCollaborationService, Mock, Mock]
|
||||
) -> None:
|
||||
collaboration_service, repository, socketio = service
|
||||
repository.get_sid_mapping.return_value = {"workflow_id": "wf-1", "user_id": "u-1"}
|
||||
repository.get_current_leader.return_value = "sid-old"
|
||||
repository.list_sessions.return_value = [
|
||||
{
|
||||
"user_id": "u-2",
|
||||
"username": "B",
|
||||
"avatar": None,
|
||||
"sid": "sid-2",
|
||||
"connected_at": 1,
|
||||
"graph_active": True,
|
||||
},
|
||||
{
|
||||
"user_id": "u-3",
|
||||
"username": "C",
|
||||
"avatar": None,
|
||||
"sid": "sid-3",
|
||||
"connected_at": 2,
|
||||
"graph_active": True,
|
||||
},
|
||||
]
|
||||
def test_relay_collaboration_event_sync_request_reelects_active_leader(self, real_service: ServiceFixture) -> None:
|
||||
collaboration_service, repository, socketio = real_service
|
||||
seed_session(repository, "sid-2")
|
||||
repository.set_leader("wf-1", "sid-old")
|
||||
seed_sessions(
|
||||
repository,
|
||||
[
|
||||
{
|
||||
"user_id": "u-2",
|
||||
"username": "B",
|
||||
"avatar": None,
|
||||
"sid": "sid-2",
|
||||
"connected_at": 1,
|
||||
"graph_active": True,
|
||||
},
|
||||
{
|
||||
"user_id": "u-3",
|
||||
"username": "C",
|
||||
"avatar": None,
|
||||
"sid": "sid-3",
|
||||
"connected_at": 2,
|
||||
"graph_active": True,
|
||||
},
|
||||
],
|
||||
)
|
||||
socketio.call.return_value = {"success": True, "hash": "hash-2", "updatedAt": 456}
|
||||
payload = {
|
||||
"type": "sync_request",
|
||||
@@ -904,14 +919,13 @@ class TestWorkflowCollaborationService:
|
||||
|
||||
assert result[1] == 200
|
||||
assert result[0]["success"] is True
|
||||
repository.delete_leader.assert_called_once_with("wf-1")
|
||||
repository.set_leader.assert_called_once_with("wf-1", "sid-2")
|
||||
assert repository.get_current_leader("wf-1") == "sid-2"
|
||||
broadcast_leader_change.assert_called_once_with("wf-1", "sid-2")
|
||||
socketio.call.assert_called_once_with(
|
||||
"collaboration_update",
|
||||
{
|
||||
"type": "sync_request",
|
||||
"userId": "u-1",
|
||||
"userId": "u-2",
|
||||
"data": {"reason": "join", "requestId": "req-1"},
|
||||
"timestamp": 123,
|
||||
},
|
||||
@@ -920,12 +934,12 @@ class TestWorkflowCollaborationService:
|
||||
)
|
||||
|
||||
def test_relay_collaboration_event_sync_request_returns_when_no_active_leader(
|
||||
self, service: tuple[WorkflowCollaborationService, Mock, Mock]
|
||||
self, real_service: ServiceFixture
|
||||
) -> None:
|
||||
collaboration_service, repository, socketio = service
|
||||
repository.get_sid_mapping.return_value = {"workflow_id": "wf-1", "user_id": "u-1"}
|
||||
repository.get_current_leader.return_value = "sid-old"
|
||||
repository.list_sessions.return_value = []
|
||||
collaboration_service, repository, socketio = real_service
|
||||
seed_session(repository, "sid-2")
|
||||
repository.set_leader("wf-1", "sid-old")
|
||||
seed_sessions(repository, [])
|
||||
payload = {
|
||||
"type": "sync_request",
|
||||
"data": {"reason": "join", "requestId": "req-1"},
|
||||
@@ -939,13 +953,13 @@ class TestWorkflowCollaborationService:
|
||||
result = collaboration_service.relay_collaboration_event("sid-2", payload)
|
||||
|
||||
assert result == ({"msg": "no_active_leader", "requestId": "req-1"}, 503)
|
||||
repository.delete_leader.assert_called_once_with("wf-1")
|
||||
assert repository.get_current_leader("wf-1") is None
|
||||
socketio.call.assert_not_called()
|
||||
|
||||
def test_relay_graph_event_unauthorized(self, service: tuple[WorkflowCollaborationService, Mock, Mock]) -> None:
|
||||
def test_relay_graph_event_unauthorized(self, real_service: ServiceFixture) -> None:
|
||||
# Arrange
|
||||
collaboration_service, repository, _socketio = service
|
||||
repository.get_sid_mapping.return_value = None
|
||||
collaboration_service, repository, _socketio = real_service
|
||||
assert repository.get_sid_mapping("sid-1") is None
|
||||
|
||||
# Act
|
||||
result = collaboration_service.relay_graph_event("sid-1", {"nodes": []})
|
||||
@@ -953,21 +967,21 @@ class TestWorkflowCollaborationService:
|
||||
# Assert
|
||||
assert result == ({"msg": "unauthorized"}, 401)
|
||||
|
||||
def test_disconnect_session_no_mapping(self, service: tuple[WorkflowCollaborationService, Mock, Mock]) -> None:
|
||||
def test_disconnect_session_no_mapping(self, real_service: ServiceFixture) -> None:
|
||||
# Arrange
|
||||
collaboration_service, repository, _socketio = service
|
||||
repository.get_sid_mapping.return_value = None
|
||||
collaboration_service, repository, _socketio = real_service
|
||||
assert repository.get_sid_mapping("sid-1") is None
|
||||
|
||||
# Act
|
||||
collaboration_service.disconnect_session("sid-1")
|
||||
|
||||
# Assert
|
||||
repository.delete_session.assert_not_called()
|
||||
assert repository.list_sessions("wf-1") == []
|
||||
|
||||
def test_disconnect_session_cleans_up(self, service: tuple[WorkflowCollaborationService, Mock, Mock]) -> None:
|
||||
def test_disconnect_session_cleans_up(self, real_service: ServiceFixture) -> None:
|
||||
# Arrange
|
||||
collaboration_service, repository, _socketio = service
|
||||
repository.get_sid_mapping.return_value = {"workflow_id": "wf-1", "user_id": "u-1"}
|
||||
collaboration_service, repository, _socketio = real_service
|
||||
seed_session(repository, "sid-1")
|
||||
|
||||
with (
|
||||
patch.object(collaboration_service, "handle_leader_disconnect") as handle_leader_disconnect,
|
||||
@@ -977,7 +991,8 @@ class TestWorkflowCollaborationService:
|
||||
collaboration_service.disconnect_session("sid-1")
|
||||
|
||||
# Assert
|
||||
repository.delete_session.assert_called_once_with("wf-1", "sid-1")
|
||||
assert not repository.session_exists("wf-1", "sid-1")
|
||||
assert repository.get_sid_mapping("sid-1") is None
|
||||
handle_leader_disconnect.assert_called_once_with("wf-1", "sid-1")
|
||||
broadcast_online_users.assert_called_once_with("wf-1")
|
||||
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
"""Stateful Redis command transport for collaboration service tests."""
|
||||
|
||||
import json
|
||||
from collections.abc import Sequence
|
||||
from hashlib import sha1
|
||||
|
||||
from repositories.workflow_collaboration_repository import WorkflowCollaborationRepository, WorkflowSessionInfo
|
||||
from redis.lock import Lock
|
||||
|
||||
from repositories.workflow_collaboration_repository import (
|
||||
_UPDATE_SESSION_GRAPH_ACTIVE_LUA,
|
||||
WorkflowCollaborationRepository,
|
||||
WorkflowSessionInfo,
|
||||
)
|
||||
|
||||
|
||||
class RedisState:
|
||||
@@ -18,6 +26,30 @@ class RedisState:
|
||||
return value if isinstance(value, bytes) else str(value).encode()
|
||||
|
||||
def execute(self, command: str, *args: object, **_kwargs: object) -> object:
|
||||
if command == "EVAL":
|
||||
script, numkeys, workflow_key, sid, active, sequence = args
|
||||
assert script == _UPDATE_SESSION_GRAPH_ACTIVE_LUA
|
||||
assert numkeys == 1
|
||||
fields = self.hashes.get(self._bytes(workflow_key), {})
|
||||
raw = fields.get(self._bytes(sid))
|
||||
if raw is None:
|
||||
return 0
|
||||
data = json.loads(raw)
|
||||
incoming = int(str(sequence))
|
||||
if incoming <= data.get("graph_active_sequence", -1):
|
||||
return 0
|
||||
data.update(graph_active=active == "1", graph_active_sequence=incoming)
|
||||
fields[self._bytes(sid)] = json.dumps(data).encode()
|
||||
return 1
|
||||
if command == "EVALSHA":
|
||||
script_hash, numkeys, lock_key, token = args
|
||||
assert script_hash == sha1(Lock.LUA_RELEASE_SCRIPT.encode()).hexdigest()
|
||||
assert numkeys == 1
|
||||
key = self._bytes(lock_key)
|
||||
if self.values.get(key) != self._bytes(token):
|
||||
return 0
|
||||
del self.values[key]
|
||||
return 1
|
||||
key = self._bytes(args[0])
|
||||
match command:
|
||||
case "GET":
|
||||
|
||||
Reference in New Issue
Block a user