from __future__ import annotations from app.services.answer_correction import ( SOURCE_ANSWER_NOT_FOUND, UNKNOWN_MARKS_PROMPT, build_answer_correction_output, ) def test_physics_derivation_correction_marks_missing_change_in_velocity() -> None: output = build_answer_correction_output( question="Derive v = u + at.", student_answer="Acceleration is velocity by time, so v = u + at.", subject="Physics", board="Kerala HSE", class_level="+2", marks=5, ) joined = " ".join(output["marks_lost"]).lower() assert "acceleration definition is incomplete" in joined assert "change in velocity" in joined assert "a = (v - u) / t" in output["corrected_board_answer"] assert output["score"].startswith("Estimated score:") def test_physics_formula_with_spaces_detected() -> None: output = build_answer_correction_output( question="Derive v = u + at.", student_answer="We know a = change in velocity / time. So a = (v-u)/t. Therefore v = u + at.", subject="Physics", marks=5, ) corrected = output["corrected_board_answer"].lower() assert "v = u + at" in corrected assert "estimated score" in output["score"].lower() lost_joined = " ".join(output["marks_lost"]).lower() assert "symbol" not in lost_joined or "define" not in lost_joined def test_chemistry_numerical_co2_11g_correction() -> None: output = build_answer_correction_output( question="Calculate number of moles in 11 g of CO2. Molar mass of CO2 = 44 g/mol.", student_answer="Moles = mass x molar mass = 11 x 44 = 484 mol.", subject="Chemistry", marks=3, ) corrected = output["corrected_board_answer"].lower() assert "11" in corrected assert "44" in corrected assert "0.25" in corrected assert "co2" in corrected assert "mol" in corrected lost_joined = " ".join(output["marks_lost"]).lower() assert "wrong formula" in lost_joined or "multiplication" in lost_joined or "division" in lost_joined score = output["score"].lower() assert "estimated" in score assert "/3" in score or "/ 3" in score def test_chemistry_numerical_water_18g_still_works() -> None: output = build_answer_correction_output( question="Calculate the number of moles in 18 g of water.", student_answer="Water has 1 mole.", subject="Chemistry", marks=3, ) missing = " ".join(output["missing_keywords"]).lower() corrected = output["corrected_board_answer"].lower() assert "given" in missing assert "formula" in corrected assert "substitution" in corrected assert "mol" in corrected assert "18" in corrected assert "h2o" in corrected or "water" in corrected def test_maths_proof_correction_without_given_to_prove() -> None: output = build_answer_correction_output( question="Prove sin^2 x + cos^2 x = 1.", student_answer="It is a formula, so sin^2 x + cos^2 x = 1.", subject="Mathematics", marks=3, ) corrected = output["corrected_board_answer"].lower() assert "given" in corrected assert "to prove" in corrected assert "pythagoras theorem" in corrected lost_joined = " ".join(output["marks_lost"]).lower() score = output["score"].lower() assert "estimated" in score assert "/3" in score or "/ 3" in score assert len(output["marks_lost"]) >= 1 def test_maths_proof_with_full_steps_gets_fewer_marks_lost() -> None: output = build_answer_correction_output( question="Prove sin^2 x + cos^2 x = 1.", student_answer=( "Let opposite side = a, adjacent side = b, hypotenuse = c. " "sin x = a/c, cos x = b/c. sin^2 x + cos^2 x = a^2/c^2 + b^2/c^2 " "= (a^2 + b^2)/c^2 = c^2/c^2 = 1. Hence proved." ), subject="Mathematics", marks=3, ) lost_joined = " ".join(output["marks_lost"]).lower() assert "too short" not in lost_joined def test_source_mismatch_does_not_pretend_boolean_logic_supports_physics(client) -> None: source = client.post( "/sources/text", json={ "title": "Number Systems and Boolean Logic", "text": ( "Binary number system. Decimal to binary conversion. Boolean logic. " "AND, OR, NOT gates. Truth table examples. Three simple exam questions." ), "source_type": "notes", "subject": "Computer Science", "chapter": "Boolean Logic", }, ) assert source.status_code == 201 source_id = source.json()["id"] response = client.post( "/generate/answer-correction", json={ "topic": "Derive v = u + at.", "subject": "Physics", "source_id": source_id, "source_ids": [source_id], "language_preference": "English", "options": { "question": "Derive v = u + at.", "student_answer": "Acceleration is velocity by time, so v = u + at.", "marks": "5", }, }, ) assert response.status_code == 200 output = response.json()["output"] assert output["source_truth"] == SOURCE_ANSWER_NOT_FOUND assert output["corrected_board_answer"] == SOURCE_ANSWER_NOT_FOUND assert output["source_evidence"] == [] def test_unknown_marks_asks_or_states_assumed_value() -> None: output = build_answer_correction_output( question="Derive v = u + at.", student_answer="Acceleration is velocity by time, so v = u + at.", subject="Physics", marks="not sure", ) assert "Assuming this is a 5-mark answer" in output["mark_scheme_assumption"] assert UNKNOWN_MARKS_PROMPT in output["mark_scheme_assumption"] assert output["score"].startswith("Estimated score:") def test_estimated_rubric_label_appears_when_no_official_scheme() -> None: output = build_answer_correction_output( question="Derive v = u + at.", student_answer="Acceleration is velocity by time.", subject="Physics", marks=5, ) assert output["official_scheme_available"] is False assert "estimated" in output["score"].lower() assert "Assuming" in output["mark_scheme_assumption"]