113 lines
4.3 KiB
Python
113 lines
4.3 KiB
Python
"""深度诊断色板规则映射与降级模板的纯函数单测(无 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)
|