193 lines
6.8 KiB
Python
193 lines
6.8 KiB
Python
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()
|