"""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