File size: 4,979 Bytes
0e6887b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | """相容性报告渲染测试(compatibility-engine-upgrade 任务 6.4)。
覆盖需求 3.1(渲染收归 report_service)、3.2(北欧色板 + 图标/文字标签,色觉可达性)、
3.3(二维风险矩阵)、3.4(KPI / 局限性 / RPN)。
"""
from __future__ import annotations
from kernel.services import Services
from kernel.skill_base import ExtractedData
from services.report_service import (
NORDIC_RISK_COLORS,
ReportService,
risk_label,
)
from skills.compatibility.skill import SKILL
class _StubLLM:
def complete(self, system, user, *, temperature=0.3, **kw):
class _R:
ok = True
content = "机理分析:伯胺与还原糖经美拉德反应缩合。"
return _R()
def _assemble(smiles="NCCO", excipients="乳糖\n硬脂酸镁", lang="zh"):
from kernel.skill_base import RawInput
svc = Services(llm=_StubLLM(), lang=lang)
raw = RawInput(smiles=smiles, excipient=excipients)
data = SKILL.extract(raw, svc)
result = SKILL.compute(data)
sections = SKILL.explain(result, svc)
html = ReportService().assemble(SKILL.meta, result, sections, lang=lang)
return result, sections, html
def test_matrix_and_kpi_rendered_by_report_service_not_explain():
"""矩阵/结构由 report_service 渲染;explain 不产出其 HTML(需求 3.1)。"""
result, sections, html = _assemble()
# explain 仅机理叙述。
assert "risk_matrix" not in sections.sections
assert "structure" not in sections.sections
# report_service 渲染矩阵与 KPI。
assert "compat-matrix" in html
assert "kpi-row" in html
def test_risk_presentation_has_icon_and_text_not_color_only():
"""风险呈现同时含图标与文字标签,不单独依赖颜色(需求 3.2.3 色觉可达性)。"""
_, _, html = _assemble()
# 文字标签存在。
assert "高风险" in html
# 图标(任一风险图标)存在。
assert ("⛔" in html) or ("⚠" in html) or ("✓" in html)
def test_nordic_palette_used_not_legacy_red():
"""风险矩阵单元格使用北欧低饱和色板(赭红 #c25b4e)渲染。"""
_, _, html = _assemble()
assert NORDIC_RISK_COLORS["high"] in html # 赭红
# 矩阵单元格内联样式使用北欧色板。
assert f'background:{NORDIC_RISK_COLORS["high"]}' in html.replace(" ", "")
def test_limitations_section_present():
"""报告含独立"局限性与验证需求"小节(需求 3.4.2)。"""
_, _, html = _assemble()
assert "局限性与验证需求" in html
def test_overall_risk_localized_not_raw_enum():
"""整体风险显示本地化标签,不残留英文枚举(需求 0.1)。"""
_, _, html = _assemble()
assert "高风险" in html
# 元数据条不应出现裸枚举 "high" 作为风险值展示。
assert ">high<" not in html
def test_uncertain_when_no_functional_groups():
"""无官能团(无 SMILES)→ 整体不确定,报告显示"不确定"(需求 2.5.1 / 0.1)。"""
result, _, html = _assemble(smiles="", excipients="乳糖")
assert result.summary["overall_risk"] == "uncertain"
assert "不确定" in html
def test_english_report_localizes_risk_labels():
"""英文报告风险标签为英文(需求 0.1.2 / NFR-5)。"""
_, _, html = _assemble(lang="en")
assert "High risk" in html
assert "高风险" not in html
def test_risk_label_helper_handles_missing_enum():
"""risk_label 对缺失/非法枚举回退为"不确定"(需求 0.1.3)。"""
assert risk_label(None, "zh") == "不确定"
assert risk_label("bogus", "en") == "Uncertain"
assert risk_label("high", "zh") == "高风险"
def test_mitigation_strategies_present_deterministically_without_llm():
"""缓解策略按反应机理组织、确定性呈现,不依赖 LLM(需求 3.4.4)。"""
result, _, html = _assemble(smiles="NCCO", excipients="乳糖")
# compute 确定性产出缓解策略。
mits = result.summary.get("mitigations")
assert mits, "应有按机理组织的缓解策略"
assert any(m["reaction"] == "maillard" for m in mits)
# report_service 渲染独立小节(无 LLM 时仍在,_assemble 用 StubLLM 但策略来自 compute)。
assert "可执行缓解策略" in html
def test_mitigation_strategies_are_qualitative_no_numbers():
"""缓解策略为定性文本,不含未引用的定量数值(HP-1)。"""
from skills.compatibility import matrix
excipients = [{"name": "乳糖", "display_zh": "乳糖",
"reactions": [{"reaction": "maillard", "risk": "high"}]}]
mits = matrix.mitigation_strategies(excipients)
import re
for m in mits:
for s in m["strategies_zh"] + m["strategies_en"]:
# 不应出现 kJ/mol、pKa= 数字、k= 数字等定量模式。
assert not re.search(r"\d+\s*(?:kJ|kcal)\s*/\s*mol", s)
assert not re.search(r"pKa\s*=\s*\d", s)
|