@@ -1656,5 +1656,181 @@ def test_try_raw_download_returns_content_on_200(self):
16561656 assert result == b'hello world'
16571657
16581658
1659+ class TestVirtualFilePackageYamlGeneration :
1660+ """Tests that apm.yml for virtual packages is always valid YAML."""
1661+
1662+ def _make_dep_ref (self , virtual_path ):
1663+ """Helper: build a minimal DependencyReference for a virtual file."""
1664+ from apm_cli .models .apm_package import DependencyReference
1665+ dep_ref = Mock (spec = DependencyReference )
1666+ dep_ref .is_virtual = True
1667+ dep_ref .virtual_path = virtual_path
1668+ dep_ref .reference = "main"
1669+ dep_ref .repo_url = "github/awesome-copilot"
1670+ dep_ref .get_virtual_package_name .return_value = "awesome-copilot-swe-subagent"
1671+ dep_ref .to_github_url .return_value = f"https://github.com/github/awesome-copilot/blob/main/{ virtual_path } "
1672+ dep_ref .is_virtual_file .return_value = True
1673+ dep_ref .VIRTUAL_FILE_EXTENSIONS = [".prompt.md" , ".instructions.md" , ".chatmode.md" , ".agent.md" ]
1674+ return dep_ref
1675+
1676+ def _make_collection_dep_ref (self , virtual_path ):
1677+ """Helper: build a minimal DependencyReference for a virtual collection."""
1678+ from apm_cli .models .apm_package import DependencyReference
1679+ dep_ref = Mock (spec = DependencyReference )
1680+ dep_ref .is_virtual = True
1681+ dep_ref .virtual_path = virtual_path
1682+ dep_ref .reference = "main"
1683+ dep_ref .repo_url = "github/my-org"
1684+ dep_ref .get_virtual_package_name .return_value = "my-org-my-collection"
1685+ dep_ref .to_github_url .return_value = f"https://github.com/github/my-org/blob/main/{ virtual_path } "
1686+ dep_ref .is_virtual_collection .return_value = True
1687+ return dep_ref
1688+
1689+ def test_yaml_with_colon_in_description (self , tmp_path ):
1690+ """apm.yml must be valid when the agent description contains a colon."""
1691+ import yaml
1692+
1693+ agent_content = (
1694+ b"---\n "
1695+ b"name: 'SWE'\n "
1696+ b"description: 'Senior software engineer subagent for implementation tasks:"
1697+ b" feature development, debugging, refactoring, and testing.'\n "
1698+ b"tools: ['vscode']\n "
1699+ b"---\n \n ## Body\n "
1700+ )
1701+
1702+ dep_ref = self ._make_dep_ref ("agents/swe-subagent.agent.md" )
1703+ target_path = tmp_path / "pkg"
1704+
1705+ downloader = GitHubPackageDownloader ()
1706+ with patch .dict (os .environ , {}, clear = True ), _CRED_FILL_PATCH :
1707+ with patch .object (downloader , "download_raw_file" , return_value = agent_content ):
1708+ downloader .download_virtual_file_package (dep_ref , target_path )
1709+
1710+ apm_yml_path = target_path / "apm.yml"
1711+ assert apm_yml_path .exists (), "apm.yml was not created"
1712+
1713+ content = apm_yml_path .read_text (encoding = "utf-8" )
1714+ parsed = yaml .safe_load (content ) # must not raise
1715+
1716+ expected = (
1717+ "Senior software engineer subagent for implementation tasks:"
1718+ " feature development, debugging, refactoring, and testing."
1719+ )
1720+ assert parsed ["description" ] == expected
1721+
1722+ def test_yaml_with_colon_in_name (self , tmp_path ):
1723+ """apm.yml must be valid even when the package name contains a colon."""
1724+ import yaml
1725+
1726+ dep_ref = self ._make_dep_ref ("agents/my-agent.agent.md" )
1727+ dep_ref .get_virtual_package_name .return_value = "org-name: special"
1728+
1729+ agent_content = b"---\n name: 'plain'\n description: 'plain'\n ---\n "
1730+ target_path = tmp_path / "pkg"
1731+
1732+ downloader = GitHubPackageDownloader ()
1733+ with patch .dict (os .environ , {}, clear = True ), _CRED_FILL_PATCH :
1734+ with patch .object (downloader , "download_raw_file" , return_value = agent_content ):
1735+ downloader .download_virtual_file_package (dep_ref , target_path )
1736+
1737+ content = (target_path / "apm.yml" ).read_text (encoding = "utf-8" )
1738+ parsed = yaml .safe_load (content )
1739+ assert parsed ["name" ] == "org-name: special"
1740+
1741+ def test_yaml_without_special_characters_still_valid (self , tmp_path ):
1742+ """apm.yml generation must still work for ordinary descriptions."""
1743+ import yaml
1744+
1745+ agent_content = (
1746+ b"---\n "
1747+ b"name: 'Simple Agent'\n "
1748+ b"description: 'A simple agent without special chars'\n "
1749+ b"---\n "
1750+ )
1751+
1752+ dep_ref = self ._make_dep_ref ("agents/simple.agent.md" )
1753+ target_path = tmp_path / "pkg"
1754+
1755+ downloader = GitHubPackageDownloader ()
1756+ with patch .dict (os .environ , {}, clear = True ), _CRED_FILL_PATCH :
1757+ with patch .object (downloader , "download_raw_file" , return_value = agent_content ):
1758+ downloader .download_virtual_file_package (dep_ref , target_path )
1759+
1760+ content = (target_path / "apm.yml" ).read_text (encoding = "utf-8" )
1761+ parsed = yaml .safe_load (content )
1762+ assert parsed ["description" ] == "A simple agent without special chars"
1763+
1764+ def test_collection_yaml_with_colon_in_description (self , tmp_path ):
1765+ """apm.yml for collection packages must be valid when description contains a colon."""
1766+ import yaml
1767+
1768+ # A minimal .collection.yml whose description contains ":"
1769+ collection_manifest = (
1770+ b"id: my-collection\n "
1771+ b"name: My Collection\n "
1772+ b"description: 'A collection for tasks: feature development, debugging.'\n "
1773+ b"items:\n "
1774+ b" - path: agents/my-agent.agent.md\n "
1775+ b" kind: agent\n "
1776+ )
1777+ agent_file = b"---\n name: My Agent\n ---\n ## Body\n "
1778+
1779+ dep_ref = self ._make_collection_dep_ref ("collections/my-collection" )
1780+ target_path = tmp_path / "pkg"
1781+
1782+ downloader = GitHubPackageDownloader ()
1783+
1784+ def _fake_download (dep_ref_arg , path , ref ):
1785+ if "collection" in path :
1786+ return collection_manifest
1787+ return agent_file
1788+
1789+ with patch .dict (os .environ , {}, clear = True ), _CRED_FILL_PATCH :
1790+ with patch .object (downloader , "download_raw_file" , side_effect = _fake_download ):
1791+ downloader .download_collection_package (dep_ref , target_path )
1792+
1793+ content = (target_path / "apm.yml" ).read_text (encoding = "utf-8" )
1794+ parsed = yaml .safe_load (content ) # must not raise
1795+
1796+ assert parsed ["description" ] == "A collection for tasks: feature development, debugging."
1797+
1798+ def test_collection_yaml_with_colon_in_tags (self , tmp_path ):
1799+ """apm.yml for collection packages must be valid when tags contain a colon."""
1800+ import yaml
1801+
1802+ collection_manifest = (
1803+ b"id: tagged-collection\n "
1804+ b"name: Tagged\n "
1805+ b"description: Normal description\n "
1806+ b"tags:\n "
1807+ b" - 'scope: engineering'\n "
1808+ b" - plain-tag\n "
1809+ b"items:\n "
1810+ b" - path: agents/my-agent.agent.md\n "
1811+ b" kind: agent\n "
1812+ )
1813+ agent_file = b"---\n name: My Agent\n ---\n ## Body\n "
1814+
1815+ dep_ref = self ._make_collection_dep_ref ("collections/tagged-collection" )
1816+ target_path = tmp_path / "pkg"
1817+
1818+ downloader = GitHubPackageDownloader ()
1819+
1820+ def _fake_download (dep_ref_arg , path , ref ):
1821+ if "collection" in path :
1822+ return collection_manifest
1823+ return agent_file
1824+
1825+ with patch .dict (os .environ , {}, clear = True ), _CRED_FILL_PATCH :
1826+ with patch .object (downloader , "download_raw_file" , side_effect = _fake_download ):
1827+ downloader .download_collection_package (dep_ref , target_path )
1828+
1829+ content = (target_path / "apm.yml" ).read_text (encoding = "utf-8" )
1830+ parsed = yaml .safe_load (content )
1831+
1832+ assert parsed ["tags" ] == ["scope: engineering" , "plain-tag" ]
1833+
1834+
16591835if __name__ == '__main__' :
16601836 pytest .main ([__file__ ])
0 commit comments