"""Step 6 聚合装配的快速回归测试(不调用 LLM)。""" import os import tempfile import unittest import zipfile import xml.etree.ElementTree as ET from docx import Document as DocxDocument from models import BidOutline from step6_exporting.assembler import ( _HeadingRecord, _build_index_table_from_template, _load_table_template, assemble_step5_document, ) _W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" def _w(tag: str) -> str: return f"{{{_W_NS}}}{tag}" def _element_text(element) -> str: return "".join(node.text or "" for node in element.iter(_w("t"))) def _make_chapter(path: str, title: str, sub_headings) -> None: doc = DocxDocument() section = doc.sections[0] section.header.paragraphs[0].text = "章节原生页眉" section.footer.paragraphs[0].text = "章节原生页脚" doc.add_paragraph(title, style="Heading 1") for heading in sub_headings: doc.add_paragraph(heading, style="Heading 2") doc.add_paragraph(f"这是 {heading} 的响应正文,包含数字 123 和英文 English。") doc.save(path) def _make_template(path: str) -> None: doc = DocxDocument() section = doc.sections[0] section.different_first_page_header_footer = True default_header = section.header default_header.paragraphs[0].text = "默认页眉" first_header = section.first_page_header first_header.paragraphs[0].text = "首页页眉" footer = section.footer footer.paragraphs[0].text = "页脚" doc.add_paragraph("模板占位正文") doc.save(path) def _make_index_table_template(path: str) -> None: doc = DocxDocument() table = doc.add_table(rows=2, cols=4) header = ["序号", "主要内容概述", "章节", "详细内容所在投标文件页次"] for col, text in enumerate(header): table.cell(0, col).text = text for col, text in enumerate(["1", "示例", "第一章", "P1"]): table.cell(1, col).text = text doc.save(path) class Step6AssemblerTests(unittest.TestCase): def test_assemble_produces_cover_toc_index_and_chapters(self): temp_dir = tempfile.mkdtemp() template_path = os.path.join(temp_dir, "template.docx") _make_template(template_path) ch1 = os.path.join(temp_dir, "1.docx") ch2 = os.path.join(temp_dir, "2.docx") ch3 = os.path.join(temp_dir, "3.docx") _make_chapter(ch1, "第一章 投标人资格、资信证明", ["十七、“★” 要求承诺函"]) _make_chapter(ch2, "第二章 投标报价", ["五、报价得分"]) _make_chapter(ch3, "第三章 需求理解", ["一、服务目标定位"]) outline = BidOutline(project_name="测试项目") outline.evaluation_index_entries = [ { "entry_type": "scoring", "source_id": "SC-01#1", "criterion_id": "SC-01", "display_name": "报价得分", "requirement": "报价得分=报价分值×(评标基准价/评审价)", "score": 10.0, "final_heading_id": "2.5", "final_heading_title": "五、报价得分", "final_heading_path": "投标报价 → 五、报价得分", }, { "entry_type": "rejection", "source_id": "RI-01", "criterion_id": "", "display_name": "资格条件", "requirement": "投标人不满足《中华人民共和国政府采购法》第二十二条规定。", "score": None, "final_heading_id": "1.17", "final_heading_title": "十七、“★” 要求承诺函", "final_heading_path": "投标人资格、资信证明 → 十七、“★” 要求承诺函", }, ] output_path = os.path.join(temp_dir, "aggregated.docx") records = [ {"id": "1", "title": "投标人资格、资信证明", "artifact_path": ch1, "status": "complete"}, {"id": "2", "title": "投标报价", "artifact_path": ch2, "status": "complete"}, {"id": "3", "title": "需求理解", "artifact_path": ch3, "status": "complete"}, ] report = assemble_step5_document( records, outline, output_path, template_path=template_path, project_name="测试项目", ) self.assertTrue(os.path.isfile(output_path)) self.assertEqual(report.index_row_count, 1) self.assertTrue(report.toc_inserted) self.assertEqual(report.chapter_count, 3) with zipfile.ZipFile(output_path, "r") as output_zip: names = set(output_zip.namelist()) self.assertIn("word/document.xml", names) document_root = ET.fromstring(output_zip.read("word/document.xml")) body = document_root.find(_w("body")) self.assertIsNotNone(body) body_text = _element_text(body) self.assertIn("测试项目", body_text) self.assertIn("目 录", body_text) self.assertIn("第一章 投标人资格、资信证明", body_text) self.assertIn("第二章 投标报价", body_text) self.assertIn("第三章 需求理解", body_text) self.assertIn("商务部分", body_text) self.assertIn("技术部分", body_text) self.assertIn("主要内容概述", body_text) instr_texts = [node.text or "" for node in body.iter(_w("instrText"))] combined = " ".join(instr_texts) self.assertIn("TOC", combined) self.assertIn("PAGEREF", combined) self.assertNotIn("第二十二条", body_text) header1 = output_zip.read("word/header1.xml") header2 = output_zip.read("word/header2.xml") footer1 = output_zip.read("word/footer1.xml") self.assertIn("章节原生页眉".encode("utf-8"), header1) self.assertIn("章节原生页脚".encode("utf-8"), footer1) # 正文 run 应统一为宋体 + Times New Roman。 for run in body.iter(_w("r")): if _element_text(run).startswith("这是"): r_pr = run.find(_w("rPr")) self.assertIsNotNone(r_pr) fonts = r_pr.find(_w("rFonts")) self.assertIsNotNone(fonts) self.assertEqual(fonts.get(_w("eastAsia")), "宋体") self.assertEqual(fonts.get(_w("ascii")), "Times New Roman") break else: self.fail("未找到正文 run 以验证字体") def test_index_table_reuses_extracted_template(self): temp_dir = tempfile.mkdtemp() table_template_path = os.path.join(temp_dir, "index_table.docx") _make_index_table_template(table_template_path) template_tbl = _load_table_template(table_template_path) self.assertIsNotNone(template_tbl) records = [ _HeadingRecord(chapter_title="投标报价", text="第二章 投标报价", level=1, bookmark="CodexIdx1"), _HeadingRecord(chapter_title="投标报价", text="五、报价得分", level=2, bookmark="CodexIdx2"), ] entries = [ { "entry_type": "scoring", "display_name": "报价得分", "score": 10.0, "final_heading_path": "投标报价 → 五、报价得分", } ] table, row_count = _build_index_table_from_template(template_tbl, entries, records) self.assertEqual(row_count, 1) rows = table.findall(_w("tr")) self.assertEqual(len(rows), 2) header_cells = [_element_text(tc).strip() for tc in rows[0].findall(_w("tc"))] self.assertEqual(header_cells, ["序号", "主要内容概述", "章节", "详细内容所在投标文件页次"]) data_cells = [_element_text(tc).strip() for tc in rows[1].findall(_w("tc"))] self.assertEqual(data_cells[0], "1") self.assertIn("报价得分", data_cells[1]) self.assertEqual(data_cells[2], "第二章 投标报价 → 五、报价得分") if __name__ == "__main__": unittest.main()