feat(writer): docx 注入原型提前(T17 架构审查整改,OV8)

- T17 (OV8, P1): 新建 src/genesis/writer/ 包
  - docx_injector.py: DocxInjector 用原生 python-docx 实现 §6.6 占位符注入
    - 章节级 {{section:id}} → 内容块 docx 元素序列(heading/paragraph/table)
    - 行内 {{meta}} → 元信息填充
    - 残留检查: 未替换 {{...}} 抛 DocxInjectError
    - 格式精度: 注入 heading 继承模板 Heading 样式,原内容不被破坏
  - 新增 test_docx_injector.py(5 用例)
  - 同步 design.md §6.7 渲染链路 T17 原型说明
- TDD: RED(模块缺失)→ GREEN(聚焦 5 passed)→ 全量 245 passed / 100.00%(1361 stmts/340 br)
This commit is contained in:
lhl
2026-08-12 22:47:43 +08:00
parent d2e651bad0
commit e3c714d5d9
5 changed files with 236 additions and 0 deletions
+89
View File
@@ -0,0 +1,89 @@
"""docx 注入原型测试(T17,OV8)。
OV8 裁定:最难成功标准(格式精度)排关键路径末尾 → docx 注入原型提前验证。
本文件测试 DocxInjector:模板占位符替换(章节级/行内)、残留检查、格式精度
(注入 heading 继承模板 Heading 样式,原有内容样式不被破坏)。
"""
from __future__ import annotations
import pytest
from docx import Document
from genesis.writer.docx_injector import (
Block,
DocxInjectError,
DocxInjector,
)
def _make_template(tmp_path, body: str) -> str:
doc = Document()
doc.add_paragraph("{{doc_title}}") # 行内占位符
doc.add_paragraph(body) # 章节占位符所在段落
doc.add_paragraph("尾部固定内容")
path = tmp_path / "template.docx"
doc.save(str(path))
return str(path)
def _section_blocks() -> list[Block]:
return [
Block(kind="heading", text="3.1 テーブル一覧", level=2),
Block(kind="paragraph", text="以下がDB表定义です。"),
Block(kind="table", rows=[["テーブル", "説明"], ["TB001", "社員"]]),
]
# ---------- 章节级占位符替换 ----------
def test_section_placeholder_replaced(tmp_path):
tpl = _make_template(tmp_path, "{{section:db_design}}")
inj = DocxInjector(tpl)
out = inj.inject({"db_design": _section_blocks()}, {"doc_title": "概要設計書"})
full_text = "\n".join(p.text for p in out.paragraphs)
assert "{{section:db_design}}" not in full_text
assert "3.1 テーブル一覧" in full_text
assert "以下がDB表定义です。" in full_text
# ---------- 行内占位符替换 ----------
def test_inline_meta_replaced(tmp_path):
tpl = _make_template(tmp_path, "{{section:db_design}}")
inj = DocxInjector(tpl)
out = inj.inject({"db_design": []}, {"doc_title": "概要設計書"})
assert "{{doc_title}}" not in "\n".join(p.text for p in out.paragraphs)
assert "概要設計書" in "\n".join(p.text for p in out.paragraphs)
# ---------- 占位符残留检查 ----------
def test_residue_detection_raises(tmp_path):
tpl = _make_template(tmp_path, "{{section:unknown_chapter}}")
inj = DocxInjector(tpl)
with pytest.raises(DocxInjectError, match="残留"):
inj.inject({}, {"doc_title": "X"})
# ---------- 格式精度:heading 继承模板样式 ----------
def test_heading_inherits_template_style(tmp_path):
tpl = _make_template(tmp_path, "{{section:db_design}}")
inj = DocxInjector(tpl)
out = inj.inject({"db_design": _section_blocks()}, {"doc_title": "T"})
# 注入的 heading blocklevel=2)应渲染为模板中存在的 Heading 2 样式段落
heading_paras = [p for p in out.paragraphs if p.style.name == "Heading 2"]
assert any("3.1 テーブル一覧" in p.text for p in heading_paras)
def test_original_content_style_preserved(tmp_path):
tpl = _make_template(tmp_path, "{{section:db_design}}")
inj = DocxInjector(tpl)
out = inj.inject({"db_design": []}, {"doc_title": "T"})
# 模板原有段落(尾部固定内容)在注入后仍存在且未被破坏
assert any("尾部固定内容" in p.text for p in out.paragraphs)