This is an automated email from the ASF dual-hosted git repository.
FreeOnePlus pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris-mcp-server.git
The following commit(s) were added to refs/heads/master by this push:
new 779e558 fix: type prompt retrieval failures (#96)
779e558 is described below
commit 779e558e4f49cdcd09ef3ebfe44f1e3d25f4c160
Author: Yijia Su <[email protected]>
AuthorDate: Wed Jul 29 17:43:53 2026 +0800
fix: type prompt retrieval failures (#96)
Co-authored-by: FreeOnePlus <[email protected]>
---
MCP-2026-07-28-DEVELOPMENT-LEDGER.md | 50 +++++++--
doris_mcp_server/protocol.py | 46 +++++++-
doris_mcp_server/tools/prompts_manager.py | 96 +++++++++++++----
test/protocol/stdio_capability_server.py | 52 ++++++++-
test/protocol/test_mcp_v2_protocol.py | 168 ++++++++++++++++++++++++++++++
test/tools/test_prompts_manager_errors.py | 110 +++++++++++++++++++
6 files changed, 491 insertions(+), 31 deletions(-)
diff --git a/MCP-2026-07-28-DEVELOPMENT-LEDGER.md
b/MCP-2026-07-28-DEVELOPMENT-LEDGER.md
index d53cc3c..4b79774 100644
--- a/MCP-2026-07-28-DEVELOPMENT-LEDGER.md
+++ b/MCP-2026-07-28-DEVELOPMENT-LEDGER.md
@@ -132,7 +132,7 @@
| `CORE-001` | P1 | list 异常不再返回空列表 | PROTO-002 | DB/权限/内部错误与真实空列表可区分 |
`BACKLOG` |
| `CORE-002` | P1 | Tool 错误使用 `isError=true` | PROTO-002 |
可恢复业务错误对模型可见;内部异常为稳定协议错误 | `DONE` |
| `CORE-003` | P1 | Resource not found 使用 `-32602` | PROTO-002 | 不存在 URI 返回
Invalid Params,不返回错误正文成功 | `DONE` |
-| `CORE-004` | P1 | Prompt 错误类型化 | PROTO-002 | 缺参数、未知 prompt、DB 上下文失败语义不同 |
`READY` |
+| `CORE-004` | P1 | Prompt 错误类型化 | PROTO-002 | 缺参数、未知 prompt、DB 上下文失败语义不同 |
`DONE` |
| `CORE-005` | P1 | 单一 Tool Definition Registry | PROTO-002 |
schema、policy、handler、审计和文档同源 | `BACKLOG` |
| `CORE-006` | P1 | `/live` 与 `/ready` 分离 | 无 | Doris 不可用时 live 可真、ready
必假;探针有短超时 | `BACKLOG` |
| `CORE-007` | P1 | 显式跨调用 handle | PROTO-003, SEC-015 | 状态不依赖协议 session;handle
绑定 principal/expiry | `BACKLOG` |
@@ -409,17 +409,53 @@ missing resource: doris://table/__core_003_missing__
连接通过 SSH key 和临时本地隧道完成;凭据未写入仓库或台账,探针完成后服务与隧道均已关闭。
+提交与评审回执:
+
+- commit:`5e57a2c fix: return invalid params for missing resources`
+- Draft
PR:[apache/doris-mcp-server#95](https://github.com/apache/doris-mcp-server/pull/95)
+
+### CORE-004
+
+Prompt 的三类失败现在拥有独立、稳定的协议语义:
+
+| 场景 | JSON-RPC code | `promptErrorCode` |
+|---|---:|---|
+| 未知 Prompt | `-32602` | `UNKNOWN_PROMPT` |
+| 缺少必填参数 | `-32602` | `MISSING_REQUIRED_ARGUMENT` |
+| 数据库上下文不可用 | `-32603` | `DATABASE_CONTEXT_UNAVAILABLE` |
+
+缺少必填参数时 `data.argument` 指明参数名。数据库异常详情只写服务端日志,协议正文不回传连接或堆栈细节。该语义同时适用于现代和 legacy
协议,旧客户端不再把 Prompt 故障误当成成功内容。
+
+数据库上下文查询增加独立 10 秒预算,避免底层连接池恢复时间超过客户端请求预算;连接管理器的 context API 与旧式 get/release
API 都保证在成功、异常和取消时归还连接。
+
+自动化验证:
+
+- Streamable HTTP:三类错误契约、错误后成功恢复;
+- 真实子进程 STDIO:三类错误契约、错误后成功恢复;
+- STDIO legacy:未知 Prompt 返回 `-32602`;
+- manager:未知 Prompt、缺参数、DB 异常、超时和两类连接归还路径;
+- 完整 pytest:`343 passed / 57 skipped / 0 failed / 247 warnings`;
+- `uv lock --check`、作用域 Ruff、`compileall`、`uv build` 全部通过。
+
+真实 Doris 与故障注入验证:
+
+- HTTP modern/legacy:未知 Prompt、缺参数后均可继续从 `hhm_dt_sim` 生成真实数据库上下文;
+- STDIO modern/legacy:未知 Prompt、缺参数后均可继续从 `hhm_dt_sim` 生成真实数据库上下文;
+- HTTP 不可达端口故障注入:10 秒内返回 `-32603`,随后 `prompts/list` 成功;
+- STDIO 子进程 manager 故障注入:返回 `-32603`,随后有效 Prompt 成功。
+
+完全不可达 Doris 的真实 STDIO 进程会在协议协商前退出,无法进入 Prompt handler;这是启动/就绪与故障恢复边界,继续由
`CORE-006` / `TEST-005` 跟踪,未计作本项通过证据。
+
## 11. 下一开发批次
批次:`BATCH-02-CONFORMANCE-AND-ERROR-SEMANTICS`
按以下顺序推进:
-1. `CORE-004`:Prompt 错误类型化;
-2. `CORE-010` / `CORE-011` / `COMPAT-001`:修复真实 Doris 已复现缺陷;
-3. `TEST-003`:运行官方 `server-stateless` Conformance;
-4. `TEST-005` / `TEST-012`:补权限不足、超时、故障恢复和工具错误路径;
-5. `PROTO-018` / `DOC-001` / `DOC-002`:版本单一来源和迁移文档;
-6. `SEC-003`~`SEC-005`:进入下一安全批,完成非 loopback fail-closed。
+1. `CORE-010` / `CORE-011` / `COMPAT-001`:修复真实 Doris 已复现缺陷;
+2. `TEST-003`:运行官方 `server-stateless` Conformance;
+3. `TEST-005` / `TEST-012`:补权限不足、超时、故障恢复和工具错误路径;
+4. `PROTO-018` / `DOC-001` / `DOC-002`:版本单一来源和迁移文档;
+5. `SEC-003`~`SEC-005`:进入下一安全批,完成非 loopback fail-closed。
`REL-001` 已达成。`REL-002` 仍由官方 Conformance、完整真实 Doris 矩阵、P0/P1 安全项、Compose
和发布门阻塞。
diff --git a/doris_mcp_server/protocol.py b/doris_mcp_server/protocol.py
index 3047598..6d77f09 100644
--- a/doris_mcp_server/protocol.py
+++ b/doris_mcp_server/protocol.py
@@ -29,6 +29,7 @@ from mcp.server.context import CallNext
from mcp.server.transport_security import TransportSecuritySettings
from mcp.shared.exceptions import MCPError
from mcp.types import (
+ INTERNAL_ERROR,
INVALID_PARAMS,
LATEST_PROTOCOL_VERSION,
MISSING_REQUIRED_CLIENT_CAPABILITY,
@@ -113,6 +114,12 @@ _RESOURCE_INVALID_PARAMS_MESSAGES = {
"RESOURCE_NOT_FOUND": "Resource not found",
}
+_PROMPT_INVALID_PARAMS_MESSAGES = {
+ "UNKNOWN_PROMPT": "Prompt not found",
+ "MISSING_REQUIRED_ARGUMENT": "Missing required prompt argument",
+}
+_PROMPT_DATABASE_CONTEXT_ERROR = "DATABASE_CONTEXT_UNAVAILABLE"
+
def _decode_resource_request_error(payload: str) -> tuple[str, str] | None:
"""Decode only manager errors that are safe to classify as client input."""
@@ -222,10 +229,41 @@ def create_doris_mcp_server(
) -> GetPromptResult:
del ctx
authorize_operation(get_current_auth_context(), "get_prompt")
- return await prompts_manager.get_prompt(
- params.name,
- dict(params.arguments or {}),
- )
+ try:
+ return await prompts_manager.get_prompt(
+ params.name,
+ dict(params.arguments or {}),
+ )
+ except Exception as exc:
+ prompt_error_code = getattr(exc, "error_code", None)
+ message = _PROMPT_INVALID_PARAMS_MESSAGES.get(prompt_error_code)
+ if message is not None:
+ data = {
+ "name": params.name,
+ "promptErrorCode": prompt_error_code,
+ }
+ argument = getattr(exc, "argument", None)
+ if isinstance(argument, str):
+ data["argument"] = argument
+ raise MCPError(
+ code=INVALID_PARAMS,
+ message=message,
+ data=data,
+ ) from exc
+ if prompt_error_code == _PROMPT_DATABASE_CONTEXT_ERROR:
+ logger.exception(
+ "Database context failed while rendering prompt %s",
+ params.name,
+ )
+ raise MCPError(
+ code=INTERNAL_ERROR,
+ message="Database context unavailable",
+ data={
+ "name": params.name,
+ "promptErrorCode": prompt_error_code,
+ },
+ ) from exc
+ raise
private_no_cache = CacheHint(ttl_ms=0, scope="private")
server = Server(
diff --git a/doris_mcp_server/tools/prompts_manager.py
b/doris_mcp_server/tools/prompts_manager.py
index e034e1e..313a0ab 100644
--- a/doris_mcp_server/tools/prompts_manager.py
+++ b/doris_mcp_server/tools/prompts_manager.py
@@ -19,6 +19,8 @@ Apache Doris MCP Prompts Manager
Provides standardized management of query templates and intelligent prompts
"""
+import asyncio
+from contextlib import asynccontextmanager
from datetime import datetime
from typing import Any
@@ -61,13 +63,60 @@ class PromptTemplate:
return content
+class UnknownPromptError(ValueError):
+ """The requested prompt name is not registered."""
+
+ error_code = "UNKNOWN_PROMPT"
+
+
+class MissingPromptArgumentError(ValueError):
+ """A registered prompt is missing one required template argument."""
+
+ error_code = "MISSING_REQUIRED_ARGUMENT"
+
+ def __init__(self, argument: str):
+ super().__init__(f"Missing required parameter: {argument}")
+ self.argument = argument
+
+
+class PromptDatabaseContextError(RuntimeError):
+ """The database context needed to render a prompt is unavailable."""
+
+ error_code = "DATABASE_CONTEXT_UNAVAILABLE"
+
+
class DorisPromptsManager:
"""Apache Doris Prompts Manager"""
- def __init__(self, connection_manager: DorisConnectionManager):
+ def __init__(
+ self,
+ connection_manager: DorisConnectionManager,
+ database_context_timeout_seconds: float = 10.0,
+ ):
self.connection_manager = connection_manager
+ self.database_context_timeout_seconds =
database_context_timeout_seconds
self.templates = self._init_prompt_templates()
+ @asynccontextmanager
+ async def _connection_context(self, session_id: str = "system"):
+ manager_context = getattr(
+ self.connection_manager,
+ "get_connection_context",
+ None,
+ )
+ if manager_context:
+ async with manager_context(session_id) as connection:
+ yield connection
+ return
+
+ connection = await self.connection_manager.get_connection(session_id)
+ try:
+ yield connection
+ finally:
+ release = getattr(self.connection_manager, "release_connection",
None)
+ if release:
+ await release(session_id, connection)
+
def _init_prompt_templates(self) -> dict[str, PromptTemplate]:
"""Initialize prompt templates"""
templates = {}
@@ -344,7 +393,7 @@ Please provide complete monitoring solution and
implementation recommendations."
async def get_prompt(self, name: str, arguments: dict[str, Any]) ->
GetPromptResult:
"""Get content of specific prompt template"""
if name not in self.templates:
- raise ValueError(f"Prompt template named '{name}' not found")
+ raise UnknownPromptError(f"Prompt template named '{name}' not
found")
template = self.templates[name]
@@ -383,7 +432,7 @@ Please generate accurate and efficient SQL queries based on
the above requiremen
if arg.name in arguments:
processed[arg.name] = arguments[arg.name]
elif arg.required:
- raise ValueError(f"Missing required parameter: {arg.name}")
+ raise MissingPromptArgumentError(arg.name)
else:
# Provide default handling for optional parameters
processed[arg.name] = self._get_default_argument_text(arg.name)
@@ -411,9 +460,18 @@ Please generate accurate and efficient SQL queries based
on the above requiremen
async def _get_database_context(self) -> str:
"""Get database context information"""
try:
- connection = await self.connection_manager.get_connection("system")
+ return await asyncio.wait_for(
+ self._query_database_context(),
+ timeout=self.database_context_timeout_seconds,
+ )
+ except Exception as e:
+ raise PromptDatabaseContextError(
+ "Database context unavailable"
+ ) from e
- # Get basic database information
+ async def _query_database_context(self) -> str:
+ """Query the prompt context within the caller's bounded deadline."""
+ async with self._connection_context("system") as connection:
db_info_sql = """
SELECT
COUNT(*) as table_count,
@@ -424,10 +482,12 @@ Please generate accurate and efficient SQL queries based
on the above requiremen
"""
auth_context = get_auth_context()
- db_result = await connection.execute(db_info_sql,
auth_context=auth_context)
+ db_result = await connection.execute(
+ db_info_sql,
+ auth_context=auth_context,
+ )
db_info = db_result.data[0] if db_result.data else {}
- # Get main table list
tables_sql = """
SELECT
table_name,
@@ -440,24 +500,24 @@ Please generate accurate and efficient SQL queries based
on the above requiremen
LIMIT 10
"""
- tables_result = await connection.execute(tables_sql,
auth_context=auth_context)
+ tables_result = await connection.execute(
+ tables_sql,
+ auth_context=auth_context,
+ )
- context = f"""Current database statistics:
+ context = f"""Current database statistics:
- Total number of tables: {db_info.get("table_count", 0)}
- Total data rows: {db_info.get("total_rows", 0):,}
Main data tables:"""
- for table in tables_result.data:
- context += f"\n- {table['table_name']}"
- if table.get("table_comment"):
- context += f": {table['table_comment']}"
- context += f" ({table.get('table_rows', 0):,} rows)"
+ for table in tables_result.data:
+ context += f"\n- {table['table_name']}"
+ if table.get("table_comment"):
+ context += f": {table['table_comment']}"
+ context += f" ({table.get('table_rows', 0):,} rows)"
- return context
-
- except Exception as e:
- return f"Unable to get database context information: {str(e)}"
+ return context
def get_templates_by_category(self, category: str) -> list[PromptTemplate]:
"""Get templates by category"""
diff --git a/test/protocol/stdio_capability_server.py
b/test/protocol/stdio_capability_server.py
index 5de86bd..fecac41 100644
--- a/test/protocol/stdio_capability_server.py
+++ b/test/protocol/stdio_capability_server.py
@@ -21,7 +21,15 @@ import json
import logging
from mcp.server.stdio import stdio_server
-from mcp.types import ClientCapabilities, GetPromptResult, Prompt, Resource,
Tool
+from mcp.types import (
+ ClientCapabilities,
+ GetPromptResult,
+ Prompt,
+ PromptMessage,
+ Resource,
+ TextContent,
+ Tool,
+)
from doris_mcp_server.protocol import create_doris_mcp_server
@@ -58,6 +66,19 @@ class OneToolManager:
return "{}"
+class PromptFixtureError(Exception):
+ def __init__(
+ self,
+ message: str,
+ *,
+ error_code: str,
+ argument: str | None = None,
+ ):
+ super().__init__(message)
+ self.error_code = error_code
+ self.argument = argument
+
+
class EmptyPromptsManager:
async def list_prompts(self) -> list[Prompt]:
return []
@@ -67,7 +88,34 @@ class EmptyPromptsManager:
name: str,
arguments: dict,
) -> GetPromptResult:
- raise ValueError(f"Unknown prompt: {name}")
+ if name == "missing":
+ raise PromptFixtureError(
+ "Prompt not found",
+ error_code="UNKNOWN_PROMPT",
+ )
+ if name == "needs_argument" and "required" not in arguments:
+ raise PromptFixtureError(
+ "Missing required argument",
+ error_code="MISSING_REQUIRED_ARGUMENT",
+ argument="required",
+ )
+ if name == "database_failure":
+ raise PromptFixtureError(
+ "Database context failed",
+ error_code="DATABASE_CONTEXT_UNAVAILABLE",
+ )
+ return GetPromptResult(
+ description=name,
+ messages=[
+ PromptMessage(
+ role="user",
+ content=TextContent(
+ type="text",
+ text=f"Explain {arguments.get('sql', '')}",
+ ),
+ )
+ ],
+ )
async def main() -> None:
diff --git a/test/protocol/test_mcp_v2_protocol.py
b/test/protocol/test_mcp_v2_protocol.py
index c32045f..e8736cf 100644
--- a/test/protocol/test_mcp_v2_protocol.py
+++ b/test/protocol/test_mcp_v2_protocol.py
@@ -90,11 +90,40 @@ class StubToolsManager:
return json.dumps({"name": name, "arguments": arguments})
+class PromptFixtureError(Exception):
+ def __init__(
+ self,
+ message: str,
+ *,
+ error_code: str,
+ argument: str | None = None,
+ ):
+ super().__init__(message)
+ self.error_code = error_code
+ self.argument = argument
+
+
class StubPromptsManager:
async def list_prompts(self) -> list[Prompt]:
return [Prompt(name="explain", description="Explain a query.")]
async def get_prompt(self, name: str, arguments: dict) -> GetPromptResult:
+ if name == "missing":
+ raise PromptFixtureError(
+ "Prompt not found",
+ error_code="UNKNOWN_PROMPT",
+ )
+ if name == "needs_argument" and "required" not in arguments:
+ raise PromptFixtureError(
+ "Missing required argument",
+ error_code="MISSING_REQUIRED_ARGUMENT",
+ argument="required",
+ )
+ if name == "database_failure":
+ raise PromptFixtureError(
+ "Database context failed",
+ error_code="DATABASE_CONTEXT_UNAVAILABLE",
+ )
return GetPromptResult(
description=name,
messages=[
@@ -225,6 +254,28 @@ def modern_resource_headers(uri: str) -> dict[str, str]:
}
+def modern_prompt_request(
+ request_id: int,
+ name: str,
+ arguments: dict | None = None,
+) -> dict:
+ request = modern_request(request_id, "prompts/get")
+ request["params"].update(
+ {
+ "name": name,
+ "arguments": arguments or {},
+ }
+ )
+ return request
+
+
+def modern_prompt_headers(name: str) -> dict[str, str]:
+ return {
+ **modern_headers("prompts/get"),
+ "Mcp-Name": name,
+ }
+
+
@pytest.mark.asyncio
async def
test_http_discover_is_stateless_and_unknown_method_does_not_kill_server():
app = create_test_server().streamable_http_app(
@@ -473,6 +524,80 @@ async def
test_http_resource_not_found_is_invalid_params_and_server_recovers():
assert recovered.json()["result"]["contents"][0]["uri"] == valid_uri
[email protected]
+async def test_http_prompt_errors_are_typed_and_server_recovers():
+ app = create_test_server().streamable_http_app(
+ json_response=True,
+ stateless_http=True,
+ host="127.0.0.1",
+ transport_security=create_transport_security("127.0.0.1"),
+ )
+
+ async with (
+ app.router.lifespan_context(app),
+ httpx2.ASGITransport(app) as transport,
+ httpx2.AsyncClient(
+ transport=transport,
+ base_url="http://127.0.0.1:3000",
+ ) as client,
+ ):
+ unknown = await client.post(
+ "/mcp",
+ json=modern_prompt_request(1, "missing"),
+ headers=modern_prompt_headers("missing"),
+ )
+ assert unknown.status_code == 400
+ assert unknown.json()["error"] == {
+ "code": -32602,
+ "message": "Prompt not found",
+ "data": {
+ "name": "missing",
+ "promptErrorCode": "UNKNOWN_PROMPT",
+ },
+ }
+
+ missing_argument = await client.post(
+ "/mcp",
+ json=modern_prompt_request(2, "needs_argument"),
+ headers=modern_prompt_headers("needs_argument"),
+ )
+ assert missing_argument.status_code == 400
+ assert missing_argument.json()["error"] == {
+ "code": -32602,
+ "message": "Missing required prompt argument",
+ "data": {
+ "name": "needs_argument",
+ "promptErrorCode": "MISSING_REQUIRED_ARGUMENT",
+ "argument": "required",
+ },
+ }
+
+ database_failure = await client.post(
+ "/mcp",
+ json=modern_prompt_request(3, "database_failure"),
+ headers=modern_prompt_headers("database_failure"),
+ )
+ assert database_failure.status_code == 200
+ assert database_failure.json()["error"] == {
+ "code": -32603,
+ "message": "Database context unavailable",
+ "data": {
+ "name": "database_failure",
+ "promptErrorCode": "DATABASE_CONTEXT_UNAVAILABLE",
+ },
+ }
+
+ recovered = await client.post(
+ "/mcp",
+ json=modern_prompt_request(4, "explain", {"sql": "SELECT 1"}),
+ headers=modern_prompt_headers("explain"),
+ )
+ assert recovered.status_code == 200
+ assert recovered.json()["result"]["messages"][0]["content"]["text"] ==
(
+ "Explain SELECT 1"
+ )
+
+
@pytest.mark.asyncio
async def test_stdio_validates_capabilities_versions_and_process_survival():
server_script = Path(__file__).with_name("stdio_capability_server.py")
@@ -527,3 +652,46 @@ async def
test_stdio_validates_capabilities_versions_and_process_survival():
assert [tool.name for tool in (await legacy.list_tools()).tools] ==
["echo"]
legacy_error = await legacy.read_resource("doris://table/missing")
assert json.loads(legacy_error.contents[0].text)["error_code"] ==
"RESOURCE_NOT_FOUND"
+
+
[email protected]
+async def test_stdio_prompt_errors_are_typed_and_process_survives():
+ server_script = Path(__file__).with_name("stdio_capability_server.py")
+ server_params = StdioServerParameters(
+ command=sys.executable,
+ args=[str(server_script)],
+ )
+
+ async with Client(stdio_client(server_params)) as modern:
+ with pytest.raises(MCPError) as unknown:
+ await modern.get_prompt("missing", {})
+ assert unknown.value.code == -32602
+ assert unknown.value.data == {
+ "name": "missing",
+ "promptErrorCode": "UNKNOWN_PROMPT",
+ }
+
+ with pytest.raises(MCPError) as missing_argument:
+ await modern.get_prompt("needs_argument", {})
+ assert missing_argument.value.code == -32602
+ assert missing_argument.value.data == {
+ "name": "needs_argument",
+ "promptErrorCode": "MISSING_REQUIRED_ARGUMENT",
+ "argument": "required",
+ }
+
+ with pytest.raises(MCPError) as database_failure:
+ await modern.get_prompt("database_failure", {})
+ assert database_failure.value.code == -32603
+ assert database_failure.value.data == {
+ "name": "database_failure",
+ "promptErrorCode": "DATABASE_CONTEXT_UNAVAILABLE",
+ }
+
+ recovered = await modern.get_prompt("explain", {"sql": "SELECT 1"})
+ assert recovered.messages[0].content.text == "Explain SELECT 1"
+
+ async with Client(stdio_client(server_params), mode="legacy") as legacy:
+ with pytest.raises(MCPError) as legacy_unknown:
+ await legacy.get_prompt("missing", {})
+ assert legacy_unknown.value.code == -32602
diff --git a/test/tools/test_prompts_manager_errors.py
b/test/tools/test_prompts_manager_errors.py
new file mode 100644
index 0000000..dc2178b
--- /dev/null
+++ b/test/tools/test_prompts_manager_errors.py
@@ -0,0 +1,110 @@
+import asyncio
+from contextlib import asynccontextmanager
+
+import pytest
+
+from doris_mcp_server.tools.prompts_manager import DorisPromptsManager
+
+
+class FailingConnection:
+ async def execute(self, sql, params=None, auth_context=None):
+ raise RuntimeError("database context backend failed")
+
+
+class FailingConnectionManager:
+ def __init__(self):
+ self.acquires = 0
+ self.releases = 0
+ self.connection = FailingConnection()
+
+ async def get_connection(self, session_id):
+ self.acquires += 1
+ return self.connection
+
+ async def release_connection(self, session_id, connection):
+ assert connection is self.connection
+ self.releases += 1
+
+
+class ContextFailingConnectionManager:
+ def __init__(self):
+ self.acquires = 0
+ self.releases = 0
+ self.connection = FailingConnection()
+
+ @asynccontextmanager
+ async def get_connection_context(self, session_id):
+ self.acquires += 1
+ try:
+ yield self.connection
+ finally:
+ self.releases += 1
+
+
+class HangingConnectionManager:
+ async def get_connection(self, session_id):
+ await asyncio.Event().wait()
+
+
[email protected]
+async def test_unknown_prompt_has_stable_request_error_code():
+ manager = DorisPromptsManager(FailingConnectionManager())
+
+ with pytest.raises(ValueError) as error:
+ await manager.get_prompt("missing", {})
+
+ assert error.value.error_code == "UNKNOWN_PROMPT"
+
+
[email protected]
+async def test_missing_required_argument_names_the_argument():
+ manager = DorisPromptsManager(FailingConnectionManager())
+
+ with pytest.raises(ValueError) as error:
+ await manager.get_prompt("sales_analysis", {})
+
+ assert error.value.error_code == "MISSING_REQUIRED_ARGUMENT"
+ assert error.value.argument == "date_range"
+
+
[email protected]
[email protected](
+ "connection_manager",
+ [
+ FailingConnectionManager,
+ ContextFailingConnectionManager,
+ ],
+)
+async def test_database_context_failure_is_typed_and_releases_connection(
+ connection_manager,
+):
+ connections = connection_manager()
+ manager = DorisPromptsManager(connections)
+
+ with pytest.raises(RuntimeError) as error:
+ await manager.get_prompt(
+ "sales_analysis",
+ {"date_range": "last 30 days"},
+ )
+
+ assert error.value.error_code == "DATABASE_CONTEXT_UNAVAILABLE"
+ assert isinstance(error.value.__cause__, RuntimeError)
+ assert connections.acquires == 1
+ assert connections.releases == 1
+
+
[email protected]
+async def test_database_context_timeout_becomes_typed_failure():
+ manager = DorisPromptsManager(
+ HangingConnectionManager(),
+ database_context_timeout_seconds=0.01,
+ )
+
+ with pytest.raises(RuntimeError) as error:
+ await manager.get_prompt(
+ "sales_analysis",
+ {"date_range": "last 30 days"},
+ )
+
+ assert error.value.error_code == "DATABASE_CONTEXT_UNAVAILABLE"
+ assert isinstance(error.value.__cause__, TimeoutError)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]