"""相容性报告渲染测试(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)