feat(report): 形象深度诊断全链路——POST /reports/{id}/full 派生 tier=full 报告,复用 basic 输入与 32 维结果;full 流水线(无视觉阶段)LLM 扩写四大块(十二季定位/规则色板/4场景穿搭/30天计划),三重校验+语义重试+规则兜底降级;删除终身一次硬校验(40920 下线,重复生成由配额兜底);validation 枚举一致性校验放宽(十二季词表);迁移 0003 SSOT 补 full_report 规则行
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
"""深度诊断色板规则映射与降级模板的纯函数单测(无 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)
|
||||
Reference in New Issue
Block a user