"""验证文档批次三个独立实现的核心行为和安全边界。""" from __future__ import annotations import json import subprocess import tempfile import unittest from pathlib import Path from docx import Document from openpyxl import Workbook ROOT = Path(__file__).resolve().parents[2] MD_SCRIPT = ROOT / "plugins/doc/skills/md-to-docx/scripts/convert.py" XLSX_SCRIPT = ROOT / "plugins/doc/skills/xlsx-to-md/scripts/convert.py" ARCHIVE_SCRIPT = ROOT / "plugins/doc/skills/archive/scripts/archive.py" class DocumentBatchTests(unittest.TestCase): """使用临时目录执行真实转换,不依赖仓库外测试材料。""" def run_script(self, script: Path, *args: object) -> subprocess.CompletedProcess[str]: """使用当前受控 Python 运行时执行目标脚本。""" return subprocess.run( [str(Path(__import__("sys").executable)), str(script), *(str(value) for value in args)], capture_output=True, text=True, encoding="utf-8", check=False, ) def test_markdown_to_docx_preserves_common_blocks(self) -> None: """标题、列表、表格和代码块应生成可重新打开的 DOCX。""" with tempfile.TemporaryDirectory() as temp: folder = Path(temp); source = folder / "sample.md"; output = folder / "sample.docx" source.write_text("# 标题\n\n- 项目\n\n| 名称 | 值 |\n| --- | --- |\n| A | 1 |\n\n```py\nprint('ok')\n```\n", encoding="utf-8") result = self.run_script(MD_SCRIPT, source) self.assertEqual(0, result.returncode, result.stderr) document = Document(output) self.assertEqual("标题", document.paragraphs[0].text) self.assertEqual(1, len(document.tables)) self.assertNotEqual(0, self.run_script(MD_SCRIPT, source).returncode) def test_xlsx_to_markdown_handles_merges_and_escaping(self) -> None: """合并区域应填充值,表格特殊字符应安全转义。""" with tempfile.TemporaryDirectory() as temp: folder = Path(temp); source = folder / "sample.xlsx" workbook = Workbook(); sheet = workbook.active; sheet.title = "数据" sheet.append(["名称", "值"]); sheet.append(["A|B", "=1+1"]) sheet.merge_cells("A3:B3"); sheet["A3"] = "合并" workbook.create_sheet("空表"); workbook.save(source) result = self.run_script(XLSX_SCRIPT, source) self.assertEqual(0, result.returncode, result.stderr) markdown = source.with_suffix(".md").read_text(encoding="utf-8") self.assertIn(r"A\|B", markdown) self.assertIn("| 合并 | 合并 |", markdown) self.assertIn("_空工作表_", markdown) def test_archive_is_dry_run_first_and_keeps_source(self) -> None: """预演不得写目标,执行后仍须保留来源并只抽取指定章节。""" with tempfile.TemporaryDirectory() as temp: root = Path(temp); source = root / "specs/api.md"; source.parent.mkdir() source.write_text("---\nowner: team\n---\n# 概述\n正文\n## API\n接口\n## 其他\n忽略\n", encoding="utf-8") config = root / "rules.json" config.write_text(json.dumps({"version": 1, "archiveRoot": "archive", "rules": [{"match": "specs/*.md", "target": "{stem}.md", "section": "API", "stripFrontmatter": True, "requiredText": ["接口"]}]}, ensure_ascii=False), encoding="utf-8") target = root / "archive/api.md" preview = self.run_script(ARCHIVE_SCRIPT, "--root", root, "--config", config) self.assertEqual(0, preview.returncode, preview.stderr); self.assertFalse(target.exists()) applied = self.run_script(ARCHIVE_SCRIPT, "--root", root, "--config", config, "--apply") self.assertEqual(0, applied.returncode, applied.stderr) self.assertTrue(source.exists()); self.assertEqual("## API\n接口\n", target.read_text(encoding="utf-8")) if __name__ == "__main__": unittest.main()