"""确定性视觉提供者:同输入同输出、问卷驱动、枚举合法、工厂拒绝未知名。""" from __future__ import annotations import pytest from packages.report.dimensions import DIMENSION_BY_KEY from packages.report.vision import ( BODY_SHAPES, FACE_SHAPES, SEASON_CODES, SKIN_TONES, STYLE_TYPES, DeterministicVisionProvider, VisionProvider, build_vision_provider, dimensions_touched, ) from packages.shared.config import Settings from packages.shared.errors import AppError, ErrorCode _PHOTO = {"object_key": "upload/report/photo-001.jpg"} _QUESTIONNAIRE = { "gender": "女", "height": 165, "weight": 50, "birthday": "1998-06-01", } async def test_same_input_same_output() -> None: provider = DeterministicVisionProvider() first = await provider.analyze(_PHOTO, _QUESTIONNAIRE) second = await provider.analyze(dict(_PHOTO), dict(_QUESTIONNAIRE)) assert first == second async def test_satisfies_protocol() -> None: assert isinstance(DeterministicVisionProvider(), VisionProvider) async def test_shape_contract_and_enum_values() -> None: features = await DeterministicVisionProvider().analyze(_PHOTO, _QUESTIONNAIRE) expected_keys = { "body_shape", "body_features", "body_ratio", "face_shape", "face_proportions", "facial_details", "face_depth", "skin_lab", "skin_tone", "face_hue", "face_lightness", "face_chroma", "face_contrast", "season_code", "season_detailed", "volume", "curve_straight", "motion", "gene_style_scores", "hair_status", "hair_color", } assert set(features) == expected_keys assert features["body_shape"] in BODY_SHAPES assert features["face_shape"] in FACE_SHAPES assert features["skin_tone"] in SKIN_TONES assert features["season_code"] in SEASON_CODES assert set(features["gene_style_scores"]) == set(STYLE_TYPES) # 色彩轴连续值域与标签三档。 for axis in ("face_hue", "face_lightness", "face_chroma"): assert -1 <= features[axis]["value"] <= 1 assert features[axis]["label"] assert 0 <= features["face_depth"] <= 10 assert 0.58 <= features["body_ratio"] <= 0.68 async def test_different_questionnaire_drives_different_body_shape() -> None: provider = DeterministicVisionProvider() slim = await provider.analyze(_PHOTO, {"height": 165, "weight": 45}) # BMI≈16.5 heavy = await provider.analyze(_PHOTO, {"height": 165, "weight": 95}) # BMI≈34.9 assert slim["body_shape"] in ("A", "H") # 过瘦区间:梨/矩形 assert heavy["body_shape"] == "O" # 高 BMI:苹果形 assert slim["body_shape"] != heavy["body_shape"] async def test_questionnaire_self_report_overrides_hash() -> None: # §3.6 降级路径:问卷自评是真实信号,优先于 mock 哈希。 provider = DeterministicVisionProvider() plain = await provider.analyze(_PHOTO, {}) self_reported = await provider.analyze( _PHOTO, {"body_shape_self": "X", "face_shape_self": "圆", "skin_tone_self": "粉一白"}, ) assert self_reported["body_shape"] == "X" assert self_reported["face_shape"] == "圆" assert self_reported["skin_tone"] == "粉一白" # 自评肤色反推冷轴:粉系=冷 → 季型落在冷季(夏/冬)。 assert self_reported["season_code"] in ("summer", "winter") assert plain["season_code"] in SEASON_CODES async def test_different_photo_changes_features() -> None: provider = DeterministicVisionProvider() a = await provider.analyze({"object_key": "a.jpg"}, {}) b = await provider.analyze({"object_key": "b.jpg"}, {}) assert a != b async def test_missing_object_key_still_deterministic() -> None: provider = DeterministicVisionProvider() empty_meta: dict[str, str] = {} assert await provider.analyze(empty_meta, {}) == await provider.analyze({}, {}) def test_dimensions_touched_maps_to_dimension_keys() -> None: features = { "body_shape": "A", "skin_lab": {"l": 60.0, "a": 1.0, "b": 20.0}, # 中间量,不是维度 "gene_style_scores": {"自然": 9}, } touched = dimensions_touched(features) assert touched == ["body_shape", "gene_style"] assert all(key in DIMENSION_BY_KEY for key in touched) def test_factory_default_is_deterministic() -> None: settings = Settings(_env_file=None) provider = build_vision_provider(settings) assert isinstance(provider, DeterministicVisionProvider) def test_factory_rejects_unknown_provider() -> None: settings = Settings(_env_file=None, report_vision_provider="rtmpose") with pytest.raises(AppError) as excinfo: build_vision_provider(settings) assert excinfo.value.code == ErrorCode.CONFIG_ERROR assert excinfo.value.data["report_vision_provider"] == "rtmpose"