Files
document-haness/tests/test_markdown_audit_regressions.py
T

252 lines
8.1 KiB
Python

from __future__ import annotations
import sys
from pathlib import Path
import pytest
from helpers import FIXTURES, read_json, run_cli, write_json
CANONICAL_SCRIPTS = (
Path(__file__).resolve().parents[1]
/ "skills"
/ "technical-doc-flow"
/ "scripts"
)
sys.path.insert(0, str(CANONICAL_SCRIPTS))
from lint_document import ( # noqa: E402
markdown_inline_links,
markdown_reference_links,
parse_headings,
reference_definitions,
)
from markdown_structure import inline_code_spans # noqa: E402
def lint_custom(
tmp_path: Path,
document: str,
*,
logic: dict | None = None,
ledger: dict | None = None,
):
document_path = tmp_path / "document.md"
logic_path = tmp_path / "logic.json"
ledger_path = tmp_path / "ledger.json"
output = tmp_path / "lint.json"
document_path.write_text(document, encoding="utf-8")
write_json(logic_path, logic or read_json(FIXTURES / "good" / "logic-map.json"))
write_json(
ledger_path,
ledger or read_json(FIXTURES / "good" / "term-ledger.json"),
)
result = run_cli(
"lint_document.py",
"--document",
document_path,
"--logic-map",
logic_path,
"--term-ledger",
ledger_path,
"--reader-contract",
FIXTURES / "good" / "reader-contract.json",
"--output",
output,
)
return result, read_json(output)
def test_multiline_labels_stop_at_markdown_block_boundaries() -> None:
assert markdown_inline_links("[x\n## y](TODO)") == []
text = "[id]: #missing\n\n[x\n## y][id]\n"
definitions = reference_definitions(text)
assert definitions
links = markdown_reference_links(text, definitions)
assert all(link.start != text.index("[x") for link in links)
@pytest.mark.parametrize("marker", ["01.", "001)"])
def test_leading_zero_one_list_marker_ends_multiline_inline_code(marker: str) -> None:
assert inline_code_spans(f"text `a\n{marker} TODO\nc` tail") == []
def test_html_comment_block_ends_multiline_inline_code() -> None:
assert inline_code_spans("text `a\n<!-- TODO -->\nb` tail") == []
@pytest.mark.parametrize(
"definition",
[
'[id]: #missing "long\n title"',
'[id]: #missing\n "long\n title"',
'[id]:\n #missing "long\n title"',
],
)
def test_multiline_reference_titles_are_fully_parsed(definition: str) -> None:
text = f"{definition}\n\n[go][id]\n"
definitions = reference_definitions(text)
assert len(definitions) == 1
assert definitions[0].destination == "#missing"
assert text[definitions[0].start : definitions[0].end].rstrip() == definition
@pytest.mark.parametrize(
"definition",
['[id]: <#missing>"title"', "[id]: #missing (a(b)"],
)
def test_invalid_reference_title_grammar_does_not_create_definition(
definition: str,
) -> None:
assert reference_definitions(f"{definition}\n\n[go][id]\n") == []
def test_noninterrupting_ordered_marker_cannot_create_reference_definition() -> None:
text = "paragraph\n2. [id]: #missing\n\n[go][id]\n"
assert reference_definitions(text) == []
def test_reference_definition_inside_raw_html_does_not_resolve_link() -> None:
text = "<div>\n[id]: #ghost\n</div>\n\n[go][id]\n"
definitions = reference_definitions(text)
assert definitions == []
assert markdown_reference_links(text, definitions) == []
def test_multiline_setext_heading_cannot_impersonate_logic_heading(
tmp_path: Path,
) -> None:
document = (FIXTURES / "good" / "document.md").read_text(encoding="utf-8")
document = document.replace(
"## 문제",
"독자가 읽어야 할 흐름이다.\n문제\n---",
)
result, report = lint_custom(tmp_path, document)
assert result.returncode == 1
assert "DOC-L001" in {item["rule_id"] for item in report["findings"]}
def test_noninterrupting_ordered_line_remains_part_of_setext_heading() -> None:
headings = parse_headings("paragraph\n2. text\n---\n")
assert [(heading.level, heading.text) for heading in headings] == [
(2, "paragraph\n2. text")
]
@pytest.mark.parametrize(
"wrapper",
["**{value}**", "[{value}](docs/x)", "`{value}`"],
)
def test_core_claim_search_uses_reader_visible_formatting(
tmp_path: Path,
wrapper: str,
) -> None:
document = (FIXTURES / "good" / "document.md").read_text(encoding="utf-8")
logic = read_json(FIXTURES / "good" / "logic-map.json")
core = logic["core_claim"]
document = document.replace(
core,
core.replace("장애를", wrapper.format(value="장애를")),
1,
)
result, report = lint_custom(tmp_path, document, logic=logic)
assert result.returncode == 0, report["findings"]
def test_core_claim_wholly_inside_inline_code_is_not_explanatory_prose(
tmp_path: Path,
) -> None:
document = (FIXTURES / "good" / "document.md").read_text(encoding="utf-8")
logic = read_json(FIXTURES / "good" / "logic-map.json")
core = logic["core_claim"]
document = document.replace(core, f"`{core}`", 1)
result, report = lint_custom(tmp_path, document, logic=logic)
assert result.returncode == 1
assert "DOC-L002" in {item["rule_id"] for item in report["findings"]}
@pytest.mark.parametrize("wrapper", ["**{value}**", "[{value}](docs/x)"])
def test_term_first_use_search_uses_reader_visible_formatting(
tmp_path: Path,
wrapper: str,
) -> None:
document = (FIXTURES / "good" / "document.md").read_text(encoding="utf-8")
ledger = read_json(FIXTURES / "good" / "term-ledger.json")
first_use = ledger["terms"][0]["first_use"]
document = document.replace(
first_use,
first_use.replace("한 번", wrapper.format(value="한 번")),
1,
)
result, report = lint_custom(tmp_path, document, ledger=ledger)
assert result.returncode == 0, report["findings"]
@pytest.mark.parametrize(
"raw_marker",
["[근거:C1]", "<!-- claim:C1 -->"],
)
def test_raw_code_html_cannot_supply_claim_marker(
tmp_path: Path,
raw_marker: str,
) -> None:
document = (FIXTURES / "good" / "document.md").read_text(encoding="utf-8")
document = document.replace("<!-- claim:C1 -->", "")
document += f"\n<pre>\n{raw_marker}\n</pre>\n"
result, report = lint_custom(tmp_path, document)
assert result.returncode == 1
assert "DOC-L003" in {item["rule_id"] for item in report["findings"]}
def test_inline_markdown_inside_raw_div_remains_literal_reader_text(
tmp_path: Path,
) -> None:
document = (FIXTURES / "good" / "document.md").read_text(encoding="utf-8")
document += "\n<div>\n`TODO`\n</div>\n"
result, report = lint_custom(tmp_path, document)
assert result.returncode == 1
assert "DOC-M001" in {item["rule_id"] for item in report["findings"]}
def test_tag_shaped_script_data_cannot_supply_explicit_anchor(tmp_path: Path) -> None:
document = (FIXTURES / "good" / "document.md").read_text(encoding="utf-8")
document += (
"\n<script>\nconst x = '<a id=\"ghost\">';\n</script>\n"
"\n[이동](#ghost)\n"
)
result, report = lint_custom(tmp_path, document)
assert result.returncode == 1
assert any(
item["rule_id"] == "DOC-M003" and item["context"] == "ghost"
for item in report["findings"]
)
def test_heading_slugs_preserve_escaped_underscores_and_resolve_references() -> None:
headings = parse_headings(
"## \\_foo\\_\n## foo\\_bar\n## [visible][id]\n\n[id]: /url\n"
)
assert [heading.slug for heading in headings] == ["_foo_", "foo_bar", "visible"]
@pytest.mark.parametrize(
"table",
[
"> 항목 | 설명\n> --- | ---\n> 하나 | {cell}\n",
"- 항목 | 설명\n --- | ---\n 하나 | {cell}\n",
],
)
def test_nested_gfm_tables_do_not_consume_prose_budgets(
tmp_path: Path,
table: str,
) -> None:
document = (FIXTURES / "good" / "document.md").read_text(encoding="utf-8")
document += "\n" + table.format(cell="긴 설명이다. " * 120)
result, report = lint_custom(tmp_path, document)
assert result.returncode == 0, report["findings"]
assert not {
item["rule_id"] for item in report["findings"]
} & {"DOC-P001", "DOC-P002"}