Files
semantica/tests/evals/test_decision_evaluators.py
T
KevinandSameer Kadam 30a91a3a78 feat(evals): add per-metric objective support to runner (closes #1091) (#1092)
* chore: ignore .worktrees directory

* feat(evals): add eval metric and result models

* feat(evals): add evaluator registry

* feat(evals): add exact/regex/range/length evaluators

* feat(evals): add keyword/levenshtein/rouge/llm-as-judge evaluators

* feat(evals): add decision_scores composite evaluator

* feat(evals): add evaluation runner

* feat(evals): expose public API and module proxy

* fix(evals): resolve __all__ names and repair usage example

* docs(evals): add usage docs and changelog entry

* style(evals): tidy evaluator metadata and wiring comments

* fix(evals): honor expected arg and classify error metrics

* fix(evals): export get_evaluator and fix shared meta default

* fix(evals): guard provenance check against non-dict metadata

* docs: add objective layer design spec for semantica.evals

* docs: refine objective spec for consistency with AIP Evals semantics

* docs: add implementation plan for evals objective layer

* docs: fix plan tests to use module-level pytest import

* feat(evals): add per-metric objective support to runner

* docs(evals): document per-metric objectives

* docs(evals): fix minimize example threshold to demonstrate pass

* fix(evals): validate objective config shape strictly

* docs(evals): clarify objective examples and Boolean semantics

* fix(evals): honor direction-only minimize, fail fast on objectives, deep-merge case config

- minimize without threshold is now a no-op, matching maximize (issue #1091
  requires thresholds to be optional for both directions)
- objective config is parsed for every case before any target_fn/evaluator
  runs, so an invalid per-case objective rejects the run up front
- per-case evaluator config deep-merges over the global config so a case
  that overrides one setting keeps the run-level objective
- regression tests for all three, plus updated docs/CHANGELOG

Addresses 3 of 4 Qodo findings on #1092 (the 4th, 'result models defined
twice', is a false positive: types live in types.py)

* fix: finalize eval objectives review

---------

Co-authored-by: Sameer Kadam <sskadam6305@gmail.com>
2026-09-02 18:47:28 +05:30

137 lines
5.0 KiB
Python

"""Tests for the decision_scores composite evaluator."""
import pytest
from datetime import datetime
from semantica.context.decision_models import Decision
from semantica.evals import registry as reg
def _decision(**overrides):
base = dict(
decision_id="d1",
category="loan",
scenario="mortgage application",
reasoning="strong credit history",
outcome="approved",
confidence=0.95,
timestamp=datetime(2026, 1, 1),
decision_maker="loan_officer",
)
base.update(overrides)
return Decision(**base)
class TestDecisionScores:
def test_full_pass(self):
d = _decision(metadata={"provenance": {"prov_record": "rid-1"}})
r = reg.get_evaluator("decision_scores")(
d, config={"expected_outcome": "approved"}
)
assert r.passed
assert r.meta["decision_outcome"] is True
assert r.meta["provenance"] is True
def test_outcome_mismatch(self):
d = _decision(metadata={"provenance": {"prov_record": "rid-1"}})
r = reg.get_evaluator("decision_scores")(
d, config={"expected_outcome": "denied"}
)
assert not r.passed
assert r.meta["decision_outcome"] is False
def test_outcome_from_expected_argument(self):
d = _decision(metadata={"provenance": {"prov_record": "rid-1"}})
r = reg.get_evaluator("decision_scores")(d, expected="approved")
assert r.passed
assert r.meta["decision_outcome"] is True
def test_outcome_mismatch_via_expected_argument(self):
d = _decision(metadata={"provenance": {"prov_record": "rid-1"}})
r = reg.get_evaluator("decision_scores")(d, expected="denied")
assert not r.passed
assert r.meta["decision_outcome"] is False
assert "decision_outcome" in r.meta["reasons"]
def test_outcome_check_skipped_when_no_expected(self):
d = _decision(metadata={"provenance": {"prov_record": "rid-1"}})
r = reg.get_evaluator("decision_scores")(d)
assert "decision_outcome" not in r.meta
def test_confidence_out_of_range(self):
d = _decision(metadata={"provenance": {"prov_record": "rid-1"}}, confidence=0.4)
r = reg.get_evaluator("decision_scores")(
d, config={"expected_outcome": "approved", "min_confidence": 0.8}
)
assert not r.passed
assert r.meta["decision_confidence"] is False
def test_missing_provenance_fails(self):
d = _decision(metadata={})
r = reg.get_evaluator("decision_scores")(d, config={"expected_outcome": "approved"})
assert not r.passed
assert r.meta["provenance"] is False
def test_missing_required_fields(self):
d = _decision(reasoning="")
r = reg.get_evaluator("decision_scores")(d, config={"expected_outcome": "approved"})
assert not r.passed
assert r.meta["reasoning"] is False
def test_dict_input_coerced(self):
d = _decision(metadata={"provenance": {"prov_record": "rid-1"}})
as_dict = d.to_dict()
r = reg.get_evaluator("decision_scores")(
as_dict, config={"expected_outcome": "approved"}
)
assert r.passed
def test_malformed_dict_is_error_not_crash(self):
r = reg.get_evaluator("decision_scores")({"foo": "bar"}, config={})
assert not r.passed
assert r.meta.get("error")
def test_non_dict_metadata_is_error_not_crash(self):
bad = _decision(metadata="not-a-dict")
r = reg.get_evaluator("decision_scores")(bad, config={})
assert not r.passed
assert r.meta["provenance"] is False
def test_policy_compliance_check(self):
class FakePolicyEngine:
def check_compliance(self, decision, policy_id):
return True
d = _decision(metadata={"provenance": {"prov_record": "rid-1"}})
r = reg.get_evaluator("decision_scores")(
d, config={
"expected_outcome": "approved",
"policy_engine": FakePolicyEngine(),
"policy_id": "p1",
"expected_policy_compliant": True,
}
)
assert r.meta["policy"] is True
def test_policy_mismatch_fails(self):
class FakePolicyEngine:
def check_compliance(self, decision, policy_id):
return False
d = _decision(metadata={"provenance": {"prov_record": "rid-1"}})
r = reg.get_evaluator("decision_scores")(
d, config={
"policy_engine": FakePolicyEngine(),
"policy_id": "p1",
"expected_policy_compliant": True,
}
)
assert not r.passed
assert r.meta["policy"] is False
def test_causal_chain_gate(self):
d = _decision(metadata={"provenance": {"prov_record": "rid-1"}}, decision_id="only-decision")
with pytest.raises(NotImplementedError):
reg.get_evaluator("decision_scores")(
d, config={"causal_chain_exists": True, "graph_store": object()}
)