haiku.rag/evaluations/tests/test_reference_configs.py
Yiorgis Gozadinos 18e1127375
Report the multi-database gates and which surfaces were reached
The two hard gates are pass/fail, not rates: any scope leak or attribution
error is a bug. The report names the offending cases and prints a greppable
GATES: PASSED / FAILED line, since this dataset is an acceptance gate rather
than a number to watch drift on.

The attribution gate covers only families where exactly one database is
correct. B5 is answered by two, so citing both is right there and policing
it would report a bug that is not one.

Surface coverage comes from the model's own Python, now carried on the run
result: counting executions said how often code ran but never which surface
it reached. Coverage is measured, not required, so a surface nothing reaches
is a finding about whether it earns its place in the VFS.

answer_correct is recorded as a score rather than a boolean, since booleans
become assertions and the run reads its accuracy headline from scores. The
two gates stay boolean assertions, which is what they are.

DatasetSpec grows a report_hook so this lives with the dataset instead of
becoming a key check in the shared QA reporting.

The reference-config invariant said a dataset with a deterministic evaluator
must declare no judge. That is not true here: RefusalJudge runs on any case
carrying an answerability label, which B6 and B7 do, so its sampling has to
be pinned. The assertion now requires a pinned block wherever a judge runs
and allows its absence only as the claim that none does.
2026-08-27 09:26:52 +03:00

62 lines
1.9 KiB
Python

from pathlib import Path
import pytest
from evaluations.datasets import DATASETS
from haiku.rag.config import load_yaml_config
from haiku.rag.config.models import AppConfig
CONFIG_DIR = Path(__file__).parent.parent / "configs"
PINNED_JUDGE_SAMPLING = {
"temperature": 0.6,
"max_tokens": 16384,
"extra_body": {
"top_p": 0.95,
"top_k": 20,
"min_p": 0,
"chat_template_kwargs": {"reasoning_effort": "low"},
},
}
def _config_paths() -> list[Path]:
return sorted(CONFIG_DIR.glob("*.yaml"))
def _load(path: Path) -> AppConfig:
return AppConfig.model_validate(load_yaml_config(path))
def test_configs_present() -> None:
assert _config_paths(), f"no reference configs found in {CONFIG_DIR}"
@pytest.mark.parametrize("path", _config_paths(), ids=lambda p: p.stem)
def test_config_validates(path: Path) -> None:
_load(path)
@pytest.mark.parametrize("path", _config_paths(), ids=lambda p: p.stem)
def test_filename_names_a_dataset(path: Path) -> None:
assert path.stem in DATASETS
@pytest.mark.parametrize("path", _config_paths(), ids=lambda p: p.stem)
def test_judge_pinned_where_the_judge_runs(path: Path) -> None:
"""Wherever a judge runs, its sampling must be frozen so accuracy stays
comparable across runs.
A dataset without its own qa_evaluator is scored by the LLM judge and must
carry the block. A dataset with a deterministic evaluator may still need one,
because RefusalJudge runs on any case carrying an answerability label; where
it declares no judge it is asserting that no case does.
"""
judge = _load(path).evaluations.judge
if DATASETS[path.stem].qa_evaluator is not None and judge is None:
return
assert judge is not None
assert judge.temperature == PINNED_JUDGE_SAMPLING["temperature"]
assert judge.max_tokens == PINNED_JUDGE_SAMPLING["max_tokens"]
assert judge.extra_body == PINNED_JUDGE_SAMPLING["extra_body"]