| """相容性报告渲染测试(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() |
| |
| assert "risk_matrix" not in sections.sections |
| assert "structure" not in sections.sections |
| |
| 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 |
| |
| 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="乳糖") |
| |
| mits = result.summary.get("mitigations") |
| assert mits, "应有按机理组织的缓解策略" |
| assert any(m["reaction"] == "maillard" for m in mits) |
| |
| 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"]: |
| |
| assert not re.search(r"\d+\s*(?:kJ|kcal)\s*/\s*mol", s) |
| assert not re.search(r"pKa\s*=\s*\d", s) |
|
|