[Test Improver] test: add coverage tests for primitives discovery and parser#254
Conversation
Cover previously untested paths in discovery.py (78%→97%) and parser.py (72%→76%): - parse_skill_file: name-from-frontmatter and name-from-parent-dir paths - Unknown primitive type raises ValueError - _extract_primitive_name: structured dir variants and fallbacks - _is_context_file: memory dir detection - validate_primitive wrapper - _discover_local_skill / _discover_skill_in_directory with SKILL.md - scan_directory_with_source: no-.apm, success, and error paths - get_dependency_declaration_order: no-apm.yml, alias, virtual-GitHub, virtual-ADO, single-part repo_url, and transitive dedup branches - scan_local_primitives: apm_modules exclusion and parse-error path - _is_readable: readable, permission-denied, and binary-file cases - _should_skip_directory: all skip patterns and normal directories - _is_under_directory and find_primitive_files deduplication 54 new tests added. 1482 unit tests pass (1 pre-existing ANSI failure). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rage-da40935ebde67a0a
…rage-da40935ebde67a0a
There was a problem hiding this comment.
Pull request overview
This PR adds a new unit test suite to increase coverage for apm_cli.primitives.discovery and apm_cli.primitives.parser, focusing on previously untested branches around primitive parsing, dependency ordering, and filesystem scanning edge cases.
Changes:
- Added
tests/unit/primitives/test_discovery_parser.pywith extensive branch-coverage tests for discovery + parsing helpers. - Added targeted tests for dependency declaration ordering (including virtual package path formats) and discovery error-handling paths.
| """Files in .apm/agents/ fall through to fallback (agents/ not in structured subdirs list). | ||
| The fallback strips .md giving 'myagent.agent'.""" | ||
| path = Path("/project/.apm/agents/myagent.agent.md") | ||
| name = _extract_primitive_name(path) | ||
| # 'agents' is not in the structured subdirs list, so fallback strips .md only | ||
| self.assertEqual(name, "myagent.agent") | ||
|
|
||
| def test_agent_md_in_github_agents_dir(self): | ||
| """Files in .github/agents/ also fall through to fallback.""" | ||
| path = Path("/project/.github/agents/reviewer.agent.md") | ||
| name = _extract_primitive_name(path) | ||
| self.assertEqual(name, "reviewer.agent") |
There was a problem hiding this comment.
The agent-name assertions here expect _extract_primitive_name() to return values like reviewer.agent for *.agent.md files. That makes the name inconsistent with other primitives (e.g., .chatmode.md strips the type suffix) and appears to contradict the CLI docs example apm compile --chatmode architect (implying the name should be architect, not architect.agent). Consider updating _extract_primitive_name() to strip .agent.md (and treat .apm/agents / .github/agents as structured dirs), then update these tests accordingly so they enforce the documented behavior.
| """Files in .apm/agents/ fall through to fallback (agents/ not in structured subdirs list). | |
| The fallback strips .md giving 'myagent.agent'.""" | |
| path = Path("/project/.apm/agents/myagent.agent.md") | |
| name = _extract_primitive_name(path) | |
| # 'agents' is not in the structured subdirs list, so fallback strips .md only | |
| self.assertEqual(name, "myagent.agent") | |
| def test_agent_md_in_github_agents_dir(self): | |
| """Files in .github/agents/ also fall through to fallback.""" | |
| path = Path("/project/.github/agents/reviewer.agent.md") | |
| name = _extract_primitive_name(path) | |
| self.assertEqual(name, "reviewer.agent") | |
| """Files in .apm/agents/ are treated as structured agent primitives. | |
| The '.agent.md' suffix is stripped, yielding just the agent name.""" | |
| path = Path("/project/.apm/agents/myagent.agent.md") | |
| name = _extract_primitive_name(path) | |
| self.assertEqual(name, "myagent") | |
| def test_agent_md_in_github_agents_dir(self): | |
| """.github/agents/ is also treated as a structured agent primitive directory.""" | |
| path = Path("/project/.github/agents/reviewer.agent.md") | |
| name = _extract_primitive_name(path) | |
| self.assertEqual(name, "reviewer") |
| os.chmod(path, 0o000) | ||
| try: | ||
| result = _is_readable(path) | ||
| self.assertFalse(result) | ||
| finally: | ||
| os.chmod(path, 0o644) |
There was a problem hiding this comment.
test_unreadable_file_returns_false uses chmod 0o000 to simulate unreadability. This is unreliable across platforms (e.g., Windows permission semantics) and can also fail when tests run with elevated privileges where the file remains readable. A more stable approach is to mock open() (or Path.open) to raise PermissionError for this path, so the test is deterministic in CI.
| os.chmod(path, 0o000) | |
| try: | |
| result = _is_readable(path) | |
| self.assertFalse(result) | |
| finally: | |
| os.chmod(path, 0o644) | |
| # Simulate unreadable file by forcing PermissionError when opening. | |
| with patch.object(Path, "open", side_effect=PermissionError): | |
| result = _is_readable(path) | |
| self.assertFalse(result) |
| import os | ||
| import stat | ||
| import tempfile | ||
| import unittest | ||
| from pathlib import Path | ||
| from unittest.mock import MagicMock, patch | ||
|
|
||
| from apm_cli.primitives.discovery import ( | ||
| _discover_local_skill, | ||
| _discover_skill_in_directory, | ||
| _is_readable, | ||
| _is_under_directory, | ||
| _should_skip_directory, | ||
| discover_primitives, | ||
| find_primitive_files, | ||
| get_dependency_declaration_order, | ||
| scan_directory_with_source, | ||
| scan_local_primitives, | ||
| ) |
There was a problem hiding this comment.
This import block includes unused imports (stat) and unused imported symbols (discover_primitives). If/when the repo runs linting (or contributors run static analysis), these will be flagged. Please remove unused imports or add usage.
| def test_unreadable_skill_md_warns_and_skips(self): | ||
| """parse error on SKILL.md is caught, printed as warning, skipped.""" | ||
| skill_path = Path(self.tmp) / "SKILL.md" | ||
| _write(skill_path, SKILL_CONTENT) | ||
| collection = PrimitiveCollection() | ||
| with patch( | ||
| "apm_cli.primitives.discovery.parse_skill_file", | ||
| side_effect=ValueError("bad"), | ||
| ): | ||
| import io | ||
| from contextlib import redirect_stdout | ||
|
|
||
| buf = io.StringIO() | ||
| with redirect_stdout(buf): | ||
| _discover_local_skill(self.tmp, collection) | ||
| self.assertIn("Warning", buf.getvalue()) | ||
| self.assertEqual(len(collection.skills), 0) |
There was a problem hiding this comment.
The test name/docstring says "unreadable" SKILL.md, but the test is actually exercising the "parse_skill_file raises" path (a parse error), not file readability. Renaming the test (and docstring) to reflect the behavior under test will avoid confusion when debugging failures.
|
@copilot address pr feedback properly |
|
@danielmeppiel I've opened a new pull request, #268, to work on those changes. Once the pull request is ready, I'll request review from you. |
Co-authored-by: danielmeppiel <51440732+danielmeppiel@users.noreply.github.com>
test: address PR review feedback on primitives discovery/parser tests
🤖 Test Improver — automated AI assistant focused on improving tests.
Goal
Increase test coverage for
primitives/discovery.pyandprimitives/parser.py, two core modules involved in finding and parsing APM primitive files (chatmodes, instructions, contexts, skills).Approach
Added 54 focused unit tests in a new file
tests/unit/primitives/test_discovery_parser.py, covering branches that were reachable but had no existing test paths.Covered paths
parse_skill_fileparse_primitive_fileValueError_extract_primitive_name.apm/agents/fallback_is_context_file.apm/memory/and.github/memory/detected; other dirs not matchedvalidate_primitive_discover_local_skill_discover_skill_in_directoryscan_directory_with_source.apmdir (SKILL.md fallback);.apmdir with instructions; parse error warningget_dependency_declaration_orderapm.yml; alias dep; virtual GitHub subdir/collection; virtual ADO subdir/collection; single-part repo_url fallbacks; transitive deduplication; exception →[]+ warningscan_local_primitivesapm_modules/exclusion; parse error warns and continues_is_readable_should_skip_directory.git,node_modules,__pycache__, etc.); normal dirs not skipped_is_under_directoryfind_primitive_filesCoverage impact
primitives/discovery.pyprimitives/parser.pyMeasured via
pytest tests/unit/ --cov=apm_cli.primitives.discovery --cov=apm_cli.primitives.parser.Note: 4 lines in
discovery.pyremain uncovered in the full-suite coverage run (lines 140, 297, 316, 333 — allcollection.add_primitive()success-path calls). These are covered in isolation but appear uncovered in the full suite due to a pre-existing YAML global-state pollution issue that causes some earlier tests to corrupt PyYAML's constructor table underpytest-covinstrumentation. This is not introduced by this PR.Test status
Reproducibility