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

1import json 1abcdefghi

2from pathlib import Path 1abcdefghi

3 

4import pytest 1abcdefghi

5 

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) 

14 

15 

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

25 

26 

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

46 

47 

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

62 

63 

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 ) 

74 

75 result = scan_python_distributions(site_packages) 1jklmnopqr

76 

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

86 

87 

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 ) 

107 

108 result = scan_python_distributions(site_packages) 2F zbAbH BbCbJ DbEb

109 

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 ) 

116 

117 

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 ) 

126 

127 result = scan_node_packages(node_modules) 2y } ~ A abbbC cbdb

128 

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

138 

139 

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 ) 

153 

154 result = scan_node_packages(node_modules) 2] ;b=b_ ?b@b{ [b]b

155 

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

158 

159 

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 ] 

166 

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;=

180 

181 result = scan_node_packages(node_modules) 1s-.t/:u;=

182 

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 ] 

189 

190 

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 ) 

198 

199 result = scan_node_packages(node_modules) 2^ ^b_b` `b{b| |b}b

200 

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

204 

205 

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 ) 

218 

219 result = scan_python_distributions(site_packages) 2U LbMb6 NbOb+ PbQb

220 

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

224 

225 

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 ) 

240 

241 result = scan_python_distributions(site_packages) 2K FbGbW HbIb8 JbKb

242 

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

246 

247 

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 ) 

264 

265 result = scan_python_distributions(site_packages) 2z nbobB pbqbD rbsb

266 

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

270 

271 

272def test_scan_python_distributions_warns_when_site_packages_is_missing(tmp_path): 1abcdefghi

273 result = scan_python_distributions(tmp_path / "missing") 2McNcOcPcQcRcScTcUc

274 

275 assert result.skills == [] 2McNcOcPcQcRcScTcUc

276 assert result.warnings == [ 2McNcOcPcQcRcScTcUc

277 f"Site-packages directory not found: {tmp_path / 'missing'}" 

278 ] 

279 

280 

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

284 

285 result = scan_python_distributions(site_packages) 2~bacbcccdcecfcgchc

286 

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

290 

291 

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

296 

297 result = scan_python_distributions(site_packages) 2icjckclcmcncocpcqc

298 

299 assert result.skills == [] 2icjckclcmcncocpcqc

300 assert result.warnings == [] 2icjckclcmcncocpcqc

301 

302 

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 ) 

311 

312 result = scan_python_distributions(site_packages) 2RbSbTbUbVbWbXbYbZb

313 

314 assert result.skills == [] 2RbSbTbUbVbWbXbYbZb

315 assert len(result.warnings) == 1 2RbSbTbUbVbWbXbYbZb

316 assert "Skipping invalid distribution metadata" in result.warnings[0] 2RbSbTbUbVbWbXbYbZb

317 

318 

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 ) 

330 

331 result = scan_python_distributions(site_packages) 2? 0b1b@ 2b3b[ 4b5b

332 

333 assert result.warnings == [] 2? 0b1b@ 2b3b[ 4b5b

334 assert [skill.name for skill in result.skills] == ["demo-skill"] 2? 0b1b@ 2b3b[ 4b5b

335 

336 

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 ) 

374 

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

376 

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

380 

381 

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 ) 

391 

392 result = scan_python_distributions(site_packages) 2V $b%b7 'b(b, )b*b

393 

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

397 

398 

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 ) 

419 

420 assert _read_editable_source_root(dist_info) is None 2rcsctcucvcwcxcyczcAcBcCcDcEcFcGcHcIcJcKcLc

421 

422 

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

425 

426 

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 ) 

442 

443 result = scan_python_distributions(site_packages) 2v hbibw jbkbx lbmb

444 

445 assert result.skills == [] 2v hbibw jbkbx lbmb

446 assert result.warnings == [] 2v hbibw jbkbx lbmb

447 

448 

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 ) 

458 

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 ) 

465 

466 assert result.skills == [] 2eb+b,bfb-b.bgb/b:b

467 assert result.warnings == [] 2eb+b,bfb-b.bgb/b:b

468 

469 

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 ) 

483 

484 result = scan_python_distributions(site_packages) 2E tbubG vbwbI xbyb

485 

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