# -*- coding: utf-8 -*- """tests/docgen/test_templates.py —— 内置模版系统验收用例(DC-UNIT-TPL-*)。 2026-09-04 手工测试需求2:套用模版生成可预览 HTML。 口径:公开函数/文件系统断言;全部离线(不依赖 LLM/网络/vstore 服务)。 执行:bash tests/docgen/run_all.sh unit """ from __future__ import annotations import json import re import sys from pathlib import Path import pytest REPO = Path(__file__).resolve().parents[2] sys.path.insert(0, str(REPO)) sys.path.insert(0, str(REPO / "sdk")) from app.services.docgen import templating # noqa: E402 pytestmark = pytest.mark.unit # Story 禁用属性(双渲染纪律:浏览器与 PyMuPDF Story 共用 css) BANNED_PATTERNS = ( re.compile(r"position\s*:", re.IGNORECASE), re.compile(r"display\s*:\s*(flex|grid)", re.IGNORECASE), re.compile(r"transform\s*:", re.IGNORECASE), re.compile(r"@media", re.IGNORECASE), re.compile(r"@page\s*\{[^}]*margin", re.IGNORECASE), re.compile(r"var\(--", re.IGNORECASE), re.compile(r"[vhvw]h\b|[vhvw]w\b", re.IGNORECASE), ) class TestTemplates: def test_DC_UNIT_TPL_01_registry_loads(self): registry = templating.load_registry() assert isinstance(registry, dict) entries = registry.get("templates") or [] keys = [e.get("key") for e in entries if isinstance(e, dict)] assert len(keys) == 7 assert set(keys) == { "official", "report", "application", "contract", "speech", "plan", "document", } assert registry.get("fallback") == "document" @pytest.mark.parametrize( "text,expected", [ ("帮我写一份会议通知", "official"), ("起草一份请示", "official"), ("写一个调研报告", "report"), ("拟一份年度工作总结", "report"), ("写一份项目申报书", "application"), ("起草一份采购合同", "contract"), ("写个讲话稿", "speech"), ("写一份感谢信", "speech"), ("制定一个实施方案", "plan"), ("写个说明书", "document"), ("写一篇论文", "document"), ("", "document"), ], ) def test_DC_UNIT_TPL_02_classify(self, text, expected): assert templating.classify_doc_type(text) == expected def test_DC_UNIT_TPL_03_render_html_structure(self): html, css = templating.render_html( "# 关于安全生产的通知\n\n## 一、总体要求\n\n正文内容。", title="关于安全生产的通知", key="official", ) assert "
" in html and "
" in html assert 'class="doc-title"' in html assert "关于安全生产的通知" in html assert "正文内容" in html assert "{{TITLE}}" not in html assert css and "doc-table" in css or True def test_DC_UNIT_TPL_04_style_hints_overlay(self): hints = { "body_font": "宋体", "body_size": 14, "headings": {"1": {"font": "黑体", "size": 18}}, } _, css = templating.render_html( "# 标题\n\n正文。", title="t", key="report", style_hints=hints ) assert "宋体" in css assert "font-size: 14.0pt" in css assert "font-size: 18.0pt" in css def test_DC_UNIT_TPL_05_never_raises(self): for markdown in ("", "| a | b |\n|---|---|", "```\nfence\n```", "### 标题"): html, css = templating.render_html(markdown, title="", key="document") assert isinstance(html, str) and isinstance(css, str) # 乱 key 回落 fallback html, _ = templating.render_html("x", title="", key="nonexistent-key") assert "
" in html def test_DC_UNIT_TPL_06_css_story_safe(self): tpl_dir = templating.templates_dir() for css_file in sorted(tpl_dir.glob("*.css")): content = css_file.read_text(encoding="utf-8") # 剥离注释后再扫(注释里的禁用词不算违规) content = re.sub(r"/\*[\s\S]*?\*/", "", content) for pattern in BANNED_PATTERNS: assert not pattern.search(content), ( f"{css_file.name} 含 Story 禁用属性:{pattern.pattern}" ) def test_DC_UNIT_TPL_07_wrapper_single_main_xml(self): tpl_dir = templating.templates_dir() for html_file in sorted(tpl_dir.glob("*.html")): content = html_file.read_text(encoding="utf-8") assert content.count("
") == 1, f"{html_file.name} main 注入点不唯一" assert "" in content # main 必须为空注入点(渲染时整体替换) m = re.search(r"
\s*
", content) assert m is not None, f"{html_file.name} main 非空注入点" def test_DC_UNIT_TPL_08_tables_marked(self): html, _ = templating.render_html( "| 列一 | 列二 |\n| --- | --- |\n| 值1 | 值2 |", title="t", key="document", ) assert 'class="doc-table"' in html def test_DC_UNIT_TPL_09_letterhead(self): # 2026-09-04:红头文字可编辑持久化——letterhead 参数覆盖,空回落模版默认 html, _ = templating.render_html( "# 标题\n\n正文。", title="t", key="official", letterhead="某某农业科学研究院" ) assert "某某农业科学研究院" in html assert "红头文件" not in html # 空 letterhead → 模版默认 html2, _ = templating.render_html("# 标题", title="t", key="official") assert "红头文件" in html2 # 无红头区模板(contract):letterhead 不影响 html3, _ = templating.render_html("# 标题", title="t", key="contract", letterhead="x") assert "{{LETTERHEAD}}" not in html3 @pytest.mark.parametrize( "text,expected", [ ("帮我写一个十一放假通知,红头用三亚海古纪生物科技有限公司", "三亚海古纪生物科技有限公司"), ("红头:某市农业农村局,写一份通知", "某市农业农村局"), ("写一份通知,红头为某某研究所", "某某研究所"), ("帮我写一份普通通知", ""), ], ) def test_DC_UNIT_TPL_10_letterhead_prompt_extract(self, text, expected): # 2026-09-04:prompt「红头用XX」→ 生成即采用指定红头 got = templating.extract_letterhead_from_prompt(text) if expected: assert got == expected else: assert got == ""