Files
2026Technology-Competition/tests/test_word_template_parser.py
T
lhl d9aa3a3325 fix(writer): 修复概要设计书输出塌缩与章间引用缺失,并补 parser 防灾
- Writer: 表格表头行/Table Grid 边框、列表 List Bullet/Number 样式、行内字符格式不再塌缩(外视 #6 反转)
- Writer: 打通章间引用(WriterState 摘要 → 后章 prompt prior_summaries)
- Writer: 删除 _chunk_source 死代码,章节数据经 DataGate 控 token 预算
- Writer: 章节结果落盘快照,命中即跳过 LLM(录制重拍可续跑,损坏快照自动忽略)
- Parser: 按 body 顺序遍历正文+单元格(含嵌套表、合并单元格去重),修复表格内锚点漏检导致的静默丢章
- Parser/服务层: .xls 显式拒绝(可操作提示),上传即校验扩展名,rules 对齐 docx-only
- 测试: 全量 680 通过,覆盖率 99.37%(红线 99%)
2026-09-15 21:30:55 +08:00

180 lines
6.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from genesis.data_models import ChapterMarker, ParsedTemplate
from genesis.parsers._word_common import heading_level
from genesis.parsers.word_template_parser import WordTemplateParser
from tests.docx_helpers import new_document, save_document
def test_heading_level_parses_numeric_suffix():
assert heading_level("Heading 1") == 1
assert heading_level("Heading 2") == 2
assert heading_level("Heading 3") == 3
def test_heading_level_fallback_on_invalid():
# 兜底分支(非数字 / 无后缀)→ 1,分支覆盖必须命中
assert heading_level("Heading X") == 1
assert heading_level("Heading") == 1
def test_parse_extracts_heading_levels(tmp_path):
doc = new_document()
doc.add_heading("1. はじめに", level=1)
doc.add_heading("2.1 画面遷移図", level=2)
doc.add_heading("2.1.1 詳細", level=3)
path = save_document(tmp_path, doc)
result = WordTemplateParser().parse(path)
assert isinstance(result, ParsedTemplate)
headings = [s for s in result.sections if s.type == "heading"]
assert [(s.name, s.level) for s in headings] == [
("1. はじめに", 1),
("2.1 画面遷移図", 2),
("2.1.1 詳細", 3),
]
def test_parse_extracts_bookmark(tmp_path):
from docx.oxml.ns import qn
doc = new_document()
para = doc.add_paragraph("アンカー")
bm_start = para._p.makeelement(qn("w:bookmarkStart"), {qn("w:id"): "0", qn("w:name"): "template_start"})
para._p.insert(0, bm_start)
# 无 name 的书签:覆盖 if name 假分支,应被跳过
bm_anon = para._p.makeelement(qn("w:bookmarkStart"), {qn("w:id"): "1"})
para._p.insert(1, bm_anon)
path = save_document(tmp_path, doc)
result = WordTemplateParser().parse(path)
bookmarks = [s for s in result.sections if s.type == "bookmark"]
assert [s.name for s in bookmarks] == ["template_start"]
def test_parse_extracts_placeholders(tmp_path):
doc = new_document()
doc.add_paragraph("{{doc_title}}")
doc.add_paragraph("{{section:introduction}}")
doc.add_paragraph("{{section:function_list}}")
path = save_document(tmp_path, doc)
result = WordTemplateParser().parse(path)
assert result.placeholders == {
"doc_title": "{{doc_title}}",
"section:introduction": "{{section:introduction}}",
"section:function_list": "{{section:function_list}}",
}
ph = [s for s in result.sections if s.type == "placeholder"]
assert [s.name for s in ph] == ["doc_title", "section:introduction", "section:function_list"]
def test_parse_invalid_placeholder_kept_as_text(tmp_path):
doc = new_document()
doc.add_paragraph("{{ invalid }}")
doc.add_paragraph("ただの {text}")
path = save_document(tmp_path, doc)
result = WordTemplateParser().parse(path)
assert result.placeholders == {}
assert [s for s in result.sections if s.type == "placeholder"] == []
def test_parse_empty_document(tmp_path):
doc = new_document()
path = save_document(tmp_path, doc)
result = WordTemplateParser().parse(path)
assert result.sections == []
assert result.placeholders == {}
assert "Normal" in result.styles["defined"]
def test_parse_styles_collected(tmp_path):
doc = new_document()
doc.add_heading("章", level=1)
doc.add_paragraph("本文")
path = save_document(tmp_path, doc)
result = WordTemplateParser().parse(path)
assert "Heading 1" in result.styles["used"]
assert "Normal" in result.styles["used"]
assert "Heading 1" in result.styles["defined"]
def test_parse_extracts_placeholder_case_insensitive(tmp_path):
# 宽容:键名大小写不敏感 → 归一为小写 section:id
doc = new_document()
doc.add_paragraph("{{Section:2}}")
path = save_document(tmp_path, doc)
result = WordTemplateParser().parse(path)
assert result.placeholders == {"section:2": "{{Section:2}}"}
ph = [s for s in result.sections if s.type == "placeholder"]
assert [s.name for s in ph] == ["section:2"]
def test_parse_extracts_placeholder_fullwidth_colon(tmp_path):
# 宽容:全角冒号(:)也视为分隔符 → 归一为半角 section:id
doc = new_document()
doc.add_paragraph("{{section2}}")
path = save_document(tmp_path, doc)
result = WordTemplateParser().parse(path)
assert result.placeholders == {"section:2": "{{section2}}"}
ph = [s for s in result.sections if s.type == "placeholder"]
assert [s.name for s in ph] == ["section:2"]
# ---------- P2-1:表格单元格内的占位符(此前漏检 → 静默丢章) ----------
def _doc_with_cell_placeholder():
"""H1 → 表格(单元格内含锚点) → H1。锚点必须归属其前的 H1。"""
doc = new_document()
doc.add_heading("1. はじめに", level=1)
tbl = doc.add_table(rows=1, cols=1)
tbl.rows[0].cells[0].paragraphs[0].text = "{{section:introduction}}"
doc.add_heading("2. 機能一覧", level=1)
doc.add_paragraph("{{section:function_list}}")
return doc
def test_parse_placeholder_inside_table_cell(tmp_path):
path = save_document(tmp_path, _doc_with_cell_placeholder())
result = WordTemplateParser().parse(path)
assert "section:introduction" in result.placeholders
ph = [s.name for s in result.sections if s.type == "placeholder"]
assert "section:introduction" in ph
def test_parse_table_placeholder_keeps_document_order(tmp_path):
"""单元格锚点须按文档顺序落位(在其前的 H1 与在其后的 H1 之间),不得错绑。"""
path = save_document(tmp_path, _doc_with_cell_placeholder())
result = WordTemplateParser().parse(path)
names = [(s.type, s.name) for s in result.sections]
i_intro = names.index(("heading", "1. はじめに"))
i_ph = names.index(("placeholder", "section:introduction"))
i_func = names.index(("heading", "2. 機能一覧"))
assert i_intro < i_ph < i_func
def test_parse_merged_cell_placeholder_not_duplicated(tmp_path):
"""合并单元格的 row.cells 会重复指向同一 tc → 不得重复收集。"""
doc = new_document()
doc.add_heading("1. 章", level=1)
tbl = doc.add_table(rows=1, cols=2)
tbl.rows[0].cells[0].merge(tbl.rows[0].cells[1])
tbl.rows[0].cells[0].paragraphs[0].text = "{{section:db_design}}"
path = save_document(tmp_path, doc)
result = WordTemplateParser().parse(path)
ph = [s.name for s in result.sections if s.type == "placeholder"]
assert ph.count("section:db_design") == 1