"""Tests for friendly_error's handling of AI upstream error messages. These cover the DifyChatService error_code → user-facing copy mapping that the doctor workstation must apply when the server-side AI assistant returns ``ok=false`` with a Chinese ``error`` string. The legacy behaviour simply echoed the raw text, which made incidents like ``UPSTREAM_REJECTED`` opaque to clinicians. """ from __future__ import annotations from types import SimpleNamespace import pytest from doctor_workstation.core.errors import ( ApiBusinessError, ApiHttpError, ApiProtocolError, ApiTimeoutError, ApiTransportError, AuthenticationExpiredError, ) from doctor_workstation.ui.widgets import _ai_upstream_hint, friendly_error @pytest.mark.parametrize( ("raw", "expected_fragment"), [ # 当上游 Dify 服务 HTTP 4xx 时返回的"UPSTREAM_REJECTED"文案。 ("模型未能处理本次请求", "稍后重试"), ("AI 服务凭据无效或无权限", "联系管理员"), ("AI 服务配置无效", "联系管理员"), ("AI 报告功能未启用", "联系管理员"), ("不支持的 AI 模型", "联系管理员"), ("该模型服务尚未完整配置", "联系管理员"), ("病例数据编码失败", "联系管理员"), ("无法初始化 AI 请求", "联系管理员"), ("暂时无法连接 AI 服务,请稍后重试", "网络"), ("模型服务繁忙,请稍后重试", "稍后重试"), ("模型响应超时,请稍后重试", "稍后重试"), ("AI 助手未返回内容,请重试", "稍后重试"), ], ) def test_friendly_error_translates_ai_upstream_strings(raw: str, expected_fragment: str) -> None: """Upstream Dify messages should be replaced with actionable copy.""" rendered = friendly_error(raw) assert expected_fragment in rendered # 上游原文不应该再原样透传。 assert rendered != raw def test_friendly_error_passes_through_unrelated_chinese_text() -> None: """中文业务文案不属于上游错误时,必须原样透传,避免误伤。""" text = "AI 返回的处方草稿格式不符合要求,请重试" assert friendly_error(text) == text def test_friendly_error_handles_api_business_error_with_upstream_payload() -> None: """服务端通过 ApiBusinessError(code=0) 透传时仍要触发映射。""" err = ApiBusinessError("模型未能处理本次请求", code=0) rendered = friendly_error(err) assert "稍后重试" in rendered assert "联系管理员" in rendered def test_friendly_error_keeps_existing_transport_mappings() -> None: """对网络/超时/未授权等既有规则的回归保护。""" assert "证书" in friendly_error(Exception("certificate_verify_failed")) assert "网络" in friendly_error(ApiTransportError("connection refused")) assert "重新登录" in friendly_error(AuthenticationExpiredError("expired")) timeout_render = friendly_error(ApiTimeoutError("timed out")) assert "超时" in timeout_render http_render = friendly_error(ApiHttpError("boom", status_code=503)) assert "503" in http_render protocol_render = friendly_error(ApiProtocolError("api response envelope invalid")) assert "数据格式" in protocol_render def test_friendly_error_returns_default_for_empty_string() -> None: """Fallback 应当落到"操作未完成"而不是崩溃。""" assert friendly_error(SimpleNamespace(__str__=lambda self: " ")) == "操作未完成,请稍后重试。" def test_ai_upstream_hint_returns_none_for_unrelated_text() -> None: assert _ai_upstream_hint("AI 返回的处方草稿格式不符合要求,请重试") is None assert _ai_upstream_hint("connection refused") is None assert _ai_upstream_hint("") is None