Implemented the next missing parity slice around SDK core types.
This commit is contained in:
+1
-1
@@ -181,7 +181,7 @@ Missing:
|
|||||||
- [x] Release notes checking from `setup.ts` (local CHANGELOG.md, no network/cache layer)
|
- [x] Release notes checking from `setup.ts` (local CHANGELOG.md, no network/cache layer)
|
||||||
- [ ] Full `entrypoints/cli.tsx` parity (version flag, feature flags, env setup, dynamic imports)
|
- [ ] Full `entrypoints/cli.tsx` parity (version flag, feature flags, env setup, dynamic imports)
|
||||||
- [ ] Full `entrypoints/init.ts` parity (settings validation, OAuth, policy limits, telemetry, cleanup handlers)
|
- [ ] Full `entrypoints/init.ts` parity (settings validation, OAuth, policy limits, telemetry, cleanup handlers)
|
||||||
- [ ] SDK entrypoint (`entrypoints/sdk/` — controlTypes, coreTypes, runtimeTypes, settingsTypes, toolTypes)
|
- [ ] SDK entrypoint (`entrypoints/sdk/` — controlTypes, coreTypes, runtimeTypes, settingsTypes, toolTypes) — partial: HOOK_EVENTS, EXIT_REASONS, ModelUsage, ThinkingConfig, MCP server configs, JsonSchemaOutputFormat ported in `src/sdk_core_types.py`
|
||||||
- [x] Sandbox types/network config schema (`entrypoints/sandboxTypes.ts`)
|
- [x] Sandbox types/network config schema (`entrypoints/sandboxTypes.ts`)
|
||||||
|
|
||||||
## 3. Prompt Assembly
|
## 3. Prompt Assembly
|
||||||
|
|||||||
@@ -0,0 +1,319 @@
|
|||||||
|
"""Foundational SDK types — Python port of entrypoints/sdk/coreTypes.ts.
|
||||||
|
|
||||||
|
Mirrors the public constants and a starter set of dataclasses for the
|
||||||
|
serializable types defined in `entrypoints/sdk/coreSchemas.ts`. The full
|
||||||
|
schema set (1800+ lines of Zod) is too large to port wholesale; this file
|
||||||
|
covers the foundational pieces other modules already reference and gives
|
||||||
|
later slices a place to extend.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any, Literal
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Const arrays (HOOK_EVENTS, EXIT_REASONS) — mirror coreTypes.ts top-level
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
HOOK_EVENTS: tuple[str, ...] = (
|
||||||
|
'PreToolUse',
|
||||||
|
'PostToolUse',
|
||||||
|
'PostToolUseFailure',
|
||||||
|
'Notification',
|
||||||
|
'UserPromptSubmit',
|
||||||
|
'SessionStart',
|
||||||
|
'SessionEnd',
|
||||||
|
'Stop',
|
||||||
|
'StopFailure',
|
||||||
|
'SubagentStart',
|
||||||
|
'SubagentStop',
|
||||||
|
'PreCompact',
|
||||||
|
'PostCompact',
|
||||||
|
'PermissionRequest',
|
||||||
|
'PermissionDenied',
|
||||||
|
'Setup',
|
||||||
|
'TeammateIdle',
|
||||||
|
'TaskCreated',
|
||||||
|
'TaskCompleted',
|
||||||
|
'Elicitation',
|
||||||
|
'ElicitationResult',
|
||||||
|
'ConfigChange',
|
||||||
|
'WorktreeCreate',
|
||||||
|
'WorktreeRemove',
|
||||||
|
'InstructionsLoaded',
|
||||||
|
'CwdChanged',
|
||||||
|
'FileChanged',
|
||||||
|
)
|
||||||
|
|
||||||
|
EXIT_REASONS: tuple[str, ...] = (
|
||||||
|
'clear',
|
||||||
|
'resume',
|
||||||
|
'logout',
|
||||||
|
'prompt_input_exit',
|
||||||
|
'other',
|
||||||
|
'bypass_permissions_disabled',
|
||||||
|
)
|
||||||
|
|
||||||
|
API_KEY_SOURCES: tuple[str, ...] = ('user', 'project', 'org', 'temporary', 'oauth')
|
||||||
|
CONFIG_SCOPES: tuple[str, ...] = ('local', 'user', 'project')
|
||||||
|
SDK_BETAS: tuple[str, ...] = ('context-1m-2025-08-07',)
|
||||||
|
|
||||||
|
|
||||||
|
ApiKeySource = Literal['user', 'project', 'org', 'temporary', 'oauth']
|
||||||
|
ConfigScope = Literal['local', 'user', 'project']
|
||||||
|
SdkBeta = Literal['context-1m-2025-08-07']
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Usage / model
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ModelUsage:
|
||||||
|
input_tokens: int
|
||||||
|
output_tokens: int
|
||||||
|
cache_read_input_tokens: int
|
||||||
|
cache_creation_input_tokens: int
|
||||||
|
web_search_requests: int
|
||||||
|
cost_usd: float
|
||||||
|
context_window: int
|
||||||
|
max_output_tokens: int
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: dict[str, Any]) -> 'ModelUsage':
|
||||||
|
return cls(
|
||||||
|
input_tokens=int(data['inputTokens']),
|
||||||
|
output_tokens=int(data['outputTokens']),
|
||||||
|
cache_read_input_tokens=int(data['cacheReadInputTokens']),
|
||||||
|
cache_creation_input_tokens=int(data['cacheCreationInputTokens']),
|
||||||
|
web_search_requests=int(data['webSearchRequests']),
|
||||||
|
cost_usd=float(data['costUSD']),
|
||||||
|
context_window=int(data['contextWindow']),
|
||||||
|
max_output_tokens=int(data['maxOutputTokens']),
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
'inputTokens': self.input_tokens,
|
||||||
|
'outputTokens': self.output_tokens,
|
||||||
|
'cacheReadInputTokens': self.cache_read_input_tokens,
|
||||||
|
'cacheCreationInputTokens': self.cache_creation_input_tokens,
|
||||||
|
'webSearchRequests': self.web_search_requests,
|
||||||
|
'costUSD': self.cost_usd,
|
||||||
|
'contextWindow': self.context_window,
|
||||||
|
'maxOutputTokens': self.max_output_tokens,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Thinking config
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ThinkingAdaptive:
|
||||||
|
type: Literal['adaptive'] = 'adaptive'
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return {'type': self.type}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ThinkingEnabled:
|
||||||
|
budget_tokens: int | None = None
|
||||||
|
type: Literal['enabled'] = 'enabled'
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
out: dict[str, Any] = {'type': self.type}
|
||||||
|
if self.budget_tokens is not None:
|
||||||
|
out['budgetTokens'] = self.budget_tokens
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ThinkingDisabled:
|
||||||
|
type: Literal['disabled'] = 'disabled'
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return {'type': self.type}
|
||||||
|
|
||||||
|
|
||||||
|
ThinkingConfig = ThinkingAdaptive | ThinkingEnabled | ThinkingDisabled
|
||||||
|
|
||||||
|
|
||||||
|
def thinking_config_from_dict(data: dict[str, Any]) -> ThinkingConfig:
|
||||||
|
kind = data.get('type')
|
||||||
|
if kind == 'adaptive':
|
||||||
|
return ThinkingAdaptive()
|
||||||
|
if kind == 'enabled':
|
||||||
|
budget = data.get('budgetTokens')
|
||||||
|
return ThinkingEnabled(
|
||||||
|
budget_tokens=int(budget) if budget is not None else None,
|
||||||
|
)
|
||||||
|
if kind == 'disabled':
|
||||||
|
return ThinkingDisabled()
|
||||||
|
raise ValueError(f'unknown thinking config type: {kind!r}')
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# MCP server configs
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class McpStdioServerConfig:
|
||||||
|
command: str
|
||||||
|
args: list[str] | None = None
|
||||||
|
env: dict[str, str] | None = None
|
||||||
|
type: Literal['stdio'] = 'stdio'
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
out: dict[str, Any] = {'type': self.type, 'command': self.command}
|
||||||
|
if self.args is not None:
|
||||||
|
out['args'] = list(self.args)
|
||||||
|
if self.env is not None:
|
||||||
|
out['env'] = dict(self.env)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class McpSSEServerConfig:
|
||||||
|
url: str
|
||||||
|
headers: dict[str, str] | None = None
|
||||||
|
type: Literal['sse'] = 'sse'
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
out: dict[str, Any] = {'type': self.type, 'url': self.url}
|
||||||
|
if self.headers is not None:
|
||||||
|
out['headers'] = dict(self.headers)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class McpHttpServerConfig:
|
||||||
|
url: str
|
||||||
|
headers: dict[str, str] | None = None
|
||||||
|
type: Literal['http'] = 'http'
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
out: dict[str, Any] = {'type': self.type, 'url': self.url}
|
||||||
|
if self.headers is not None:
|
||||||
|
out['headers'] = dict(self.headers)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class McpSdkServerConfig:
|
||||||
|
name: str
|
||||||
|
type: Literal['sdk'] = 'sdk'
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return {'type': self.type, 'name': self.name}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class McpClaudeAIProxyServerConfig:
|
||||||
|
url: str
|
||||||
|
id: str
|
||||||
|
type: Literal['claudeai-proxy'] = 'claudeai-proxy'
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return {'type': self.type, 'url': self.url, 'id': self.id}
|
||||||
|
|
||||||
|
|
||||||
|
McpServerConfigForProcessTransport = (
|
||||||
|
McpStdioServerConfig
|
||||||
|
| McpSSEServerConfig
|
||||||
|
| McpHttpServerConfig
|
||||||
|
| McpSdkServerConfig
|
||||||
|
)
|
||||||
|
McpServerStatusConfig = (
|
||||||
|
McpServerConfigForProcessTransport | McpClaudeAIProxyServerConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def mcp_server_config_from_dict(data: dict[str, Any]) -> McpServerStatusConfig:
|
||||||
|
"""Discriminate by `type` and route to the matching dataclass."""
|
||||||
|
raw_type = data.get('type')
|
||||||
|
if raw_type is None or raw_type == 'stdio':
|
||||||
|
command = data.get('command')
|
||||||
|
if not isinstance(command, str):
|
||||||
|
raise ValueError('stdio server config requires a string command')
|
||||||
|
return McpStdioServerConfig(
|
||||||
|
command=command,
|
||||||
|
args=list(data['args']) if isinstance(data.get('args'), list) else None,
|
||||||
|
env=dict(data['env']) if isinstance(data.get('env'), dict) else None,
|
||||||
|
)
|
||||||
|
if raw_type == 'sse':
|
||||||
|
return McpSSEServerConfig(
|
||||||
|
url=str(data['url']),
|
||||||
|
headers=dict(data['headers']) if isinstance(data.get('headers'), dict) else None,
|
||||||
|
)
|
||||||
|
if raw_type == 'http':
|
||||||
|
return McpHttpServerConfig(
|
||||||
|
url=str(data['url']),
|
||||||
|
headers=dict(data['headers']) if isinstance(data.get('headers'), dict) else None,
|
||||||
|
)
|
||||||
|
if raw_type == 'sdk':
|
||||||
|
return McpSdkServerConfig(name=str(data['name']))
|
||||||
|
if raw_type == 'claudeai-proxy':
|
||||||
|
return McpClaudeAIProxyServerConfig(
|
||||||
|
url=str(data['url']),
|
||||||
|
id=str(data['id']),
|
||||||
|
)
|
||||||
|
raise ValueError(f'unknown MCP server config type: {raw_type!r}')
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Output format
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class JsonSchemaOutputFormat:
|
||||||
|
schema: dict[str, Any] = field(default_factory=dict)
|
||||||
|
type: Literal['json_schema'] = 'json_schema'
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return {'type': self.type, 'schema': dict(self.schema)}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: dict[str, Any]) -> 'JsonSchemaOutputFormat':
|
||||||
|
if data.get('type') != 'json_schema':
|
||||||
|
raise ValueError(
|
||||||
|
f'expected output format type "json_schema", got {data.get("type")!r}'
|
||||||
|
)
|
||||||
|
schema = data.get('schema')
|
||||||
|
if not isinstance(schema, dict):
|
||||||
|
raise ValueError('json_schema output format requires a "schema" mapping')
|
||||||
|
return cls(schema=dict(schema))
|
||||||
|
|
||||||
|
|
||||||
|
OutputFormat = JsonSchemaOutputFormat
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'HOOK_EVENTS',
|
||||||
|
'EXIT_REASONS',
|
||||||
|
'API_KEY_SOURCES',
|
||||||
|
'CONFIG_SCOPES',
|
||||||
|
'SDK_BETAS',
|
||||||
|
'ApiKeySource',
|
||||||
|
'ConfigScope',
|
||||||
|
'SdkBeta',
|
||||||
|
'ModelUsage',
|
||||||
|
'ThinkingAdaptive',
|
||||||
|
'ThinkingEnabled',
|
||||||
|
'ThinkingDisabled',
|
||||||
|
'ThinkingConfig',
|
||||||
|
'thinking_config_from_dict',
|
||||||
|
'McpStdioServerConfig',
|
||||||
|
'McpSSEServerConfig',
|
||||||
|
'McpHttpServerConfig',
|
||||||
|
'McpSdkServerConfig',
|
||||||
|
'McpClaudeAIProxyServerConfig',
|
||||||
|
'McpServerConfigForProcessTransport',
|
||||||
|
'McpServerStatusConfig',
|
||||||
|
'mcp_server_config_from_dict',
|
||||||
|
'JsonSchemaOutputFormat',
|
||||||
|
'OutputFormat',
|
||||||
|
]
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
"""Tests for SDK core types ported from entrypoints/sdk/coreTypes.ts."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from src.sdk_core_types import (
|
||||||
|
API_KEY_SOURCES,
|
||||||
|
CONFIG_SCOPES,
|
||||||
|
EXIT_REASONS,
|
||||||
|
HOOK_EVENTS,
|
||||||
|
SDK_BETAS,
|
||||||
|
JsonSchemaOutputFormat,
|
||||||
|
McpClaudeAIProxyServerConfig,
|
||||||
|
McpHttpServerConfig,
|
||||||
|
McpSdkServerConfig,
|
||||||
|
McpSSEServerConfig,
|
||||||
|
McpStdioServerConfig,
|
||||||
|
ModelUsage,
|
||||||
|
ThinkingAdaptive,
|
||||||
|
ThinkingDisabled,
|
||||||
|
ThinkingEnabled,
|
||||||
|
mcp_server_config_from_dict,
|
||||||
|
thinking_config_from_dict,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class HookEventsTest(unittest.TestCase):
|
||||||
|
def test_includes_known_events(self) -> None:
|
||||||
|
for required in (
|
||||||
|
'PreToolUse',
|
||||||
|
'PostToolUse',
|
||||||
|
'UserPromptSubmit',
|
||||||
|
'SessionStart',
|
||||||
|
'PreCompact',
|
||||||
|
'WorktreeCreate',
|
||||||
|
'CwdChanged',
|
||||||
|
'FileChanged',
|
||||||
|
):
|
||||||
|
self.assertIn(required, HOOK_EVENTS)
|
||||||
|
|
||||||
|
def test_no_duplicates(self) -> None:
|
||||||
|
self.assertEqual(len(HOOK_EVENTS), len(set(HOOK_EVENTS)))
|
||||||
|
|
||||||
|
|
||||||
|
class ExitReasonsTest(unittest.TestCase):
|
||||||
|
def test_known_exit_reasons(self) -> None:
|
||||||
|
for required in (
|
||||||
|
'clear', 'resume', 'logout', 'prompt_input_exit',
|
||||||
|
'other', 'bypass_permissions_disabled',
|
||||||
|
):
|
||||||
|
self.assertIn(required, EXIT_REASONS)
|
||||||
|
|
||||||
|
|
||||||
|
class EnumLiteralsTest(unittest.TestCase):
|
||||||
|
def test_api_key_sources(self) -> None:
|
||||||
|
self.assertEqual(set(API_KEY_SOURCES), {'user', 'project', 'org', 'temporary', 'oauth'})
|
||||||
|
|
||||||
|
def test_config_scopes(self) -> None:
|
||||||
|
self.assertEqual(set(CONFIG_SCOPES), {'local', 'user', 'project'})
|
||||||
|
|
||||||
|
def test_sdk_betas_known(self) -> None:
|
||||||
|
self.assertIn('context-1m-2025-08-07', SDK_BETAS)
|
||||||
|
|
||||||
|
|
||||||
|
class ModelUsageTest(unittest.TestCase):
|
||||||
|
def test_round_trip(self) -> None:
|
||||||
|
raw = {
|
||||||
|
'inputTokens': 100, 'outputTokens': 50,
|
||||||
|
'cacheReadInputTokens': 10, 'cacheCreationInputTokens': 5,
|
||||||
|
'webSearchRequests': 0, 'costUSD': 0.001,
|
||||||
|
'contextWindow': 200000, 'maxOutputTokens': 8192,
|
||||||
|
}
|
||||||
|
usage = ModelUsage.from_dict(raw)
|
||||||
|
self.assertEqual(usage.input_tokens, 100)
|
||||||
|
self.assertEqual(usage.cost_usd, 0.001)
|
||||||
|
self.assertEqual(usage.to_dict(), raw)
|
||||||
|
|
||||||
|
|
||||||
|
class ThinkingConfigTest(unittest.TestCase):
|
||||||
|
def test_adaptive(self) -> None:
|
||||||
|
cfg = thinking_config_from_dict({'type': 'adaptive'})
|
||||||
|
self.assertIsInstance(cfg, ThinkingAdaptive)
|
||||||
|
self.assertEqual(cfg.to_dict(), {'type': 'adaptive'})
|
||||||
|
|
||||||
|
def test_enabled_with_budget(self) -> None:
|
||||||
|
cfg = thinking_config_from_dict({'type': 'enabled', 'budgetTokens': 1024})
|
||||||
|
self.assertIsInstance(cfg, ThinkingEnabled)
|
||||||
|
self.assertEqual(cfg.to_dict(), {'type': 'enabled', 'budgetTokens': 1024})
|
||||||
|
|
||||||
|
def test_enabled_without_budget(self) -> None:
|
||||||
|
cfg = thinking_config_from_dict({'type': 'enabled'})
|
||||||
|
self.assertEqual(cfg.to_dict(), {'type': 'enabled'})
|
||||||
|
|
||||||
|
def test_disabled(self) -> None:
|
||||||
|
cfg = thinking_config_from_dict({'type': 'disabled'})
|
||||||
|
self.assertIsInstance(cfg, ThinkingDisabled)
|
||||||
|
|
||||||
|
def test_unknown_type_raises(self) -> None:
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
thinking_config_from_dict({'type': 'something-else'})
|
||||||
|
|
||||||
|
|
||||||
|
class McpServerConfigTest(unittest.TestCase):
|
||||||
|
def test_stdio_default_type(self) -> None:
|
||||||
|
cfg = mcp_server_config_from_dict({'command': 'npx'})
|
||||||
|
self.assertIsInstance(cfg, McpStdioServerConfig)
|
||||||
|
self.assertEqual(cfg.command, 'npx')
|
||||||
|
|
||||||
|
def test_stdio_with_args_env(self) -> None:
|
||||||
|
cfg = mcp_server_config_from_dict({
|
||||||
|
'type': 'stdio',
|
||||||
|
'command': 'node',
|
||||||
|
'args': ['./mcp.js'],
|
||||||
|
'env': {'TOKEN': 'xyz'},
|
||||||
|
})
|
||||||
|
out = cfg.to_dict()
|
||||||
|
self.assertEqual(out['command'], 'node')
|
||||||
|
self.assertEqual(out['args'], ['./mcp.js'])
|
||||||
|
self.assertEqual(out['env'], {'TOKEN': 'xyz'})
|
||||||
|
|
||||||
|
def test_sse(self) -> None:
|
||||||
|
cfg = mcp_server_config_from_dict({'type': 'sse', 'url': 'https://x.example'})
|
||||||
|
self.assertIsInstance(cfg, McpSSEServerConfig)
|
||||||
|
|
||||||
|
def test_http_with_headers(self) -> None:
|
||||||
|
cfg = mcp_server_config_from_dict({
|
||||||
|
'type': 'http',
|
||||||
|
'url': 'https://x.example',
|
||||||
|
'headers': {'Auth': 'Bearer 1'},
|
||||||
|
})
|
||||||
|
self.assertIsInstance(cfg, McpHttpServerConfig)
|
||||||
|
self.assertEqual(cfg.headers, {'Auth': 'Bearer 1'})
|
||||||
|
|
||||||
|
def test_sdk(self) -> None:
|
||||||
|
cfg = mcp_server_config_from_dict({'type': 'sdk', 'name': 'my-sdk'})
|
||||||
|
self.assertIsInstance(cfg, McpSdkServerConfig)
|
||||||
|
|
||||||
|
def test_claudeai_proxy(self) -> None:
|
||||||
|
cfg = mcp_server_config_from_dict({
|
||||||
|
'type': 'claudeai-proxy',
|
||||||
|
'url': 'https://claude.ai/p',
|
||||||
|
'id': 'abc',
|
||||||
|
})
|
||||||
|
self.assertIsInstance(cfg, McpClaudeAIProxyServerConfig)
|
||||||
|
|
||||||
|
def test_unknown_type_raises(self) -> None:
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
mcp_server_config_from_dict({'type': 'mystery'})
|
||||||
|
|
||||||
|
def test_stdio_requires_command(self) -> None:
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
mcp_server_config_from_dict({'type': 'stdio'})
|
||||||
|
|
||||||
|
|
||||||
|
class JsonSchemaOutputFormatTest(unittest.TestCase):
|
||||||
|
def test_round_trip(self) -> None:
|
||||||
|
fmt = JsonSchemaOutputFormat.from_dict({
|
||||||
|
'type': 'json_schema',
|
||||||
|
'schema': {'type': 'object', 'properties': {}},
|
||||||
|
})
|
||||||
|
self.assertEqual(fmt.schema['type'], 'object')
|
||||||
|
self.assertEqual(fmt.to_dict()['type'], 'json_schema')
|
||||||
|
|
||||||
|
def test_rejects_wrong_type(self) -> None:
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
JsonSchemaOutputFormat.from_dict({'type': 'text', 'schema': {}})
|
||||||
|
|
||||||
|
def test_requires_schema_mapping(self) -> None:
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
JsonSchemaOutputFormat.from_dict({'type': 'json_schema'})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user