""" 身体年龄. This is the one calculation in the app that produces a health verdict out of thin air, so the properties that matter are: it never invents a number from missing inputs, it moves in the right direction, and it says what it did. """ import pytest from services import fitness_age as fa def est(**kwargs): base = {"age": 36, "sex": "male", "vo2max": 40, "resting_hr": 60, "bmi": 22.0} base.update(kwargs) return fa.estimate(**base) class TestMissingInputs: def test_no_age(self): r = est(age=None) assert r["value"] is None assert "出生日期" in r["missing"] def test_no_sex(self): r = est(sex=None) assert r["value"] is None assert "性别" in r["missing"] def test_unknown_sex_is_treated_as_missing(self): """'other' has no reference table; guessing one would be worse than saying the estimate cannot be made.""" r = est(sex="other") assert r["value"] is None assert "性别" in r["missing"] def test_no_vo2max(self): r = est(vo2max=None) assert r["value"] is None assert any("VO₂max" in m for m in r["missing"]) def test_missing_message_explains_how_to_get_vo2max(self): r = est(vo2max=None) assert "跑步" in " ".join(r["missing"]) def test_all_missing_are_listed_at_once(self): r = est(age=None, sex=None, vo2max=None) assert len(r["missing"]) == 3, "the user should fix everything in one go" def test_basis_is_returned_even_when_the_estimate_cannot_be_made(self): assert est(age=None)["basis"]["steps"] def test_optional_inputs_are_genuinely_optional(self): r = est(resting_hr=None, bmi=None) assert r["value"] is not None class TestDirection: def test_higher_vo2max_gives_a_younger_body_age(self): assert est(vo2max=45)["value"] < est(vo2max=33)["value"] def test_lower_resting_hr_gives_a_younger_body_age(self): assert est(resting_hr=48)["value"] < est(resting_hr=78)["value"] def test_bmi_outside_the_healthy_band_never_helps(self): healthy = est(bmi=22)["value"] assert est(bmi=31)["value"] >= healthy assert est(bmi=16)["value"] >= healthy def test_bmi_inside_the_band_does_not_move_it(self): assert est(bmi=18.5)["value"] == est(bmi=24.9)["value"] == est(bmi=22)["value"] def test_women_need_less_vo2max_for_the_same_body_age(self): """The reference distributions differ by sex; using one table for both would systematically flatter men and penalise women.""" assert est(sex="female", vo2max=37)["value"] < est(sex="male", vo2max=37)["value"] def test_monotonic_across_the_whole_range(self): values = [est(vo2max=v)["value"] for v in range(25, 55)] assert values == sorted(values, reverse=True), values class TestBounds: def test_never_exceeds_the_deviation_cap_downwards(self): r = est(age=60, vo2max=60, resting_hr=40, bmi=22) assert r["value"] >= 60 - fa.MAX_DEVIATION def test_never_exceeds_the_deviation_cap_upwards(self): r = est(age=30, vo2max=15, resting_hr=95, bmi=40) assert r["value"] <= 30 + fa.MAX_DEVIATION def test_floor(self): assert est(age=25, vo2max=70)["value"] >= fa.AGE_FLOOR def test_ceiling(self): assert est(age=84, vo2max=12, resting_hr=100, bmi=45)["value"] <= fa.AGE_CEILING def test_clamping_is_disclosed(self): """A clamped number is not the raw result, and the UI says so — that only works if the flag is set.""" assert est(age=60, vo2max=75, resting_hr=40)["clamped"] is True def test_an_ordinary_result_is_not_flagged_as_clamped(self): assert est(age=40, vo2max=37, resting_hr=60, bmi=22)["clamped"] is False def test_rhr_adjustment_is_capped(self): """Without the cap a very high resting heart rate alone would drive the whole estimate.""" moderate = est(resting_hr=90)["value"] extreme = est(resting_hr=200)["value"] assert extreme - moderate <= 2 def test_bmi_adjustment_is_capped(self): assert est(bmi=60)["value"] - est(bmi=32)["value"] <= 3 class TestExtrapolation: def test_above_the_youngest_reference_row_keeps_extrapolating(self): """A hard floor here put everyone fitter than the median 25-year-old at exactly the same body age — a cliff right where this app's users sit.""" a = fa._interpolate_age(46, fa.VO2_MEDIAN["male"]) b = fa._interpolate_age(50, fa.VO2_MEDIAN["male"]) assert a > b, "a fitter reading must still move the number" def test_below_the_oldest_row_keeps_extrapolating(self): old = fa._interpolate_age(20, fa.VO2_MEDIAN["male"]) assert old > fa.VO2_MEDIAN["male"][-1][0] def test_interpolates_between_rows(self): table = fa.VO2_MEDIAN["male"] # 42.5 sits midway between the 25-year (44) and 35-year (41) rows. age = fa._interpolate_age(42.5, table) assert 29 < age < 31 def test_reference_tables_decrease_with_age(self): for sex, table in fa.VO2_MEDIAN.items(): ages = [a for a, _ in table] vo2s = [v for _, v in table] assert ages == sorted(ages), sex assert vo2s == sorted(vo2s, reverse=True), sex class TestDamping: """Individual VO2max spread (SD ~7) dwarfs the age decline (~0.35/yr), so an undamped estimate reads 20 for anyone reasonably fit — which is what it did, and why a 34-year-old with VO2max 46 was told he was 21.""" def test_a_fit_thirtysomething_is_not_told_he_is_twenty(self): r = fa.estimate(age=34, sex="male", vo2max=46, resting_hr=62, bmi=26.0) assert r["value"] >= 25, r def test_damping_pulls_towards_the_real_age(self): r = est(age=34, vo2max=46) base = r["steps"][0] assert base["raw"] < base["years"] < 34 def test_the_raw_value_is_still_reported(self): """Damping is a choice, so the undamped figure stays visible rather than being quietly replaced.""" base = est()["steps"][0] assert "raw" in base and "damping" in base def test_an_average_person_lands_near_their_real_age(self): r = fa.estimate(age=35, sex="male", vo2max=41, resting_hr=60, bmi=22) assert abs(r["delta"]) <= 1, r def test_damping_does_not_flip_the_direction(self): assert est(vo2max=48)["value"] < est(vo2max=32)["value"] def test_basis_discloses_the_damping_factor(self): step = fa.BASIS["steps"][0] assert "阻尼" in step["detail"] class TestTransparency: def test_every_step_is_reported(self): r = est() labels = [s["label"] for s in r["steps"]] assert "VO₂max 基准" in labels assert "静息心率" in labels assert "BMI" in labels def test_steps_carry_the_input_they_used(self): r = est(resting_hr=59) hr = next(s for s in r["steps"] if s["label"] == "静息心率") assert "59" in hr["input"] def test_omitted_inputs_produce_no_step(self): r = est(resting_hr=None, bmi=None) assert [s["label"] for s in r["steps"]] == ["VO₂max 基准"] def test_delta_matches_the_reported_ages(self): r = est(age=36, vo2max=42) assert r["delta"] == r["value"] - r["chronologicalAge"] def test_basis_names_a_source_for_every_step(self): for step in fa.BASIS["steps"]: assert step["source"], step def test_basis_states_this_is_not_garmins_number(self): assert "Garmin" in fa.BASIS["summary"] def test_basis_carries_a_medical_caveat(self): assert "医" in fa.BASIS["caveat"] def test_basis_constants_match_the_code(self): """The prose describes the arithmetic; if someone tunes a constant the description must not silently keep the old number.""" rhr = next(s for s in fa.BASIS["steps"] if "心率" in s["name"]) assert f"{fa.RHR_REFERENCE:.0f}" in rhr["detail"] bmi = next(s for s in fa.BASIS["steps"] if "BMI" in s["name"]) assert str(fa.BMI_LOW) in bmi["detail"] and str(fa.BMI_HIGH) in bmi["detail"] class TestEndpoint: def test_requires_auth(self, client): assert client.get("/api/health/fitness-age").status_code == 401 def test_reports_what_is_missing_for_an_empty_profile(self, client, auth): body = client.get("/api/health/fitness-age", headers=auth).get_json() assert body["value"] is None assert body["missing"] def test_uses_the_saved_profile_and_the_latest_readings( self, client, auth, user, db, seed_health ): client.put("/api/settings", headers=auth, json={"birthDate": "1990-05-04", "sex": "male", "heightCm": 178, "weightKg": 72}) seed_health([{"date": "2026-08-20", "heart_rate": 58}]) db.execute( "UPDATE health_data SET vo2max = ? WHERE user_id = ?", [42, user["id"]] ) body = client.get("/api/health/fitness-age", headers=auth).get_json() assert body["value"] is not None, body assert body["chronologicalAge"] == 36 def test_falls_back_to_an_older_vo2max_reading( self, client, auth, user, db, seed_health ): """VO2max only refreshes after an outdoor run, so the last recorded value is still the current one.""" client.put("/api/settings", headers=auth, json={"birthDate": "1990-05-04", "sex": "male"}) seed_health([{"date": "2026-06-01"}, {"date": "2026-08-20"}]) db.execute( "UPDATE health_data SET vo2max = ? WHERE user_id = ? AND date = ?", [44, user["id"], "2026-06-01"], ) body = client.get("/api/health/fitness-age", headers=auth).get_json() assert body["value"] is not None, "an old VO2max still counts"