core/tests/test_constraint_reader.py
Shay 4ddaef4d00 feat(constraint): two-category reader — prose to ConstraintProblem (R2 C5-C9)
generate/constraint_comprehension/reader.py: read_constraint_problem comprehends two-category constraint prose into a typed ConstraintProblem via five recognizers — C5 category-pair (from coefficient clauses; >2 -> too_many_categories), C6 coefficient (Each <cat> holds/has/costs/is-worth <N> <unit>; shared measured unit), C7 total-count (collective-unit sentence -> x+y=N; absent -> missing_total_count), C8 weighted-total (measured-unit sentence -> ax+by=T; absent -> missing_weighted_total), C9 query-target (How many <cat> -> the asked unknown). Refuses, never mis-assembles.

Reconciles the sketch's 'no equal coefficients' note: equal coefficients are a SOLVER refusal (indistinguishable_weights), not a reader one — the reader reads r2-010 setup_correct and the solver refuses it. run_reader lane + 'reader' CLI subcommand. R2 reader: setup_correct 10 / setup_wrong 0 / refused_correct 3; R1 7/0/3 and 15-case 15/0/0 unchanged. Off-serving. 8 reader tests incl. full read->solve->verify chain + meaningful-fail guards.
2026-06-07 07:34:55 -07:00

99 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Tests for the R2 two-category reader (C5C9).
Pins the wrong=0 reader contract: every well-formed fixture reads to EXACTLY the gold setup
signature, every ``reader_refuses`` fixture refuses with its gold reason, and — proving each
reader slice end to end — every solved fixture reads -> solves -> ties to its labeled answer.
The defensive guards (coefficient unit mismatch, off-category query) are exercised by
construction; the too-many-categories guard is shown meaningful-fail against a 2-category twin.
"""
from __future__ import annotations
from evals.constraint_oracle.runner import _load_r2_gold, gold_to_problem, run_reader
from evals.constraint_oracle.signature import constraint_setup_signature
from generate.answer_choices.verify import ChoiceVerdict, verify_answer_choice
from generate.constraint_comprehension.reader import read_constraint_problem
from generate.constraint_comprehension.solver import answer_constraint_problem
from generate.meaning_graph.reader import Refusal
def _by_expect(expect: str) -> list[dict]:
return [f for f in _load_r2_gold() if f["expect"] == expect]
def test_reader_lane_is_wrong_zero_and_complete() -> None:
r = run_reader()
assert r["setup_wrong"] == 0
assert r["reason_mismatch"] == 0
assert r["setup_refused"] == 0 # every well-formed fixture reads at C9
assert r["setup_correct"] == 10 # 7 solved + 3 solver_refuses (all have valid setups)
assert r["refused_correct"] == 3 # the three reader_refuses fixtures
def test_reader_reads_every_well_formed_fixture_to_gold_signature() -> None:
for fx in _by_expect("solved") + _by_expect("solver_refuses"):
out = read_constraint_problem(fx["text"])
assert not isinstance(out, Refusal), f"{fx['id']} refused: {getattr(out, 'reason', '')}"
assert constraint_setup_signature(out) == constraint_setup_signature(gold_to_problem(fx)), fx["id"]
def test_reader_refuses_every_reader_refuse_fixture_with_its_reason() -> None:
for fx in _by_expect("reader_refuses"):
out = read_constraint_problem(fx["text"])
assert isinstance(out, Refusal), fx["id"]
assert out.reason == fx["reader_reason"], f"{fx['id']}: {out.reason} != {fx['reader_reason']}"
def test_read_solve_verify_end_to_end_for_solved() -> None:
# The full chain a reader slice must prove: read -> solve -> tie to the labeled option.
for fx in _by_expect("solved"):
problem = read_constraint_problem(fx["text"])
assert not isinstance(problem, Refusal), fx["id"]
value = answer_constraint_problem(problem)
assert value == fx["gold"], fx["id"]
verdict = verify_answer_choice(value, fx["options"], fx["answer"], noun=problem.query.unit)
assert isinstance(verdict, ChoiceVerdict) and verdict.status == "consistent"
assert verdict.computed_label == fx["answer"]
def test_read_then_solver_refuses_for_solver_refuse_fixtures() -> None:
# The reader reads the setup; the SOLVER owns solvability — read correct, solve refuses.
for fx in _by_expect("solver_refuses"):
problem = read_constraint_problem(fx["text"])
assert not isinstance(problem, Refusal), fx["id"]
out = answer_constraint_problem(problem)
assert isinstance(out, Refusal) and out.reason == fx["solver_reason"], fx["id"]
# --- defensive guards (no gold fixture; exercised by construction) --------------------- #
def test_coefficient_unit_mismatch_refuses() -> None:
out = read_constraint_problem(
"A shop has 5 things, all cars and trucks. Each car has 4 wheels and each truck costs "
"3 dollars. The things total 20 wheels. How many cars are there?"
)
assert isinstance(out, Refusal) and out.reason == "coefficient_unit_mismatch"
def test_off_category_query_refuses() -> None:
out = read_constraint_problem(
"A school rents 6 buses for a trip. Each large bus holds 50 students and each small bus "
"holds 30 students. The buses carry 260 students in total. How many vans are there?"
)
assert isinstance(out, Refusal) and out.reason == "query_target_not_a_category"
def test_too_many_categories_is_meaningful_fail_against_two_category_twin() -> None:
# The 3-category text refuses; the SAME template with exactly two categories reads.
three = read_constraint_problem(
"A lot has 10 vehicles. Each car has 4 wheels, each motorcycle has 2 wheels, and each "
"truck has 6 wheels. Together the vehicles have 34 wheels. How many cars are there?"
)
assert isinstance(three, Refusal) and three.reason == "too_many_categories"
two = read_constraint_problem(
"A lot has 10 vehicles. Each car has 4 wheels and each motorcycle has 2 wheels. "
"Together the vehicles have 32 wheels. How many cars are there?"
)
assert not isinstance(two, Refusal)
assert {u.symbol for u in two.unknowns} == {"car", "motorcycle"}