hackathon / tests /llm /test_explainer.py
mekosotto's picture
feat(llm): explainer with deterministic template + OpenRouter fallback
e5c1c61
raw
history blame
2.75 kB
"""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