Coverage for tests / test_scanner.py: 100%
223 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-03 20:19 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-03 20:19 +0000
1import json 1abcdefghi
2from pathlib import Path 1abcdefghi
4import pytest 1abcdefghi
6import library_skills.scanner as scanner 1abcdefghi
7from library_skills.scanner import ( 1abcdefghi
8 _is_relative_to,
9 _read_editable_source_root,
10 _scan_editable_direct_url,
11 scan_node_packages,
12 scan_python_distributions,
13)
16def write_skill(root: Path, name: str, description: str = "Demo skill.") -> Path: 1abcdefghi
17 skill_dir = root / ".agents" / "skills" / name 2E ebv ] y ^ s j z ? F K L M N O P Q R S T U V k l G fbw _ A ` t m B @ H W X Y Z 0 1 2 3 4 5 6 7 n o I gbx { C | u p D [ J 8 9 ! # $ % ' ( ) * + , q r
18 skill_dir.mkdir(parents=True) 2E ebv ] y ^ s j z ? F K L M N O P Q R S T U V k l G fbw _ A ` t m B @ H W X Y Z 0 1 2 3 4 5 6 7 n o I gbx { C | u p D [ J 8 9 ! # $ % ' ( ) * + , q r
19 skill_md = skill_dir / "SKILL.md" 2E ebv ] y ^ s j z ? F K L M N O P Q R S T U V k l G fbw _ A ` t m B @ H W X Y Z 0 1 2 3 4 5 6 7 n o I gbx { C | u p D [ J 8 9 ! # $ % ' ( ) * + , q r
20 skill_md.write_text( 2E ebv ] y ^ s j z ? F K L M N O P Q R S T U V k l G fbw _ A ` t m B @ H W X Y Z 0 1 2 3 4 5 6 7 n o I gbx { C | u p D [ J 8 9 ! # $ % ' ( ) * + , q r
21 f"---\nname: {name}\ndescription: {description}\n---\n",
22 encoding="utf-8",
23 )
24 return skill_md 2E ebv ] y ^ s j z ? F K L M N O P Q R S T U V k l G fbw _ A ` t m B @ H W X Y Z 0 1 2 3 4 5 6 7 n o I gbx { C | u p D [ J 8 9 ! # $ % ' ( ) * + , q r
27def write_dist_info( 1abcdefghi
28 site_packages: Path,
29 dist_name: str,
30 *,
31 version: str = "1.0.0",
32 record_paths: list[str] | None = None,
33) -> Path:
34 dist_info = site_packages / f"{dist_name.replace('-', '_')}-{version}.dist-info" 1Evjz?FKLMNOPQRSTUVklGwmB@HWXYZ01234567noIxpD[J89!#$%'()*+,qr
35 dist_info.mkdir(parents=True) 1Evjz?FKLMNOPQRSTUVklGwmB@HWXYZ01234567noIxpD[J89!#$%'()*+,qr
36 dist_info.joinpath("METADATA").write_text( 1Evjz?FKLMNOPQRSTUVklGwmB@HWXYZ01234567noIxpD[J89!#$%'()*+,qr
37 f"Metadata-Version: 2.4\nName: {dist_name}\nVersion: {version}\n",
38 encoding="utf-8",
39 )
40 if record_paths is not None: 1Evjz?FKLMNOPQRSTUVklGwmB@HWXYZ01234567noIxpD[J89!#$%'()*+,qr
41 dist_info.joinpath("RECORD").write_text( 1jzFLMNOPQRSTUVklmBHXYZ01234567nopDJ9!#$%'()*+,qr
42 "".join(f"{path},,\n" for path in record_paths),
43 encoding="utf-8",
44 )
45 return dist_info 1Evjz?FKLMNOPQRSTUVklGwmB@HWXYZ01234567noIxpD[J89!#$%'()*+,qr
48def write_node_package( 1abcdefghi
49 node_modules: Path,
50 package_name: str,
51 *,
52 version: str = "1.0.0",
53 skill_name: str = "node-skill",
54) -> Path:
55 package_root = node_modules / package_name 2] y ^ } ~ _ A ` abbb{ C | cbdb
56 skill_md = write_skill(package_root, skill_name) 2] y ^ } ~ _ A ` abbb{ C | cbdb
57 package_root.joinpath("package.json").write_text( 2] y ^ } ~ _ A ` abbb{ C | cbdb
58 json.dumps({"name": package_name, "version": version}),
59 encoding="utf-8",
60 )
61 return skill_md 2] y ^ } ~ _ A ` abbb{ C | cbdb
64def test_scan_python_distributions_discovers_record_based_skills(tmp_path): 1abcdefghi
65 site_packages = tmp_path / "site-packages" 1jklmnopqr
66 package_root = site_packages / "demo_pkg" 1jklmnopqr
67 skill_md = write_skill(package_root, "demo-skill") 1jklmnopqr
68 write_dist_info( 1jklmnopqr
69 site_packages,
70 "demo-pkg",
71 version="1.2.3",
72 record_paths=[skill_md.relative_to(site_packages).as_posix()],
73 )
75 result = scan_python_distributions(site_packages) 1jklmnopqr
77 assert result.warnings == [] 1jklmnopqr
78 assert len(result.skills) == 1 1jklmnopqr
79 skill = result.skills[0] 1jklmnopqr
80 assert skill.name == "demo-skill" 1jklmnopqr
81 assert skill.description == "Demo skill." 1jklmnopqr
82 assert skill.package_name == "demo-pkg" 1jklmnopqr
83 assert skill.package_version == "1.2.3" 1jklmnopqr
84 assert skill.path == skill_md.resolve() 1jklmnopqr
85 assert skill.skill_dir == skill_md.parent 1jklmnopqr
88def test_scan_python_distributions_parses_folded_skill_description(tmp_path): 1abcdefghi
89 site_packages = tmp_path / "site-packages" 2F zbAbH BbCbJ DbEb
90 package_root = site_packages / "demo_pkg" 2F zbAbH BbCbJ DbEb
91 skill_md = write_skill(package_root, "demo-skill") 2F zbAbH BbCbJ DbEb
92 skill_md.write_text( 2F zbAbH BbCbJ DbEb
93 "---\n"
94 "name: demo-skill\n"
95 "description: >\n"
96 " This is a very long\n"
97 " sentence that should be\n"
98 " parsed as a single line.\n"
99 "---\n",
100 encoding="utf-8",
101 )
102 write_dist_info( 2F zbAbH BbCbJ DbEb
103 site_packages,
104 "demo-pkg",
105 record_paths=[skill_md.relative_to(site_packages).as_posix()],
106 )
108 result = scan_python_distributions(site_packages) 2F zbAbH BbCbJ DbEb
110 assert result.warnings == [] 2F zbAbH BbCbJ DbEb
111 assert len(result.skills) == 1 2F zbAbH BbCbJ DbEb
112 assert ( 2F zbAbH BbCbJ DbEb
113 result.skills[0].description
114 == "This is a very long sentence that should be parsed as a single line."
115 )
118def test_scan_node_packages_discovers_package_skills(tmp_path): 1abcdefghi
119 node_modules = tmp_path / "node_modules" 2y } ~ A abbbC cbdb
120 skill_md = write_node_package( 2y } ~ A abbbC cbdb
121 node_modules,
122 "@scope/demo-pkg",
123 version="2.0.0",
124 skill_name="node-skill",
125 )
127 result = scan_node_packages(node_modules) 2y } ~ A abbbC cbdb
129 assert result.warnings == [] 2y } ~ A abbbC cbdb
130 assert len(result.skills) == 1 2y } ~ A abbbC cbdb
131 skill = result.skills[0] 2y } ~ A abbbC cbdb
132 assert skill.name == "node-skill" 2y } ~ A abbbC cbdb
133 assert skill.description == "Demo skill." 2y } ~ A abbbC cbdb
134 assert skill.package_name == "@scope/demo-pkg" 2y } ~ A abbbC cbdb
135 assert skill.package_version == "2.0.0" 2y } ~ A abbbC cbdb
136 assert skill.path == skill_md.resolve() 2y } ~ A abbbC cbdb
137 assert skill.skill_dir == skill_md.parent.resolve() 2y } ~ A abbbC cbdb
140def test_scan_node_packages_deduplicates_resolved_skill_dirs(tmp_path, monkeypatch): 1abcdefghi
141 node_modules = tmp_path / "node_modules" 2] ;b=b_ ?b@b{ [b]b
142 skill_md = write_node_package( 2] ;b=b_ ?b@b{ [b]b
143 node_modules,
144 "demo-pkg",
145 skill_name="node-skill",
146 )
147 package_root = skill_md.parents[3] 2] ;b=b_ ?b@b{ [b]b
148 monkeypatch.setattr( 2] ;b=b_ ?b@b{ [b]b
149 scanner,
150 "_iter_node_package_roots",
151 lambda _node_modules: [package_root, package_root],
152 )
154 result = scan_node_packages(node_modules) 2] ;b=b_ ?b@b{ [b]b
156 assert result.warnings == [] 2] ;b=b_ ?b@b{ [b]b
157 assert [skill.name for skill in result.skills] == ["node-skill"] 2] ;b=b_ ?b@b{ [b]b
160def test_scan_node_packages_warns_for_missing_or_invalid_metadata(tmp_path): 1abcdefghi
161 missing = scan_node_packages(tmp_path / "missing") 1s-.t/:u;=
162 assert missing.skills == [] 1s-.t/:u;=
163 assert missing.warnings == [ 1s-.t/:u;=
164 f"node_modules directory not found: {tmp_path / 'missing'}"
165 ]
167 node_modules = tmp_path / "node_modules" 1s-.t/:u;=
168 node_modules.mkdir() 1s-.t/:u;=
169 (node_modules / "README.md").write_text("not a package", encoding="utf-8") 1s-.t/:u;=
170 (node_modules / ".bin").mkdir() 1s-.t/:u;=
171 (node_modules / ".vite-temp").mkdir() 1s-.t/:u;=
172 package_root = node_modules / "bad-pkg" 1s-.t/:u;=
173 write_skill(package_root, "bad-skill") 1s-.t/:u;=
174 array_package = node_modules / "array-pkg" 1s-.t/:u;=
175 array_package.mkdir() 1s-.t/:u;=
176 array_package.joinpath("package.json").write_text("[]", encoding="utf-8") 1s-.t/:u;=
177 nameless_package = node_modules / "nameless-pkg" 1s-.t/:u;=
178 nameless_package.mkdir() 1s-.t/:u;=
179 nameless_package.joinpath("package.json").write_text("{}", encoding="utf-8") 1s-.t/:u;=
181 result = scan_node_packages(node_modules) 1s-.t/:u;=
183 assert result.skills == [] 1s-.t/:u;=
184 assert result.warnings == [ 1s-.t/:u;=
185 f"Skipping invalid package metadata: {array_package}",
186 f"Skipping invalid package metadata: {package_root}",
187 f"Skipping invalid package metadata: {nameless_package}",
188 ]
191def test_scan_node_packages_warns_for_invalid_skill_metadata(tmp_path): 1abcdefghi
192 node_modules = tmp_path / "node_modules" 2^ ^b_b` `b{b| |b}b
193 skill_md = write_node_package(node_modules, "demo-pkg", skill_name="actual-name") 2^ ^b_b` `b{b| |b}b
194 skill_md.write_text( 2^ ^b_b` `b{b| |b}b
195 "---\nname: different-name\ndescription: Demo skill.\n---\n",
196 encoding="utf-8",
197 )
199 result = scan_node_packages(node_modules) 2^ ^b_b` `b{b| |b}b
201 assert result.skills == [] 2^ ^b_b` `b{b| |b}b
202 assert len(result.warnings) == 1 2^ ^b_b` `b{b| |b}b
203 assert "must match parent directory name" in result.warnings[0] 2^ ^b_b` `b{b| |b}b
206def test_scan_python_distributions_warns_for_invalid_skill_metadata(tmp_path): 1abcdefghi
207 site_packages = tmp_path / "site-packages" 2U LbMb6 NbOb+ PbQb
208 skill_md = write_skill(site_packages / "demo_pkg", "actual-name") 2U LbMb6 NbOb+ PbQb
209 skill_md.write_text( 2U LbMb6 NbOb+ PbQb
210 "---\nname: different-name\ndescription: Demo skill.\n---\n",
211 encoding="utf-8",
212 )
213 write_dist_info( 2U LbMb6 NbOb+ PbQb
214 site_packages,
215 "demo-pkg",
216 record_paths=[skill_md.relative_to(site_packages).as_posix()],
217 )
219 result = scan_python_distributions(site_packages) 2U LbMb6 NbOb+ PbQb
221 assert result.skills == [] 2U LbMb6 NbOb+ PbQb
222 assert len(result.warnings) == 1 2U LbMb6 NbOb+ PbQb
223 assert "must match parent directory name" in result.warnings[0] 2U LbMb6 NbOb+ PbQb
226def test_scan_python_distributions_uses_editable_direct_url_fallback(tmp_path): 1abcdefghi
227 site_packages = tmp_path / "site-packages" 2K FbGbW HbIb8 JbKb
228 source_root = tmp_path / "source" 2K FbGbW HbIb8 JbKb
229 skill_md = write_skill(source_root / "demo_pkg", "editable-skill") 2K FbGbW HbIb8 JbKb
230 dist_info = write_dist_info(site_packages, "editable-pkg") 2K FbGbW HbIb8 JbKb
231 dist_info.joinpath("direct_url.json").write_text( 2K FbGbW HbIb8 JbKb
232 json.dumps(
233 {
234 "url": source_root.as_uri(),
235 "dir_info": {"editable": True},
236 }
237 ),
238 encoding="utf-8",
239 )
241 result = scan_python_distributions(site_packages) 2K FbGbW HbIb8 JbKb
243 assert result.warnings == [] 2K FbGbW HbIb8 JbKb
244 assert [skill.name for skill in result.skills] == ["editable-skill"] 2K FbGbW HbIb8 JbKb
245 assert result.skills[0].path == skill_md.resolve() 2K FbGbW HbIb8 JbKb
248def test_scan_python_distributions_does_not_claim_nested_site_packages_skills( 1abcdefghi
249 tmp_path,
250):
251 source_root = tmp_path / "source" 2z nbobB pbqbD rbsb
252 site_packages = source_root / ".venv" / "lib" / "python3.12" / "site-packages" 2z nbobB pbqbD rbsb
253 dependency_skill = write_skill(site_packages / "fastapi", "fastapi") 2z nbobB pbqbD rbsb
254 editable_dist_info = write_dist_info(site_packages, "aaa-editable-pkg") 2z nbobB pbqbD rbsb
255 editable_dist_info.joinpath("direct_url.json").write_text( 2z nbobB pbqbD rbsb
256 json.dumps({"url": source_root.as_uri(), "dir_info": {"editable": True}}),
257 encoding="utf-8",
258 )
259 write_dist_info( 2z nbobB pbqbD rbsb
260 site_packages,
261 "fastapi",
262 record_paths=[dependency_skill.relative_to(site_packages).as_posix()],
263 )
265 result = scan_python_distributions(site_packages) 2z nbobB pbqbD rbsb
267 assert result.warnings == [] 2z nbobB pbqbD rbsb
268 assert [skill.name for skill in result.skills] == ["fastapi"] 2z nbobB pbqbD rbsb
269 assert result.skills[0].package_name == "fastapi" 2z nbobB pbqbD rbsb
272def test_scan_python_distributions_warns_when_site_packages_is_missing(tmp_path): 1abcdefghi
273 result = scan_python_distributions(tmp_path / "missing") 2McNcOcPcQcRcScTcUc
275 assert result.skills == [] 2McNcOcPcQcRcScTcUc
276 assert result.warnings == [ 2McNcOcPcQcRcScTcUc
277 f"Site-packages directory not found: {tmp_path / 'missing'}"
278 ]
281def test_scan_python_distributions_warns_for_invalid_distribution_metadata(tmp_path): 1abcdefghi
282 site_packages = tmp_path / "site-packages" 2~bacbcccdcecfcgchc
283 (site_packages / "invalid-1.0.0.dist-info").mkdir(parents=True) 2~bacbcccdcecfcgchc
285 result = scan_python_distributions(site_packages) 2~bacbcccdcecfcgchc
287 assert result.skills == [] 2~bacbcccdcecfcgchc
288 assert len(result.warnings) == 1 2~bacbcccdcecfcgchc
289 assert "Skipping invalid distribution metadata" in result.warnings[0] 2~bacbcccdcecfcgchc
292def test_scan_python_distributions_skips_dist_info_files(tmp_path): 1abcdefghi
293 site_packages = tmp_path / "site-packages" 2icjckclcmcncocpcqc
294 site_packages.mkdir() 2icjckclcmcncocpcqc
295 (site_packages / "not-a-directory.dist-info").write_text("", encoding="utf-8") 2icjckclcmcncocpcqc
297 result = scan_python_distributions(site_packages) 2icjckclcmcncocpcqc
299 assert result.skills == [] 2icjckclcmcncocpcqc
300 assert result.warnings == [] 2icjckclcmcncocpcqc
303def test_scan_python_distributions_warns_for_metadata_without_name(tmp_path): 1abcdefghi
304 site_packages = tmp_path / "site-packages" 2RbSbTbUbVbWbXbYbZb
305 dist_info = site_packages / "demo-1.0.0.dist-info" 2RbSbTbUbVbWbXbYbZb
306 dist_info.mkdir(parents=True) 2RbSbTbUbVbWbXbYbZb
307 dist_info.joinpath("METADATA").write_text( 2RbSbTbUbVbWbXbYbZb
308 "Metadata-Version: 2.4\nVersion: 1.0.0\n",
309 encoding="utf-8",
310 )
312 result = scan_python_distributions(site_packages) 2RbSbTbUbVbWbXbYbZb
314 assert result.skills == [] 2RbSbTbUbVbWbXbYbZb
315 assert len(result.warnings) == 1 2RbSbTbUbVbWbXbYbZb
316 assert "Skipping invalid distribution metadata" in result.warnings[0] 2RbSbTbUbVbWbXbYbZb
319def test_scan_python_distributions_ignores_empty_non_skill_and_duplicate_records( 1abcdefghi
320 tmp_path,
321):
322 site_packages = tmp_path / "site-packages" 2? 0b1b@ 2b3b[ 4b5b
323 skill_md = write_skill(site_packages / "demo_pkg", "demo-skill") 2? 0b1b@ 2b3b[ 4b5b
324 dist_info = write_dist_info(site_packages, "demo-pkg") 2? 0b1b@ 2b3b[ 4b5b
325 record_path = skill_md.relative_to(site_packages).as_posix() 2? 0b1b@ 2b3b[ 4b5b
326 dist_info.joinpath("RECORD").write_text( 2? 0b1b@ 2b3b[ 4b5b
327 f"\nnot/a/skill.txt,,\n{record_path},,\n{record_path},,\n",
328 encoding="utf-8",
329 )
331 result = scan_python_distributions(site_packages) 2? 0b1b@ 2b3b[ 4b5b
333 assert result.warnings == [] 2? 0b1b@ 2b3b[ 4b5b
334 assert [skill.name for skill in result.skills] == ["demo-skill"] 2? 0b1b@ 2b3b[ 4b5b
337@pytest.mark.parametrize( 1abcdefghi
338 ("skill_text", "expected_warning"),
339 [
340 ("name: no-frontmatter\n", "missing YAML frontmatter"),
341 ("---\nname: unterminated\n", "unterminated YAML frontmatter"),
342 ("---\ndescription: Missing name.\n---\n", "missing required 'name' field"),
343 (
344 "---\nname: Invalid_Name\ndescription: Invalid name.\n---\n",
345 "invalid 'name' field",
346 ),
347 ("---\nname: demo-skill\n---\n", "missing required 'description' field"),
348 (
349 "---\nthis line has no separator\n"
350 "name: demo-skill\ndescription: Demo skill.\n---\n",
351 "invalid YAML frontmatter",
352 ),
353 ("---\n---\n", "missing required 'name' field"),
354 ("---\n[]\n---\n", "YAML frontmatter must be a mapping"),
355 (
356 f"---\nname: demo-skill\ndescription: {'x' * 1025}\n---\n",
357 "'description' field must be at most 1024 characters",
358 ),
359 ],
360)
361def test_scan_python_distributions_warns_for_invalid_skill_files( 1abcdefghi
362 tmp_path,
363 skill_text,
364 expected_warning,
365):
366 site_packages = tmp_path / "site-packages" 2L M N O P Q R S T 6b7bX Y Z 0 1 2 3 4 5 8b9b9 ! # $ % ' ( ) * !b#b
367 skill_md = write_skill(site_packages / "demo_pkg", "demo-skill") 2L M N O P Q R S T 6b7bX Y Z 0 1 2 3 4 5 8b9b9 ! # $ % ' ( ) * !b#b
368 skill_md.write_text(skill_text, encoding="utf-8") 2L M N O P Q R S T 6b7bX Y Z 0 1 2 3 4 5 8b9b9 ! # $ % ' ( ) * !b#b
369 write_dist_info( 2L M N O P Q R S T 6b7bX Y Z 0 1 2 3 4 5 8b9b9 ! # $ % ' ( ) * !b#b
370 site_packages,
371 "demo-pkg",
372 record_paths=[skill_md.relative_to(site_packages).as_posix()],
373 )
375 result = scan_python_distributions(site_packages) 2L M N O P Q R S T 6b7bX Y Z 0 1 2 3 4 5 8b9b9 ! # $ % ' ( ) * !b#b
377 assert result.skills == [] 2L M N O P Q R S T 6b7bX Y Z 0 1 2 3 4 5 8b9b9 ! # $ % ' ( ) * !b#b
378 assert len(result.warnings) == 1 2L M N O P Q R S T 6b7bX Y Z 0 1 2 3 4 5 8b9b9 ! # $ % ' ( ) * !b#b
379 assert expected_warning in result.warnings[0] 2L M N O P Q R S T 6b7bX Y Z 0 1 2 3 4 5 8b9b9 ! # $ % ' ( ) * !b#b
382def test_scan_python_distributions_warns_when_skill_file_is_not_utf8(tmp_path): 1abcdefghi
383 site_packages = tmp_path / "site-packages" 2V $b%b7 'b(b, )b*b
384 skill_md = write_skill(site_packages / "demo_pkg", "demo-skill") 2V $b%b7 'b(b, )b*b
385 skill_md.write_bytes(b"\xff") 2V $b%b7 'b(b, )b*b
386 write_dist_info( 2V $b%b7 'b(b, )b*b
387 site_packages,
388 "demo-pkg",
389 record_paths=[skill_md.relative_to(site_packages).as_posix()],
390 )
392 result = scan_python_distributions(site_packages) 2V $b%b7 'b(b, )b*b
394 assert result.skills == [] 2V $b%b7 'b(b, )b*b
395 assert len(result.warnings) == 1 2V $b%b7 'b(b, )b*b
396 assert "could not read SKILL.md" in result.warnings[0] 2V $b%b7 'b(b, )b*b
399@pytest.mark.parametrize( 1abcdefghi
400 "payload",
401 [
402 [],
403 {"dir_info": {"editable": False}, "url": "file:///tmp/demo"},
404 {"dir_info": {"editable": True}, "url": 1},
405 {"dir_info": {"editable": True}, "url": "https://example.com/demo"},
406 {"dir_info": {"editable": True}, "url": "file:///path/that/does/not/exist"},
407 ],
408)
409def test_read_editable_source_root_rejects_invalid_direct_url_payloads( 1abcdefghi
410 tmp_path,
411 payload,
412):
413 dist_info = tmp_path / "demo-1.0.0.dist-info" 2rcsctcucvcwcxcyczcAcBcCcDcEcFcGcHcIcJcKcLc
414 dist_info.mkdir() 2rcsctcucvcwcxcyczcAcBcCcDcEcFcGcHcIcJcKcLc
415 dist_info.joinpath("direct_url.json").write_text( 2rcsctcucvcwcxcyczcAcBcCcDcEcFcGcHcIcJcKcLc
416 json.dumps(payload),
417 encoding="utf-8",
418 )
420 assert _read_editable_source_root(dist_info) is None 2rcsctcucvcwcxcyczcAcBcCcDcEcFcGcHcIcJcKcLc
423def test_is_relative_to_returns_false_for_unrelated_paths(tmp_path): 1abcdefghi
424 assert _is_relative_to(tmp_path / "one", tmp_path / "two") is False 2VcWcXcYcZc0c1c2c3c
427def test_scan_editable_direct_url_skips_symlinked_skills_outside_source_root( 1abcdefghi
428 tmp_path,
429):
430 site_packages = tmp_path / "site-packages" 2v hbibw jbkbx lbmb
431 source_root = tmp_path / "source" 2v hbibw jbkbx lbmb
432 outside_root = tmp_path / "outside" 2v hbibw jbkbx lbmb
433 skill_link_dir = source_root / "demo_pkg" / ".agents" / "skills" / "linked-skill" 2v hbibw jbkbx lbmb
434 skill_link_dir.mkdir(parents=True) 2v hbibw jbkbx lbmb
435 outside_skill_md = write_skill(outside_root / "demo_pkg", "linked-skill") 2v hbibw jbkbx lbmb
436 (skill_link_dir / "SKILL.md").symlink_to(outside_skill_md) 2v hbibw jbkbx lbmb
437 dist_info = write_dist_info(site_packages, "editable-pkg") 2v hbibw jbkbx lbmb
438 dist_info.joinpath("direct_url.json").write_text( 2v hbibw jbkbx lbmb
439 json.dumps({"url": source_root.as_uri(), "dir_info": {"editable": True}}),
440 encoding="utf-8",
441 )
443 result = scan_python_distributions(site_packages) 2v hbibw jbkbx lbmb
445 assert result.skills == [] 2v hbibw jbkbx lbmb
446 assert result.warnings == [] 2v hbibw jbkbx lbmb
449def test_scan_editable_direct_url_skips_seen_skill_dirs(tmp_path): 1abcdefghi
450 dist_info = tmp_path / "editable-1.0.0.dist-info" 2eb+b,bfb-b.bgb/b:b
451 source_root = tmp_path / "source" 2eb+b,bfb-b.bgb/b:b
452 skill_md = write_skill(source_root / "demo_pkg", "editable-skill") 2eb+b,bfb-b.bgb/b:b
453 dist_info.mkdir() 2eb+b,bfb-b.bgb/b:b
454 dist_info.joinpath("direct_url.json").write_text( 2eb+b,bfb-b.bgb/b:b
455 json.dumps({"url": source_root.as_uri(), "dir_info": {"editable": True}}),
456 encoding="utf-8",
457 )
459 result = _scan_editable_direct_url( 2eb+b,bfb-b.bgb/b:b
460 dist_info=dist_info,
461 package_name="editable-pkg",
462 package_version="1.0.0",
463 seen_skill_dirs={skill_md.parent},
464 )
466 assert result.skills == [] 2eb+b,bfb-b.bgb/b:b
467 assert result.warnings == [] 2eb+b,bfb-b.bgb/b:b
470def test_scan_editable_direct_url_reports_invalid_skill_warning(tmp_path): 1abcdefghi
471 site_packages = tmp_path / "site-packages" 2E tbubG vbwbI xbyb
472 source_root = tmp_path / "source" 2E tbubG vbwbI xbyb
473 skill_md = write_skill(source_root / "demo_pkg", "editable-skill") 2E tbubG vbwbI xbyb
474 skill_md.write_text( 2E tbubG vbwbI xbyb
475 "---\nname: different-name\ndescription: Demo skill.\n---\n",
476 encoding="utf-8",
477 )
478 dist_info = write_dist_info(site_packages, "editable-pkg") 2E tbubG vbwbI xbyb
479 dist_info.joinpath("direct_url.json").write_text( 2E tbubG vbwbI xbyb
480 json.dumps({"url": source_root.as_uri(), "dir_info": {"editable": True}}),
481 encoding="utf-8",
482 )
484 result = scan_python_distributions(site_packages) 2E tbubG vbwbI xbyb
486 assert result.skills == [] 2E tbubG vbwbI xbyb
487 assert len(result.warnings) == 1 2E tbubG vbwbI xbyb
488 assert "must match parent directory name" in result.warnings[0] 2E tbubG vbwbI xbyb