Files
6krrt/tests/test_feedback.py
adlee-was-taken e28443cb36 feat: POST /outcome — the only ground truth the router can get
Everything else this router records is a proxy. Structural checks know whether
code parses. The local checker guesses whether prose looks right. Neither knows
whether the answer did the job. The client does: it ran the tests, or used the
answer, or watched it fail.

Clients report against the provider's completion id, which they already
receive in the response body and on every stream chunk. energy_observations
and verifications now store that id so a report has something to join on.

Two properties make this the highest-value signal available.

It is the only quality signal that survives streaming. A retry cannot reach a
streamed response -- the bytes are already gone -- but a report arrives
afterwards and works identically either way. Every agent client streams, so
without this the main workflow had verification and feedback but no route from
outcome back into routing.

And its successes count. feedback.py folds client outcomes in BOTH directions,
unlike checks where only failures do. That asymmetry is deliberate: a parser
reporting 'ok' means the code parsed, which is weak evidence that would
inflate every score toward the ceiling, while a client reporting 'succeeded'
means the work worked.

An unknown request_id returns 404 rather than being quietly accepted. A client
whose reports go nowhere should find out rather than train nothing.

Verified end to end on both paths, including a streamed completion reported as
failed after the fact.

Tests 227 -> 232.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018xTPER7K8fNyKiuqNvTCTa
2026-08-17 22:32:04 -04:00

241 lines
8.1 KiB
Python

"""Tests for feedback.py — folding observed failures into proficiency.
The design decision under test is the asymmetry: failures are recorded,
passes are not. A structural 'ok' only means the code parsed, so treating it
as a quality sample would flood self_eval_score with 1.0s and wash out the
benchmark's discrimination — the coding categories already sit at 1.00 for
every model, and this would spread that flatness everywhere.
"""
import sqlite3
from pathlib import Path
import pytest
from config import load_config
from feedback import FAILURE_VERDICTS, apply_failures, summarize, unapplied_failures
ROOT = Path(__file__).resolve().parent.parent
SCHEMA_SQL = (ROOT / "schema.sql").read_text()
CFG = load_config(ROOT / "config.yaml")
@pytest.fixture
def db(tmp_path):
conn = sqlite3.connect(tmp_path / "t.db")
conn.row_factory = sqlite3.Row
conn.executescript(SCHEMA_SQL)
conn.execute(
"""
INSERT INTO models (model_id, provider, base_model_id, availability, last_updated)
VALUES ('m', 'nw', 'm', 'active', '2026-08-17T00:00:00+00:00')
"""
)
conn.commit()
yield conn
conn.close()
def _verify(conn, verdict, category="coding_general", kind="structural"):
conn.execute(
"""
INSERT INTO verifications (model_id, provider, task_category, kind, verdict, observed_at)
VALUES ('m', 'nw', ?, ?, ?, '2026-08-17T00:00:00+00:00')
""",
(category, kind, verdict),
)
conn.commit()
def _prof(conn):
return conn.execute(
"SELECT self_eval_score s, self_eval_samples n FROM proficiency WHERE model_id='m'"
).fetchone()
# --- only failures are folded in ------------------------------------------
def test_passes_are_not_recorded_as_samples(db):
# A structural 'ok' means "it parsed", not "it was correct". Recording it
# as a 1.0 would inflate every model toward the ceiling.
for _ in range(5):
_verify(db, "ok")
assert unapplied_failures(db) == []
def test_unverifiable_is_not_evidence(db):
# The checker had nothing to say; that says nothing about the model
for _ in range(5):
_verify(db, "unverifiable")
assert unapplied_failures(db) == []
@pytest.mark.parametrize("verdict", FAILURE_VERDICTS)
def test_failures_are_collected(db, verdict):
_verify(db, verdict)
assert len(unapplied_failures(db)) == 1
def test_a_failure_drags_the_score_down_proportionally(db):
from proficiency_store import add_self_eval
# Given: a benchmark score of 0.9 over 9 samples
add_self_eval(db, CFG, "m", "nw", "coding_general", [0.9] * 9)
_verify(db, "malformed")
apply_failures(db, CFG, summarize(unapplied_failures(db)), dry_run=False)
row = _prof(db)
# One 0.0 folded into the running mean: (0.9*9 + 0)/10
assert row["n"] == 10
assert row["s"] == pytest.approx(0.81)
def test_a_model_that_never_fails_keeps_its_benchmark_score(db):
from proficiency_store import add_self_eval
add_self_eval(db, CFG, "m", "nw", "coding_general", [0.9] * 9)
for _ in range(20):
_verify(db, "ok")
apply_failures(db, CFG, summarize(unapplied_failures(db)), dry_run=False)
row = _prof(db)
assert (row["n"], row["s"]) == (9, pytest.approx(0.9))
# --- idempotence ----------------------------------------------------------
def test_a_failure_is_applied_only_once(db):
_verify(db, "malformed")
apply_failures(db, CFG, summarize(unapplied_failures(db)), dry_run=False)
first = _prof(db)["n"]
# Re-running must not punish the model again for the same bad response
assert unapplied_failures(db) == []
apply_failures(db, CFG, summarize(unapplied_failures(db)), dry_run=False)
assert _prof(db)["n"] == first
def test_dry_run_changes_nothing(db):
_verify(db, "truncated")
apply_failures(db, CFG, summarize(unapplied_failures(db)), dry_run=True)
assert _prof(db) is None
assert len(unapplied_failures(db)) == 1
# --- grouping -------------------------------------------------------------
def test_failures_group_by_model_and_category(db):
_verify(db, "malformed", category="coding_general")
_verify(db, "malformed", category="coding_general")
_verify(db, "truncated", category="debugging")
grouped = summarize(unapplied_failures(db))
assert {k[2]: len(v) for k, v in grouped.items()} == {
"coding_general": 2,
"debugging": 1,
}
def test_failures_without_a_category_are_skipped(db):
# Nothing to attribute them to — proficiency is per-category
db.execute(
"""
INSERT INTO verifications (model_id, provider, task_category, kind, verdict, observed_at)
VALUES ('m', 'nw', NULL, 'structural', 'malformed', '2026-08-17T00:00:00+00:00')
"""
)
db.commit()
assert unapplied_failures(db) == []
def test_both_check_kinds_count(db):
_verify(db, "malformed", kind="structural")
_verify(db, "malformed", kind="local_llm")
assert len(unapplied_failures(db)) == 2
# --- attribution: not every failure is the model's fault -------------------
def _verify_capped(conn, verdict="truncated", category="coding_general"):
conn.execute(
"""
INSERT INTO verifications (model_id, provider, task_category, kind, verdict,
observed_at, model_attributable)
VALUES ('m', 'nw', ?, 'structural', ?, '2026-08-17T00:00:00+00:00', 0)
""",
(category, verdict),
)
conn.commit()
def test_client_capped_truncation_is_not_the_models_fault(db):
# Found by forcing it: a request with max_tokens=40 truncates, and without
# this any agent using a tight cap would systematically drag down whatever
# model it routed to.
_verify_capped(db)
assert unapplied_failures(db) == []
def test_capped_failures_are_still_recorded_for_visibility(db):
# The response really was unusable — it just says nothing about the model
_verify_capped(db)
n = db.execute("SELECT COUNT(*) c FROM verifications WHERE verdict='truncated'").fetchone()["c"]
assert n == 1
def test_attributable_and_capped_failures_are_separated(db):
_verify(db, "malformed") # model's fault
_verify_capped(db) # client's cap
rows = unapplied_failures(db)
assert len(rows) == 1
assert rows[0]["verdict"] == "malformed"
# --- client outcomes: the only two-way evidence ----------------------------
def _outcome(conn, verdict, category="coding_general"):
conn.execute(
"""
INSERT INTO verifications (model_id, provider, task_category, kind, verdict, observed_at)
VALUES ('m', 'nw', ?, 'client_outcome', ?, '2026-08-17T00:00:00+00:00')
""",
(category, verdict),
)
conn.commit()
def test_a_client_reported_success_counts_as_a_positive_sample(db):
# Unlike a parser's 'ok' — which means the code parsed — this means the
# client ran it and the work worked. That is the only ground truth here.
from proficiency_store import add_self_eval
add_self_eval(db, CFG, "m", "nw", "coding_general", [0.5] * 3)
_outcome(db, "succeeded")
apply_failures(db, CFG, summarize(unapplied_failures(db)), dry_run=False)
row = _prof(db)
assert row["n"] == 4
assert row["s"] == pytest.approx((0.5 * 3 + 1.0) / 4)
def test_a_client_reported_failure_counts_against(db):
from proficiency_store import add_self_eval
add_self_eval(db, CFG, "m", "nw", "coding_general", [1.0] * 3)
_outcome(db, "failed")
apply_failures(db, CFG, summarize(unapplied_failures(db)), dry_run=False)
assert _prof(db)["s"] == pytest.approx(0.75)
def test_a_structural_pass_still_contributes_nothing(db):
# The asymmetry that matters: 'ok' from a parser is weak evidence and
# would inflate every score toward the ceiling
for _ in range(10):
_verify(db, "ok")
assert unapplied_failures(db) == []
def test_successes_and_failures_mix_into_one_rate(db):
for _ in range(3):
_outcome(db, "succeeded")
_outcome(db, "failed")
apply_failures(db, CFG, summarize(unapplied_failures(db)), dry_run=False)
row = _prof(db)
assert (row["n"], row["s"]) == (4, pytest.approx(0.75))