import io import unittest import zipfile from unittest.mock import patch from services.assistant_materials import material, apply_candidate, demo_assets from services.skill_executor import execute_skill class MaterialTests(unittest.TestCase): def test_docx_paragraph_table_locators(self): from docx import Document doc = Document() doc.add_paragraph("collection.evidence:到账记录") table = doc.add_table(rows=1, cols=2) table.cell(0, 0).text = "due_diligence.liabilities" table.cell(0, 1).text = "13200" buffer = io.BytesIO() doc.save(buffer) result = material("P", "材料.docx", buffer.getvalue(), "tester") locators = [c["locator"] for c in result["chunks"]] self.assertEqual(locators, ["正文第1段", "表1第1行"]) self.assertEqual(len(result["candidates"]), 2) self.assertNotIn("页", "".join(locators)) def test_xlsx_formula_without_cache_is_not_calculated(self): buffer = io.BytesIO() with zipfile.ZipFile(buffer, "w") as z: z.writestr("xl/workbook.xml", '') z.writestr("xl/_rels/workbook.xml.rels", '') z.writestr("xl/worksheets/sheet1.xml", 'financial.total_liabilitiesSUM(B2:B3)') result = material("P", "财报.xlsx", buffer.getvalue(), "tester") self.assertEqual(result["status"], "partial") self.assertIn("财务!B1公式无缓存", result["warnings"][0]) self.assertIsNone(result["candidates"][0]["value"]) def test_pdf_page_and_scan_failure(self): import fitz with fitz.open() as pdf: pdf.new_page().insert_text((40, 50), "collection.evidence: receipt-001") pdf.new_page() content = pdf.tobytes() with patch.dict("os.environ", {"XINGBANG_OCR_TESSDATA": ""}): result = material("P", "材料.pdf", content, "tester") self.assertEqual([c["locator"] for c in result["chunks"]], ["第1页", "第2页"]) self.assertEqual(result["status"], "partial") self.assertIn("OCR", result["warnings"][0]) def test_asset_dedup_does_not_inflate_amount(self): result = demo_assets({"id": "P", "facts": {"lease_asset_value": "5400", "duplicate_asset_count": 1}}) candidates = {c["field"]: c for c in result["candidates"]} self.assertEqual(candidates["lease.total_value"]["value"], "5400") self.assertEqual(candidates["lease.duplicate_count"]["value"], "1") self.assertTrue(candidates["lease.total_value"]["formula_version"]) def test_normalization_keeps_raw_and_rejects_mixed_currency(self): project = {"id": "P"} candidate = {"field": "financial.total_liabilities", "value": "132000000.01", "unit": "元", "currency": "CNY", "period": "2025", "scope": "合并"} evidence = apply_candidate(project, candidate, "tester", "核对") self.assertEqual(evidence["value"], "13200.000001") self.assertEqual(evidence["raw_value"], "132000000.01") self.assertEqual(evidence["unit"], "万元") skill = {"id": "ratio", "version": "1", "engine": "ratio", "numerator": "a", "denominator": "b", "output": "r"} with self.assertRaises(ValueError): execute_skill(skill, {"a": "10", "b": "5"}, {"a": [{**evidence, "id": "a"}], "b": [{**evidence, "id": "b", "currency": "USD"}]}) def test_bad_zip_and_unsupported_macro_are_rejected(self): for name in ("broken.docx", "macro.xlsm", "unknown.exe"): with self.assertRaises(ValueError): material("P", name, b"invalid", "tester") if __name__ == "__main__": unittest.main()