"""Tests for src.llm.explainer. The deterministic template path is exhaustively tested here. The LLM path is exercised only by env-gated integration tests in test_explainer_integration.py (NOT run in CI by default). """ from __future__ import annotations import os import pytest from src.llm.explainer import ExplainPayload, explain def _payload(**overrides) -> ExplainPayload: """Build a representative ExplainPayload; overrides win.""" base: ExplainPayload = { "smiles": "CCO", "label": 1, "label_text": "permeable", "confidence": 0.82, "top_features": [ {"feature": "fp_341", "shap_value": 0.045}, {"feature": "fp_902", "shap_value": -0.031}, {"feature": "fp_77", "shap_value": 0.022}, ], "calibration": {"threshold": 0.80, "precision": 0.92, "support": 18}, "drift_z": 0.42, "user_question": "Why was this molecule predicted as permeable?", } base.update(overrides) return base class TestTemplateExplain: """Day-7 T3A: deterministic-template path of the explainer.""" def test_template_path_is_deterministic(self, monkeypatch): """Same input → byte-identical rationale string. No randomness.""" monkeypatch.setenv("NEUROBRIDGE_DISABLE_LLM", "1") out_a = explain(_payload()) out_b = explain(_payload()) assert out_a["rationale"] == out_b["rationale"] assert out_a["source"] == "template" assert out_b["source"] == "template" assert out_a["model"] is None def test_template_includes_top_feature_names(self, monkeypatch): """Rationale must mention the SHAP features so jurors see attribution.""" monkeypatch.setenv("NEUROBRIDGE_DISABLE_LLM", "1") result = explain(_payload()) for feat in ("fp_341", "fp_902", "fp_77"): assert feat in result["rationale"], ( f"expected feature {feat!r} in rationale, got {result['rationale']!r}" ) def test_template_includes_label_text(self, monkeypatch): """The verdict word ('permeable' / 'non-permeable') must appear.""" monkeypatch.setenv("NEUROBRIDGE_DISABLE_LLM", "1") result = explain(_payload(label=0, label_text="non-permeable")) assert "non-permeable" in result["rationale"] def test_disable_flag_forces_template_even_with_key_set(self, monkeypatch): """NEUROBRIDGE_DISABLE_LLM=1 wins over OPENROUTER_API_KEY presence.""" monkeypatch.setenv("NEUROBRIDGE_DISABLE_LLM", "1") monkeypatch.setenv("OPENROUTER_API_KEY", "sk-fake-not-used") result = explain(_payload()) assert result["source"] == "template" assert result["model"] is None