mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-24 06:24:18 +02:00
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com> Co-authored-by: Marvin Context Protocol <41898282+Marvin Context Protocol@users.noreply.github.com> Co-authored-by: voidborne-d <voidborne-d@users.noreply.github.com> Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: d 🔹 <258577966+voidborne-d@users.noreply.github.com> Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Co-authored-by: nightcityblade <nightcityblade@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Bill Easton <strawgate@users.noreply.github.com> Co-authored-by: Sumanshu Nankana <sumanshunankana@gmail.com> Co-authored-by: Eric Robinson <ericrobinson@indeed.com> Co-authored-by: Martim Santos <martimfasantos@gmail.com> Co-authored-by: d 🔹 <liusway405@gmail.com> Co-authored-by: Matthieu B <66959271+mtthidoteu@users.noreply.github.com> Co-authored-by: Sascha Buehrle <47737812+saschabuehrle@users.noreply.github.com> Co-authored-by: Hakancan <142545736+hkc5@users.noreply.github.com> Co-authored-by: nightcityblade <jackchen@haloailabs.com> Co-authored-by: Matt Hallowell <17804673+mhallo@users.noreply.github.com> Co-authored-by: nate nowack <thrast36@gmail.com> Co-authored-by: Bill Easton <williamseaston@gmail.com> Co-authored-by: Marcus Shu <46469249+shulkx@users.noreply.github.com> Co-authored-by: Rushabh Doshi <radoshi@gmail.com> Co-authored-by: AIKAWA Shigechika <shige@aikawa.jp> Co-authored-by: Jeremy Simon <simonjer805@gmail.com> Co-authored-by: Miguel Miranda Dias <7780875+pandego@users.noreply.github.com> Co-authored-by: Anthony James Padavano <padavano.anthony@gmail.com> Co-authored-by: Mostafa Kamal <hiremostafa@gmail.com> Fix auto-close MRE script posting comment without closing (#3386) Fix WorkOS token scope verification bypass 🤖 Generated with Codex (#3407) Fix initialize McpError fallthrough 🤖 Generated with Codex (#3413) Fix transform arg collisions with passthrough params (#3431) Fix get_* returning None when latest version is disabled (#3439) Fix get_* returning None when latest version is disabled (#3421) Fix server lifespan overlap teardown (#3415) Fix $ref output schema object detection regression (#3420) resolved annotations (#3429) Fix async partial callables rejected by iscoroutinefunction (#3438) Fix async partial callables rejected by iscoroutinefunction (#3423) fix: add version to components (#3458) fix: use intent-based flag for OIDC scope patch in load_access_token (#3465) Fixes #3461 fix: normalize Google scope shorthands and surface valid_scopes (#3477) fix: resolve ty 0.0.23 type-checking errors and bump pin (#3481) fix: shield lifespan teardown from cancellation (#3480) fix: forward custom_route endpoints from mounted servers (#3462) fix updates _get_additional_http_routes() to traverse providers, Fixes #3457 fix: remove hardcoded version from CLI help text (#3456) fix: monty 0.0.8 compatibility, drop external_functions from constructor (#3468) fix: task test teardown hanging 5s per test (#3499) Closes #3498 fix: validate workspace path is a directory before cursor install (#3440) Fixes #3426 fix: handle re.error from malformed URI templates in build_regex (#3501) fix: reject empty/OIDC-only required_scopes in AzureProvider (#3503) fix: restrict $ref resolution to local refs only (SSRF/LFI) (#3502) fix warnings and timeouts (#3504) close upgrade check issue when build passes (#3505) Closes #3484 fix: URL-encode path params to prevent SSRF/path traversal (GHSA-vv7q-7jx5-f767) (#3507) fix: prevent path traversal in skill download (#3493) fix: prefer IdP-granted scopes over client-requested scopes in OAuthProxy (#3492) fix: remove unrelated transform and http.py changes from PR scope fix: remove forced follow_redirects from httpx_client_factory calls (#3496) fix: stop passing follow_redirects to httpx_client_factory fix: restore follow_redirects=True for custom httpx client factories Closes #3509 fix: CSRF double-submit cookie check in consent flow (#3519) fix: validate server names in install commands (#3522) fix: use raw strings for regex in pytest.raises match (#3523) fix: reject refresh tokens used as Bearer access tokens (#3524) fix: route ResourcesAsTools/PromptsAsTools through server middleware (#3495) fix: resolve Pyright "Module is not callable" on @tool, @resource, @prompt decorators (#3540) fix: filter warnings by message in KEY_PREFIX test (#3549) fix: suppress output schema for ToolResult subclass annotations (#3548) fix: increase sleep duration in proxy cache tests (#3567) fix: store absolute token expiry to prevent stale expires_in on reload (#3572) fix: preserve tool properties named 'title' during schema compression (#3582) Fix loopback redirect URI port matching per RFC 8252 §7.3 (#3589) Fix app tool routing: visibility check and middleware propagation (#3591) Fix query parameter serialization to respect OpenAPI explode/style settings (#3595) Fix dev apps form: union types, textarea support, JSON parsing (#3597) fix(google): replace deprecated /oauth2/v1/tokeninfo with /oauth2/v3/userinfo (#3603) fix: resolve EntraOBOToken dependency injection through MultiAuth (#3609) fix(docs): correct misleading stateless_http header (#3622) fix: filesystem provider import machinery (#3626) Closes #3625 (issues 2, 3, 6) fix: recover StdioTransport after subprocess exits (#3630) fix(server): preserve mounted tool task metadata (#3632) fix: scope deprecation warning filter to FastMCPDeprecationWarning (#3649) fix imports, add PrefabAppConfig (#3650) fix: resolve CurrentFastMCP/ctx.fastmcp to child server in mounted background tasks (#3651) Fix blocking docs issues: chart imports, Select API, Rx consistency (#3652) closed by default (#3657) Fix prompt caching middleware missing wrap/unwrap round-trip (#3666) fix: serialize object query params per OpenAPI style/explode rules (#3662) Fixes #2857 fix: HTTP request headers not accessible in background task workers (#3631) fix: restore HTTP headers in worker execution path for background tasks (#3681) fix: strip discriminator after dereferencing schemas (#3682) fix: remove stale ty:ignore directives for ty 0.0.26 (#3684) Fix docs gaps in app provider pages (#3690) fix: dev apps log panel UX improvements (#3698) fix dev server empty string args (#3700)
516 lines
17 KiB
Python
516 lines
17 KiB
Python
"""Tests for enum-based elicitation, multi-select, and default values."""
|
|
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
|
|
import pytest
|
|
from pydantic import BaseModel, Field
|
|
|
|
from fastmcp import Context, FastMCP
|
|
from fastmcp.client.client import Client
|
|
from fastmcp.client.elicitation import ElicitResult
|
|
from fastmcp.exceptions import ToolError
|
|
from fastmcp.server.elicitation import (
|
|
AcceptedElicitation,
|
|
get_elicitation_schema,
|
|
validate_elicitation_json_schema,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def fastmcp_server():
|
|
mcp = FastMCP("TestServer")
|
|
|
|
@dataclass
|
|
class Person:
|
|
name: str
|
|
|
|
@mcp.tool
|
|
async def ask_for_name(context: Context) -> str:
|
|
result = await context.elicit(
|
|
message="What is your name?",
|
|
response_type=Person,
|
|
)
|
|
if result.action == "accept":
|
|
assert isinstance(result, AcceptedElicitation)
|
|
assert isinstance(result.data, Person)
|
|
return f"Hello, {result.data.name}!"
|
|
else:
|
|
return "No name provided."
|
|
|
|
@mcp.tool
|
|
def simple_test() -> str:
|
|
return "Hello!"
|
|
|
|
return mcp
|
|
|
|
|
|
async def test_elicitation_implicit_acceptance(fastmcp_server):
|
|
"""Test that elicitation handler can return data directly without ElicitResult wrapper."""
|
|
|
|
async def elicitation_handler(message, response_type, params, ctx):
|
|
# Return data directly without wrapping in ElicitResult
|
|
# This should be treated as implicit acceptance
|
|
return response_type(name="Bob")
|
|
|
|
async with Client(
|
|
fastmcp_server, elicitation_handler=elicitation_handler
|
|
) as client:
|
|
result = await client.call_tool("ask_for_name")
|
|
assert result.data == "Hello, Bob!"
|
|
|
|
|
|
async def test_elicitation_implicit_acceptance_must_be_dict(fastmcp_server):
|
|
"""Test that elicitation handler can return data directly without ElicitResult wrapper."""
|
|
|
|
async def elicitation_handler(message, response_type, params, ctx):
|
|
# Return data directly without wrapping in ElicitResult
|
|
# This should be treated as implicit acceptance
|
|
return "Bob"
|
|
|
|
async with Client(
|
|
fastmcp_server, elicitation_handler=elicitation_handler
|
|
) as client:
|
|
with pytest.raises(
|
|
ToolError,
|
|
match="Elicitation responses must be serializable as a JSON object",
|
|
):
|
|
await client.call_tool("ask_for_name")
|
|
|
|
|
|
def test_enum_elicitation_schema_inline():
|
|
"""Test that enum schemas are generated inline without $ref/$defs for MCP compatibility."""
|
|
|
|
class Priority(Enum):
|
|
LOW = "low"
|
|
MEDIUM = "medium"
|
|
HIGH = "high"
|
|
|
|
@dataclass
|
|
class TaskRequest:
|
|
title: str
|
|
priority: Priority
|
|
|
|
# Generate elicitation schema
|
|
schema = get_elicitation_schema(TaskRequest)
|
|
|
|
# Verify no $defs section exists (enums should be inlined)
|
|
assert "$defs" not in schema, (
|
|
"Schema should not contain $defs - enums must be inline"
|
|
)
|
|
|
|
# Verify no $ref in properties
|
|
for prop_name, prop_schema in schema.get("properties", {}).items():
|
|
assert "$ref" not in prop_schema, (
|
|
f"Property {prop_name} contains $ref - should be inline"
|
|
)
|
|
|
|
# Verify the priority field has inline enum values
|
|
priority_schema = schema["properties"]["priority"]
|
|
assert "enum" in priority_schema, "Priority should have enum values inline"
|
|
assert priority_schema["enum"] == ["low", "medium", "high"]
|
|
assert priority_schema.get("type") == "string"
|
|
|
|
# Verify title field is a simple string
|
|
assert schema["properties"]["title"]["type"] == "string"
|
|
|
|
|
|
def test_enum_elicitation_schema_inline_untitled():
|
|
"""Test that enum schemas generate simple enum pattern (no automatic titles)."""
|
|
|
|
class TaskStatus(Enum):
|
|
NOT_STARTED = "not_started"
|
|
IN_PROGRESS = "in_progress"
|
|
COMPLETED = "completed"
|
|
ON_HOLD = "on_hold"
|
|
|
|
@dataclass
|
|
class TaskUpdate:
|
|
task_id: str
|
|
status: TaskStatus
|
|
|
|
# Generate elicitation schema
|
|
schema = get_elicitation_schema(TaskUpdate)
|
|
|
|
# Verify enum is inline
|
|
assert "$defs" not in schema
|
|
assert "$ref" not in str(schema)
|
|
|
|
status_schema = schema["properties"]["status"]
|
|
# Should generate simple enum pattern (no automatic title generation)
|
|
assert "enum" in status_schema
|
|
assert "oneOf" not in status_schema
|
|
assert "enumNames" not in status_schema
|
|
assert status_schema["enum"] == [
|
|
"not_started",
|
|
"in_progress",
|
|
"completed",
|
|
"on_hold",
|
|
]
|
|
|
|
|
|
async def test_dict_based_titled_single_select():
|
|
"""Test dict-based titled single-select enum."""
|
|
mcp = FastMCP("TestServer")
|
|
|
|
@mcp.tool
|
|
async def my_tool(ctx: Context) -> str:
|
|
result = await ctx.elicit(
|
|
"Choose priority",
|
|
response_type={
|
|
"low": {"title": "Low Priority"},
|
|
"high": {"title": "High Priority"},
|
|
},
|
|
)
|
|
if result.action == "accept":
|
|
assert isinstance(result, AcceptedElicitation)
|
|
assert isinstance(result.data, str)
|
|
return result.data
|
|
return "declined"
|
|
|
|
async def elicitation_handler(message, response_type, params, ctx):
|
|
# Verify schema follows SEP-1330 pattern with type: "string"
|
|
schema = params.requestedSchema
|
|
assert schema["type"] == "object"
|
|
assert "value" in schema["properties"]
|
|
value_schema = schema["properties"]["value"]
|
|
assert value_schema["type"] == "string"
|
|
assert "oneOf" in value_schema
|
|
one_of = value_schema["oneOf"]
|
|
assert {"const": "low", "title": "Low Priority"} in one_of
|
|
assert {"const": "high", "title": "High Priority"} in one_of
|
|
|
|
return ElicitResult(action="accept", content={"value": "low"})
|
|
|
|
async with Client(mcp, elicitation_handler=elicitation_handler) as client:
|
|
result = await client.call_tool("my_tool", {})
|
|
assert result.data == "low"
|
|
|
|
|
|
async def test_list_list_multi_select_untitled():
|
|
"""Test list[list[str]] for multi-select untitled shorthand."""
|
|
mcp = FastMCP("TestServer")
|
|
|
|
@mcp.tool
|
|
async def my_tool(ctx: Context) -> str:
|
|
result = await ctx.elicit(
|
|
"Choose tags",
|
|
response_type=[["bug", "feature", "documentation"]],
|
|
)
|
|
if result.action == "accept":
|
|
assert isinstance(result, AcceptedElicitation)
|
|
assert isinstance(result.data, list)
|
|
return ",".join(result.data) # type: ignore[no-matching-overload] # ty:ignore[no-matching-overload]
|
|
return "declined"
|
|
|
|
async def elicitation_handler(message, response_type, params, ctx):
|
|
# Verify schema has array with enum pattern
|
|
schema = params.requestedSchema
|
|
assert schema["type"] == "object"
|
|
assert "value" in schema["properties"]
|
|
value_schema = schema["properties"]["value"]
|
|
assert value_schema["type"] == "array"
|
|
assert "enum" in value_schema["items"]
|
|
assert value_schema["items"]["enum"] == ["bug", "feature", "documentation"]
|
|
|
|
return ElicitResult(action="accept", content={"value": ["bug", "feature"]})
|
|
|
|
async with Client(mcp, elicitation_handler=elicitation_handler) as client:
|
|
result = await client.call_tool("my_tool", {})
|
|
assert result.data == "bug,feature"
|
|
|
|
|
|
async def test_list_dict_multi_select_titled():
|
|
"""Test list[dict] for multi-select titled."""
|
|
mcp = FastMCP("TestServer")
|
|
|
|
@mcp.tool
|
|
async def my_tool(ctx: Context) -> str:
|
|
result = await ctx.elicit(
|
|
"Choose priorities",
|
|
response_type=[
|
|
{
|
|
"low": {"title": "Low Priority"},
|
|
"high": {"title": "High Priority"},
|
|
}
|
|
],
|
|
)
|
|
if result.action == "accept":
|
|
assert isinstance(result, AcceptedElicitation)
|
|
assert isinstance(result.data, list)
|
|
return ",".join(result.data) # type: ignore[no-matching-overload] # ty:ignore[no-matching-overload]
|
|
return "declined"
|
|
|
|
async def elicitation_handler(message, response_type, params, ctx):
|
|
# Verify schema has array with SEP-1330 compliant items (anyOf pattern)
|
|
schema = params.requestedSchema
|
|
assert schema["type"] == "object"
|
|
assert "value" in schema["properties"]
|
|
value_schema = schema["properties"]["value"]
|
|
assert value_schema["type"] == "array"
|
|
items_schema = value_schema["items"]
|
|
assert "anyOf" in items_schema
|
|
any_of = items_schema["anyOf"]
|
|
assert {"const": "low", "title": "Low Priority"} in any_of
|
|
assert {"const": "high", "title": "High Priority"} in any_of
|
|
|
|
return ElicitResult(action="accept", content={"value": ["low", "high"]})
|
|
|
|
async with Client(mcp, elicitation_handler=elicitation_handler) as client:
|
|
result = await client.call_tool("my_tool", {})
|
|
assert result.data == "low,high"
|
|
|
|
|
|
async def test_list_enum_multi_select():
|
|
"""Test list[Enum] for multi-select with enum in dataclass field."""
|
|
|
|
class Priority(Enum):
|
|
LOW = "low"
|
|
MEDIUM = "medium"
|
|
HIGH = "high"
|
|
|
|
@dataclass
|
|
class TaskRequest:
|
|
priorities: list[Priority]
|
|
|
|
schema = get_elicitation_schema(TaskRequest)
|
|
|
|
priorities_schema = schema["properties"]["priorities"]
|
|
assert priorities_schema["type"] == "array"
|
|
assert "items" in priorities_schema
|
|
items_schema = priorities_schema["items"]
|
|
# Should have enum pattern for untitled enums
|
|
assert "enum" in items_schema
|
|
assert items_schema["enum"] == ["low", "medium", "high"]
|
|
|
|
|
|
async def test_list_enum_multi_select_direct():
|
|
"""Test list[Enum] type annotation passed directly to ctx.elicit()."""
|
|
mcp = FastMCP("TestServer")
|
|
|
|
class Priority(Enum):
|
|
LOW = "low"
|
|
MEDIUM = "medium"
|
|
HIGH = "high"
|
|
|
|
@mcp.tool
|
|
async def my_tool(ctx: Context) -> str:
|
|
result = await ctx.elicit(
|
|
"Choose priorities",
|
|
response_type=list[Priority], # Type annotation for multi-select
|
|
)
|
|
if result.action == "accept":
|
|
assert isinstance(result, AcceptedElicitation)
|
|
assert isinstance(result.data, list)
|
|
priorities = result.data
|
|
return ",".join(
|
|
[p.value if isinstance(p, Priority) else str(p) for p in priorities]
|
|
)
|
|
return "declined"
|
|
|
|
async def elicitation_handler(message, response_type, params, ctx):
|
|
# Verify schema has array with enum pattern
|
|
schema = params.requestedSchema
|
|
assert schema["type"] == "object"
|
|
assert "value" in schema["properties"]
|
|
value_schema = schema["properties"]["value"]
|
|
assert value_schema["type"] == "array"
|
|
assert "enum" in value_schema["items"]
|
|
assert value_schema["items"]["enum"] == ["low", "medium", "high"]
|
|
|
|
return ElicitResult(action="accept", content={"value": ["low", "high"]})
|
|
|
|
async with Client(mcp, elicitation_handler=elicitation_handler) as client:
|
|
result = await client.call_tool("my_tool", {})
|
|
assert result.data == "low,high"
|
|
|
|
|
|
async def test_validation_allows_enum_arrays():
|
|
"""Test validation accepts arrays with enum items."""
|
|
schema = {
|
|
"type": "object",
|
|
"properties": {
|
|
"priorities": {
|
|
"type": "array",
|
|
"items": {"enum": ["low", "medium", "high"]},
|
|
}
|
|
},
|
|
}
|
|
validate_elicitation_json_schema(schema) # Should not raise
|
|
|
|
|
|
async def test_validation_allows_enum_arrays_with_anyof():
|
|
"""Test validation accepts arrays with anyOf enum pattern (SEP-1330 compliant)."""
|
|
schema = {
|
|
"type": "object",
|
|
"properties": {
|
|
"priorities": {
|
|
"type": "array",
|
|
"items": {
|
|
"anyOf": [
|
|
{"const": "low", "title": "Low Priority"},
|
|
{"const": "high", "title": "High Priority"},
|
|
]
|
|
},
|
|
}
|
|
},
|
|
}
|
|
validate_elicitation_json_schema(schema) # Should not raise
|
|
|
|
|
|
async def test_validation_rejects_non_enum_arrays():
|
|
"""Test validation still rejects arrays of objects."""
|
|
schema = {
|
|
"type": "object",
|
|
"properties": {
|
|
"users": {
|
|
"type": "array",
|
|
"items": {"type": "object", "properties": {"name": {"type": "string"}}},
|
|
}
|
|
},
|
|
}
|
|
with pytest.raises(TypeError, match="array of objects"):
|
|
validate_elicitation_json_schema(schema)
|
|
|
|
|
|
async def test_validation_rejects_primitive_arrays():
|
|
"""Test validation rejects arrays of primitives without enum pattern."""
|
|
schema = {
|
|
"type": "object",
|
|
"properties": {
|
|
"names": {"type": "array", "items": {"type": "string"}},
|
|
},
|
|
}
|
|
with pytest.raises(TypeError, match="arrays are only allowed"):
|
|
validate_elicitation_json_schema(schema)
|
|
|
|
|
|
class TestElicitationDefaults:
|
|
"""Test suite for default values in elicitation schemas."""
|
|
|
|
def test_string_default_preserved(self):
|
|
"""Test that string defaults are preserved in the schema."""
|
|
|
|
class Model(BaseModel):
|
|
email: str = Field(default="[email protected]")
|
|
|
|
schema = get_elicitation_schema(Model)
|
|
props = schema.get("properties", {})
|
|
|
|
assert "email" in props
|
|
assert "default" in props["email"]
|
|
assert props["email"]["default"] == "[email protected]"
|
|
assert props["email"]["type"] == "string"
|
|
|
|
def test_integer_default_preserved(self):
|
|
"""Test that integer defaults are preserved in the schema."""
|
|
|
|
class Model(BaseModel):
|
|
count: int = Field(default=50)
|
|
|
|
schema = get_elicitation_schema(Model)
|
|
props = schema.get("properties", {})
|
|
|
|
assert "count" in props
|
|
assert "default" in props["count"]
|
|
assert props["count"]["default"] == 50
|
|
assert props["count"]["type"] == "integer"
|
|
|
|
def test_number_default_preserved(self):
|
|
"""Test that number defaults are preserved in the schema."""
|
|
|
|
class Model(BaseModel):
|
|
price: float = Field(default=3.14)
|
|
|
|
schema = get_elicitation_schema(Model)
|
|
props = schema.get("properties", {})
|
|
|
|
assert "price" in props
|
|
assert "default" in props["price"]
|
|
assert props["price"]["default"] == 3.14
|
|
assert props["price"]["type"] == "number"
|
|
|
|
def test_boolean_default_preserved(self):
|
|
"""Test that boolean defaults are preserved in the schema."""
|
|
|
|
class Model(BaseModel):
|
|
enabled: bool = Field(default=False)
|
|
|
|
schema = get_elicitation_schema(Model)
|
|
props = schema.get("properties", {})
|
|
|
|
assert "enabled" in props
|
|
assert "default" in props["enabled"]
|
|
assert props["enabled"]["default"] is False
|
|
assert props["enabled"]["type"] == "boolean"
|
|
|
|
def test_enum_default_preserved(self):
|
|
"""Test that enum defaults are preserved in the schema."""
|
|
|
|
class Priority(Enum):
|
|
LOW = "low"
|
|
MEDIUM = "medium"
|
|
HIGH = "high"
|
|
|
|
class Model(BaseModel):
|
|
choice: Priority = Field(default=Priority.MEDIUM)
|
|
|
|
schema = get_elicitation_schema(Model)
|
|
props = schema.get("properties", {})
|
|
|
|
assert "choice" in props
|
|
assert "default" in props["choice"]
|
|
assert props["choice"]["default"] == "medium"
|
|
assert "enum" in props["choice"]
|
|
assert props["choice"]["type"] == "string"
|
|
|
|
def test_all_defaults_preserved_together(self):
|
|
"""Test that all default types are preserved when used together."""
|
|
|
|
class Priority(Enum):
|
|
A = "A"
|
|
B = "B"
|
|
|
|
class Model(BaseModel):
|
|
string_field: str = Field(default="[email protected]")
|
|
integer_field: int = Field(default=50)
|
|
number_field: float = Field(default=3.14)
|
|
boolean_field: bool = Field(default=False)
|
|
enum_field: Priority = Field(default=Priority.A)
|
|
|
|
schema = get_elicitation_schema(Model)
|
|
props = schema.get("properties", {})
|
|
|
|
assert props["string_field"]["default"] == "[email protected]"
|
|
assert props["integer_field"]["default"] == 50
|
|
assert props["number_field"]["default"] == 3.14
|
|
assert props["boolean_field"]["default"] is False
|
|
assert props["enum_field"]["default"] == "A"
|
|
|
|
def test_mixed_defaults_and_required(self):
|
|
"""Test that fields with defaults are not in required list."""
|
|
|
|
class Model(BaseModel):
|
|
required_field: str = Field(description="Required field")
|
|
optional_with_default: int = Field(default=42)
|
|
|
|
schema = get_elicitation_schema(Model)
|
|
props = schema.get("properties", {})
|
|
required = schema.get("required", [])
|
|
|
|
assert "required_field" in required
|
|
assert "optional_with_default" not in required
|
|
assert props["optional_with_default"]["default"] == 42
|
|
|
|
def test_compress_schema_preserves_defaults(self):
|
|
"""Test that compress_schema() doesn't strip default values."""
|
|
|
|
class Model(BaseModel):
|
|
string_field: str = Field(default="test")
|
|
integer_field: int = Field(default=42)
|
|
|
|
schema = get_elicitation_schema(Model)
|
|
props = schema.get("properties", {})
|
|
|
|
assert "default" in props["string_field"]
|
|
assert "default" in props["integer_field"]
|