| 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"] |
|
|