import importlib.util import json import pathlib import sys import tempfile import unittest SCRIPT = ( pathlib.Path(__file__).resolve().parents[1] / "scripts" / "semantic_heading_repair.py" ) SPEC = importlib.util.spec_from_file_location("semantic_heading_repair", SCRIPT) repair = importlib.util.module_from_spec(SPEC) assert SPEC.loader is not None sys.modules[SPEC.name] = repair SPEC.loader.exec_module(repair) class SemanticHeadingRepairTest(unittest.TestCase): def test_inspect_reports_only_real_atx_headings(self): with tempfile.TemporaryDirectory() as tmp: source = pathlib.Path(tmp) / "source.md" source.write_text( "\n".join( [ "# Title", "", "## Section", "```md", "# Not a heading", "```", "> # quoted content", " # indented code", "### Child", "", ] ), encoding="utf-8", ) payload = repair.inspect_payload(source) self.assertEqual( [(item["line"], item["level"], item["text"]) for item in payload["headings"]], [(1, 1, "Title"), (3, 2, "Section"), (9, 3, "Child")], ) def test_write_inspection_file_uses_utf8(self): with tempfile.TemporaryDirectory() as tmp: source = pathlib.Path(tmp) / "source.md" output = pathlib.Path(tmp) / "heading-map.json" source.write_text("# 标题\n", encoding="utf-8") repair.write_inspection(source, output, output_format="json") payload = json.loads(output.read_text(encoding="utf-8")) self.assertEqual(payload["headings"][0]["text"], "标题") def test_apply_plan_rewrites_only_requested_heading_levels(self): with tempfile.TemporaryDirectory() as tmp: source = pathlib.Path(tmp) / "source.md" output = pathlib.Path(tmp) / "fixed.md" report = pathlib.Path(tmp) / "report.md" plan = pathlib.Path(tmp) / "plan.json" source.write_text( "\n".join( [ "# 1", "", "## GPT", "", "# Wrong parent", "", "## Wrong child", "", "```md", "# Not a heading", "```", "", ] ), encoding="utf-8", ) plan.write_text( json.dumps( { "mode": "discussion", "edits": [ { "line": 5, "from_level": 1, "to_level": 3, "reason": "GPT reply content belongs under ## GPT.", }, { "line": 7, "from_level": 2, "to_level": 4, "reason": "Child heading follows the repaired parent.", }, ], }, ensure_ascii=False, ), encoding="utf-8", ) changed = repair.apply_plan(source, plan, output, report) self.assertEqual(changed, 2) self.assertEqual( output.read_text(encoding="utf-8"), "\n".join( [ "# 1", "", "## GPT", "", "### Wrong parent", "", "#### Wrong child", "", "```md", "# Not a heading", "```", "", ] ), ) report_text = report.read_text(encoding="utf-8") self.assertIn("| 5 | 1 | 3 | Wrong parent |", report_text) self.assertIn("GPT reply content belongs under ## GPT.", report_text) def test_apply_plan_rejects_stale_or_wrong_source_plans(self): with tempfile.TemporaryDirectory() as tmp: source = pathlib.Path(tmp) / "source.md" plan = pathlib.Path(tmp) / "plan.json" source.write_text("# Title\n\n## Section\n", encoding="utf-8") plan.write_text( json.dumps( { "edits": [ { "line": 3, "from_level": 1, "to_level": 3, "reason": "This plan was made for another source.", } ] }, ensure_ascii=False, ), encoding="utf-8", ) with self.assertRaisesRegex(ValueError, "expected level 1"): repair.apply_plan(source, plan, pathlib.Path(tmp) / "fixed.md", None) def test_prepare_batch_scaffold_accepts_file_array(self): with tempfile.TemporaryDirectory() as tmp: root = pathlib.Path(tmp) first = root / "one.md" second = root / "two.md" output_dir = root / "out" first.write_text("# One\n\n# Bad\n", encoding="utf-8") second.write_text("# Two\n\n## Good\n", encoding="utf-8") items = repair.prepare_batch_scaffold( [first, second], mode="artifact", output_dir=output_dir, ) self.assertEqual(len(items), 2) for item in items: self.assertEqual(item["mode"], "artifact") self.assertTrue(pathlib.Path(item["heading_map"]).exists()) self.assertTrue(pathlib.Path(item["plan"]).exists()) self.assertTrue(str(item["fixed"]).endswith(".fixed.md")) plan_payload = json.loads(pathlib.Path(item["plan"]).read_text(encoding="utf-8")) self.assertEqual(plan_payload["mode"], "artifact") self.assertEqual(plan_payload["edits"], []) batch_report = output_dir / "fix-title-batch-report.md" self.assertIn("one.md", batch_report.read_text(encoding="utf-8")) if __name__ == "__main__": unittest.main()