# -*- 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 == ""