feat: 2026-07-11 全量更新 - 代办集成+会议室预定+知识迭代修复+UI统一+Bug修复

== 已部署上线 (9项) ==
- 代办事项真实数据源集成 (企微审批API 8bug修复链)
- H5/坐席端 Logo样式统一+绿色背景
- 视频引导页修复 (localStorage key v2)
- 坐席端 v9 Vue版本修复 (ElMessage._context)
- 截图按钮 v10 修复 (getDisplayMedia user gesture)
- 扫码样式恢复+H5扫码登录跳转修复
- H5截图快捷键提示

== 代码完成待部署 (3项) ==
- 知识迭代3Bug修复 (#8 POST端点/#7 MERGE幂等/#6 过期检查)
- 会议室预定-小鱼易联终端 (40文件, 40/40测试通过)
- IT资产升级审批推送 (asset_service.py)

== 需求文档 (2项) ==
- 坐席端AI辅助消息框-PRD (4项新功能确认)
- 坐席端布局优化建议 v2.0 (7天计划)

== 新增文档 ==
- 日报-2026-07-11.md
- 知识迭代Bug修复报告-20260711.md
- 会议室预定-部署指南.md
- CHANGELOG.md 更新

== 测试 ==
- test_todo_integration.py: 40/40
- test_meetingroom.py: 40/40
- test_bugfix_ki_suggestions.py: 21/21
This commit is contained in:
Simon
2026-07-11 23:13:10 +08:00
parent 3d152fc8eb
commit bea288e414
928 changed files with 85169 additions and 54205 deletions
@@ -0,0 +1,725 @@
# =============================================================================
# IT智能服务台 — 审批意图检测(/approval/detect-intent)测试
# =============================================================================
# 测试覆盖:
# 1. 关键词预过滤逻辑(_keyword_prefilter 单元测试)
# 2. 降级兜底逻辑(_fallback_detect 单元测试)
# 3. 端点集成测试:
# a. 关键词未命中 → 直接返回 false(不调 Dify)
# b. 关键词命中 + Dify 高置信度 → is_approval_request=true
# c. 关键词命中 + Dify 低置信度 → is_approval_request=false
# d. Dify 调用失败 → 降级兜底
# e. Dify 未配置 → 降级兜底
# f. Dify 返回 is_approval=false → is_approval_request=false
# 4. /approval/keywords 端点测试
# 5. /approval/jump 端点测试
#
# Round 2 变更:所有端点已改用 success_response() 包装,
# 响应格式为 {code: 0, data: {...}, message: "success"}
# 测试断言已适配新格式:data["data"]["field"] 而非 data["field"]
# =============================================================================
import pytest
from unittest.mock import AsyncMock, patch
from app.api.approval import (
_keyword_prefilter,
_fallback_detect,
KEYWORD_TO_APPROVAL_TYPE,
APPROVAL_PREFILTER_KEYWORDS,
)
from app.config import settings
# =============================================================================
# 单元测试:_keyword_prefilter
# =============================================================================
class TestKeywordPrefilter:
"""测试关键词预过滤函数。"""
def test_keyword_hit_device(self):
"""包含设备相关关键词时返回 True。"""
assert _keyword_prefilter("我需要申请一台电脑") is True
def test_keyword_hit_vpn(self):
"""包含 VPN 关键词时返回 True(大小写不敏感)。"""
assert _keyword_prefilter("VPN 连不上了") is True
assert _keyword_prefilter("vpn 连不上了") is True
def test_keyword_hit_software(self):
"""包含软件相关关键词时返回 True。"""
assert _keyword_prefilter("想申请一个软件") is True
def test_keyword_hit_generic(self):
"""包含通用关键词'申请'时返回 True。"""
assert _keyword_prefilter("我想申请一个东西") is True
def test_keyword_miss_normal_message(self):
"""普通对话消息不包含审批关键词时返回 False。"""
assert _keyword_prefilter("你好,今天天气怎么样") is False
assert _keyword_prefilter("谢谢,问题解决了") is False
def test_keyword_miss_empty_string(self):
"""空字符串返回 False。"""
assert _keyword_prefilter("") is False
def test_keyword_miss_none(self):
"""None 返回 False。"""
assert _keyword_prefilter(None) is False # type: ignore[arg-type]
def test_keyword_case_insensitive(self):
"""关键词匹配大小写不敏感。"""
assert _keyword_prefilter("我的VPN坏了") is True
assert _keyword_prefilter("我的vpn坏了") is True
assert _keyword_prefilter("我的Vpn坏了") is True
def test_keyword_partial_match(self):
"""关键词是子串匹配('设备''设备申请'中也能命中)。"""
assert _keyword_prefilter("设备申请流程是什么") is True
def test_keyword_all_prefilter_keywords_work(self):
"""验证 APPROVAL_PREFILTER_KEYWORDS 中的每个关键词都能被命中。"""
for kw in APPROVAL_PREFILTER_KEYWORDS:
# 构造包含该关键词的文本
text = f"测试文本包含{kw}关键词"
assert _keyword_prefilter(text) is True, f"关键词 '{kw}' 未被命中"
# =============================================================================
# 单元测试:_fallback_detect
# =============================================================================
class TestFallbackDetect:
"""测试降级兜底检测函数。"""
def test_fallback_returns_true_for_keyword_text(self):
"""包含关键词的文本,兜底返回 is_approval_request=True。"""
is_approval, confidence, approval_type = _fallback_detect("我需要申请电脑")
assert is_approval is True
assert confidence == 0.6
def test_fallback_returns_correct_type_device(self):
"""设备相关关键词映射到'设备申请'"""
_, _, approval_type = _fallback_detect("我要申请电脑")
assert approval_type == "设备申请"
def test_fallback_returns_correct_type_vpn(self):
"""VPN 关键词映射到'账号权限申请'"""
_, _, approval_type = _fallback_detect("VPN连不上了")
assert approval_type == "账号权限申请"
def test_fallback_returns_correct_type_software(self):
"""软件关键词映射到'软件服务申请'"""
_, _, approval_type = _fallback_detect("想安装一个软件")
assert approval_type == "软件服务申请"
def test_fallback_returns_correct_type_disposal(self):
"""资产处置关键词映射到'资产处置申请'
注意:测试文本不能同时包含'电脑'(→设备申请)等排在'报废'前面的关键词,
否则 _fallback_detect 会因字典遍历顺序返回先匹配的类型。
"""
_, _, approval_type = _fallback_detect("这台机器要报废")
assert approval_type == "资产处置申请"
def test_fallback_returns_correct_type_office_supplies(self):
"""办公用品关键词映射到'办公用品申请'"""
_, _, approval_type = _fallback_detect("办公用品超额了")
assert approval_type == "办公用品申请"
def test_fallback_confidence_is_0_6(self):
"""兜底置信度固定为 0.6(低于阈值 0.7)。"""
_, confidence, _ = _fallback_detect("申请设备")
assert confidence == 0.6
def test_fallback_empty_text_returns_true_no_type(self):
"""空文本兜底仍返回 True(预过滤已通过),但 approval_type 为 None。"""
is_approval, confidence, approval_type = _fallback_detect("")
assert is_approval is True
assert confidence == 0.6
assert approval_type is None
def test_fallback_generic_keyword_no_type_mapping(self):
"""'申请'在预过滤列表但不在 KEYWORD_TO_APPROVAL_TYPE 中,兜底 approval_type 为 None。"""
# "申请" 在 APPROVAL_PREFILTER_KEYWORDS 中,但不在 KEYWORD_TO_APPROVAL_TYPE 中
is_approval, _, approval_type = _fallback_detect("申请")
assert is_approval is True
assert approval_type is None
def test_fallback_first_match_wins(self):
"""兜底遍历 KEYWORD_TO_APPROVAL_TYPE,命中第一个关键词即返回(字典有序)。"""
# 包含多个关键词的文本,应返回第一个匹配的类型
_, _, approval_type = _fallback_detect("设备和VPN都坏了")
# KEYWORD_TO_APPROVAL_TYPE 中"设备"排在"VPN"前面
assert approval_type is not None
assert approval_type in KEYWORD_TO_APPROVAL_TYPE.values()
# =============================================================================
# 集成测试:/approval/detect-intent 端点
# =============================================================================
# Round 2: 响应格式已改为 {code: 0, data: {...}, message: "success"}
# 所有断言通过 data["data"]["field"] 访问业务字段
# =============================================================================
class TestDetectApprovalIntentEndpoint:
"""测试 /approval/detect-intent API 端点。"""
@pytest.mark.asyncio
async def test_keyword_not_hit_returns_false_no_dify(self, client):
"""关键词未命中 → 直接返回 false,不调 Difysource=keyword_prefilter。"""
response = await client.post(
"/approval/detect-intent",
json={"text": "你好,今天天气怎么样"},
)
assert response.status_code == 200
data = response.json()
# 统一响应格式验证
assert data["code"] == 0
assert data["message"] == "success"
# 业务字段验证
inner = data["data"]
assert inner["is_approval_request"] is False
assert inner["confidence"] == 0.0
assert inner["approval_type"] is None
assert inner["source"] == "keyword_prefilter"
@pytest.mark.asyncio
async def test_keyword_not_hit_empty_text(self, client):
"""空文本 → 关键词未命中,返回 false。"""
response = await client.post(
"/approval/detect-intent",
json={"text": ""},
)
assert response.status_code == 200
data = response.json()
assert data["code"] == 0
inner = data["data"]
assert inner["is_approval_request"] is False
assert inner["source"] == "keyword_prefilter"
@pytest.mark.asyncio
async def test_dify_high_confidence_returns_true(self, client, monkeypatch):
"""关键词命中 + Dify 返回高置信度(≥0.7) → is_approval_request=true, source=dify。"""
# 设置 Dify 配置(模拟已配置)
monkeypatch.setattr(settings, "approval_dify_base_url", "http://test-dify")
monkeypatch.setattr(settings, "approval_dify_api_key", "test-api-key")
monkeypatch.setattr(settings, "approval_confidence_threshold", 0.7)
# Mock Dify 返回高置信度
mock_dify = AsyncMock(return_value={
"is_approval_request": True,
"confidence": 0.95,
"approval_type": "设备申请",
})
with patch("app.api.approval._call_dify_approval_intent", mock_dify):
response = await client.post(
"/approval/detect-intent",
json={"text": "我需要申请一台笔记本电脑"},
)
assert response.status_code == 200
data = response.json()
assert data["code"] == 0
assert data["message"] == "success"
inner = data["data"]
assert inner["is_approval_request"] is True
assert inner["confidence"] == 0.95
assert inner["approval_type"] == "设备申请"
assert inner["source"] == "dify"
# 验证 Dify 被调用了一次
mock_dify.assert_called_once()
@pytest.mark.asyncio
async def test_dify_confidence_at_threshold_returns_true(self, client, monkeypatch):
"""置信度恰好等于阈值(0.7) → is_approval_request=true>= 判断)。"""
monkeypatch.setattr(settings, "approval_dify_base_url", "http://test-dify")
monkeypatch.setattr(settings, "approval_dify_api_key", "test-api-key")
monkeypatch.setattr(settings, "approval_confidence_threshold", 0.7)
mock_dify = AsyncMock(return_value={
"is_approval_request": True,
"confidence": 0.7,
"approval_type": "账号权限申请",
})
with patch("app.api.approval._call_dify_approval_intent", mock_dify):
response = await client.post(
"/approval/detect-intent",
json={"text": "VPN 账号申请"},
)
data = response.json()
assert data["code"] == 0
inner = data["data"]
assert inner["is_approval_request"] is True
assert inner["confidence"] == 0.7
assert inner["source"] == "dify"
@pytest.mark.asyncio
async def test_dify_low_confidence_returns_false(self, client, monkeypatch):
"""关键词命中 + Dify 返回低置信度(<0.7) → is_approval_request=false, source=dify。"""
monkeypatch.setattr(settings, "approval_dify_base_url", "http://test-dify")
monkeypatch.setattr(settings, "approval_dify_api_key", "test-api-key")
monkeypatch.setattr(settings, "approval_confidence_threshold", 0.7)
mock_dify = AsyncMock(return_value={
"is_approval_request": True,
"confidence": 0.5,
"approval_type": "设备申请",
})
with patch("app.api.approval._call_dify_approval_intent", mock_dify):
response = await client.post(
"/approval/detect-intent",
json={"text": "我想申请一台电脑"},
)
assert response.status_code == 200
data = response.json()
assert data["code"] == 0
inner = data["data"]
assert inner["is_approval_request"] is False
assert inner["confidence"] == 0.5
assert inner["approval_type"] == "设备申请"
assert inner["source"] == "dify"
@pytest.mark.asyncio
async def test_dify_returns_is_approval_false(self, client, monkeypatch):
"""Dify 明确返回 is_approval_request=false → 端点返回 false。"""
monkeypatch.setattr(settings, "approval_dify_base_url", "http://test-dify")
monkeypatch.setattr(settings, "approval_dify_api_key", "test-api-key")
mock_dify = AsyncMock(return_value={
"is_approval_request": False,
"confidence": 0.3,
"approval_type": None,
})
with patch("app.api.approval._call_dify_approval_intent", mock_dify):
response = await client.post(
"/approval/detect-intent",
json={"text": "我的电脑卡了"},
)
data = response.json()
assert data["code"] == 0
inner = data["data"]
assert inner["is_approval_request"] is False
assert inner["confidence"] == 0.3
assert inner["approval_type"] is None
assert inner["source"] == "dify"
@pytest.mark.asyncio
async def test_dify_failure_fallback(self, client, monkeypatch):
"""关键词命中 + Dify 调用失败 → 降级兜底, source=fallback。"""
monkeypatch.setattr(settings, "approval_dify_base_url", "http://test-dify")
monkeypatch.setattr(settings, "approval_dify_api_key", "test-api-key")
monkeypatch.setattr(settings, "approval_confidence_threshold", 0.7)
# Mock Dify 抛出异常(模拟网络错误/超时)
mock_dify = AsyncMock(side_effect=Exception("Dify 服务不可达"))
with patch("app.api.approval._call_dify_approval_intent", mock_dify):
response = await client.post(
"/approval/detect-intent",
json={"text": "我需要申请一台电脑"},
)
assert response.status_code == 200
data = response.json()
assert data["code"] == 0
inner = data["data"]
# 兜底返回 is_approval_request=True, confidence=0.6
assert inner["is_approval_request"] is True
assert inner["confidence"] == 0.6
assert inner["approval_type"] == "设备申请"
assert inner["source"] == "fallback"
@pytest.mark.asyncio
async def test_dify_not_configured_fallback(self, client, monkeypatch):
"""Dify 未配置(base_url/api_key 为空)→ 降级兜底。"""
# 确保配置为空
monkeypatch.setattr(settings, "approval_dify_base_url", "")
monkeypatch.setattr(settings, "approval_dify_api_key", "")
response = await client.post(
"/approval/detect-intent",
json={"text": "VPN 账号申请"},
)
assert response.status_code == 200
data = response.json()
assert data["code"] == 0
inner = data["data"]
assert inner["is_approval_request"] is True
assert inner["confidence"] == 0.6
assert inner["approval_type"] == "账号权限申请"
assert inner["source"] == "fallback"
@pytest.mark.asyncio
async def test_fallback_with_generic_keyword_no_type(self, client, monkeypatch):
"""兜底场景:'申请'在预过滤列表但无类型映射 → approval_type=None。"""
monkeypatch.setattr(settings, "approval_dify_base_url", "")
monkeypatch.setattr(settings, "approval_dify_api_key", "")
response = await client.post(
"/approval/detect-intent",
json={"text": "我要申请一个东西"},
)
data = response.json()
assert data["code"] == 0
inner = data["data"]
assert inner["is_approval_request"] is True
assert inner["confidence"] == 0.6
assert inner["approval_type"] is None
assert inner["source"] == "fallback"
@pytest.mark.asyncio
async def test_custom_threshold_override(self, client, monkeypatch):
"""自定义阈值:阈值设为 0.5Dify 返回 0.6 → is_approval_request=true。"""
monkeypatch.setattr(settings, "approval_dify_base_url", "http://test-dify")
monkeypatch.setattr(settings, "approval_dify_api_key", "test-api-key")
monkeypatch.setattr(settings, "approval_confidence_threshold", 0.5)
mock_dify = AsyncMock(return_value={
"is_approval_request": True,
"confidence": 0.6,
"approval_type": "设备申请",
})
with patch("app.api.approval._call_dify_approval_intent", mock_dify):
response = await client.post(
"/approval/detect-intent",
json={"text": "申请设备"},
)
data = response.json()
assert data["code"] == 0
inner = data["data"]
# 阈值 0.5,置信度 0.6 >= 0.5 → True
assert inner["is_approval_request"] is True
assert inner["source"] == "dify"
@pytest.mark.asyncio
async def test_employee_id_passed_to_dify(self, client, monkeypatch):
"""验证 employee_id 被传递给 Dify 调用。"""
monkeypatch.setattr(settings, "approval_dify_base_url", "http://test-dify")
monkeypatch.setattr(settings, "approval_dify_api_key", "test-api-key")
mock_dify = AsyncMock(return_value={
"is_approval_request": True,
"confidence": 0.9,
"approval_type": "设备申请",
})
with patch("app.api.approval._call_dify_approval_intent", mock_dify):
await client.post(
"/approval/detect-intent",
json={"text": "申请电脑", "employee_id": "test_emp_001"},
)
# 验证 employee_id 被传给 Dify
mock_dify.assert_called_once_with("申请电脑", "test_emp_001")
@pytest.mark.asyncio
async def test_employee_id_defaults_to_empty_string(self, client, monkeypatch):
"""未提供 employee_id 时,传给 Dify 的是空字符串。"""
monkeypatch.setattr(settings, "approval_dify_base_url", "http://test-dify")
monkeypatch.setattr(settings, "approval_dify_api_key", "test-api-key")
mock_dify = AsyncMock(return_value={
"is_approval_request": True,
"confidence": 0.9,
"approval_type": "设备申请",
})
with patch("app.api.approval._call_dify_approval_intent", mock_dify):
await client.post(
"/approval/detect-intent",
json={"text": "申请电脑"},
)
# 验证 employee_id 默认为空字符串
mock_dify.assert_called_once_with("申请电脑", "")
@pytest.mark.asyncio
async def test_response_has_all_required_fields(self, client):
"""验证响应包含 ApprovalDetectIntentResponse 的所有必需字段。"""
response = await client.post(
"/approval/detect-intent",
json={"text": "你好"},
)
data = response.json()
# 统一响应格式验证
assert data["code"] == 0
assert data["message"] == "success"
# 业务字段验证
inner = data["data"]
# ApprovalDetectIntentResponse 必需字段
assert "is_approval_request" in inner
assert "confidence" in inner
assert "source" in inner
# approval_type 是 Optional,可以为 None 但字段必须存在
assert "approval_type" in inner
@pytest.mark.asyncio
async def test_response_field_types(self, client):
"""验证响应字段类型正确。"""
response = await client.post(
"/approval/detect-intent",
json={"text": "你好"},
)
data = response.json()
assert data["code"] == 0
inner = data["data"]
assert isinstance(inner["is_approval_request"], bool)
assert isinstance(inner["confidence"], (int, float))
assert isinstance(inner["source"], str)
@pytest.mark.asyncio
async def test_dify_not_called_when_keyword_misses(self, client, monkeypatch):
"""关键词未命中时,Dify 不被调用。"""
monkeypatch.setattr(settings, "approval_dify_base_url", "http://test-dify")
monkeypatch.setattr(settings, "approval_dify_api_key", "test-api-key")
mock_dify = AsyncMock(return_value={
"is_approval_request": True,
"confidence": 0.9,
"approval_type": "设备申请",
})
with patch("app.api.approval._call_dify_approval_intent", mock_dify):
response = await client.post(
"/approval/detect-intent",
json={"text": "今天天气真好"},
)
# Dify 不应被调用
mock_dify.assert_not_called()
data = response.json()
assert data["code"] == 0
assert data["data"]["source"] == "keyword_prefilter"
# =============================================================================
# 集成测试:/approval/keywords 端点
# =============================================================================
# Round 2 新增:验证 /approval/keywords 端点使用 success_response() 包装
# =============================================================================
class TestApprovalKeywordsEndpoint:
"""测试 /approval/keywords API 端点。"""
@pytest.mark.asyncio
async def test_keywords_response_format(self, client):
"""验证 /approval/keywords 响应使用统一格式 {code, data, message}。"""
response = await client.get("/approval/keywords")
assert response.status_code == 200
data = response.json()
# 统一响应格式验证
assert data["code"] == 0
assert data["message"] == "success"
# data 应为列表
assert isinstance(data["data"], list)
@pytest.mark.asyncio
async def test_keywords_empty_when_no_templates(self, client):
"""无审批模板配置时,返回空列表。"""
# 测试环境未设置 APPROVAL_TEMPLATE_RESOURCE / APPROVAL_TEMPLATE_DEVICE
# APPROVAL_TEMPLATES 为空字典
response = await client.get("/approval/keywords")
assert response.status_code == 200
data = response.json()
assert data["code"] == 0
assert data["data"] == []
@pytest.mark.asyncio
async def test_keywords_with_mocked_templates(self, client):
"""模拟有审批模板时,返回关键词列表。"""
mock_templates = {
"tpl_resource_001": {
"id": "tpl_resource_001",
"name": "资源申请",
"type": "jump",
"keywords": ["申请资源", "要资源"],
},
"tpl_device_001": {
"id": "tpl_device_001",
"name": "设备申请",
"type": "api",
"keywords": ["申请设备", "要设备"],
},
}
with patch("app.api.approval.APPROVAL_TEMPLATES", mock_templates):
response = await client.get("/approval/keywords")
assert response.status_code == 200
data = response.json()
assert data["code"] == 0
assert data["message"] == "success"
keywords = data["data"]
assert len(keywords) == 4 # 2 模板 × 2 关键词
# 验证每个关键词条目的结构
for item in keywords:
assert "keyword" in item
assert "template_id" in item
assert "template_name" in item
assert "type" in item
# 验证包含预期的关键词
keyword_values = [item["keyword"] for item in keywords]
assert "申请资源" in keyword_values
assert "申请设备" in keyword_values
@pytest.mark.asyncio
async def test_keywords_data_is_list_type(self, client):
"""验证 data 字段始终为列表类型(即使为空)。"""
response = await client.get("/approval/keywords")
data = response.json()
assert isinstance(data["data"], list)
# =============================================================================
# 集成测试:/approval/jump 端点
# =============================================================================
# Round 2 新增:验证 /approval/jump 端点使用 success_response() 包装
# =============================================================================
class TestApprovalJumpEndpoint:
"""测试 /approval/jump API 端点。"""
@pytest.mark.asyncio
async def test_jump_template_not_found(self, client):
"""模板不存在时返回 404。"""
response = await client.post(
"/approval/jump",
json={"template_id": "non_existent_template"},
)
assert response.status_code == 404
assert "模板不存在" in response.json()["detail"]
@pytest.mark.asyncio
async def test_jump_type_mismatch(self, client):
"""模板类型不支持跳转(type != 'jump')时返回 400。"""
mock_templates = {
"tpl_api_001": {
"id": "tpl_api_001",
"name": "API审批",
"type": "api", # 非 jump 类型
"keywords": ["测试"],
},
}
with patch("app.api.approval.APPROVAL_TEMPLATES", mock_templates):
response = await client.post(
"/approval/jump",
json={"template_id": "tpl_api_001"},
)
assert response.status_code == 400
assert "不支持跳转方式" in response.json()["detail"]
@pytest.mark.asyncio
async def test_jump_success_response_format(self, client):
"""跳转成功时返回统一格式 {code: 0, data: {url, template_name}, message}。"""
mock_templates = {
"tpl_jump_001": {
"id": "tpl_jump_001",
"name": "资源申请",
"type": "jump",
"keywords": ["申请资源"],
},
}
with patch("app.api.approval.APPROVAL_TEMPLATES", mock_templates):
response = await client.post(
"/approval/jump",
json={"template_id": "tpl_jump_001"},
)
assert response.status_code == 200
data = response.json()
# 统一响应格式验证
assert data["code"] == 0
assert data["message"] == "success"
# 业务字段验证
inner = data["data"]
assert "url" in inner
assert "template_name" in inner
assert inner["template_name"] == "资源申请"
# URL 应包含 template_id
assert "tpl_jump_001" in inner["url"]
@pytest.mark.asyncio
async def test_jump_url_contains_template_id(self, client):
"""跳转 URL 中包含 template_id。"""
mock_templates = {
"tpl_jump_002": {
"id": "tpl_jump_002",
"name": "设备申请",
"type": "jump",
"keywords": ["申请设备"],
},
}
with patch("app.api.approval.APPROVAL_TEMPLATES", mock_templates):
response = await client.post(
"/approval/jump",
json={"template_id": "tpl_jump_002"},
)
data = response.json()
inner = data["data"]
assert "tpl_jump_002" in inner["url"]
@pytest.mark.asyncio
async def test_jump_with_employee_id(self, client):
"""携带 employee_id 时跳转正常返回(employee_id 可选字段)。"""
mock_templates = {
"tpl_jump_003": {
"id": "tpl_jump_003",
"name": "资源申请",
"type": "jump",
"keywords": ["申请资源"],
},
}
with patch("app.api.approval.APPROVAL_TEMPLATES", mock_templates):
response = await client.post(
"/approval/jump",
json={
"template_id": "tpl_jump_003",
"employee_id": "test_emp_001",
},
)
assert response.status_code == 200
data = response.json()
assert data["code"] == 0
assert data["data"]["template_name"] == "资源申请"
@pytest.mark.asyncio
async def test_jump_missing_template_id(self, client):
"""缺少 template_id 时返回 422Pydantic 校验失败)。"""
response = await client.post(
"/approval/jump",
json={},
)
assert response.status_code == 422