"""相容性确定性回归基准(compatibility-engine-upgrade 任务 8.1)。 金标准用例覆盖经典相互作用 + 纠偏 + 别名,断言风险**档位**与命中**反应集合**。 确定性、离线运行(直接喂官能团 id,不依赖网络/LLM/RDKit),守护规则/SMARTS 改动 不致非预期回归(需求 1.4 / 2.2 / 2.3)。 """ from __future__ import annotations import pytest from skills.compatibility import rules def _assess(group_ids, excipients): return rules.evaluate([{"id": g} for g in group_ids], excipients) def _excipient_reactions(assessment, idx=0): er = assessment.excipient_results[idx] return er.risk, {r.reaction for r in er.reactions} # --------------------------------------------------------------------------- # 经典相互作用金标准 # --------------------------------------------------------------------------- @pytest.mark.parametrize( "group_ids,excipient,expected_overall,expected_reactions", [ # 美拉德:伯胺 + 还原糖(乳糖)→ 高风险 (["primary_amine"], "乳糖", rules.RISK_HIGH, {"maillard"}), # 碱催化水解:酯 + 偏碱(硬脂酸镁)→ 高风险 (["ester"], "硬脂酸镁", rules.RISK_HIGH, {"hydrolysis"}), # 内酯 + 碱 → 高风险水解 (["lactone"], "硬脂酸镁", rules.RISK_HIGH, {"hydrolysis"}), # 氧化:酚 + 过氧化物(聚维酮)→ 中风险 (["phenol"], "聚维酮", rules.RISK_MEDIUM, {"oxidation"}), # 氧化:巯基 + 过氧化物 → 高风险 (["thiol"], "聚维酮", rules.RISK_HIGH, {"oxidation"}), # 酸碱:羧基 + 偏碱 → 中风险 (["carboxylic_acid"], "硬脂酸镁", rules.RISK_MEDIUM, {"acid_base"}), # 光降解:酚 + 二氧化钛(光催化剂 + 金属)→ 中风险(氧化 + 光降解) (["phenol"], "二氧化钛", rules.RISK_MEDIUM, {"oxidation", "photodegradation"}), # Michael:Michael 受体 + 亚硫酸盐 → 中风险 (["michael_acceptor"], "亚硫酸氢钠", rules.RISK_MEDIUM, {"michael"}), ], ) def test_classic_interactions(group_ids, excipient, expected_overall, expected_reactions): a = _assess(group_ids, [excipient]) assert a.overall_risk == expected_overall, (group_ids, excipient) risk, reactions = _excipient_reactions(a) assert expected_reactions.issubset(reactions), (reactions, expected_reactions) # --------------------------------------------------------------------------- # 纠偏:淀粉 / MCC 假阳性(需求 2.2) # --------------------------------------------------------------------------- def test_starch_amine_not_high_maillard(): """淀粉 + 伯胺 → 中风险(经还原糖杂质途径),而非默认高风险。""" a = _assess(["primary_amine"], ["淀粉"]) risk, reactions = _excipient_reactions(a) assert "maillard" in reactions assert risk == rules.RISK_MEDIUM assert a.overall_risk == rules.RISK_MEDIUM def test_mcc_amine_no_maillard(): """微晶纤维素 + 伯胺 → 不触发美拉德(仅可能吸附低风险)。""" a = _assess(["primary_amine"], ["微晶纤维素"]) risk, reactions = _excipient_reactions(a) assert "maillard" not in reactions def test_lactose_still_high(): """乳糖(真还原糖)+ 伯胺仍为高风险(纠偏不误伤真阳性)。""" a = _assess(["primary_amine"], ["乳糖"]) assert a.overall_risk == rules.RISK_HIGH def test_aromatic_amine_maillard_differentiated_from_aliphatic(): """芳香胺 + 还原糖 → 低风险美拉德,区别于脂肪伯/仲胺的高风险(需求 2.1.3)。""" aromatic = _assess(["aromatic_amine"], ["乳糖"]) arom_reactions = {r.reaction: r.risk for r in aromatic.excipient_results[0].reactions} assert arom_reactions.get("maillard") == rules.RISK_LOW aliphatic = _assess(["primary_amine"], ["乳糖"]) ali_reactions = {r.reaction: r.risk for r in aliphatic.excipient_results[0].reactions} assert ali_reactions.get("maillard") == rules.RISK_HIGH # 差异化:脂肪胺风险严格高于芳香胺。 assert rules.RISK_ORDER[ali_reactions["maillard"]] > rules.RISK_ORDER[arom_reactions["maillard"]] def test_aromatic_amine_oxidation_prone(): """芳香胺 + 过氧化物辅料 → 中风险氧化(苯胺类氧化敏感)。""" a = _assess(["aromatic_amine"], ["聚维酮"]) reactions = {r.reaction: r.risk for r in a.excipient_results[0].reactions} assert reactions.get("oxidation") == rules.RISK_MEDIUM # --------------------------------------------------------------------------- # 最长别名(需求 2.3) # --------------------------------------------------------------------------- @pytest.mark.parametrize( "name,wrong_parent_tag_absent", [ ("预胶化淀粉", "reducing_sugar_impulse"), # 不应带淀粉的还原糖杂质途径标签 ("交联聚维酮", None), ], ) def test_longest_alias_not_parent(name, wrong_parent_tag_absent): a = _assess(["primary_amine"], [name]) er = a.excipient_results[0] assert er.known is True if name == "预胶化淀粉": # 预胶化淀粉档案 tags=[high_moisture],不含 reducing_sugar_impurity → 不命中美拉德。 assert "reducing_sugar_impurity" not in er.tags assert "maillard" not in {r.reaction for r in er.reactions} if name == "交联聚维酮": # 交联聚维酮 tags=[peroxide](无 high_moisture),区别于母体聚维酮。 assert "high_moisture" not in er.tags # --------------------------------------------------------------------------- # 不确定 + 多辅料聚合 # --------------------------------------------------------------------------- def test_no_groups_uncertain(): a = _assess([], ["乳糖", "硬脂酸镁"]) assert a.overall_risk == rules.RISK_UNCERTAIN assert a.overall_confidence == rules.CONF_LOW def test_multi_excipient_worst_aggregation(): """多辅料取最坏:乳糖(高) + 甘露醇(无) → 整体高。""" a = _assess(["primary_amine"], ["乳糖", "甘露醇"]) assert a.overall_risk == rules.RISK_HIGH def test_determinism_repeated_runs(): first = _assess(["primary_amine", "ester"], ["乳糖", "硬脂酸镁"]).summary() again = _assess(["primary_amine", "ester"], ["乳糖", "硬脂酸镁"]).summary() assert first == again