feat(campaigns): 周期对比纳入 Markdown 导出,活动导出排版重优化
- 导出端点解析评测对象名与 completed 周期对比行(基线元信息、分析模型、现算机械 diff),渲染 `## 周期对比` 附录(趋势 + 指标变化表 + 问题演变 + 建议落实),紧跟智能分析之后;非 completed 则完全无痕 - 头部排版重优化:状态中文化、窗口与时段人类可读(24h、0h–1h)、友好时间戳、头部补评测对象名、「正式线」/「加速调试线 ×N」措辞(加速线附注压缩后实际耗时);Run 级导出不动 - 测试:渲染器黄金断言更新 + 附录/头部/缺省用例,集成测试新增导出含对比、无对比行、failed 行三例
This commit is contained in:
parent
dd3b9a5e91
commit
14b09e1ac6
@ -7,6 +7,7 @@ show) belong to generation; this module only formats.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
from datetime import datetime
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from jinja2 import Template
|
from jinja2 import Template
|
||||||
@ -213,25 +214,164 @@ def _render_analysis_lines(analysis: dict[str, Any], scenario_names: dict[str, s
|
|||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
_CAMPAIGN_STATUS_LABELS = {
|
||||||
|
"planned": "计划中", "running": "进行中", "completed": "已完成",
|
||||||
|
"cancelled": "已取消", "failed": "失败",
|
||||||
|
}
|
||||||
|
_TREND_LABELS = {"improving": "改善", "stable": "平稳", "regressing": "退化"}
|
||||||
|
_EVOLUTION_LABELS = {"new": "新增", "persisting": "持续", "resolved": "消解"}
|
||||||
|
_TRACKING_LABELS = {"addressed": "已落实", "partial": "部分落实", "unaddressed": "未落实", "new": "新增"}
|
||||||
|
|
||||||
|
|
||||||
|
def _offset(seconds: float) -> str:
|
||||||
|
"""Human-readable window offset: 86400 → 24h, 1800 → 30m, 45 → 45s."""
|
||||||
|
s = int(round(seconds))
|
||||||
|
if s % 3600 == 0:
|
||||||
|
return f"{s // 3600}h"
|
||||||
|
if s % 60 == 0:
|
||||||
|
return f"{s // 60}m"
|
||||||
|
return f"{s}s"
|
||||||
|
|
||||||
|
|
||||||
|
def _dt(iso: Optional[str]) -> str:
|
||||||
|
"""ISO timestamp → `2026-08-01 08:00`; unparseable input passes through."""
|
||||||
|
if not iso:
|
||||||
|
return "-"
|
||||||
|
try:
|
||||||
|
return datetime.fromisoformat(iso.replace("Z", "+00:00")).strftime("%Y-%m-%d %H:%M")
|
||||||
|
except ValueError:
|
||||||
|
return iso
|
||||||
|
|
||||||
|
|
||||||
|
def _diff_cell(metric: str, pair: dict[str, Any]) -> str:
|
||||||
|
"""One metric cell of the diff table: `基线 → 本期(±delta)`."""
|
||||||
|
|
||||||
|
def _val(v: Optional[float]) -> str:
|
||||||
|
if v is None:
|
||||||
|
return "—"
|
||||||
|
return _ms(v) if metric == "avg_latency_ms" else _pct(v)
|
||||||
|
|
||||||
|
delta = pair.get("delta")
|
||||||
|
if delta is None:
|
||||||
|
delta_text = "—"
|
||||||
|
elif metric == "avg_latency_ms":
|
||||||
|
delta_text = f"{delta:+.1f}ms"
|
||||||
|
else:
|
||||||
|
delta_text = f"{delta * 100:+.1f}pp"
|
||||||
|
return f"{_val(pair.get('baseline'))} → {_val(pair.get('current'))}({delta_text})"
|
||||||
|
|
||||||
|
|
||||||
|
def _render_comparison_lines(comparison: dict[str, Any], scenario_names: dict[str, str]) -> list[str]:
|
||||||
|
"""Appendix for the campaign export: narrative + mechanical metric diff.
|
||||||
|
|
||||||
|
``comparison`` carries the completed narrative result plus context resolved
|
||||||
|
on the read path (baseline name/times, model name, generated-at, diff).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _name(sid: str) -> str:
|
||||||
|
return scenario_names.get(sid, sid[:8])
|
||||||
|
|
||||||
|
result = comparison.get("result") or {}
|
||||||
|
lines = ["", "## 周期对比", ""]
|
||||||
|
meta: list[str] = []
|
||||||
|
if comparison.get("baseline_name"):
|
||||||
|
base = f"基线:「{comparison['baseline_name']}」"
|
||||||
|
if comparison.get("baseline_completed_at"):
|
||||||
|
base += f"(完成于 {_dt(comparison['baseline_completed_at'])})"
|
||||||
|
meta.append(base)
|
||||||
|
if comparison.get("model_name"):
|
||||||
|
meta.append(f"分析模型:{comparison['model_name']}")
|
||||||
|
if comparison.get("updated_at"):
|
||||||
|
meta.append(f"生成于:{_dt(comparison['updated_at'])}")
|
||||||
|
if meta:
|
||||||
|
lines += [" · ".join(meta), ""]
|
||||||
|
|
||||||
|
trend = _TREND_LABELS.get(result.get("trend"), "平稳")
|
||||||
|
lines += [f"**趋势**:{trend} — {result.get('summary') or '—'}", ""]
|
||||||
|
|
||||||
|
diff = comparison.get("metric_diff")
|
||||||
|
if diff:
|
||||||
|
lines += [
|
||||||
|
"### 指标变化",
|
||||||
|
"",
|
||||||
|
"| 维度 | 通过率 | 可用性 | 平均时延 |",
|
||||||
|
"|------|------|------|------|",
|
||||||
|
]
|
||||||
|
|
||||||
|
def _row(label: str, block: dict[str, Any]) -> str:
|
||||||
|
return (
|
||||||
|
f"| {label} | {_diff_cell('pass_rate', block['pass_rate'])} | "
|
||||||
|
f"{_diff_cell('availability', block['availability'])} | "
|
||||||
|
f"{_diff_cell('avg_latency_ms', block['avg_latency_ms'])} |"
|
||||||
|
)
|
||||||
|
|
||||||
|
lines.append(_row("整窗(总体)", diff["overall"]))
|
||||||
|
for s in diff.get("scenarios") or []:
|
||||||
|
label = s.get("scenario_name") or _name(s.get("scenario_id", ""))
|
||||||
|
lines.append(_row(label, s))
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
evolution = result.get("problem_evolution") or []
|
||||||
|
if evolution:
|
||||||
|
lines += ["### 问题演变", ""]
|
||||||
|
for p in evolution:
|
||||||
|
status = _EVOLUTION_LABELS.get(p.get("status"), "持续")
|
||||||
|
names = "、".join(_name(sid) for sid in p.get("scenario_ids") or [])
|
||||||
|
head = f"- **[{status}] {p.get('title', '')}**"
|
||||||
|
if names:
|
||||||
|
head += f"(场景:{names})"
|
||||||
|
lines.append(head)
|
||||||
|
if p.get("detail"):
|
||||||
|
lines.append(f" {p['detail']}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
tracking = result.get("suggestion_tracking") or []
|
||||||
|
if tracking:
|
||||||
|
lines += ["### 建议落实情况", ""]
|
||||||
|
for t in tracking:
|
||||||
|
status = _TRACKING_LABELS.get(t.get("status"), "未落实")
|
||||||
|
lines.append(f"- **[{status}] {t.get('text', '')}**")
|
||||||
|
if t.get("note"):
|
||||||
|
lines.append(f" {t['note']}")
|
||||||
|
lines.append("")
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def _window_line(report: dict[str, Any]) -> str:
|
||||||
|
"""Human-readable window line with the 正式线 / 加速调试线 wording."""
|
||||||
|
window = report.get("window_seconds") or 0
|
||||||
|
scale = float(report.get("time_scale") or 1)
|
||||||
|
if scale == 1:
|
||||||
|
return f"**窗口**: {_offset(window)}(正式线)"
|
||||||
|
wall = f",压缩后实际耗时约 {_offset(window / scale)}"
|
||||||
|
return f"**窗口**: {_offset(window)}(加速调试线 ×{scale:g}{wall})"
|
||||||
|
|
||||||
|
|
||||||
def render_campaign_markdown(
|
def render_campaign_markdown(
|
||||||
report: dict[str, Any],
|
report: dict[str, Any],
|
||||||
*,
|
*,
|
||||||
analysis: Optional[dict[str, Any]] = None,
|
analysis: Optional[dict[str, Any]] = None,
|
||||||
|
comparison: Optional[dict[str, Any]] = None,
|
||||||
|
target_name: Optional[str] = None,
|
||||||
scenario_names: Optional[dict[str, str]] = None,
|
scenario_names: Optional[dict[str, str]] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Render a dual-axis campaign report dict as Markdown.
|
"""Render a dual-axis campaign report dict as Markdown.
|
||||||
|
|
||||||
``analysis`` is the stored 智能分析 result dict (completed only); when
|
``analysis`` and ``comparison`` are the stored 智能分析 / 周期对比 results
|
||||||
absent the export is identical to the pre-analysis format.
|
(completed only); when absent the corresponding appendix is omitted
|
||||||
|
entirely.
|
||||||
"""
|
"""
|
||||||
s = report["summary"]
|
s = report["summary"]
|
||||||
|
status = _CAMPAIGN_STATUS_LABELS.get(report.get("status"), report.get("status") or "-")
|
||||||
|
target = target_name or report.get("target_id") or "-"
|
||||||
lines: list[str] = [
|
lines: list[str] = [
|
||||||
f"# 活动周期报告 — {report['name']}",
|
f"# 活动周期报告 — {report['name']}",
|
||||||
"",
|
"",
|
||||||
f"**状态**: {report['status']} ",
|
f"**评测对象**: {target} ",
|
||||||
f"**窗口**: {report['window_seconds']}s(倍速 {report['time_scale']}) ",
|
f"**状态**: {status} ",
|
||||||
f"**开始时间**: {report['started_at'] or '-'} ",
|
_window_line(report) + " ",
|
||||||
f"**完成时间**: {report['completed_at'] or '-'} ",
|
f"**开始时间**: {_dt(report.get('started_at'))} ",
|
||||||
|
f"**完成时间**: {_dt(report.get('completed_at'))} ",
|
||||||
"",
|
"",
|
||||||
"## 汇总",
|
"## 汇总",
|
||||||
"",
|
"",
|
||||||
@ -245,12 +385,12 @@ def render_campaign_markdown(
|
|||||||
"",
|
"",
|
||||||
"## 时间趋势",
|
"## 时间趋势",
|
||||||
"",
|
"",
|
||||||
"| 时段(秒) | 运行数 | 通过率 | 可用性 | 时延 |",
|
"| 时段 | 运行数 | 通过率 | 可用性 | 时延 |",
|
||||||
"|------|------|------|------|------|",
|
"|------|------|------|------|------|",
|
||||||
]
|
]
|
||||||
for b in report["time_trend"]:
|
for b in report["time_trend"]:
|
||||||
lines.append(
|
lines.append(
|
||||||
f"| {b['start_seconds']:.0f}–{b['end_seconds']:.0f} | {b['run_count']} | "
|
f"| {_offset(b['start_seconds'])}–{_offset(b['end_seconds'])} | {b['run_count']} | "
|
||||||
f"{_pct(b['pass_rate'])} | {_pct(b['availability'])} | {_ms(b['avg_latency_ms'])} |"
|
f"{_pct(b['pass_rate'])} | {_pct(b['availability'])} | {_ms(b['avg_latency_ms'])} |"
|
||||||
)
|
)
|
||||||
lines += [
|
lines += [
|
||||||
@ -267,4 +407,6 @@ def render_campaign_markdown(
|
|||||||
)
|
)
|
||||||
if analysis:
|
if analysis:
|
||||||
lines += _render_analysis_lines(analysis, scenario_names or {})
|
lines += _render_analysis_lines(analysis, scenario_names or {})
|
||||||
|
if comparison:
|
||||||
|
lines += _render_comparison_lines(comparison, scenario_names or {})
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|||||||
@ -134,9 +134,39 @@ async def get_campaign_report_markdown(campaign_id: str, session: Session = Depe
|
|||||||
scenario_names = {s.id: s.name for s in ScenarioRepository(session).list_all()}
|
scenario_names = {s.id: s.name for s in ScenarioRepository(session).list_all()}
|
||||||
analysis_row = CampaignAnalysisRepository(session).get_by_campaign(campaign_id)
|
analysis_row = CampaignAnalysisRepository(session).get_by_campaign(campaign_id)
|
||||||
analysis = analysis_row.get_result() if analysis_row and analysis_row.status == "completed" else None
|
analysis = analysis_row.get_result() if analysis_row and analysis_row.status == "completed" else None
|
||||||
|
target = TargetRepository(session).get(campaign.target_id)
|
||||||
|
target_name = target.name if target else None
|
||||||
|
|
||||||
|
comparison = None
|
||||||
|
cmp_row = CampaignPeriodComparisonRepository(session).get_by_campaign(campaign_id)
|
||||||
|
if cmp_row is not None and cmp_row.status == "completed" and cmp_row.get_result():
|
||||||
|
baseline = CampaignRepository(session).get(cmp_row.baseline_campaign_id)
|
||||||
|
metric_diff = (
|
||||||
|
compute_metric_diff(
|
||||||
|
build_campaign_report_dict(baseline, session),
|
||||||
|
build_campaign_report_dict(campaign, session),
|
||||||
|
)
|
||||||
|
if baseline is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
model_cfg = ModelConfigRepository(session).get(cmp_row.model_config_id) if cmp_row.model_config_id else None
|
||||||
|
model_label = f"{model_cfg.name}({model_cfg.model_name})" if model_cfg and model_cfg.model_name else (
|
||||||
|
model_cfg.name if model_cfg else None
|
||||||
|
)
|
||||||
|
comparison = {
|
||||||
|
"result": cmp_row.get_result(),
|
||||||
|
"baseline_name": baseline.name if baseline else None,
|
||||||
|
"baseline_completed_at": iso_utc(baseline.completed_at) if baseline else None,
|
||||||
|
"model_name": model_label,
|
||||||
|
"updated_at": iso_utc(cmp_row.updated_at),
|
||||||
|
"metric_diff": metric_diff,
|
||||||
|
}
|
||||||
|
|
||||||
md = render_campaign_markdown(
|
md = render_campaign_markdown(
|
||||||
generate_campaign_report(campaign, runs, scenario_names=scenario_names),
|
generate_campaign_report(campaign, runs, scenario_names=scenario_names),
|
||||||
analysis=analysis,
|
analysis=analysis,
|
||||||
|
comparison=comparison,
|
||||||
|
target_name=target_name,
|
||||||
scenario_names=scenario_names,
|
scenario_names=scenario_names,
|
||||||
)
|
)
|
||||||
return Response(
|
return Response(
|
||||||
|
|||||||
@ -407,3 +407,60 @@ async def test_post_comparison_rerun_upserts_without_new_row(client, seeded_db,
|
|||||||
select(CampaignPeriodComparisonDB).where(CampaignPeriodComparisonDB.campaign_id == current_id)
|
select(CampaignPeriodComparisonDB).where(CampaignPeriodComparisonDB.campaign_id == current_id)
|
||||||
).all()
|
).all()
|
||||||
assert len(rows) == 1
|
assert len(rows) == 1
|
||||||
|
|
||||||
|
|
||||||
|
# ── Markdown 导出纳入周期对比 ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
async def test_markdown_export_includes_completed_comparison(client, seeded_db):
|
||||||
|
_seed_analysis_default(seeded_db)
|
||||||
|
baseline_id, current_id = await _two_period_setup(client, seeded_db)
|
||||||
|
_seed_run(seeded_db, "run-base", baseline_id, pass_rate=0.5, latency=800.0)
|
||||||
|
_seed_run(seeded_db, "run-cur", current_id, pass_rate=0.9, latency=500.0)
|
||||||
|
_complete_comparison_row(seeded_db, current_id, baseline_id)
|
||||||
|
|
||||||
|
resp = await client.get(f"/api/campaigns/{current_id}/report/markdown")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
md = resp.text
|
||||||
|
|
||||||
|
# 排版重优化:头部评测对象名、中文化状态、正式线窗口
|
||||||
|
assert "**评测对象**: mock-target" in md
|
||||||
|
assert "**状态**: 已完成" in md
|
||||||
|
assert "**窗口**: 24h(正式线)" in md
|
||||||
|
|
||||||
|
# 周期对比附录:元信息 + 趋势 + 机械 diff 表
|
||||||
|
assert "## 周期对比" in md
|
||||||
|
assert "基线:「上期」" in md
|
||||||
|
assert "分析模型:analysis-cfg(m)" in md
|
||||||
|
assert "**趋势**:改善 — 通过率提升" in md
|
||||||
|
assert "| 整窗(总体) | 50.0% → 90.0%(+40.0pp) | 100.0% → 100.0%(+0.0pp) | 800ms → 500ms(-300.0ms) |" in md
|
||||||
|
assert "**[消解] 答非所问**(场景:mock-scenario)" in md
|
||||||
|
assert "**[已落实] 保持**" in md
|
||||||
|
|
||||||
|
# 智能分析附录在前,周期对比紧随其后
|
||||||
|
assert md.index("## 智能分析") < md.index("## 周期对比")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_markdown_export_without_comparison_row_has_no_section(client, seeded_db):
|
||||||
|
_seed_analysis_default(seeded_db)
|
||||||
|
_, current_id = await _two_period_setup(client, seeded_db)
|
||||||
|
_seed_run(seeded_db, "run-cur", current_id, pass_rate=0.9, latency=500.0)
|
||||||
|
|
||||||
|
resp = await client.get(f"/api/campaigns/{current_id}/report/markdown")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert "周期对比" not in resp.text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_markdown_export_skips_non_completed_comparison(client, seeded_db):
|
||||||
|
_seed_analysis_default(seeded_db)
|
||||||
|
baseline_id, current_id = await _two_period_setup(client, seeded_db)
|
||||||
|
seeded_db.add(CampaignPeriodComparisonDB(
|
||||||
|
campaign_id=current_id, baseline_campaign_id=baseline_id,
|
||||||
|
status="failed", model_config_id="mc-1", triggered_by="manual",
|
||||||
|
error="boom",
|
||||||
|
))
|
||||||
|
seeded_db.commit()
|
||||||
|
|
||||||
|
resp = await client.get(f"/api/campaigns/{current_id}/report/markdown")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert "周期对比" not in resp.text
|
||||||
|
|||||||
@ -184,7 +184,7 @@ def test_render_campaign_markdown_summary_and_axes():
|
|||||||
def test_render_campaign_markdown_empty_bucket_dashes():
|
def test_render_campaign_markdown_empty_bucket_dashes():
|
||||||
md = render_campaign_markdown(_campaign_report())
|
md = render_campaign_markdown(_campaign_report())
|
||||||
# bucket 1 has no runs: pass_rate/availability/latency all render as —
|
# bucket 1 has no runs: pass_rate/availability/latency all render as —
|
||||||
assert "| 3600–7200 | 0 | — | — | — |" in md
|
assert "| 1h–2h | 0 | — | — | — |" in md
|
||||||
|
|
||||||
|
|
||||||
def _analysis() -> dict:
|
def _analysis() -> dict:
|
||||||
@ -246,3 +246,103 @@ def test_render_campaign_markdown_analysis_falls_back_to_id_prefix():
|
|||||||
def test_render_campaign_markdown_without_analysis_unchanged():
|
def test_render_campaign_markdown_without_analysis_unchanged():
|
||||||
md = render_campaign_markdown(_campaign_report())
|
md = render_campaign_markdown(_campaign_report())
|
||||||
assert "智能分析" not in md
|
assert "智能分析" not in md
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_campaign_markdown_header_readability():
|
||||||
|
md = render_campaign_markdown(_campaign_report(), target_name="客服机器人")
|
||||||
|
assert "**评测对象**: 客服机器人" in md
|
||||||
|
assert "**状态**: 已完成" in md
|
||||||
|
assert "**窗口**: 2h(正式线)" in md
|
||||||
|
assert "**开始时间**: 2026-07-30 00:00" in md
|
||||||
|
assert "**完成时间**: 2026-07-30 02:00" in md
|
||||||
|
assert "| 0h–1h | 2 | 75.0% | 100.0% | 150ms |" in md
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_campaign_markdown_header_falls_back_to_target_id():
|
||||||
|
md = render_campaign_markdown(_campaign_report())
|
||||||
|
assert "**评测对象**: t-1" in md
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_campaign_markdown_accelerated_line():
|
||||||
|
report = _campaign_report()
|
||||||
|
report["window_seconds"] = 86400
|
||||||
|
report["time_scale"] = 4.0
|
||||||
|
md = render_campaign_markdown(report)
|
||||||
|
assert "**窗口**: 24h(加速调试线 ×4,压缩后实际耗时约 6h)" in md
|
||||||
|
|
||||||
|
|
||||||
|
def _comparison() -> dict:
|
||||||
|
"""A hand-built dict matching the comparison context the export endpoint resolves."""
|
||||||
|
return {
|
||||||
|
"result": {
|
||||||
|
"trend": "regressing",
|
||||||
|
"summary": "整体质量下滑,售后场景恶化",
|
||||||
|
"problem_evolution": [
|
||||||
|
{
|
||||||
|
"status": "persisting",
|
||||||
|
"title": "售后答非所问",
|
||||||
|
"scenario_ids": ["s-1"],
|
||||||
|
"detail": "问题仍未收敛",
|
||||||
|
},
|
||||||
|
{"status": "resolved", "title": "响应偏慢", "scenario_ids": [], "detail": ""},
|
||||||
|
],
|
||||||
|
"suggestion_tracking": [
|
||||||
|
{"status": "partial", "text": "补充售后知识库", "note": "仅覆盖部分问题"},
|
||||||
|
{"status": "new", "text": "新增建议:监控时延", "note": ""},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"baseline_name": "上一期巡检",
|
||||||
|
"baseline_completed_at": "2026-07-29T02:00:00+00:00",
|
||||||
|
"model_name": "qwen-max",
|
||||||
|
"updated_at": "2026-07-30T03:00:00+00:00",
|
||||||
|
"metric_diff": {
|
||||||
|
"overall": {
|
||||||
|
"pass_rate": {"baseline": 0.8, "current": 0.75, "delta": -0.05},
|
||||||
|
"availability": {"baseline": 1.0, "current": 0.5, "delta": -0.5},
|
||||||
|
"avg_latency_ms": {"baseline": 120.0, "current": 150.0, "delta": 30.0},
|
||||||
|
},
|
||||||
|
"scenarios": [
|
||||||
|
{
|
||||||
|
"scenario_id": "s-1",
|
||||||
|
"scenario_name": "售后场景",
|
||||||
|
"pass_rate": {"baseline": 0.7, "current": 0.6, "delta": -0.1},
|
||||||
|
"availability": {"baseline": None, "current": None, "delta": None},
|
||||||
|
"avg_latency_ms": {"baseline": 100.0, "current": 140.0, "delta": 40.0},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_campaign_markdown_appends_comparison_section():
|
||||||
|
md = render_campaign_markdown(
|
||||||
|
_campaign_report(),
|
||||||
|
comparison=_comparison(),
|
||||||
|
scenario_names={"s-1": "售后场景"},
|
||||||
|
)
|
||||||
|
assert "## 周期对比" in md
|
||||||
|
assert "基线:「上一期巡检」(完成于 2026-07-29 02:00)" in md
|
||||||
|
assert "分析模型:qwen-max" in md
|
||||||
|
assert "生成于:2026-07-30 03:00" in md
|
||||||
|
assert "**趋势**:退化 — 整体质量下滑,售后场景恶化" in md
|
||||||
|
assert "### 指标变化" in md
|
||||||
|
assert "| 整窗(总体) | 80.0% → 75.0%(-5.0pp) | 100.0% → 50.0%(-50.0pp) | 120ms → 150ms(+30.0ms) |" in md
|
||||||
|
assert "| 售后场景 | 70.0% → 60.0%(-10.0pp) | — → —(—) | 100ms → 140ms(+40.0ms) |" in md
|
||||||
|
assert "### 问题演变" in md
|
||||||
|
assert "**[持续] 售后答非所问**(场景:售后场景)" in md
|
||||||
|
assert " 问题仍未收敛" in md
|
||||||
|
assert "**[消解] 响应偏慢**" in md
|
||||||
|
assert "### 建议落实情况" in md
|
||||||
|
assert "**[部分落实] 补充售后知识库**" in md
|
||||||
|
assert " 仅覆盖部分问题" in md
|
||||||
|
assert "**[新增] 新增建议:监控时延**" in md
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_campaign_markdown_comparison_comes_after_analysis():
|
||||||
|
md = render_campaign_markdown(_campaign_report(), analysis=_analysis(), comparison=_comparison())
|
||||||
|
assert md.index("## 智能分析") < md.index("## 周期对比")
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_campaign_markdown_without_comparison_unchanged():
|
||||||
|
md = render_campaign_markdown(_campaign_report())
|
||||||
|
assert "周期对比" not in md
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user