import asyncio import sys import types # L'environnement local de test peut ne pas contenir FastAPI. # Le module endpoint utilise uniquement HTTPException ; ce substitut # permet donc de tester son contrat sans modifier le code de production. try: import fastapi # noqa: F401 except ModuleNotFoundError: fastapi_stub = types.ModuleType("fastapi") class HTTPException(Exception): def __init__( self, status_code: int, detail: str, ) -> None: super().__init__(detail) self.status_code = status_code self.detail = detail fastapi_stub.HTTPException = HTTPException sys.modules["fastapi"] = fastapi_stub from endpoints.validate_numeric import post_validate_numeric from numeric_core import extract, validate_numeric EVIDENCE = ( "Effect sizes were 1.19 and 0.57. " "There were 22 studies. " "The response rate was 20%. " "The dose was 1.5 mg. " "The 95% CI was 0.74 to 1.64." ) def test_decimal_point_with_final_period(): values = extract("Effect size 1.19.") assert len(values) == 1 assert values[0].kind == "scalar" assert values[0].lo == 1.19 def test_decimal_comma_with_final_period(): values = extract("Taille d'effet 1,19.") assert len(values) == 1 assert values[0].kind == "scalar" assert values[0].lo == 1.19 def test_two_plain_decimals_are_extracted(): values = extract("Values were 1.19 and 0.57.") assert [item.lo for item in values] == [1.19, 0.57] def test_french_decimals_match_exactly(): result = validate_numeric( "Les tailles d'effet étaient 1,19 et 0,57.", EVIDENCE, rel_tol=0.0, ) assert result["has_numbers"] is True assert result["numeric_ok"] is True assert result["unparsed_numeric"] is False def test_modified_decimal_is_rejected_exactly(): result = validate_numeric( "Effect sizes were 1.20 and 0.57.", EVIDENCE, rel_tol=0.0, ) assert result["numeric_ok"] is False def test_close_decimal_is_allowed_at_two_percent(): result = validate_numeric( "Effect sizes were 1.20 and 0.57.", EVIDENCE, rel_tol=0.02, ) assert result["numeric_ok"] is True def test_large_decimal_difference_is_rejected(): result = validate_numeric( "Effect sizes were 1.30 and 0.57.", EVIDENCE, rel_tol=0.02, ) assert result["numeric_ok"] is False def test_specialized_numeric_types_are_preserved(): assert extract("Dose 1,5 mg.")[0].kind == "dose" assert extract("Response 20,5%.")[0].kind == "percent" assert extract("95% IC 0,74 à 1,64.")[0].kind == "ci" def test_contextual_integer_mismatch_is_rejected(): result = validate_numeric( "There were 23 studies.", EVIDENCE, rel_tol=0.0, ) assert result["numeric_ok"] is False def test_endpoint_contract_uses_exact_mode(): payload = { "items": [ { "id": 1, "claim": "Effect sizes were 1.19 and 0.57.", "evidence": EVIDENCE, }, { "id": 2, "claim": "Effect sizes were 1.20 and 0.57.", "evidence": EVIDENCE, }, ], "relative_tolerance": 0.0, } response = asyncio.run( post_validate_numeric(payload) ) assert response["results"][0]["numeric_ok"] is True assert response["results"][1]["numeric_ok"] is False