"""Unit tests for the task session-scope helpers. A session scope is an opaque marker assigned to each session by TaskSupport.configure_session(). Task IDs generated by run_task() embed it so the default task handlers can tell which session created a task. See test_task_visibility.py for the end-to-end behaviour these helpers produce. """ from collections.abc import AsyncIterator from contextlib import asynccontextmanager import anyio import pytest from mcp.server import Server from mcp.server.experimental.task_scope import ( new_session_scope, scoped_task_id, session_scope_of, task_in_session_scope, task_listable_in_session_scope, ) from mcp.server.experimental.task_support import TaskSupport from mcp.server.lowlevel import NotificationOptions from mcp.server.models import InitializationOptions from mcp.server.session import ServerSession from mcp.shared.message import SessionMessage def test_new_session_scope_is_unique() -> None: assert new_session_scope() != new_session_scope() def test_scoped_task_id_round_trips_its_scope() -> None: scope = new_session_scope() task_id = scoped_task_id(scope) assert session_scope_of(task_id) == scope def test_scoped_task_ids_are_unique_within_a_scope() -> None: scope = new_session_scope() assert scoped_task_id(scope) != scoped_task_id(scope) @pytest.mark.parametrize( "task_id", [ "plain-task-id", "550e8400-e29b-41d4-a716-446655440000", # bare uuid4 "", # Right shape but the scope half is not 32 hex chars. "not-a-scope:550e8400-e29b-41d4-a716-446655440000", # Right scope half but the suffix is not a uuid4. "0123456789abcdef0123456789abcdef:not-a-uuid", # Uppercase hex is not produced by new_session_scope(). "0123456789ABCDEF0123456789ABCDEF:550e8400-e29b-41d4-a716-446655440000", ], ) def test_session_scope_of_returns_none_for_unscoped_ids(task_id: str) -> None: assert session_scope_of(task_id) is None def test_a_scoped_task_is_usable_only_from_the_scope_that_created_it() -> None: scope = new_session_scope() task_id = scoped_task_id(scope) assert task_in_session_scope(task_id, scope) is True assert task_in_session_scope(task_id, new_session_scope()) is False assert task_in_session_scope(task_id, None) is False def test_an_unscoped_task_is_usable_from_any_scope() -> None: assert task_in_session_scope("plain-task-id", new_session_scope()) is True assert task_in_session_scope("plain-task-id", None) is True def test_a_scoped_task_is_listable_only_in_the_scope_that_created_it() -> None: scope = new_session_scope() task_id = scoped_task_id(scope) assert task_listable_in_session_scope(task_id, scope) is True assert task_listable_in_session_scope(task_id, new_session_scope()) is False assert task_listable_in_session_scope(task_id, None) is False def test_an_unscoped_task_is_never_listable() -> None: assert task_listable_in_session_scope("plain-task-id", new_session_scope()) is False assert task_listable_in_session_scope("plain-task-id", None) is False @asynccontextmanager async def _make_session() -> AsyncIterator[ServerSession]: """Create a ServerSession suitable for inspecting configure_session().""" server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1) client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage | Exception](1) options = InitializationOptions( server_name="test", server_version="0", capabilities=Server("test").get_capabilities(NotificationOptions(), {}), ) async with ( server_to_client_receive, client_to_server_send, ServerSession(client_to_server_receive, server_to_client_send, options) as session, ): yield session @pytest.mark.anyio async def test_configure_session_assigns_a_scope() -> None: support = TaskSupport.in_memory() async with _make_session() as session: assert session.experimental.task_session_scope is None support.configure_session(session) assert session.experimental.task_session_scope is not None @pytest.mark.anyio async def test_configure_session_assigns_distinct_scopes_per_session() -> None: support = TaskSupport.in_memory() async with _make_session() as first, _make_session() as second: support.configure_session(first) support.configure_session(second) assert first.experimental.task_session_scope != second.experimental.task_session_scope @pytest.mark.anyio async def test_configure_session_is_idempotent() -> None: support = TaskSupport.in_memory() async with _make_session() as session: support.configure_session(session) scope = session.experimental.task_session_scope support.configure_session(session) assert session.experimental.task_session_scope == scope @pytest.mark.anyio async def test_configure_session_assigns_no_scope_to_stateless_sessions() -> None: support = TaskSupport.in_memory() async with _make_session() as session: support.configure_session(session, stateless=True) assert session.experimental.task_session_scope is None