Public Access
DEVX-118: feat: enrich lint_docs.py with single H1, max depth, line length, code block lang, orphan checks
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 9s
Post-merge / vikunja (push) Successful in 19s
Post-merge / configure-repo (push) Successful in 16s
Post-merge / release (push) Successful in 40s
Post-merge / sync-wiki (push) Successful in 43s
Post-merge / publish (push) Successful in 28s
Post-merge / badges (push) Failing after 36s
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 9s
Post-merge / vikunja (push) Successful in 19s
Post-merge / configure-repo (push) Successful in 16s
Post-merge / release (push) Successful in 40s
Post-merge / sync-wiki (push) Successful in 43s
Post-merge / publish (push) Successful in 28s
Post-merge / badges (push) Failing after 36s
- Add check_single_h1: each markdown file should have at most one H1 - Add check_max_heading_depth: headings should not exceed H4 (configurable) - Add check_line_length: warn on lines >120 chars (non-blocking — badge URLs) - Add check_code_block_languages: fenced code blocks must specify a language - Add check_orphan_docs: warn on docs not linked from index.md or mapping.json - Fix all code blocks in docs to specify language (text for plain blocks) - Fix duplicate H1 in .vale/styles/devx/README.md - Add 18 new tests for full coverage of new checks Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
bb700ab969
commit
fb342e7b9d
@@ -9,11 +9,16 @@ from pathlib import Path
|
||||
from click.testing import CliRunner
|
||||
|
||||
from devx.ci.lint_docs import (
|
||||
check_code_block_languages,
|
||||
check_docs_structure,
|
||||
check_duplicate_headings,
|
||||
check_heading_hierarchy,
|
||||
check_internal_links,
|
||||
check_line_length,
|
||||
check_max_heading_depth,
|
||||
check_orphan_docs,
|
||||
check_required_files,
|
||||
check_single_h1,
|
||||
check_stale_docs,
|
||||
check_todo_fixme,
|
||||
check_trailing_whitespace,
|
||||
@@ -362,6 +367,100 @@ class TestCheckDuplicateHeadings:
|
||||
assert issues == []
|
||||
|
||||
|
||||
class TestCheckSingleH1:
|
||||
def test_single_h1_ok(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "README.md").write_text("# Title\n## Section\n")
|
||||
issues = check_single_h1(tmp_path)
|
||||
assert issues == []
|
||||
|
||||
def test_multiple_h1_fails(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "README.md").write_text("# Title 1\n# Title 2\n")
|
||||
issues = check_single_h1(tmp_path)
|
||||
assert len(issues) == 1
|
||||
assert "2 H1" in issues[0]
|
||||
|
||||
def test_no_h1_ok(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "README.md").write_text("## Section\n")
|
||||
issues = check_single_h1(tmp_path)
|
||||
assert issues == []
|
||||
|
||||
|
||||
class TestCheckMaxHeadingDepth:
|
||||
def test_ok(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "README.md").write_text("# H1\n## H2\n### H3\n#### H4\n")
|
||||
issues = check_max_heading_depth(tmp_path)
|
||||
assert issues == []
|
||||
|
||||
def test_too_deep(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "README.md").write_text("# H1\n##### H5\n")
|
||||
issues = check_max_heading_depth(tmp_path)
|
||||
assert len(issues) == 1
|
||||
assert "H5" in issues[0]
|
||||
|
||||
|
||||
class TestCheckLineLength:
|
||||
def test_ok(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "README.md").write_text("# Short line\n")
|
||||
issues = check_line_length(tmp_path)
|
||||
assert issues == []
|
||||
|
||||
def test_too_long(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "README.md").write_text("# " + "x" * 200 + "\n")
|
||||
issues = check_line_length(tmp_path)
|
||||
assert len(issues) == 1
|
||||
assert "202" in issues[0]
|
||||
|
||||
|
||||
class TestCheckCodeBlockLanguages:
|
||||
def test_with_language(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "README.md").write_text("```python\nprint('hi')\n```\n")
|
||||
issues = check_code_block_languages(tmp_path)
|
||||
assert issues == []
|
||||
|
||||
def test_without_language(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "README.md").write_text("```\nplain text\n```\n")
|
||||
issues = check_code_block_languages(tmp_path)
|
||||
assert len(issues) == 1
|
||||
assert "without language" in issues[0]
|
||||
|
||||
def test_closing_fence_not_flagged(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "README.md").write_text("```python\nprint('hi')\n```\n")
|
||||
issues = check_code_block_languages(tmp_path)
|
||||
assert issues == []
|
||||
|
||||
|
||||
class TestCheckOrphanDocs:
|
||||
def test_no_orphans(self, tmp_path: Path) -> None:
|
||||
docs = tmp_path / "docs"
|
||||
docs.mkdir()
|
||||
(docs / "index.md").write_text("# Home\n[link](page.md)\n")
|
||||
(docs / "page.md").write_text("# Page\n")
|
||||
issues = check_orphan_docs(tmp_path, docs)
|
||||
assert issues == []
|
||||
|
||||
def test_orphan_found(self, tmp_path: Path) -> None:
|
||||
docs = tmp_path / "docs"
|
||||
docs.mkdir()
|
||||
(docs / "index.md").write_text("# Home\n")
|
||||
(docs / "page.md").write_text("# Page\n")
|
||||
issues = check_orphan_docs(tmp_path, docs)
|
||||
assert len(issues) == 1
|
||||
assert "orphan" in issues[0]
|
||||
|
||||
def test_no_docs_dir(self, tmp_path: Path) -> None:
|
||||
issues = check_orphan_docs(tmp_path, tmp_path / "docs")
|
||||
assert issues == []
|
||||
|
||||
def test_referenced_in_mapping(self, tmp_path: Path) -> None:
|
||||
docs = tmp_path / "docs"
|
||||
docs.mkdir()
|
||||
(docs / "index.md").write_text("# Home\n")
|
||||
(docs / "mapping.json").write_text(json.dumps({"page.md": "Page"}))
|
||||
(docs / "page.md").write_text("# Page\n")
|
||||
issues = check_orphan_docs(tmp_path, docs)
|
||||
assert issues == []
|
||||
|
||||
|
||||
class TestMain:
|
||||
def test_passes_clean_repo(self, tmp_path: Path) -> None:
|
||||
"""A clean repo with all files should pass."""
|
||||
@@ -434,3 +533,59 @@ class TestMain:
|
||||
# Stale docs are warnings, not errors
|
||||
assert result.exit_code == 0
|
||||
assert "stale" in result.output
|
||||
|
||||
def test_line_length_warning(self, tmp_path: Path) -> None:
|
||||
"""--check-line-length should warn but not fail."""
|
||||
(tmp_path / "README.md").write_text("# " + "x" * 200 + "\n")
|
||||
(tmp_path / "AGENTS.md").write_text("# AGENTS\n")
|
||||
(tmp_path / "CHANGELOG.md").write_text("# Changelog\n")
|
||||
docs = tmp_path / "docs"
|
||||
docs.mkdir()
|
||||
(docs / "index.md").write_text("# Home\n")
|
||||
(docs / "mapping.json").write_text(json.dumps({"index.md": "Home"}))
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--root", str(tmp_path), "--check-line-length"])
|
||||
assert result.exit_code == 0
|
||||
assert "long lines" in result.output
|
||||
|
||||
def test_line_length_many_warnings(self, tmp_path: Path) -> None:
|
||||
"""More than 10 long lines should show '... and N more'."""
|
||||
long_line = "x" * 200 + "\n"
|
||||
(tmp_path / "README.md").write_text(long_line * 15)
|
||||
(tmp_path / "AGENTS.md").write_text("# AGENTS\n")
|
||||
(tmp_path / "CHANGELOG.md").write_text("# Changelog\n")
|
||||
docs = tmp_path / "docs"
|
||||
docs.mkdir()
|
||||
(docs / "index.md").write_text("# Home\n")
|
||||
(docs / "mapping.json").write_text(json.dumps({"index.md": "Home"}))
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--root", str(tmp_path), "--check-line-length"])
|
||||
assert result.exit_code == 0
|
||||
assert "more" in result.output
|
||||
|
||||
def test_orphan_docs_warning(self, tmp_path: Path) -> None:
|
||||
"""--check-orphans should warn but not fail."""
|
||||
(tmp_path / "README.md").write_text("# Title\n")
|
||||
(tmp_path / "AGENTS.md").write_text("# AGENTS\n")
|
||||
(tmp_path / "CHANGELOG.md").write_text("# Changelog\n")
|
||||
docs = tmp_path / "docs"
|
||||
docs.mkdir()
|
||||
(docs / "index.md").write_text("# Home\n")
|
||||
(docs / "mapping.json").write_text(json.dumps({"index.md": "Home"}))
|
||||
(docs / "orphan.md").write_text("# Orphan\n")
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--root", str(tmp_path), "--check-orphans"])
|
||||
assert result.exit_code == 0
|
||||
assert "orphan" in result.output
|
||||
|
||||
def test_orphan_docs_invalid_mapping(self, tmp_path: Path) -> None:
|
||||
"""Invalid mapping.json should not crash orphan check."""
|
||||
docs = tmp_path / "docs"
|
||||
docs.mkdir()
|
||||
(docs / "index.md").write_text("# Home\n")
|
||||
(docs / "mapping.json").write_text("invalid json{")
|
||||
(docs / "page.md").write_text("# Page\n")
|
||||
# Should not raise — just returns issues
|
||||
issues = check_orphan_docs(tmp_path, docs)
|
||||
assert len(issues) == 1
|
||||
assert "orphan" in issues[0]
|
||||
|
||||
Reference in New Issue
Block a user