import importlib.util import os import sys import tempfile import unittest from unittest.mock import patch from docx import Document from models import ( BidOutline, Chapter, ChapterType, ExtractedTable, ProjectData, TenderAnalysis, ) SCRIPT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) if SCRIPT_DIR not in sys.path: sys.path.insert(0, SCRIPT_DIR) SPEC = importlib.util.spec_from_file_location( "manual_test_step4", os.path.join(SCRIPT_DIR, "test_step4.py") ) MODULE = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(MODULE) class Step4ReportingTests(unittest.TestCase): def test_reference_bid_loader_extracts_paragraphs_and_tables(self): table = ExtractedTable( table_id="REF-T01", table_type="bid_detail_table", source_type="reference_docx", ) project_data = ProjectData(project_id="test", project_name="测试项目") with tempfile.TemporaryDirectory() as directory: reference_path = os.path.join(directory, "reference.docx") Document().save(reference_path) with ( patch( "doc_reader.reader.read_docx_paragraph_texts_et", return_value=["参考投书正文"], ), patch( "step1_parsing.table_extractor.extract_reference_tables_with_llm", return_value=[table], ) as extractor, ): count = MODULE._load_reference_bid( reference_path, project_data, directory ) self.assertEqual(count, 1) self.assertEqual(project_data.reference_bids[0].content, "参考投书正文") self.assertIs(project_data.reference_tables[0], table) extractor.assert_called_once_with( reference_path, output_dir=os.path.join(directory, "内容提取_参考投书"), ) def test_report_exposes_step2_project_fill_fields(self): analysis = TenderAnalysis( project_name="测试", agency_name="采购中心", project_fields={ "服务内容": "物业管理服务", "服务要求": "符合质量考核要求", "服务期限": "三年", }, ) report = MODULE._build_report( BidOutline(project_name="测试", chapters=[]), analysis, ) self.assertEqual(report["project_fields"]["采购机构名称"], "采购中心") self.assertEqual(report["project_fields"]["服务期限"], "三年") def test_unchanged_template_child_is_not_reported_as_empty(self): mapped = Chapter( id="1.2", title="响应能力", level=2, direct_scoring_bindings=["SC-01#1"], supplement_content="针对本项目建立可核验的响应机制。", ) unchanged = Chapter( id="1.1", title="模板原有说明", level=2, from_template=True ) top = Chapter( id="1", title="需求理解", chapter_type=ChapterType.TECHNICAL, children=[unchanged, mapped], preserve_template_layout=True, template_fill_completed=True, generated_content="模板原文", content_blocks=[ {"order": 0, "block_type": "template_base"}, {"order": 1, "block_type": "native_table_plan"}, ], ) outline = BidOutline( project_name="测试", chapters=[top], evaluation_index_entries=[{ "entry_id": "SC-01#1", "entry_type": "scoring", "final_heading_id": "1.2", }], ) report = MODULE._build_report(outline) self.assertTrue(report["passed"], report["errors"]) self.assertEqual(report["chapters"][0]["supplement_node_count"], 1) def test_missing_direct_supplement_fails_gate(self): node = Chapter( id="1.1", title="响应能力", level=2, direct_scoring_bindings=["SC-01#1"], ) outline = BidOutline( project_name="测试", chapters=[Chapter(id="1", title="技术章", children=[node])], evaluation_index_entries=[{ "entry_id": "SC-01#1", "entry_type": "scoring", "final_heading_id": "1.1", }], ) report = MODULE._build_report(outline) self.assertFalse(report["passed"]) self.assertIn("直接落点缺少补充正文", report["errors"][0]) def test_unresolved_placeholder_in_chapter_artifact_fails_gate(self): with tempfile.TemporaryDirectory() as directory: artifact_path = os.path.join(directory, "chapter.docx") document = Document() document.add_paragraph("项目编号:%%项目编号%%") document.save(artifact_path) chapter = Chapter( id="1", title="投标人资格、资信证明", artifact_path=artifact_path, ) report = MODULE._build_report(BidOutline( project_name="测试", chapters=[chapter], )) self.assertFalse(report["passed"]) self.assertTrue(any( "章节产物仍有未解析占位符" in error for error in report["errors"] )) if __name__ == "__main__": unittest.main()