Files
prefecthq--fastmcp/tests/client/tasks/test_task_context_validation.py
wehub-resource-sync 60e0ffc959
Upgrade checks / Notify on failure (push) Has been cancelled
Upgrade checks / Close issue on success (push) Has been cancelled
Schema Crash Test / Real-world schema crash test (232K schemas) (push) Has been cancelled
Run static analysis / static_analysis (push) Has been cancelled
Tests / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Tests / Tests with lowest-direct dependencies (push) Has been cancelled
Tests / MCP conformance tests (push) Has been cancelled
Tests / Integration tests (push) Has been cancelled
Tests / Package install smoke (push) Has been cancelled
Upgrade checks / Static analysis (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Upgrade checks / Integration tests (push) Has been cancelled
Update MCPServerConfig Schema / update-config-schema (push) Has been cancelled
Update SDK Documentation / update-sdk-docs (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:59 +08:00

223 lines
7.7 KiB
Python

"""
Tests for Task client context validation.
Verifies that Task methods properly validate client context and that
cached results remain accessible outside context.
"""
import pytest
from fastmcp import FastMCP
from fastmcp.client import Client
@pytest.fixture
async def task_server():
"""Create a test server with background tasks."""
mcp = FastMCP("context-test-server")
@mcp.tool(task=True)
async def background_tool(value: str) -> str:
"""Tool that runs in background."""
return f"Result: {value}"
@mcp.prompt(task=True)
async def background_prompt(topic: str) -> str:
"""Prompt that runs in background."""
return f"Prompt about {topic}"
@mcp.resource("file://background.txt", task=True)
async def background_resource() -> str:
"""Resource that runs in background."""
return "Background resource content"
return mcp
async def test_task_status_outside_context_raises(task_server):
"""Calling task.status() outside client context raises error."""
task = None
async with Client(task_server) as client:
task = await client.call_tool("background_tool", {"value": "test"}, task=True)
assert not task.returned_immediately
# Now outside context
with pytest.raises(RuntimeError, match="outside client context"):
await task.status()
async def test_task_result_outside_context_raises(task_server):
"""Calling task.result() outside context raises error."""
task = None
async with Client(task_server) as client:
task = await client.call_tool("background_tool", {"value": "test"}, task=True)
assert not task.returned_immediately
# Now outside context
with pytest.raises(RuntimeError, match="outside client context"):
await task.result()
async def test_task_wait_outside_context_raises(task_server):
"""Calling task.wait() outside context raises error."""
task = None
async with Client(task_server) as client:
task = await client.call_tool("background_tool", {"value": "test"}, task=True)
assert not task.returned_immediately
# Now outside context
with pytest.raises(RuntimeError, match="outside client context"):
await task.wait()
async def test_task_cancel_outside_context_raises(task_server):
"""Calling task.cancel() outside context raises error."""
task = None
async with Client(task_server) as client:
task = await client.call_tool("background_tool", {"value": "test"}, task=True)
assert not task.returned_immediately
# Now outside context
with pytest.raises(RuntimeError, match="outside client context"):
await task.cancel()
async def test_cached_tool_task_accessible_outside_context(task_server):
"""Tool tasks with cached results work outside context."""
task = None
async with Client(task_server) as client:
task = await client.call_tool("background_tool", {"value": "test"}, task=True)
assert not task.returned_immediately
# Get result once to cache it
result1 = await task.result()
assert result1.data == "Result: test"
# Now outside context
# Should work because result is cached
result2 = await task.result()
assert result2 is result1 # Same object
assert result2.data == "Result: test"
@pytest.mark.xfail(
reason="SDK v2 has no `task` field on GetPromptRequestParams / "
"ReadResourceRequestParams; prompt/resource task submission is not "
"wire-expressible and always graceful-degrades (sdk-feedback #3).",
strict=True,
)
async def test_cached_prompt_task_accessible_outside_context(task_server):
"""Prompt tasks with cached results work outside context."""
task = None
async with Client(task_server) as client:
task = await client.get_prompt(
"background_prompt", {"topic": "test"}, task=True
)
assert not task.returned_immediately
# Get result once to cache it
result1 = await task.result()
assert result1.description == "Prompt that runs in background."
# Now outside context
# Should work because result is cached
result2 = await task.result()
assert result2 is result1 # Same object
assert result2.description == "Prompt that runs in background."
@pytest.mark.xfail(
reason="SDK v2 has no `task` field on GetPromptRequestParams / "
"ReadResourceRequestParams; prompt/resource task submission is not "
"wire-expressible and always graceful-degrades (sdk-feedback #3).",
strict=True,
)
async def test_cached_resource_task_accessible_outside_context(task_server):
"""Resource tasks with cached results work outside context."""
task = None
async with Client(task_server) as client:
task = await client.read_resource("file://background.txt", task=True)
assert not task.returned_immediately
# Get result once to cache it
result1 = await task.result()
assert len(result1) > 0
# Now outside context
# Should work because result is cached
result2 = await task.result()
assert result2 is result1 # Same object
async def test_uncached_status_outside_context_raises(task_server):
"""Even after caching result, status() still requires client context."""
task = None
async with Client(task_server) as client:
task = await client.call_tool("background_tool", {"value": "test"}, task=True)
assert not task.returned_immediately
# Cache the result
await task.result()
# Now outside context
# result() works (cached)
result = await task.result()
assert result.data == "Result: test"
# But status() still needs client connection
with pytest.raises(RuntimeError, match="outside client context"):
await task.status()
async def test_task_await_syntax_outside_context_raises(task_server):
"""Using await task syntax outside context raises error for background tasks."""
task = None
async with Client(task_server) as client:
task = await client.call_tool("background_tool", {"value": "test"}, task=True)
assert not task.returned_immediately
# Now outside context
with pytest.raises(RuntimeError, match="outside client context"):
await task # Same as await task.result()
async def test_task_await_syntax_works_for_cached_results(task_server):
"""Using await task syntax works outside context when result is cached."""
task = None
async with Client(task_server) as client:
task = await client.call_tool("background_tool", {"value": "test"}, task=True)
result1 = await task # Cache it
# Now outside context
result2 = await task # Should work (cached)
assert result2 is result1
assert result2.data == "Result: test"
async def test_multiple_result_calls_return_same_cached_object(task_server):
"""Multiple result() calls return the same cached object."""
async with Client(task_server) as client:
task = await client.call_tool("background_tool", {"value": "test"}, task=True)
result1 = await task.result()
result2 = await task.result()
result3 = await task.result()
# Should all be the same object (cached)
assert result1 is result2
assert result2 is result3
async def test_background_task_properties_accessible_outside_context(task_server):
"""Background task properties like task_id accessible outside context."""
task = None
async with Client(task_server) as client:
task = await client.call_tool("background_tool", {"value": "test"}, task=True)
task_id_inside = task.task_id
assert not task.returned_immediately
# Now outside context
# Properties should still be accessible (they don't need client connection)
assert task.task_id == task_id_inside
assert task.returned_immediately is False