core/tests/test_rate_conversion_solving.py
Shay 1c59f331f4 feat(rate): exact time-unit conversion for single-rate (R3.2b-e)
Option A (text-faithful): RateProblem gains time_unit (the duration's ORIGINAL unit; time stays int; defaults to the rate denominator). The SOLVER converts via convert_time -> exact Fraction -> int-or-refuse(non_integer); Fraction never leaves the solver, no floats. The reader accepts a convertible (minute<->hour) duration mismatch (keeps original time_unit); a non-convertible one still refuses rate_unit_mismatch. Signature includes time_unit ('30 minutes' != '30 hours').

Gold: r3-09 flips reader_refuses -> SOLVED (60 mph for 30 min = 30 mile, exact 1/2 hour; distractor 1800 rejected); new r3-13 non-convertible (3 gallons) stays rate_unit_mismatch. R3 gold 13/13; reader 9/0/0 -> answers 7/0/6 (setup_correct 8->9). Failure-family: rate_unit_mismatch now non-convertible-only -> still unsupported_rate_duration proposal surface; router-hygiene invariant stays green; R1 7/0/3, R2 10/0/0 unchanged. 119-test smoke green incl. invariants. Off-serving, no float path.
2026-06-08 05:43:51 -07:00

49 lines
2.1 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.

"""End-to-end conversion solving (R3.2c/d) — the extra cases pinned in the R3.2 plan."""
from __future__ import annotations
from fractions import Fraction
from generate.meaning_graph.reader import Refusal
from generate.rate_comprehension.model import RateProblem
from generate.rate_comprehension.reader import read_rate_problem
from generate.rate_comprehension.solver import solve_rate
from generate.rate_comprehension.units import RateUnit
_MPH = RateUnit("mile", "hour")
def test_30_minutes_at_60_mph_is_30_miles() -> None:
assert solve_rate(RateProblem(_MPH, 60, 30, None, "quantity", time_unit="minute")) == 30
def test_90_minutes_at_60_mph_is_90_miles() -> None:
assert solve_rate(RateProblem(_MPH, 60, 90, None, "quantity", time_unit="minute")) == 90
def test_45_minutes_at_10_mph_is_non_integer_refused() -> None:
# 10 mile/hour × 45/60 hour = 15/2 = 7.5 -> REFUSE (never 7 or 8).
out = solve_rate(RateProblem(_MPH, 10, 45, None, "quantity", time_unit="minute"))
assert isinstance(out, Refusal) and out.reason == "non_integer_solution"
def test_minutes_rate_needs_no_conversion() -> None:
# 60 mile/minute for 30 minutes -> identity, 60 × 30 = 1800 mile.
assert solve_rate(RateProblem(RateUnit("mile", "minute"), 60, 30, None, "quantity", time_unit="minute")) == 1800
def test_non_convertible_duration_refuses_at_reader() -> None:
out = read_rate_problem("A car travels 60 miles per hour for 3 gallons. How many miles does it travel?")
assert isinstance(out, Refusal) and out.reason == "rate_unit_mismatch"
def test_convertible_duration_reads_and_solves_end_to_end() -> None:
problem = read_rate_problem("A car travels 60 miles per hour for 30 minutes. How many miles does it travel?")
assert not isinstance(problem, Refusal)
assert problem.time == 30 and problem.time_unit == "minute" # text-faithful: stored as minutes
assert solve_rate(problem) == 30
def test_solver_answer_is_int_never_float() -> None:
result = solve_rate(RateProblem(_MPH, 60, 90, None, "quantity", time_unit="minute"))
assert isinstance(result, int) and not isinstance(result, (float, Fraction))