Public Access
Post-merge / detect-type (push) Successful in 38s
Post-merge / validate-commit-msg (push) Successful in 45s
Post-merge / sync-wiki (push) Successful in 51s
Post-merge / release (push) Successful in 57s
Post-merge / badges (push) Successful in 59s
Post-merge / configure-repo (push) Successful in 1m1s
Post-merge / vikunja (push) Successful in 1m22s
232 lines
9.7 KiB
Python
232 lines
9.7 KiB
Python
"""Unit tests for devx.tools.check_agent_docs."""
|
|
|
|
import re
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from click.testing import CliRunner
|
|
|
|
from devx.tools.check_agent_docs import (
|
|
DEFAULT_REPO_PATH_PREFIXES,
|
|
DEFAULT_SCAN_DIRS,
|
|
DEFAULT_SCAN_EXTENSIONS,
|
|
DEFAULT_SCAN_FILES,
|
|
MIN_PATH_REF_LENGTH_DEFAULT,
|
|
_check_file,
|
|
_collect_doc_files,
|
|
_is_legitimate_ref,
|
|
_should_skip,
|
|
cli,
|
|
)
|
|
|
|
|
|
class TestShouldSkip:
|
|
def test_skips_excluded_path(self, tmp_path: Path) -> None:
|
|
f = tmp_path / "docs" / "retrospectives" / "r.md"
|
|
f.parent.mkdir(parents=True)
|
|
f.write_text("")
|
|
assert _should_skip(f, ["docs/retrospectives"], tmp_path) is True
|
|
|
|
def test_does_not_skip_normal(self, tmp_path: Path) -> None:
|
|
f = tmp_path / "docs" / "guide.md"
|
|
f.parent.mkdir(parents=True)
|
|
f.write_text("")
|
|
assert _should_skip(f, ["docs/retrospectives"], tmp_path) is False
|
|
|
|
def test_returns_false_for_path_outside_repo(self, tmp_path: Path) -> None:
|
|
f = Path("/tmp/some_other_path/guide.md")
|
|
assert _should_skip(f, [], tmp_path) is False
|
|
|
|
|
|
class TestIsLegitimateRef:
|
|
def test_legitimate_legacy(self) -> None:
|
|
assert _is_legitimate_ref("This is legacy code", ["legacy"]) is True
|
|
|
|
def test_not_legitimate(self) -> None:
|
|
assert _is_legitimate_ref("Use this file", ["legacy"]) is False
|
|
|
|
def test_case_insensitive(self) -> None:
|
|
assert _is_legitimate_ref("This is LEGACY", ["legacy"]) is True
|
|
|
|
|
|
class TestCollectDocFiles:
|
|
def test_collects_devin_and_docs(self, tmp_path: Path) -> None:
|
|
(tmp_path / ".devin").mkdir()
|
|
(tmp_path / ".devin" / "guide.md").write_text("")
|
|
(tmp_path / "docs").mkdir()
|
|
(tmp_path / "docs" / "api.md").write_text("")
|
|
(tmp_path / "README.md").write_text("")
|
|
|
|
files = _collect_doc_files(tmp_path, DEFAULT_SCAN_DIRS, DEFAULT_SCAN_FILES, DEFAULT_SCAN_EXTENSIONS, [])
|
|
names = {f.name for f in files}
|
|
assert "guide.md" in names
|
|
assert "api.md" in names
|
|
assert "README.md" in names
|
|
|
|
def test_excludes_paths(self, tmp_path: Path) -> None:
|
|
(tmp_path / "docs" / "retrospectives").mkdir(parents=True)
|
|
(tmp_path / "docs" / "retrospectives" / "r.md").write_text("")
|
|
(tmp_path / "docs" / "guide.md").write_text("")
|
|
|
|
files = _collect_doc_files(
|
|
tmp_path, DEFAULT_SCAN_DIRS, DEFAULT_SCAN_FILES, DEFAULT_SCAN_EXTENSIONS, ["docs/retrospectives"]
|
|
)
|
|
names = {f.name for f in files}
|
|
assert "guide.md" in names
|
|
assert "r.md" not in names
|
|
|
|
def test_deduplicates(self, tmp_path: Path) -> None:
|
|
(tmp_path / "docs").mkdir()
|
|
(tmp_path / "docs" / "api.md").write_text("")
|
|
|
|
files = _collect_doc_files(tmp_path, ["docs", "docs"], DEFAULT_SCAN_FILES, DEFAULT_SCAN_EXTENSIONS, [])
|
|
assert len(files) == 1
|
|
|
|
|
|
class TestCheckFile:
|
|
def test_detects_deleted_file_ref(self, tmp_path: Path) -> None:
|
|
doc = tmp_path / "docs" / "guide.md"
|
|
doc.parent.mkdir(parents=True)
|
|
doc.write_text("See scripts/old.py for details.\n")
|
|
issues = _check_file(
|
|
doc, tmp_path, {"scripts/old.py"}, [], [], DEFAULT_REPO_PATH_PREFIXES, MIN_PATH_REF_LENGTH_DEFAULT, []
|
|
)
|
|
assert any("deleted file" in i for i in issues)
|
|
|
|
def test_detects_nonexistent_file_ref(self, tmp_path: Path) -> None:
|
|
doc = tmp_path / "docs" / "guide.md"
|
|
doc.parent.mkdir(parents=True)
|
|
doc.write_text("See scripts/nonexistent.py for details.\n")
|
|
issues = _check_file(doc, tmp_path, set(), [], [], DEFAULT_REPO_PATH_PREFIXES, MIN_PATH_REF_LENGTH_DEFAULT, [])
|
|
assert any("non-existent file" in i for i in issues)
|
|
|
|
def test_does_not_flag_existing_file(self, tmp_path: Path) -> None:
|
|
(tmp_path / "scripts").mkdir()
|
|
(tmp_path / "scripts" / "exists.py").write_text("")
|
|
doc = tmp_path / "docs" / "guide.md"
|
|
doc.parent.mkdir(parents=True)
|
|
doc.write_text("See scripts/exists.py for details.\n")
|
|
issues = _check_file(doc, tmp_path, set(), [], [], DEFAULT_REPO_PATH_PREFIXES, MIN_PATH_REF_LENGTH_DEFAULT, [])
|
|
assert issues == []
|
|
|
|
def test_detects_deprecated_pattern(self, tmp_path: Path) -> None:
|
|
doc = tmp_path / "docs" / "guide.md"
|
|
doc.parent.mkdir(parents=True)
|
|
doc.write_text("Use ansible/envs/prod/secrets.yml for config.\n")
|
|
patterns = [re.compile(r"ansible/envs/[^/]+/secrets\.yml")]
|
|
issues = _check_file(
|
|
doc, tmp_path, set(), patterns, [], DEFAULT_REPO_PATH_PREFIXES, MIN_PATH_REF_LENGTH_DEFAULT, []
|
|
)
|
|
assert any("deprecated pattern" in i for i in issues)
|
|
|
|
def test_legitimate_ref_skips_deprecated(self, tmp_path: Path) -> None:
|
|
# Create the referenced file so the non-existent check doesn't trigger
|
|
secrets = tmp_path / "ansible" / "envs" / "prod" / "secrets.yml"
|
|
secrets.parent.mkdir(parents=True)
|
|
secrets.write_text("")
|
|
doc = tmp_path / "docs" / "guide.md"
|
|
doc.parent.mkdir(parents=True)
|
|
doc.write_text("The legacy ansible/envs/prod/secrets.yml is deprecated.\n")
|
|
patterns = [re.compile(r"ansible/envs/[^/]+/secrets\.yml")]
|
|
issues = _check_file(
|
|
doc, tmp_path, set(), patterns, ["deprecated"], DEFAULT_REPO_PATH_PREFIXES, MIN_PATH_REF_LENGTH_DEFAULT, []
|
|
)
|
|
assert issues == []
|
|
|
|
def test_unicode_error_returns_empty(self, tmp_path: Path) -> None:
|
|
doc = tmp_path / "docs" / "guide.md"
|
|
doc.parent.mkdir(parents=True)
|
|
doc.write_bytes(b"\xff\xfe\x00\x00")
|
|
issues = _check_file(doc, tmp_path, set(), [], [], DEFAULT_REPO_PATH_PREFIXES, MIN_PATH_REF_LENGTH_DEFAULT, [])
|
|
assert issues == []
|
|
|
|
def test_skips_short_ref(self, tmp_path: Path) -> None:
|
|
doc = tmp_path / "docs" / "guide.md"
|
|
doc.parent.mkdir(parents=True)
|
|
doc.write_text("See a.py for details.\n")
|
|
issues = _check_file(doc, tmp_path, set(), [], [], DEFAULT_REPO_PATH_PREFIXES, 5, [])
|
|
# "a.py" is only 4 chars, below min_path_ref_length
|
|
assert issues == []
|
|
|
|
def test_skips_ref_without_repo_prefix(self, tmp_path: Path) -> None:
|
|
doc = tmp_path / "docs" / "guide.md"
|
|
doc.parent.mkdir(parents=True)
|
|
doc.write_text("See vendor/some/long/path.py for details.\n")
|
|
issues = _check_file(doc, tmp_path, set(), [], [], DEFAULT_REPO_PATH_PREFIXES, MIN_PATH_REF_LENGTH_DEFAULT, [])
|
|
# "vendor/" is not in repo_path_prefixes
|
|
assert issues == []
|
|
|
|
def test_skip_ref_prefixes_skips_nonexistent(self, tmp_path: Path) -> None:
|
|
doc = tmp_path / "docs" / "guide.md"
|
|
doc.parent.mkdir(parents=True)
|
|
doc.write_text("See scripts/test_foo.py for details.\n")
|
|
issues = _check_file(
|
|
doc, tmp_path, set(), [], [], DEFAULT_REPO_PATH_PREFIXES, MIN_PATH_REF_LENGTH_DEFAULT, ["scripts/test_"]
|
|
)
|
|
assert issues == []
|
|
|
|
|
|
class TestCli:
|
|
def test_passes_when_no_issues(self, tmp_path: Path) -> None:
|
|
(tmp_path / "docs").mkdir()
|
|
(tmp_path / "docs" / "guide.md").write_text("All good.\n")
|
|
runner = CliRunner()
|
|
with (
|
|
patch("devx.tools.check_agent_docs._load_config", return_value={}),
|
|
patch("devx.tools.check_agent_docs.Path.cwd", return_value=tmp_path),
|
|
):
|
|
result = runner.invoke(cli, [])
|
|
assert result.exit_code == 0
|
|
assert "Passed" in result.output
|
|
|
|
def test_fails_when_stale_ref(self, tmp_path: Path) -> None:
|
|
(tmp_path / "docs").mkdir()
|
|
(tmp_path / "docs" / "guide.md").write_text("See scripts/deleted.py\n")
|
|
cfg = {"deleted_files": ["scripts/deleted.py"]}
|
|
runner = CliRunner()
|
|
with (
|
|
patch("devx.tools.check_agent_docs._load_config", return_value=cfg),
|
|
patch("devx.tools.check_agent_docs.Path.cwd", return_value=tmp_path),
|
|
):
|
|
result = runner.invoke(cli, [])
|
|
assert result.exit_code != 0
|
|
assert "FAILED" in result.output
|
|
|
|
def test_load_config_returns_empty_when_not_dict(self) -> None:
|
|
from devx.tools.check_agent_docs import _load_config
|
|
|
|
with patch("devx.tools.check_agent_docs._load_pyproject_devx", return_value={"check_agent_docs": "not a dict"}):
|
|
assert _load_config() == {}
|
|
|
|
def test_load_config_returns_dict_when_valid(self) -> None:
|
|
from devx.tools.check_agent_docs import _load_config
|
|
|
|
cfg = {"scan_dirs": ["custom"]}
|
|
with patch("devx.tools.check_agent_docs._load_pyproject_devx", return_value={"check_agent_docs": cfg}):
|
|
assert _load_config() == cfg
|
|
|
|
def test_invalid_regex_pattern_skipped(self, tmp_path: Path) -> None:
|
|
(tmp_path / "docs").mkdir()
|
|
(tmp_path / "docs" / "guide.md").write_text("All good.\n")
|
|
cfg = {"deprecated_patterns": ["[invalid"]}
|
|
runner = CliRunner()
|
|
with (
|
|
patch("devx.tools.check_agent_docs._load_config", return_value=cfg),
|
|
patch("devx.tools.check_agent_docs.Path.cwd", return_value=tmp_path),
|
|
):
|
|
result = runner.invoke(cli, [])
|
|
assert result.exit_code == 0
|
|
|
|
def test_custom_scan_dirs(self, tmp_path: Path) -> None:
|
|
custom = tmp_path / "custom_docs"
|
|
custom.mkdir()
|
|
(custom / "guide.md").write_text("See scripts/deleted.py\n")
|
|
cfg = {"scan_dirs": ["custom_docs"], "deleted_files": ["scripts/deleted.py"]}
|
|
runner = CliRunner()
|
|
with (
|
|
patch("devx.tools.check_agent_docs._load_config", return_value=cfg),
|
|
patch("devx.tools.check_agent_docs.Path.cwd", return_value=tmp_path),
|
|
):
|
|
result = runner.invoke(cli, [])
|
|
assert result.exit_code != 0
|