"""Phase 2 自檢:程式化產生各格式最小測試檔,驗證轉換器與 convert.py 端到端。 執行:.venv/Scripts/python tools/convert/selfcheck.py 邏輯壞掉時 assert 會失敗(AGENTS.md §13.4:一個可執行的檢查,不用框架)。 """ import json import pathlib import shutil import sys import tempfile sys.path.insert(0, str(pathlib.Path(__file__).parent)) import convert import from_docx import from_pdf import from_pptx import from_web import from_xlsx def make_fixtures(d): import docx as docxlib doc = docxlib.Document() doc.add_heading("支付閘道測試報告", level=1) doc.add_paragraph("逾時缺陷於壓力測試重現。") t = doc.add_table(rows=2, cols=2) t.rows[0].cells[0].text = "案例" t.rows[0].cells[1].text = "結果" t.rows[1].cells[0].text = "TC-001" t.rows[1].cells[1].text = "FAIL" doc.save(d / "report.docx") import openpyxl wb = openpyxl.Workbook() ws = wb.active ws.title = "測項" ws.append(["編號", "說明"]) ws.append(["TC-001", "逾時 | 重試"]) wb.save(d / "cases.xlsx") import fitz pdf = fitz.open() page = pdf.new_page() page.insert_text((72, 72), "Payment gateway timeout defect reproduced under load test.") pdf.save(d / "text.pdf") pdf.close() scanned = fitz.open() scanned.new_page() # 無文字層 → 應判 needs_ocr scanned.save(d / "scanned.pdf") scanned.close() from pptx import Presentation prs = Presentation() slide = prs.slides.add_slide(prs.slide_layouts[1]) slide.shapes.title.text = "結案報告" slide.notes_slide.notes_text_frame.text = "備註:法遵項目全數通過" prs.save(d / "closing.pptx") (d / "page.html").write_text( "

AML 測試指引

" + "

可疑交易監控測試需涵蓋大額交易與分散交易兩種樣態。

" * 8 + "
", encoding="utf-8") def main(): tmp = pathlib.Path(tempfile.mkdtemp(prefix="ppqa-selfcheck-")) try: fx = tmp / "fx" fx.mkdir() make_fixtures(fx) md, st = from_docx.convert(fx / "report.docx") assert st == "ok" and "# 支付閘道測試報告" in md and "| TC-001 | FAIL |" in md, md md, st = from_xlsx.convert(fx / "cases.xlsx") assert st == "ok" and "## 工作表:測項" in md and "逾時 \\| 重試" in md, md md, st = from_pdf.convert(fx / "text.pdf") assert st == "ok" and "" in md and "timeout defect" in md, md _, st = from_pdf.convert(fx / "scanned.pdf") assert st == "needs_ocr", st md, st = from_pptx.convert(fx / "closing.pptx") assert st == "ok" and "## Slide 1" in md and "法遵項目全數通過" in md, md md, st = from_web.extract((fx / "page.html").read_text(encoding="utf-8")) assert st == "ok" and "可疑交易監控" in md, md print("PASS: 5 個轉換器 + needs_ocr 偵測") # convert.py 端到端(沙盒 root) root = tmp / "root" for sub in ("raw/originals", "raw/converted"): (root / sub).mkdir(parents=True) (root / "raw/manifest.json").write_text('{"version": 1, "files": {}}', encoding="utf-8") convert.main([str(fx / "report.docx"), str(fx / "scanned.pdf"), str(fx / "page.html"), "--root", str(root)]) m = json.loads((root / "raw/manifest.json").read_text(encoding="utf-8")) origs = {k: v for k, v in m["files"].items() if v["kind"] == "original"} convs = {k: v for k, v in m["files"].items() if v["kind"] == "converted"} assert len(origs) == 3 and len(convs) == 2, m assert origs["raw/originals/scanned.pdf"]["status"] == "needs_ocr" for k, v in convs.items(): assert (root / k).is_file(), k assert m["files"][v["original_path"]]["sha256"] == v["original_sha256"] assert (root / "raw/originals/report.docx").is_file() print("PASS: convert.py 端到端 + manifest 對應") # 冪等:同檔再跑一次 → skip,manifest 不變 before = (root / "raw/manifest.json").read_text(encoding="utf-8") convert.main([str(fx / "report.docx"), "--root", str(root)]) assert (root / "raw/manifest.json").read_text(encoding="utf-8") == before print("PASS: 冪等(同 hash 跳過)") # dry-run:全新沙盒不落地 root2 = tmp / "root2" for sub in ("raw/originals", "raw/converted"): (root2 / sub).mkdir(parents=True) (root2 / "raw/manifest.json").write_text('{"version": 1, "files": {}}', encoding="utf-8") convert.main([str(fx / "report.docx"), "--dry-run", "--root", str(root2)]) m2 = json.loads((root2 / "raw/manifest.json").read_text(encoding="utf-8")) assert m2["files"] == {} and not list((root2 / "raw/originals").iterdir()) print("PASS: --dry-run 不落地") print("ALL PASS") finally: shutil.rmtree(tmp, ignore_errors=True) if __name__ == "__main__": main()