"""深度诊断色板规则映射与降级模板的纯函数单测(无 IO、无 LLM)。 红线:色板是规则映射常量——12 季型全覆盖、三档色数达标、hex 格式合法; 降级模板必须自带「通过三重校验」的性质(fail-close 的最后一道出口)。 """ from __future__ import annotations import re from packages.report.full_copy import ( TWELVE_SEASONS, FullReportCopy, build_full_result, palette_for, render_fallback_full_copy, resolve_season, ) from packages.report.validation import validate_copy _HEX_RE = re.compile(r"^#[0-9A-Fa-f]{6}$") def test_all_twelve_seasons_have_complete_palettes() -> None: assert len(TWELVE_SEASONS) == 12 for season in TWELVE_SEASONS: palette = palette_for(season) assert len(palette["best"]) >= 12, f"{season} 最佳色不足 12 色" assert len(palette["neutral"]) >= 6, f"{season} 中性色不足 6 色" assert len(palette["avoid"]) >= 6, f"{season} 避雷色不足 6 色" for role in ("best", "neutral", "avoid"): for color in palette[role]: assert color["name"], f"{season}/{role} 缺色名" assert _HEX_RE.match(color["hex"]), f"{season}/{role} 色值非法:{color['hex']}" def test_unknown_season_gets_empty_palette() -> None: assert palette_for("不存在的季型") == {"best": [], "neutral": [], "avoid": []} def test_resolve_season_prefers_detailed_value() -> None: assert resolve_season({"season_detailed": "柔夏", "season_code": "summer"}) == "柔夏" def test_resolve_season_falls_back_to_season_code() -> None: assert resolve_season({"season_code": "spring"}) == "暖春" assert resolve_season({"season_code": "winter"}) == "冷冬" assert resolve_season({"season_detailed": "编造季型", "season_code": "autumn"}) == "暖秋" def test_resolve_season_returns_none_without_any_input() -> None: assert resolve_season({}) is None assert resolve_season({"season_code": "unknown"}) is None def _minimal_dimensions() -> list[dict[str, object]]: return [ {"key": "season_code", "value": "summer", "status": "ok"}, {"key": "season_detailed", "value": "柔夏", "status": "ok"}, ] def test_fallback_full_copy_passes_validation() -> None: """降级模板自带合规性:含「柔夏型」等十二季文案也不过三重校验的误杀线。""" dimensions = _minimal_dimensions() fallback = render_fallback_full_copy( dimensions, season_detailed="柔夏", palette=palette_for("柔夏") ) violations = validate_copy(fallback.as_report_copy(), dimensions, rule_hits=[]) assert violations == [] def test_fallback_full_copy_covers_all_scenes_and_four_weeks() -> None: fallback = render_fallback_full_copy( _minimal_dimensions(), season_detailed="柔夏", palette=palette_for("柔夏") ) assert [s.key for s in fallback.scenes] == ["commute", "weekend", "formal", "date"] assert all(len(scene.outfits) >= 1 for scene in fallback.scenes) assert [w.week for w in fallback.plan30] == [1, 2, 3, 4] assert all(3 <= len(week.tasks) <= 5 for week in fallback.plan30) # 兜底文案不得出现色值(hex 只能走 full.palette 结构化字段)。 assert "#" not in fallback.as_report_copy().full_text() def test_build_full_result_shape() -> None: copy = render_fallback_full_copy( _minimal_dimensions(), season_detailed="柔夏", palette=palette_for("柔夏") ) result = build_full_result( copy, season_detailed="柔夏", season_code="summer", palette=palette_for("柔夏"), ) assert result["season"]["name_cn"] == "柔夏型" assert result["season"]["analysis"] == copy.season_analysis assert len(result["palette"]["best"]) >= 12 assert result["scenes"][0]["outfits"][0]["points"] assert result["plan30"][3]["week"] == 4 def test_as_report_copy_folds_all_free_text_into_scan_scope() -> None: """校验视图必须覆盖季型解析/场景/计划的全部自由文本,不是只查 summary。""" copy = FullReportCopy( summary="总评", sections=[], season_analysis="独一无二的季型解析", scenes=[], plan30=[], ) violations = validate_copy(copy.as_report_copy(), [], rule_hits=[]) assert any("独一无二" in v for v in violations)