rubriq / tests /test_scoring.py
devadit15's picture
Claude Fable 5
Step 1: scoring engine, improvement-prompt generator, API, 44 green tests
4b5b8fc
Raw History Blame Contribute Delete
6.06 kB
"""End-to-end: LocalScorer must behave sensibly on the curated pairs.
"Sensibly" is concrete: good outputs beat bad outputs, known flaws are
found and quoted, and improvement prompts obey the no-generic-advice
contract. Requires the two local models (cached by warmup)."""
import pytest
import samples
from rubriq.schemas import EvalRequest, FailureMode
from rubriq.scoring import LocalScorer
scorer = LocalScorer()
def run(prompt, output, model=""):
return scorer.evaluate(EvalRequest(prompt=prompt, output=output, model_name=model))
@pytest.fixture(scope="module")
def summ_good():
return run(samples.SUMM_PROMPT, samples.SUMM_GOOD)
@pytest.fixture(scope="module")
def summ_bad():
return run(samples.SUMM_PROMPT, samples.SUMM_BAD, model="gpt-4o")
class TestSummarization:
def test_good_beats_bad_overall(self, summ_good, summ_bad):
assert summ_good.overall.score > summ_bad.overall.score + 10
def test_bad_fails_word_limit(self, summ_bad):
instr = next(p for p in summ_bad.parameters if p.parameter == "instruction_following")
assert any(f.failure_mode in (FailureMode.MISSED_CONSTRAINT, FailureMode.FORMAT_VIOLATION)
for f in instr.findings)
def test_good_passes_word_limit(self, summ_good):
instr = next(p for p in summ_good.parameters if p.parameter == "instruction_following")
assert instr.score == 100.0
def test_bad_flagged_unfaithful(self, summ_bad):
faith = next(p for p in summ_bad.parameters if p.parameter == "faithfulness")
assert faith.score is not None
assert any(f.failure_mode == FailureMode.UNFAITHFUL_CONTENT for f in faith.findings)
def test_good_faithfulness_beats_bad(self, summ_good, summ_bad):
fg = next(p for p in summ_good.parameters if p.parameter == "faithfulness")
fb = next(p for p in summ_bad.parameters if p.parameter == "faithfulness")
assert fg.score > fb.score
def test_findings_quote_the_output(self, summ_bad):
faith = next(p for p in summ_bad.parameters if p.parameter == "faithfulness")
quoted = [e.quote for f in faith.findings for e in f.evidence if e.source == "output"]
assert quoted, "unfaithfulness findings must quote the offending sentences"
class TestCode:
def test_bad_code_flagged(self):
report = run(samples.CODE_PROMPT, samples.CODE_BAD)
code = next(p for p in report.parameters if p.parameter == "code_validity")
details = " ".join(f.detail for f in code.findings)
assert "parse" in details.lower()
assert "parse_log_line" in details # requested name missing
def test_good_code_clean(self):
report = run(samples.CODE_PROMPT, samples.CODE_GOOD)
code = next(p for p in report.parameters if p.parameter == "code_validity")
assert code.score >= 80
def test_honesty_note_static_only(self):
report = run(samples.CODE_PROMPT, samples.CODE_GOOD)
assert any("never executed" in n for n in report.honesty_notes)
class TestExtraction:
def test_invented_values_caught(self):
report = run(samples.EXTRACT_PROMPT, samples.EXTRACT_BAD)
g = next(p for p in report.parameters if p.parameter == "extraction_grounding")
invented = [e.quote for f in g.findings for e in f.evidence]
assert any("Erik Larsen" in q for q in invented)
assert any("Oslo Climate Center" in q for q in invented)
def test_clean_extraction_scores_full(self):
report = run(samples.EXTRACT_PROMPT, samples.EXTRACT_GOOD)
g = next(p for p in report.parameters if p.parameter == "extraction_grounding")
assert g.score == 100.0
class TestOpenQA:
def test_risk_signals_found(self):
report = run(samples.QA_OPEN_PROMPT, samples.QA_OPEN_RISKY)
risk = next(p for p in report.parameters if p.parameter == "hallucination_risk")
kinds = {f.data.get("kind") for f in risk.findings}
assert "citation_pattern" in kinds
assert "precise_statistic" in kinds
assert "url" in kinds
def test_factual_accuracy_declared_out_of_scope(self):
report = run(samples.QA_OPEN_PROMPT, samples.QA_OPEN_RISKY)
fact = next(p for p in report.parameters if p.parameter == "factual_accuracy")
assert fact.score is None and fact.requires_judge
def test_carveout_honesty_note_present(self):
report = run(samples.QA_OPEN_PROMPT, samples.QA_OPEN_RISKY)
assert any("judge" in n.lower() for n in report.honesty_notes)
class TestCreative:
def test_cliches_flagged_as_proxy(self):
report = run(samples.CREATIVE_PROMPT, samples.CREATIVE_CLICHED)
style = next(p for p in report.parameters if p.parameter == "creative_proxies")
assert style.is_proxy
assert any(f.failure_mode == FailureMode.LOW_DIVERSITY for f in style.findings)
quoted = " ".join(e.quote for f in style.findings for e in f.evidence)
assert "at the end of the day" in quoted or "heart skipped a beat" in quoted
class TestCoverage:
def test_unanswered_part_found(self):
report = run(samples.QA_MULTI_PROMPT, samples.QA_MULTI_PARTIAL)
cov = next(p for p in report.parameters if p.parameter == "question_coverage")
assert cov.score is not None and cov.score < 100
assert cov.is_proxy
quoted = " ".join(e.quote for f in cov.findings for e in f.evidence)
assert "funded" in quoted.lower() # the ignored funding question
def test_known_limitation_topic_mentioned_passes(self):
"""Documents the proxy's honest limit: a question whose topic appears
in the answer (ice cores) passes the topical check even though it was
not truly answered. Judge mode is the fix, not a stricter threshold."""
report = run(samples.QA_MULTI_TOPIC_MENTIONED, samples.QA_MULTI_PARTIAL)
cov = next(p for p in report.parameters if p.parameter == "question_coverage")
assert cov.score == 100.0
assert "topical" in cov.verdict.lower()