Coverage for tests / test_cli.py: 100%

853 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-03 20:19 +0000

1import importlib 1abcdefghi

2import json 1abcdefghi

3import sys 1abcdefghi

4from pathlib import Path 1abcdefghi

5from unittest.mock import patch 1abcdefghi

6 

7from rich.console import Console 1abcdefghi

8from typer.testing import CliRunner 1abcdefghi

9 

10import library_skills.cli as cli 1abcdefghi

11import library_skills.ui as ui 1abcdefghi

12from library_skills.cli import app 1abcdefghi

13from library_skills.installer import ( 1abcdefghi

14 TOOL_SKILL_MARKER, 

15 TOOL_SKILL_NAME, 

16 get_tool_skill_template, 

17) 

18 

19runner = CliRunner() 1abcdefghi

20 

21 

22class FakeToolkit: 1abcdefghi

23 def __init__(self, value): 1abcdefghi

24 self.value = value 2cc?d@ddc[d]dec^d_d

25 

26 def ask(self, *args, **kwargs): 1abcdefghi

27 return self.value 2cc?d@ddc[d]dec^d_d

28 

29 

30def write_project(tmp_path: Path, dependencies: list[str]) -> Path: 1abcdefghi

31 project = tmp_path / "project" 2$bmdfcVcsb3 ( { 4cSbgc| } tbTbUbVbndhc) * od+ %bpd, ~ abubqdN p ;bObE - O P 'brdicWcvb4 . bb5cWbjccbdbwbXbYbZbsdkc/ : td; (bud= ebfbxbvdQ q =bPbF ? R S )bwdlcXcgbT 8 hb6c0bmcib@ jbQb1b2bxdnc[ ] yd^ *bzd_ kblbybAdU r ?bRbG ` V W

32 site_packages = project / ".venv" / "lib" / "python3.12" / "site-packages" 2$bmdfcVcsb3 ( { 4cSbgc| } tbTbUbVbndhc) * od+ %bpd, ~ abubqdN p ;bObE - O P 'brdicWcvb4 . bb5cWbjccbdbwbXbYbZbsdkc/ : td; (bud= ebfbxbvdQ q =bPbF ? R S )bwdlcXcgbT 8 hb6c0bmcib@ jbQb1b2bxdnc[ ] yd^ *bzd_ kblbybAdU r ?bRbG ` V W

33 site_packages.mkdir(parents=True) 2$bmdfcVcsb3 ( { 4cSbgc| } tbTbUbVbndhc) * od+ %bpd, ~ abubqdN p ;bObE - O P 'brdicWcvb4 . bb5cWbjccbdbwbXbYbZbsdkc/ : td; (bud= ebfbxbvdQ q =bPbF ? R S )bwdlcXcgbT 8 hb6c0bmcib@ jbQb1b2bxdnc[ ] yd^ *bzd_ kblbybAdU r ?bRbG ` V W

34 (project / ".venv" / "pyvenv.cfg").write_text( 2$bmdfcVcsb3 ( { 4cSbgc| } tbTbUbVbndhc) * od+ %bpd, ~ abubqdN p ;bObE - O P 'brdicWcvb4 . bb5cWbjccbdbwbXbYbZbsdkc/ : td; (bud= ebfbxbvdQ q =bPbF ? R S )bwdlcXcgbT 8 hb6c0bmcib@ jbQb1b2bxdnc[ ] yd^ *bzd_ kblbybAdU r ?bRbG ` V W

35 "home = /usr/bin\n", 

36 encoding="utf-8", 

37 ) 

38 quoted_deps = ", ".join(f'"{dependency}"' for dependency in dependencies) 2$bmdfcVcsb3 ( { 4cSbgc| } tbTbUbVbndhc) * od+ %bpd, ~ abubqdN p ;bObE - O P 'brdicWcvb4 . bb5cWbjccbdbwbXbYbZbsdkc/ : td; (bud= ebfbxbvdQ q =bPbF ? R S )bwdlcXcgbT 8 hb6c0bmcib@ jbQb1b2bxdnc[ ] yd^ *bzd_ kblbybAdU r ?bRbG ` V W

39 (project / "pyproject.toml").write_text( 2$bmdfcVcsb3 ( { 4cSbgc| } tbTbUbVbndhc) * od+ %bpd, ~ abubqdN p ;bObE - O P 'brdicWcvb4 . bb5cWbjccbdbwbXbYbZbsdkc/ : td; (bud= ebfbxbvdQ q =bPbF ? R S )bwdlcXcgbT 8 hb6c0bmcib@ jbQb1b2bxdnc[ ] yd^ *bzd_ kblbybAdU r ?bRbG ` V W

40 f""" 

41[project] 

42name = "demo" 

43version = "0.1.0" 

44dependencies = [{quoted_deps}] 

45""", 

46 encoding="utf-8", 

47 ) 

48 return project 2$bmdfcVcsb3 ( { 4cSbgc| } tbTbUbVbndhc) * od+ %bpd, ~ abubqdN p ;bObE - O P 'brdicWcvb4 . bb5cWbjccbdbwbXbYbZbsdkc/ : td; (bud= ebfbxbvdQ q =bPbF ? R S )bwdlcXcgbT 8 hb6c0bmcib@ jbQb1b2bxdnc[ ] yd^ *bzd_ kblbybAdU r ?bRbG ` V W

49 

50 

51def write_workspace(tmp_path: Path) -> Path: 1abcdefghi

52 project = tmp_path / "workspace" 1msjHBIvwntkJCKxyoulLDMzA

53 site_packages = project / ".venv" / "lib" / "python3.12" / "site-packages" 1msjHBIvwntkJCKxyoulLDMzA

54 site_packages.mkdir(parents=True) 1msjHBIvwntkJCKxyoulLDMzA

55 (project / ".venv" / "pyvenv.cfg").write_text( 1msjHBIvwntkJCKxyoulLDMzA

56 "home = /usr/bin\n", 

57 encoding="utf-8", 

58 ) 

59 (project / "pyproject.toml").write_text( 1msjHBIvwntkJCKxyoulLDMzA

60 """ 

61[project] 

62name = "workspace-root" 

63version = "0.1.0" 

64dependencies = ["root-pkg>=1"] 

65 

66[tool.uv.workspace] 

67members = ["packages/*"] 

68""", 

69 encoding="utf-8", 

70 ) 

71 api = project / "packages" / "api" 1msjHBIvwntkJCKxyoulLDMzA

72 worker = project / "packages" / "worker" 1msjHBIvwntkJCKxyoulLDMzA

73 api.mkdir(parents=True) 1msjHBIvwntkJCKxyoulLDMzA

74 worker.mkdir(parents=True) 1msjHBIvwntkJCKxyoulLDMzA

75 (api / "pyproject.toml").write_text( 1msjHBIvwntkJCKxyoulLDMzA

76 """ 

77[project] 

78name = "api" 

79version = "0.1.0" 

80dependencies = ["api-pkg>=1", "workspace-lib"] 

81 

82[tool.uv.sources] 

83workspace-lib = { workspace = true } 

84""", 

85 encoding="utf-8", 

86 ) 

87 (worker / "pyproject.toml").write_text( 1msjHBIvwntkJCKxyoulLDMzA

88 """ 

89[project] 

90name = "worker" 

91version = "0.1.0" 

92dependencies = ["worker-pkg>=1"] 

93""", 

94 encoding="utf-8", 

95 ) 

96 return project 1msjHBIvwntkJCKxyoulLDMzA

97 

98 

99def write_node_workspace(tmp_path: Path) -> Path: 1abcdefghi

100 project = tmp_path / "node-workspace" 2zbX mb5 nb+b,bAbY ob6 pb-b.bBbZ qb7 rb/b:b

101 (project / "node_modules").mkdir(parents=True) 2zbX mb5 nb+b,bAbY ob6 pb-b.bBbZ qb7 rb/b:b

102 (project / "package.json").write_text( 2zbX mb5 nb+b,bAbY ob6 pb-b.bBbZ qb7 rb/b:b

103 json.dumps( 

104 { 

105 "name": "workspace-root", 

106 "workspaces": ["packages/*"], 

107 "dependencies": {"root-pkg": "^1.0.0"}, 

108 } 

109 ), 

110 encoding="utf-8", 

111 ) 

112 api = project / "packages" / "api" 2zbX mb5 nb+b,bAbY ob6 pb-b.bBbZ qb7 rb/b:b

113 worker = project / "packages" / "worker" 2zbX mb5 nb+b,bAbY ob6 pb-b.bBbZ qb7 rb/b:b

114 api.mkdir(parents=True) 2zbX mb5 nb+b,bAbY ob6 pb-b.bBbZ qb7 rb/b:b

115 worker.mkdir(parents=True) 2zbX mb5 nb+b,bAbY ob6 pb-b.bBbZ qb7 rb/b:b

116 (api / "package.json").write_text( 2zbX mb5 nb+b,bAbY ob6 pb-b.bBbZ qb7 rb/b:b

117 json.dumps( 

118 { 

119 "name": "api", 

120 "dependencies": {"api-pkg": "^1.0.0"}, 

121 "devDependencies": {"workspace-lib": "workspace:*"}, 

122 } 

123 ), 

124 encoding="utf-8", 

125 ) 

126 (worker / "package.json").write_text( 2zbX mb5 nb+b,bAbY ob6 pb-b.bBbZ qb7 rb/b:b

127 json.dumps( 

128 { 

129 "name": "worker", 

130 "optionalDependencies": {"worker-pkg": "^1.0.0"}, 

131 } 

132 ), 

133 encoding="utf-8", 

134 ) 

135 return project 2zbX mb5 nb+b,bAbY ob6 pb-b.bBbZ qb7 rb/b:b

136 

137 

138def add_node_workspace_files(project: Path) -> None: 1abcdefghi

139 (project / "node_modules").mkdir(parents=True, exist_ok=True) 1ms9!nt#$ou%'

140 (project / "package.json").write_text( 1ms9!nt#$ou%'

141 json.dumps( 

142 { 

143 "name": "workspace-root", 

144 "workspaces": ["packages/*"], 

145 "dependencies": {"root-node-pkg": "^1.0.0"}, 

146 } 

147 ), 

148 encoding="utf-8", 

149 ) 

150 api = project / "packages" / "api" 1ms9!nt#$ou%'

151 worker = project / "packages" / "worker" 1ms9!nt#$ou%'

152 (api / "package.json").write_text( 1ms9!nt#$ou%'

153 json.dumps( 

154 { 

155 "name": "api", 

156 "dependencies": {"api-node-pkg": "^1.0.0"}, 

157 } 

158 ), 

159 encoding="utf-8", 

160 ) 

161 (worker / "package.json").write_text( 1ms9!nt#$ou%'

162 json.dumps( 

163 { 

164 "name": "worker", 

165 "dependencies": {"worker-node-pkg": "^1.0.0"}, 

166 } 

167 ), 

168 encoding="utf-8", 

169 ) 

170 

171 

172def write_distribution_skill( 1abcdefghi

173 site_packages: Path, 

174 *, 

175 dist_name: str, 

176 package_dir: str, 

177 skill_name: str, 

178) -> Path: 

179 skill_dir = site_packages / package_dir / ".agents" / "skills" / skill_name 2$bm s sb3 ( { Sb| } tbTbUbVb) * + %b, ~ abubN p j H B I E - O P 'bn t vb4 . bbWbcbdbwbXbYbZb/ : ; (b= ebfbxbQ q k J C K F ? R S )bo u gbT 8 hb0bib@ jbQb1b2b[ ] ^ *b_ kblbybU r l L D M G ` V W

180 skill_dir.mkdir(parents=True) 2$bm s sb3 ( { Sb| } tbTbUbVb) * + %b, ~ abubN p j H B I E - O P 'bn t vb4 . bbWbcbdbwbXbYbZb/ : ; (b= ebfbxbQ q k J C K F ? R S )bo u gbT 8 hb0bib@ jbQb1b2b[ ] ^ *b_ kblbybU r l L D M G ` V W

181 skill_md = skill_dir / "SKILL.md" 2$bm s sb3 ( { Sb| } tbTbUbVb) * + %b, ~ abubN p j H B I E - O P 'bn t vb4 . bbWbcbdbwbXbYbZb/ : ; (b= ebfbxbQ q k J C K F ? R S )bo u gbT 8 hb0bib@ jbQb1b2b[ ] ^ *b_ kblbybU r l L D M G ` V W

182 skill_md.write_text( 2$bm s sb3 ( { Sb| } tbTbUbVb) * + %b, ~ abubN p j H B I E - O P 'bn t vb4 . bbWbcbdbwbXbYbZb/ : ; (b= ebfbxbQ q k J C K F ? R S )bo u gbT 8 hb0bib@ jbQb1b2b[ ] ^ *b_ kblbybU r l L D M G ` V W

183 f"---\nname: {skill_name}\ndescription: Demo skill.\n---\n", 

184 encoding="utf-8", 

185 ) 

186 

187 dist_info = site_packages / f"{package_dir}-1.0.0.dist-info" 2$bm s sb3 ( { Sb| } tbTbUbVb) * + %b, ~ abubN p j H B I E - O P 'bn t vb4 . bbWbcbdbwbXbYbZb/ : ; (b= ebfbxbQ q k J C K F ? R S )bo u gbT 8 hb0bib@ jbQb1b2b[ ] ^ *b_ kblbybU r l L D M G ` V W

188 dist_info.mkdir(exist_ok=True) 2$bm s sb3 ( { Sb| } tbTbUbVb) * + %b, ~ abubN p j H B I E - O P 'bn t vb4 . bbWbcbdbwbXbYbZb/ : ; (b= ebfbxbQ q k J C K F ? R S )bo u gbT 8 hb0bib@ jbQb1b2b[ ] ^ *b_ kblbybU r l L D M G ` V W

189 dist_info.joinpath("METADATA").write_text( 2$bm s sb3 ( { Sb| } tbTbUbVb) * + %b, ~ abubN p j H B I E - O P 'bn t vb4 . bbWbcbdbwbXbYbZb/ : ; (b= ebfbxbQ q k J C K F ? R S )bo u gbT 8 hb0bib@ jbQb1b2b[ ] ^ *b_ kblbybU r l L D M G ` V W

190 f"Metadata-Version: 2.4\nName: {dist_name}\nVersion: 1.0.0\n", 

191 encoding="utf-8", 

192 ) 

193 dist_info.joinpath("RECORD").write_text( 2$bm s sb3 ( { Sb| } tbTbUbVb) * + %b, ~ abubN p j H B I E - O P 'bn t vb4 . bbWbcbdbwbXbYbZb/ : ; (b= ebfbxbQ q k J C K F ? R S )bo u gbT 8 hb0bib@ jbQb1b2b[ ] ^ *b_ kblbybU r l L D M G ` V W

194 f"{skill_md.relative_to(site_packages).as_posix()},,\n", 

195 encoding="utf-8", 

196 ) 

197 return skill_dir 2$bm s sb3 ( { Sb| } tbTbUbVb) * + %b, ~ abubN p j H B I E - O P 'bn t vb4 . bbWbcbdbwbXbYbZb/ : ; (b= ebfbxbQ q k J C K F ? R S )bo u gbT 8 hb0bib@ jbQb1b2b[ ] ^ *b_ kblbybU r l L D M G ` V W

198 

199 

200def write_node_package_skill( 1abcdefghi

201 project: Path, 

202 *, 

203 package_name: str, 

204 skill_name: str, 

205) -> Path: 

206 package_root = project / "node_modules" / package_name 2m s zbX mb5 nbp 9 ! n t AbY ob6 pbq # $ o u BbZ qb7 rbr % '

207 return write_node_package_skill_at( 2m s zbX mb5 nbp 9 ! n t AbY ob6 pbq # $ o u BbZ qb7 rbr % '

208 package_root, 

209 package_name=package_name, 

210 skill_name=skill_name, 

211 ) 

212 

213 

214def write_node_package_skill_at( 1abcdefghi

215 package_root: Path, 

216 *, 

217 package_name: str, 

218 skill_name: str, 

219) -> Path: 

220 skill_dir = package_root / ".agents" / "skills" / skill_name 2m s zbX mb5 nbp 9 ! n t AbY ob6 pbq # $ o u BbZ qb7 rbr % '

221 skill_dir.mkdir(parents=True) 2m s zbX mb5 nbp 9 ! n t AbY ob6 pbq # $ o u BbZ qb7 rbr % '

222 skill_dir.joinpath("SKILL.md").write_text( 2m s zbX mb5 nbp 9 ! n t AbY ob6 pbq # $ o u BbZ qb7 rbr % '

223 f"---\nname: {skill_name}\ndescription: Node skill.\n---\n", 

224 encoding="utf-8", 

225 ) 

226 package_root.joinpath("package.json").write_text( 2m s zbX mb5 nbp 9 ! n t AbY ob6 pbq # $ o u BbZ qb7 rbr % '

227 json.dumps({"name": package_name, "version": "2.0.0"}), 

228 encoding="utf-8", 

229 ) 

230 return skill_dir 2m s zbX mb5 nbp 9 ! n t AbY ob6 pbq # $ o u BbZ qb7 rbr % '

231 

232 

233def write_distribution( 1abcdefghi

234 site_packages: Path, 

235 *, 

236 dist_name: str, 

237 package_dir: str, 

238 skill_names: list[str], 

239) -> list[Path]: 

240 skill_dirs = [ 2E {b|bF }b~bG acbc

241 write_distribution_skill( 

242 site_packages, 

243 dist_name=dist_name, 

244 package_dir=package_dir, 

245 skill_name=skill_name, 

246 ) 

247 for skill_name in skill_names 

248 ] 

249 dist_info = site_packages / f"{package_dir}-1.0.0.dist-info" 2E {b|bF }b~bG acbc

250 record_paths = [ 2E {b|bF }b~bG acbc

251 (skill_dir / "SKILL.md").relative_to(site_packages).as_posix() 

252 for skill_dir in skill_dirs 

253 ] 

254 dist_info.joinpath("RECORD").write_text( 2E {b|bF }b~bG acbc

255 "".join(f"{record_path},,\n" for record_path in record_paths), 

256 encoding="utf-8", 

257 ) 

258 return skill_dirs 2E {b|bF }b~bG acbc

259 

260 

261def make_cli_skill(tmp_path: Path, name: str = "demo-skill") -> cli.Skill: 1abcdefghi

262 skill_dir = tmp_path / "source" / ".agents" / "skills" / name 2IeJe3b0 cc4b5bKeLe6b1 dc7b8bMeNe9b2 ec!b#b

263 skill_dir.mkdir(parents=True) 2IeJe3b0 cc4b5bKeLe6b1 dc7b8bMeNe9b2 ec!b#b

264 skill_md = skill_dir / "SKILL.md" 2IeJe3b0 cc4b5bKeLe6b1 dc7b8bMeNe9b2 ec!b#b

265 skill_md.write_text( 2IeJe3b0 cc4b5bKeLe6b1 dc7b8bMeNe9b2 ec!b#b

266 f"---\nname: {name}\ndescription: Demo skill.\n---\n", 

267 encoding="utf-8", 

268 ) 

269 return cli.Skill( 2IeJe3b0 cc4b5bKeLe6b1 dc7b8bMeNe9b2 ec!b#b

270 name=name, 

271 description="Demo skill.", 

272 path=skill_md, 

273 package_name="demo-pkg", 

274 package_version="1.0.0", 

275 skill_dir=skill_dir, 

276 ) 

277 

278 

279def test_scan_filters_to_top_level_dependencies_by_default(tmp_path, monkeypatch): 1abcdefghi

280 project = write_project(tmp_path, dependencies=["top-pkg>=1"]) 1NOPQRSUVW

281 site_packages = project / ".venv" / "lib" / "python3.12" / "site-packages" 1NOPQRSUVW

282 write_distribution_skill( 1NOPQRSUVW

283 site_packages, 

284 dist_name="top-pkg", 

285 package_dir="top_pkg", 

286 skill_name="top-skill", 

287 ) 

288 write_distribution_skill( 1NOPQRSUVW

289 site_packages, 

290 dist_name="transitive-pkg", 

291 package_dir="transitive_pkg", 

292 skill_name="transitive-skill", 

293 ) 

294 monkeypatch.chdir(project) 1NOPQRSUVW

295 

296 result = runner.invoke(app, ["scan"]) 1NOPQRSUVW

297 all_result = runner.invoke(app, ["scan", "--all"]) 1NOPQRSUVW

298 

299 assert result.exit_code == 0 1NOPQRSUVW

300 assert "Target Python environment" in result.output 1NOPQRSUVW

301 assert ".venv" in result.output 1NOPQRSUVW

302 assert "Site-packages" in result.output 1NOPQRSUVW

303 assert "top-skill" in result.output 1NOPQRSUVW

304 assert "transitive-skill" not in result.output 1NOPQRSUVW

305 assert all_result.exit_code == 0 1NOPQRSUVW

306 assert "top-skill" in all_result.output 1NOPQRSUVW

307 assert "transitive-skill" in all_result.output 1NOPQRSUVW

308 

309 

310def test_workspace_member_scan_uses_workspace_venv_and_member_deps( 1abcdefghi

311 tmp_path, 

312 monkeypatch, 

313): 

314 project = write_workspace(tmp_path) 1jvwkxylzA

315 api = project / "packages" / "api" 1jvwkxylzA

316 member_venv = api / ".venv" 1jvwkxylzA

317 member_venv.mkdir() 1jvwkxylzA

318 site_packages = project / ".venv" / "lib" / "python3.12" / "site-packages" 1jvwkxylzA

319 api_skill = write_distribution_skill( 1jvwkxylzA

320 site_packages, 

321 dist_name="api-pkg", 

322 package_dir="api_pkg", 

323 skill_name="api-skill", 

324 ) 

325 write_distribution_skill( 1jvwkxylzA

326 site_packages, 

327 dist_name="workspace-lib", 

328 package_dir="workspace_lib", 

329 skill_name="workspace-lib-skill", 

330 ) 

331 write_distribution_skill( 1jvwkxylzA

332 site_packages, 

333 dist_name="root-pkg", 

334 package_dir="root_pkg", 

335 skill_name="root-skill", 

336 ) 

337 write_distribution_skill( 1jvwkxylzA

338 site_packages, 

339 dist_name="worker-pkg", 

340 package_dir="worker_pkg", 

341 skill_name="worker-skill", 

342 ) 

343 write_distribution_skill( 1jvwkxylzA

344 site_packages, 

345 dist_name="transitive-pkg", 

346 package_dir="transitive_pkg", 

347 skill_name="transitive-skill", 

348 ) 

349 (api / "src").mkdir() 1jvwkxylzA

350 monkeypatch.chdir(api / "src") 1jvwkxylzA

351 

352 result = runner.invoke(app, ["scan", "--json"]) 1jvwkxylzA

353 install_result = runner.invoke( 1jvwkxylzA

354 app, ["install", "--skill", "api-skill", "--yes", "--claude"] 

355 ) 

356 

357 payload = json.loads(result.output) 1jvwkxylzA

358 assert result.exit_code == 0 1jvwkxylzA

359 assert payload["project_root"] == str(project) 1jvwkxylzA

360 assert payload["workspace_root"] == str(project) 1jvwkxylzA

361 assert payload["workspace_member"] == str(api.resolve()) 1jvwkxylzA

362 assert payload["dependency_files"] == [str(api.resolve() / "pyproject.toml")] 1jvwkxylzA

363 assert payload["target_environment"] == str(project / ".venv") 1jvwkxylzA

364 assert "Ignoring member-local .venv" in "\n".join(payload["warnings"]) 1jvwkxylzA

365 assert [skill["name"] for skill in payload["skills"]] == [ 1jvwkxylzA

366 "api-skill", 

367 "workspace-lib-skill", 

368 ] 

369 assert install_result.exit_code == 0 1jvwkxylzA

370 installed = project / ".agents" / "skills" / "api-skill" 1jvwkxylzA

371 claude_installed = project / ".claude" / "skills" / "api-skill" 1jvwkxylzA

372 assert installed.is_symlink() 1jvwkxylzA

373 assert installed.resolve() == api_skill.resolve() 1jvwkxylzA

374 assert claude_installed.is_symlink() 1jvwkxylzA

375 assert claude_installed.resolve() == api_skill.resolve() 1jvwkxylzA

376 assert not (api / ".agents" / "skills" / "api-skill").exists() 1jvwkxylzA

377 

378 

379def test_combined_uv_and_node_workspace_member_scan_uses_both_member_deps( 1abcdefghi

380 tmp_path, 

381 monkeypatch, 

382): 

383 project = write_workspace(tmp_path) 1m9!n#$o%'

384 add_node_workspace_files(project) 1m9!n#$o%'

385 api = project / "packages" / "api" 1m9!n#$o%'

386 site_packages = project / ".venv" / "lib" / "python3.12" / "site-packages" 1m9!n#$o%'

387 write_distribution_skill( 1m9!n#$o%'

388 site_packages, 

389 dist_name="api-pkg", 

390 package_dir="api_pkg", 

391 skill_name="api-skill", 

392 ) 

393 write_distribution_skill( 1m9!n#$o%'

394 site_packages, 

395 dist_name="root-pkg", 

396 package_dir="root_pkg", 

397 skill_name="root-skill", 

398 ) 

399 write_node_package_skill( 1m9!n#$o%'

400 project, 

401 package_name="api-node-pkg", 

402 skill_name="api-node-skill", 

403 ) 

404 write_node_package_skill( 1m9!n#$o%'

405 project, 

406 package_name="root-node-pkg", 

407 skill_name="root-node-skill", 

408 ) 

409 (api / "src").mkdir() 1m9!n#$o%'

410 monkeypatch.chdir(api / "src") 1m9!n#$o%'

411 

412 result = runner.invoke(app, ["scan", "--json"]) 1m9!n#$o%'

413 

414 payload = json.loads(result.output) 1m9!n#$o%'

415 assert result.exit_code == 0 1m9!n#$o%'

416 assert payload["workspace_root"] == str(project) 1m9!n#$o%'

417 assert payload["workspace_member"] == str(api.resolve()) 1m9!n#$o%'

418 assert payload["dependency_files"] == [ 1m9!n#$o%'

419 str(api.resolve() / "pyproject.toml"), 

420 str(api.resolve() / "package.json"), 

421 ] 

422 assert [skill["name"] for skill in payload["skills"]] == [ 1m9!n#$o%'

423 "api-skill", 

424 "api-node-skill", 

425 ] 

426 

427 

428def test_workspace_root_scan_uses_root_and_all_member_deps(tmp_path, monkeypatch): 1abcdefghi

429 project = write_workspace(tmp_path) 2B ocpcC qcrcD sctc

430 site_packages = project / ".venv" / "lib" / "python3.12" / "site-packages" 2B ocpcC qcrcD sctc

431 for dist_name, package_dir, skill_name in [ 2B ocpcC qcrcD sctc

432 ("api-pkg", "api_pkg", "api-skill"), 

433 ("workspace-lib", "workspace_lib", "workspace-lib-skill"), 

434 ("root-pkg", "root_pkg", "root-skill"), 

435 ("worker-pkg", "worker_pkg", "worker-skill"), 

436 ("transitive-pkg", "transitive_pkg", "transitive-skill"), 

437 ]: 

438 write_distribution_skill( 2B ocpcC qcrcD sctc

439 site_packages, 

440 dist_name=dist_name, 

441 package_dir=package_dir, 

442 skill_name=skill_name, 

443 ) 

444 monkeypatch.chdir(project) 2B ocpcC qcrcD sctc

445 

446 result = runner.invoke(app, ["scan", "--json"]) 2B ocpcC qcrcD sctc

447 all_result = runner.invoke(app, ["scan", "--json", "--all"]) 2B ocpcC qcrcD sctc

448 

449 payload = json.loads(result.output) 2B ocpcC qcrcD sctc

450 all_payload = json.loads(all_result.output) 2B ocpcC qcrcD sctc

451 assert result.exit_code == 0 2B ocpcC qcrcD sctc

452 assert payload["workspace_member"] == "" 2B ocpcC qcrcD sctc

453 assert payload["dependency_files"] == [ 2B ocpcC qcrcD sctc

454 str(project / "pyproject.toml"), 

455 str((project / "packages" / "api").resolve() / "pyproject.toml"), 

456 str((project / "packages" / "worker").resolve() / "pyproject.toml"), 

457 ] 

458 assert [skill["name"] for skill in payload["skills"]] == [ 2B ocpcC qcrcD sctc

459 "api-skill", 

460 "root-skill", 

461 "worker-skill", 

462 "workspace-lib-skill", 

463 ] 

464 assert "transitive-skill" not in [skill["name"] for skill in payload["skills"]] 2B ocpcC qcrcD sctc

465 assert "transitive-skill" in [skill["name"] for skill in all_payload["skills"]] 2B ocpcC qcrcD sctc

466 

467 

468def test_combined_uv_and_node_workspace_root_scan_uses_all_deps( 1abcdefghi

469 tmp_path, 

470 monkeypatch, 

471): 

472 project = write_workspace(tmp_path) 2s ucvct wcxcu yczc

473 add_node_workspace_files(project) 2s ucvct wcxcu yczc

474 site_packages = project / ".venv" / "lib" / "python3.12" / "site-packages" 2s ucvct wcxcu yczc

475 write_distribution_skill( 2s ucvct wcxcu yczc

476 site_packages, 

477 dist_name="api-pkg", 

478 package_dir="api_pkg", 

479 skill_name="api-skill", 

480 ) 

481 write_distribution_skill( 2s ucvct wcxcu yczc

482 site_packages, 

483 dist_name="root-pkg", 

484 package_dir="root_pkg", 

485 skill_name="root-skill", 

486 ) 

487 write_distribution_skill( 2s ucvct wcxcu yczc

488 site_packages, 

489 dist_name="worker-pkg", 

490 package_dir="worker_pkg", 

491 skill_name="worker-skill", 

492 ) 

493 for package_name, skill_name in [ 2s ucvct wcxcu yczc

494 ("api-node-pkg", "api-node-skill"), 

495 ("root-node-pkg", "root-node-skill"), 

496 ("worker-node-pkg", "worker-node-skill"), 

497 ]: 

498 write_node_package_skill( 2s ucvct wcxcu yczc

499 project, package_name=package_name, skill_name=skill_name 

500 ) 

501 monkeypatch.chdir(project) 2s ucvct wcxcu yczc

502 

503 result = runner.invoke(app, ["scan", "--json"]) 2s ucvct wcxcu yczc

504 

505 payload = json.loads(result.output) 2s ucvct wcxcu yczc

506 assert result.exit_code == 0 2s ucvct wcxcu yczc

507 assert payload["workspace_member"] == "" 2s ucvct wcxcu yczc

508 assert payload["dependency_files"] == [ 2s ucvct wcxcu yczc

509 str(project / "pyproject.toml"), 

510 str((project / "packages" / "api").resolve() / "pyproject.toml"), 

511 str((project / "packages" / "worker").resolve() / "pyproject.toml"), 

512 str(project / "package.json"), 

513 str((project / "packages" / "api").resolve() / "package.json"), 

514 str((project / "packages" / "worker").resolve() / "package.json"), 

515 ] 

516 assert [skill["name"] for skill in payload["skills"]] == [ 2s ucvct wcxcu yczc

517 "api-skill", 

518 "root-skill", 

519 "worker-skill", 

520 "api-node-skill", 

521 "root-node-skill", 

522 "worker-node-skill", 

523 ] 

524 

525 

526def test_workspace_non_member_subdir_uses_root_and_all_member_deps( 1abcdefghi

527 tmp_path, 

528 monkeypatch, 

529): 

530 project = write_workspace(tmp_path) 2H BdCdJ DdEdL FdGd

531 docs = project / "docs" 2H BdCdJ DdEdL FdGd

532 docs.mkdir() 2H BdCdJ DdEdL FdGd

533 site_packages = project / ".venv" / "lib" / "python3.12" / "site-packages" 2H BdCdJ DdEdL FdGd

534 for dist_name, package_dir, skill_name in [ 2H BdCdJ DdEdL FdGd

535 ("api-pkg", "api_pkg", "api-skill"), 

536 ("root-pkg", "root_pkg", "root-skill"), 

537 ("worker-pkg", "worker_pkg", "worker-skill"), 

538 ]: 

539 write_distribution_skill( 2H BdCdJ DdEdL FdGd

540 site_packages, 

541 dist_name=dist_name, 

542 package_dir=package_dir, 

543 skill_name=skill_name, 

544 ) 

545 monkeypatch.chdir(docs) 2H BdCdJ DdEdL FdGd

546 

547 result = runner.invoke(app, ["scan", "--json"]) 2H BdCdJ DdEdL FdGd

548 

549 payload = json.loads(result.output) 2H BdCdJ DdEdL FdGd

550 assert result.exit_code == 0 2H BdCdJ DdEdL FdGd

551 assert payload["workspace_member"] == "" 2H BdCdJ DdEdL FdGd

552 assert [skill["name"] for skill in payload["skills"]] == [ 2H BdCdJ DdEdL FdGd

553 "api-skill", 

554 "root-skill", 

555 "worker-skill", 

556 ] 

557 

558 

559def test_workspace_scan_prints_workspace_context(tmp_path, monkeypatch): 1abcdefghi

560 project = write_workspace(tmp_path) 2I HdIdK JdKdM LdMd

561 api = project / "packages" / "api" 2I HdIdK JdKdM LdMd

562 site_packages = project / ".venv" / "lib" / "python3.12" / "site-packages" 2I HdIdK JdKdM LdMd

563 write_distribution_skill( 2I HdIdK JdKdM LdMd

564 site_packages, 

565 dist_name="api-pkg", 

566 package_dir="api_pkg", 

567 skill_name="api-skill", 

568 ) 

569 monkeypatch.chdir(api) 2I HdIdK JdKdM LdMd

570 

571 with patch.object(cli, "console", Console(width=1000)): 2I HdIdK JdKdM LdMd

572 result = runner.invoke(app, ["scan"]) 2I HdIdK JdKdM LdMd

573 

574 assert result.exit_code == 0 2I HdIdK JdKdM LdMd

575 assert "Workspace root" in result.output 2I HdIdK JdKdM LdMd

576 assert str(project) in result.output 2I HdIdK JdKdM LdMd

577 assert "Workspace member" in result.output 2I HdIdK JdKdM LdMd

578 assert str(api.resolve()) in result.output 2I HdIdK JdKdM LdMd

579 

580 

581def test_node_workspace_scan_prints_workspace_context(tmp_path, monkeypatch): 1abcdefghi

582 project = write_node_workspace(tmp_path) 2nb+b,bpb-b.brb/b:b

583 api = project / "packages" / "api" 2nb+b,bpb-b.brb/b:b

584 write_node_package_skill(project, package_name="api-pkg", skill_name="api-skill") 2nb+b,bpb-b.brb/b:b

585 monkeypatch.chdir(api) 2nb+b,bpb-b.brb/b:b

586 

587 with patch.object(cli, "console", Console(width=1000)): 2nb+b,bpb-b.brb/b:b

588 result = runner.invoke(app, ["scan"]) 2nb+b,bpb-b.brb/b:b

589 

590 assert result.exit_code == 0 2nb+b,bpb-b.brb/b:b

591 assert "Workspace root" in result.output 2nb+b,bpb-b.brb/b:b

592 assert str(project) in result.output 2nb+b,bpb-b.brb/b:b

593 assert "Workspace member" in result.output 2nb+b,bpb-b.brb/b:b

594 assert str(api.resolve()) in result.output 2nb+b,bpb-b.brb/b:b

595 

596 

597def test_node_workspace_member_scan_uses_member_deps_and_root_node_modules( 1abcdefghi

598 tmp_path, 

599 monkeypatch, 

600): 

601 project = write_node_workspace(tmp_path) 2X AcBcY CcDcZ EcFc

602 api = project / "packages" / "api" 2X AcBcY CcDcZ EcFc

603 for package_name, skill_name in [ 2X AcBcY CcDcZ EcFc

604 ("api-pkg", "api-skill"), 

605 ("workspace-lib", "workspace-lib-skill"), 

606 ("root-pkg", "root-skill"), 

607 ("worker-pkg", "worker-skill"), 

608 ("transitive-node-pkg", "transitive-node-skill"), 

609 ]: 

610 write_node_package_skill( 2X AcBcY CcDcZ EcFc

611 project, package_name=package_name, skill_name=skill_name 

612 ) 

613 (api / "src").mkdir() 2X AcBcY CcDcZ EcFc

614 monkeypatch.chdir(api / "src") 2X AcBcY CcDcZ EcFc

615 

616 result = runner.invoke(app, ["scan", "--json"]) 2X AcBcY CcDcZ EcFc

617 

618 payload = json.loads(result.output) 2X AcBcY CcDcZ EcFc

619 assert result.exit_code == 0 2X AcBcY CcDcZ EcFc

620 assert payload["project_root"] == str(project) 2X AcBcY CcDcZ EcFc

621 assert payload["workspace_root"] == str(project) 2X AcBcY CcDcZ EcFc

622 assert payload["workspace_member"] == str(api.resolve()) 2X AcBcY CcDcZ EcFc

623 assert payload["dependency_files"] == [str(api.resolve() / "package.json")] 2X AcBcY CcDcZ EcFc

624 assert payload["node_modules"] == str(project / "node_modules") 2X AcBcY CcDcZ EcFc

625 assert [skill["name"] for skill in payload["skills"]] == [ 2X AcBcY CcDcZ EcFc

626 "api-skill", 

627 "workspace-lib-skill", 

628 ] 

629 

630 

631def test_node_workspace_root_scan_uses_root_and_all_member_deps(tmp_path, monkeypatch): 1abcdefghi

632 project = write_node_workspace(tmp_path) 25 YcZc6 0c1c7 2c3c

633 for package_name, skill_name in [ 25 YcZc6 0c1c7 2c3c

634 ("api-pkg", "api-skill"), 

635 ("workspace-lib", "workspace-lib-skill"), 

636 ("root-pkg", "root-skill"), 

637 ("worker-pkg", "worker-skill"), 

638 ("transitive-node-pkg", "transitive-node-skill"), 

639 ]: 

640 write_node_package_skill( 25 YcZc6 0c1c7 2c3c

641 project, package_name=package_name, skill_name=skill_name 

642 ) 

643 monkeypatch.chdir(project) 25 YcZc6 0c1c7 2c3c

644 

645 result = runner.invoke(app, ["scan", "--json"]) 25 YcZc6 0c1c7 2c3c

646 all_result = runner.invoke(app, ["scan", "--json", "--all"]) 25 YcZc6 0c1c7 2c3c

647 

648 payload = json.loads(result.output) 25 YcZc6 0c1c7 2c3c

649 all_payload = json.loads(all_result.output) 25 YcZc6 0c1c7 2c3c

650 assert result.exit_code == 0 25 YcZc6 0c1c7 2c3c

651 assert payload["workspace_member"] == "" 25 YcZc6 0c1c7 2c3c

652 assert payload["dependency_files"] == [ 25 YcZc6 0c1c7 2c3c

653 str(project / "package.json"), 

654 str((project / "packages" / "api").resolve() / "package.json"), 

655 str((project / "packages" / "worker").resolve() / "package.json"), 

656 ] 

657 assert [skill["name"] for skill in payload["skills"]] == [ 25 YcZc6 0c1c7 2c3c

658 "api-skill", 

659 "root-skill", 

660 "worker-skill", 

661 "workspace-lib-skill", 

662 ] 

663 assert "transitive-node-skill" not in [skill["name"] for skill in payload["skills"]] 25 YcZc6 0c1c7 2c3c

664 assert "transitive-node-skill" in [skill["name"] for skill in all_payload["skills"]] 25 YcZc6 0c1c7 2c3c

665 

666 

667def test_node_workspace_non_member_subdir_uses_root_and_all_member_deps( 1abcdefghi

668 tmp_path, 

669 monkeypatch, 

670): 

671 project = write_node_workspace(tmp_path) 2mb`d{dob|d}dqb~dae

672 docs = project / "docs" 2mb`d{dob|d}dqb~dae

673 docs.mkdir() 2mb`d{dob|d}dqb~dae

674 for package_name, skill_name in [ 2mb`d{dob|d}dqb~dae

675 ("api-pkg", "api-skill"), 

676 ("root-pkg", "root-skill"), 

677 ("worker-pkg", "worker-skill"), 

678 ]: 

679 write_node_package_skill( 2mb`d{dob|d}dqb~dae

680 project, package_name=package_name, skill_name=skill_name 

681 ) 

682 monkeypatch.chdir(docs) 2mb`d{dob|d}dqb~dae

683 

684 result = runner.invoke(app, ["scan", "--json"]) 2mb`d{dob|d}dqb~dae

685 

686 payload = json.loads(result.output) 2mb`d{dob|d}dqb~dae

687 assert result.exit_code == 0 2mb`d{dob|d}dqb~dae

688 assert payload["workspace_member"] == "" 2mb`d{dob|d}dqb~dae

689 assert [skill["name"] for skill in payload["skills"]] == [ 2mb`d{dob|d}dqb~dae

690 "api-skill", 

691 "root-skill", 

692 "worker-skill", 

693 ] 

694 

695 

696def test_node_workspace_member_local_node_modules_is_not_ignored(tmp_path, monkeypatch): 1abcdefghi

697 project = write_node_workspace(tmp_path) 2zbCeDeAbEeFeBbGeHe

698 api = project / "packages" / "api" 2zbCeDeAbEeFeBbGeHe

699 write_node_package_skill( 2zbCeDeAbEeFeBbGeHe

700 project, package_name="api-pkg", skill_name="root-api-skill" 

701 ) 

702 write_node_package_skill_at( 2zbCeDeAbEeFeBbGeHe

703 api / "node_modules" / "api-pkg", 

704 package_name="api-pkg", 

705 skill_name="member-api-skill", 

706 ) 

707 monkeypatch.chdir(api) 2zbCeDeAbEeFeBbGeHe

708 

709 result = runner.invoke(app, ["scan", "--json"]) 2zbCeDeAbEeFeBbGeHe

710 

711 payload = json.loads(result.output) 2zbCeDeAbEeFeBbGeHe

712 assert result.exit_code == 0 2zbCeDeAbEeFeBbGeHe

713 assert payload["node_modules"] == str(api / "node_modules") 2zbCeDeAbEeFeBbGeHe

714 assert [skill["name"] for skill in payload["skills"]] == ["member-api-skill"] 2zbCeDeAbEeFeBbGeHe

715 

716 

717def test_scan_includes_python_and_node_package_skills(tmp_path, monkeypatch): 1abcdefghi

718 project = write_project(tmp_path, dependencies=["top-pkg>=1"]) 2p CbDbq EbFbr GbHb

719 site_packages = project / ".venv" / "lib" / "python3.12" / "site-packages" 2p CbDbq EbFbr GbHb

720 write_distribution_skill( 2p CbDbq EbFbr GbHb

721 site_packages, 

722 dist_name="top-pkg", 

723 package_dir="top_pkg", 

724 skill_name="python-skill", 

725 ) 

726 write_distribution_skill( 2p CbDbq EbFbr GbHb

727 site_packages, 

728 dist_name="transitive-pkg", 

729 package_dir="transitive_pkg", 

730 skill_name="transitive-skill", 

731 ) 

732 write_node_package_skill( 2p CbDbq EbFbr GbHb

733 project, 

734 package_name="@scope/node-pkg", 

735 skill_name="node-skill", 

736 ) 

737 write_node_package_skill( 2p CbDbq EbFbr GbHb

738 project, 

739 package_name="transitive-node-pkg", 

740 skill_name="transitive-node-skill", 

741 ) 

742 project.joinpath("package.json").write_text( 2p CbDbq EbFbr GbHb

743 json.dumps({"devDependencies": {"@scope/node-pkg": "^2.0.0"}}), 

744 encoding="utf-8", 

745 ) 

746 monkeypatch.chdir(project) 2p CbDbq EbFbr GbHb

747 

748 result = runner.invoke(app, ["scan"]) 2p CbDbq EbFbr GbHb

749 all_result = runner.invoke(app, ["scan", "--all"]) 2p CbDbq EbFbr GbHb

750 json_result = runner.invoke(app, ["scan", "--json", "--all"]) 2p CbDbq EbFbr GbHb

751 

752 assert result.exit_code == 0 2p CbDbq EbFbr GbHb

753 assert "Target Python environment" in result.output 2p CbDbq EbFbr GbHb

754 assert ".venv" in result.output 2p CbDbq EbFbr GbHb

755 assert "node_modules" in result.output 2p CbDbq EbFbr GbHb

756 assert "python-skill" in result.output 2p CbDbq EbFbr GbHb

757 assert "node-skill" in result.output 2p CbDbq EbFbr GbHb

758 assert "transitive-skill" not in result.output 2p CbDbq EbFbr GbHb

759 assert "transitive-node-skill" not in result.output 2p CbDbq EbFbr GbHb

760 assert all_result.exit_code == 0 2p CbDbq EbFbr GbHb

761 assert "transitive-skill" in all_result.output 2p CbDbq EbFbr GbHb

762 assert "transitive-node-skill" in all_result.output 2p CbDbq EbFbr GbHb

763 assert json_result.exit_code == 0 2p CbDbq EbFbr GbHb

764 payload = json.loads(json_result.output) 2p CbDbq EbFbr GbHb

765 assert payload["project_root"] == str(project) 2p CbDbq EbFbr GbHb

766 assert "installed" not in payload 2p CbDbq EbFbr GbHb

767 assert [skill["name"] for skill in payload["skills"]] == [ 2p CbDbq EbFbr GbHb

768 "python-skill", 

769 "transitive-skill", 

770 "node-skill", 

771 "transitive-node-skill", 

772 ] 

773 

774 

775def test_install_skill_can_explicitly_select_transitive_dependency( 1abcdefghi

776 tmp_path, monkeypatch 

777): 

778 project = write_project(tmp_path, dependencies=["top-pkg>=1"]) 2UbOePeYbQeRe1bSeTe

779 site_packages = project / ".venv" / "lib" / "python3.12" / "site-packages" 2UbOePeYbQeRe1bSeTe

780 transitive_skill = write_distribution_skill( 2UbOePeYbQeRe1bSeTe

781 site_packages, 

782 dist_name="transitive-pkg", 

783 package_dir="transitive_pkg", 

784 skill_name="transitive-skill", 

785 ) 

786 monkeypatch.chdir(project) 2UbOePeYbQeRe1bSeTe

787 

788 result = runner.invoke(app, ["install", "--skill", "transitive-skill", "--yes"]) 2UbOePeYbQeRe1bSeTe

789 

790 installed = project / ".agents" / "skills" / "transitive-skill" 2UbOePeYbQeRe1bSeTe

791 assert result.exit_code == 0 2UbOePeYbQeRe1bSeTe

792 assert installed.is_symlink() 2UbOePeYbQeRe1bSeTe

793 assert installed.resolve() == transitive_skill.resolve() 2UbOePeYbQeRe1bSeTe

794 

795 

796def test_yes_repairs_broken_managed_symlink_without_installing_new_skills( 1abcdefghi

797 tmp_path, 

798 monkeypatch, 

799): 

800 project = write_project(tmp_path, dependencies=["demo-pkg>=1"]) 2E {b|bF }b~bG acbc

801 site_packages = project / ".venv" / "lib" / "python3.12" / "site-packages" 2E {b|bF }b~bG acbc

802 repaired_skill, _new_skill = write_distribution( 2E {b|bF }b~bG acbc

803 site_packages, 

804 dist_name="demo-pkg", 

805 package_dir="demo_pkg", 

806 skill_names=["repaired-skill", "new-skill"], 

807 ) 

808 installed_dir = project / ".agents" / "skills" 2E {b|bF }b~bG acbc

809 installed_dir.mkdir(parents=True) 2E {b|bF }b~bG acbc

810 broken_link = installed_dir / "repaired-skill" 2E {b|bF }b~bG acbc

811 broken_link.symlink_to(project / "missing") 2E {b|bF }b~bG acbc

812 monkeypatch.chdir(project) 2E {b|bF }b~bG acbc

813 

814 result = runner.invoke(app, ["--yes"]) 2E {b|bF }b~bG acbc

815 

816 assert result.exit_code == 0 2E {b|bF }b~bG acbc

817 assert broken_link.is_symlink() 2E {b|bF }b~bG acbc

818 assert broken_link.resolve() == repaired_skill.resolve() 2E {b|bF }b~bG acbc

819 assert not (installed_dir / "new-skill").exists() 2E {b|bF }b~bG acbc

820 

821 

822def test_yes_removes_orphaned_and_unrecoverable_broken_symlinks( 1abcdefghi

823 tmp_path, 

824 monkeypatch, 

825): 

826 project = write_project(tmp_path, dependencies=[]) 2Ob@b[bPb]b^bRb_b`b

827 installed_dir = project / ".agents" / "skills" 2Ob@b[bPb]b^bRb_b`b

828 installed_dir.mkdir(parents=True) 2Ob@b[bPb]b^bRb_b`b

829 orphan_target = project / "old-target" 2Ob@b[bPb]b^bRb_b`b

830 orphan_target.mkdir() 2Ob@b[bPb]b^bRb_b`b

831 orphan_link = installed_dir / "orphan-skill" 2Ob@b[bPb]b^bRb_b`b

832 orphan_link.symlink_to(orphan_target, target_is_directory=True) 2Ob@b[bPb]b^bRb_b`b

833 broken_link = installed_dir / "broken-skill" 2Ob@b[bPb]b^bRb_b`b

834 broken_link.symlink_to(project / "missing-target", target_is_directory=True) 2Ob@b[bPb]b^bRb_b`b

835 hand_authored = installed_dir / "hand-authored" 2Ob@b[bPb]b^bRb_b`b

836 hand_authored.mkdir() 2Ob@b[bPb]b^bRb_b`b

837 monkeypatch.chdir(project) 2Ob@b[bPb]b^bRb_b`b

838 

839 result = runner.invoke(app, ["--yes"]) 2Ob@b[bPb]b^bRb_b`b

840 

841 assert result.exit_code == 0 2Ob@b[bPb]b^bRb_b`b

842 assert "Removed orphaned symlink" in result.output 2Ob@b[bPb]b^bRb_b`b

843 assert "Removed broken symlink" in result.output 2Ob@b[bPb]b^bRb_b`b

844 assert not orphan_link.is_symlink() 2Ob@b[bPb]b^bRb_b`b

845 assert not broken_link.is_symlink() 2Ob@b[bPb]b^bRb_b`b

846 assert hand_authored.is_dir() 2Ob@b[bPb]b^bRb_b`b

847 

848 

849def test_yes_repairs_existing_claude_target_without_claude_flag( 1abcdefghi

850 tmp_path, 

851 monkeypatch, 

852): 

853 project = write_project(tmp_path, dependencies=["demo-pkg>=1"]) 2- 7c8c? 9c!c` #c$c

854 site_packages = project / ".venv" / "lib" / "python3.12" / "site-packages" 2- 7c8c? 9c!c` #c$c

855 repaired_skill = write_distribution_skill( 2- 7c8c? 9c!c` #c$c

856 site_packages, 

857 dist_name="demo-pkg", 

858 package_dir="demo_pkg", 

859 skill_name="demo-skill", 

860 ) 

861 claude_dir = project / ".claude" / "skills" 2- 7c8c? 9c!c` #c$c

862 claude_dir.mkdir(parents=True) 2- 7c8c? 9c!c` #c$c

863 installed = claude_dir / "demo-skill" 2- 7c8c? 9c!c` #c$c

864 installed.symlink_to(project / "missing-target", target_is_directory=True) 2- 7c8c? 9c!c` #c$c

865 monkeypatch.chdir(project) 2- 7c8c? 9c!c` #c$c

866 

867 result = runner.invoke(app, ["--yes"]) 2- 7c8c? 9c!c` #c$c

868 

869 assert result.exit_code == 0 2- 7c8c? 9c!c` #c$c

870 assert "claude-compatible" in result.output 2- 7c8c? 9c!c` #c$c

871 assert installed.is_symlink() 2- 7c8c? 9c!c` #c$c

872 assert installed.resolve() == repaired_skill.resolve() 2- 7c8c? 9c!c` #c$c

873 

874 

875def test_check_reports_fixable_drift_without_modifying_files(tmp_path, monkeypatch): 1abcdefghi

876 project = write_project(tmp_path, dependencies=[]) 2fcUeVeicWeXelcYeZe

877 installed_dir = project / ".agents" / "skills" 2fcUeVeicWeXelcYeZe

878 installed_dir.mkdir(parents=True) 2fcUeVeicWeXelcYeZe

879 broken_link = installed_dir / "broken-skill" 2fcUeVeicWeXelcYeZe

880 broken_link.symlink_to(project / "missing-target", target_is_directory=True) 2fcUeVeicWeXelcYeZe

881 monkeypatch.chdir(project) 2fcUeVeicWeXelcYeZe

882 

883 result = runner.invoke(app, ["--check"]) 2fcUeVeicWeXelcYeZe

884 

885 assert result.exit_code == 1 2fcUeVeicWeXelcYeZe

886 assert broken_link.is_symlink() 2fcUeVeicWeXelcYeZe

887 

888 

889def test_check_ignores_tool_skill_drift_unless_requested(tmp_path, monkeypatch): 1abcdefghi

890 project = write_project(tmp_path, dependencies=[]) 2md!f#frd$f%fwd'f(f

891 monkeypatch.chdir(project) 2md!f#frd$f%fwd'f(f

892 

893 result = runner.invoke(app, ["--check"]) 2md!f#frd$f%fwd'f(f

894 

895 assert result.exit_code == 0 2md!f#frd$f%fwd'f(f

896 assert not (project / ".agents" / "skills" / TOOL_SKILL_NAME).exists() 2md!f#frd$f%fwd'f(f

897 

898 

899def test_check_tool_skill_reports_missing_default_and_claude_targets( 1abcdefghi

900 tmp_path, 

901 monkeypatch, 

902): 

903 project = write_project(tmp_path, dependencies=[]) 2VcufvfWcwfxfXcyfzf

904 monkeypatch.chdir(project) 2VcufvfWcwfxfXcyfzf

905 

906 result = runner.invoke(app, ["--check", "--tool-skill", "--claude"]) 2VcufvfWcwfxfXcyfzf

907 

908 assert result.exit_code == 1 2VcufvfWcwfxfXcyfzf

909 assert "tool skill: missing" in result.output 2VcufvfWcwfxfXcyfzf

910 assert str(Path(".agents") / "skills" / "library-skills") in result.output 2VcufvfWcwfxfXcyfzf

911 assert str(Path(".claude") / "skills" / "library-skills") in result.output 2VcufvfWcwfxfXcyfzf

912 

913 

914def test_tool_skill_installs_and_updates_copied_skill(tmp_path, monkeypatch): 1abcdefghi

915 project = write_project(tmp_path, dependencies=[]) 2;b%c'c=b(c)c?b*c+c

916 monkeypatch.chdir(project) 2;b%c'c=b(c)c?b*c+c

917 

918 result = runner.invoke(app, ["--tool-skill"]) 2;b%c'c=b(c)c?b*c+c

919 

920 installed = project / ".agents" / "skills" / TOOL_SKILL_NAME 2;b%c'c=b(c)c?b*c+c

921 assert result.exit_code == 0 2;b%c'c=b(c)c?b*c+c

922 assert installed.joinpath("SKILL.md").read_text(encoding="utf-8") == ( 2;b%c'c=b(c)c?b*c+c

923 get_tool_skill_template() 

924 ) 

925 assert installed.joinpath(TOOL_SKILL_MARKER).is_file() 2;b%c'c=b(c)c?b*c+c

926 

927 installed.joinpath("SKILL.md").write_text("stale", encoding="utf-8") 2;b%c'c=b(c)c?b*c+c

928 update_result = runner.invoke(app, ["--tool-skill"]) 2;b%c'c=b(c)c?b*c+c

929 

930 assert update_result.exit_code == 0 2;b%c'c=b(c)c?b*c+c

931 assert "tool skill: stale" in update_result.output 2;b%c'c=b(c)c?b*c+c

932 assert "hand-authored" not in update_result.output 2;b%c'c=b(c)c?b*c+c

933 assert installed.joinpath("SKILL.md").read_text(encoding="utf-8") == ( 2;b%c'c=b(c)c?b*c+c

934 get_tool_skill_template() 

935 ) 

936 

937 

938def test_no_tool_skill_skips_management(tmp_path, monkeypatch): 1abcdefghi

939 project = write_project(tmp_path, dependencies=[]) 2pd)f*fud+f,fzd-f.f

940 monkeypatch.chdir(project) 2pd)f*fud+f,fzd-f.f

941 

942 result = runner.invoke(app, ["--no-tool-skill"]) 2pd)f*fud+f,fzd-f.f

943 

944 assert result.exit_code == 0 2pd)f*fud+f,fzd-f.f

945 assert not (project / ".agents" / "skills" / TOOL_SKILL_NAME).exists() 2pd)f*fud+f,fzd-f.f

946 

947 

948def test_explicit_tool_skill_fails_on_hand_authored_directory(tmp_path, monkeypatch): 1abcdefghi

949 project = write_project(tmp_path, dependencies=[]) 2gc0e1ejc2e3emc4e5e

950 hand_authored = project / ".agents" / "skills" / TOOL_SKILL_NAME 2gc0e1ejc2e3emc4e5e

951 hand_authored.mkdir(parents=True) 2gc0e1ejc2e3emc4e5e

952 hand_authored.joinpath("SKILL.md").write_text("mine", encoding="utf-8") 2gc0e1ejc2e3emc4e5e

953 monkeypatch.chdir(project) 2gc0e1ejc2e3emc4e5e

954 

955 result = runner.invoke(app, ["--tool-skill"]) 2gc0e1ejc2e3emc4e5e

956 

957 assert result.exit_code == 1 2gc0e1ejc2e3emc4e5e

958 assert "blocked by hand-authored" in result.output 2gc0e1ejc2e3emc4e5e

959 assert hand_authored.joinpath("SKILL.md").read_text(encoding="utf-8") == "mine" 2gc0e1ejc2e3emc4e5e

960 

961 

962def test_default_sync_with_no_changes_reports_no_changes_needed( 1abcdefghi

963 tmp_path, 

964 monkeypatch, 

965): 

966 project = write_project(tmp_path, dependencies=[]) 24c4f5f5c6f7f6c8f9f

967 monkeypatch.chdir(project) 24c4f5f5c6f7f6c8f9f

968 

969 with patch.object(cli, "_select_tool_skill_interactive", lambda: False): 24c4f5f5c6f7f6c8f9f

970 result = runner.invoke(app) 24c4f5f5c6f7f6c8f9f

971 

972 assert result.exit_code == 0 24c4f5f5c6f7f6c8f9f

973 assert "No changes needed." in result.output 24c4f5f5c6f7f6c8f9f

974 

975 

976def test_default_sync_prompts_for_missing_tool_skill_without_new_skills( 1abcdefghi

977 tmp_path, 

978 monkeypatch, 

979): 

980 project = write_project(tmp_path, dependencies=["demo-pkg>=1"]) 2{ NdOdbbPdQdhbRdSd

981 site_packages = project / ".venv" / "lib" / "python3.12" / "site-packages" 2{ NdOdbbPdQdhbRdSd

982 skill_dir = write_distribution_skill( 2{ NdOdbbPdQdhbRdSd

983 site_packages, 

984 dist_name="demo-pkg", 

985 package_dir="demo_pkg", 

986 skill_name="demo-skill", 

987 ) 

988 installed_dir = project / ".agents" / "skills" 2{ NdOdbbPdQdhbRdSd

989 installed_dir.mkdir(parents=True) 2{ NdOdbbPdQdhbRdSd

990 (installed_dir / "demo-skill").symlink_to(skill_dir, target_is_directory=True) 2{ NdOdbbPdQdhbRdSd

991 monkeypatch.chdir(project) 2{ NdOdbbPdQdhbRdSd

992 

993 with patch.object(cli, "_select_tool_skill_interactive", lambda: True): 2{ NdOdbbPdQdhbRdSd

994 result = runner.invoke(app) 2{ NdOdbbPdQdhbRdSd

995 

996 tool_skill = project / ".agents" / "skills" / TOOL_SKILL_NAME 2{ NdOdbbPdQdhbRdSd

997 assert result.exit_code == 0 2{ NdOdbbPdQdhbRdSd

998 assert tool_skill.joinpath("SKILL.md").read_text(encoding="utf-8") == ( 2{ NdOdbbPdQdhbRdSd

999 get_tool_skill_template() 

1000 ) 

1001 

1002 

1003def test_list_json_reports_discovered_and_installed_skills(tmp_path, monkeypatch): 1abcdefghi

1004 project = write_project(tmp_path, dependencies=["demo-pkg>=1"]) 2+ ,c-c; .c/c^ :c;c

1005 site_packages = project / ".venv" / "lib" / "python3.12" / "site-packages" 2+ ,c-c; .c/c^ :c;c

1006 skill_dir = write_distribution_skill( 2+ ,c-c; .c/c^ :c;c

1007 site_packages, 

1008 dist_name="demo-pkg", 

1009 package_dir="demo_pkg", 

1010 skill_name="demo-skill", 

1011 ) 

1012 installed_dir = project / ".agents" / "skills" 2+ ,c-c; .c/c^ :c;c

1013 installed_dir.mkdir(parents=True) 2+ ,c-c; .c/c^ :c;c

1014 (installed_dir / "demo-skill").symlink_to(skill_dir, target_is_directory=True) 2+ ,c-c; .c/c^ :c;c

1015 monkeypatch.chdir(project) 2+ ,c-c; .c/c^ :c;c

1016 

1017 with patch.object(cli, "console", Console(width=1000)): 2+ ,c-c; .c/c^ :c;c

1018 result = runner.invoke(app, ["list", "--json"]) 2+ ,c-c; .c/c^ :c;c

1019 

1020 payload = json.loads(result.output) 2+ ,c-c; .c/c^ :c;c

1021 assert result.exit_code == 0 2+ ,c-c; .c/c^ :c;c

1022 assert payload["skills"][0]["name"] == "demo-skill" 2+ ,c-c; .c/c^ :c;c

1023 assert payload["installed"][0]["status"] == "up to date" 2+ ,c-c; .c/c^ :c;c

1024 

1025 

1026def test_list_installed_prints_no_skills_when_target_is_empty(tmp_path, monkeypatch): 1abcdefghi

1027 project = write_project(tmp_path, dependencies=[]) 2od/f:ftd;f=fyd?f@f

1028 monkeypatch.chdir(project) 2od/f:ftd;f=fyd?f@f

1029 

1030 result = runner.invoke(app, ["list", "--installed"]) 2od/f:ftd;f=fyd?f@f

1031 

1032 assert result.exit_code == 0 2od/f:ftd;f=fyd?f@f

1033 assert "No skills installed." in result.output 2od/f:ftd;f=fyd?f@f

1034 

1035 

1036def test_list_installed_prints_installed_statuses(tmp_path, monkeypatch): 1abcdefghi

1037 project = write_project(tmp_path, dependencies=["demo-pkg>=1"]) 2* =c?c: @c[c] ]c^c

1038 site_packages = project / ".venv" / "lib" / "python3.12" / "site-packages" 2* =c?c: @c[c] ]c^c

1039 skill_dir = write_distribution_skill( 2* =c?c: @c[c] ]c^c

1040 site_packages, 

1041 dist_name="demo-pkg", 

1042 package_dir="demo_pkg", 

1043 skill_name="demo-skill", 

1044 ) 

1045 installed_dir = project / ".agents" / "skills" 2* =c?c: @c[c] ]c^c

1046 installed_dir.mkdir(parents=True) 2* =c?c: @c[c] ]c^c

1047 (installed_dir / "demo-skill").symlink_to(skill_dir, target_is_directory=True) 2* =c?c: @c[c] ]c^c

1048 monkeypatch.chdir(project) 2* =c?c: @c[c] ]c^c

1049 

1050 with patch.object(cli, "console", Console(width=1000)): 2* =c?c: @c[c] ]c^c

1051 result = runner.invoke(app, ["list", "--installed"]) 2* =c?c: @c[c] ]c^c

1052 

1053 assert result.exit_code == 0 2* =c?c: @c[c] ]c^c

1054 assert "up to date" in result.output 2* =c?c: @c[c] ]c^c

1055 assert ".agents" in result.output 2* =c?c: @c[c] ]c^c

1056 assert str(project / ".agents") not in result.output 2* =c?c: @c[c] ]c^c

1057 

1058 

1059def test_install_all_copy_mode_installs_to_agents_and_claude(tmp_path, monkeypatch): 1abcdefghi

1060 project = write_project(tmp_path, dependencies=["demo-pkg>=1"]) 2| TdUdcbVdWdibXdYd

1061 site_packages = project / ".venv" / "lib" / "python3.12" / "site-packages" 2| TdUdcbVdWdibXdYd

1062 write_distribution_skill( 2| TdUdcbVdWdibXdYd

1063 site_packages, 

1064 dist_name="demo-pkg", 

1065 package_dir="demo_pkg", 

1066 skill_name="demo-skill", 

1067 ) 

1068 monkeypatch.chdir(project) 2| TdUdcbVdWdibXdYd

1069 

1070 result = runner.invoke(app, ["install", "--all", "--yes", "--copy", "--claude"]) 2| TdUdcbVdWdibXdYd

1071 

1072 agents_skill = project / ".agents" / "skills" / "demo-skill" 2| TdUdcbVdWdibXdYd

1073 claude_skill = project / ".claude" / "skills" / "demo-skill" 2| TdUdcbVdWdibXdYd

1074 assert result.exit_code == 0 2| TdUdcbVdWdibXdYd

1075 assert agents_skill.is_dir() 2| TdUdcbVdWdibXdYd

1076 assert not agents_skill.is_symlink() 2| TdUdcbVdWdibXdYd

1077 assert claude_skill.is_dir() 2| TdUdcbVdWdibXdYd

1078 assert not claude_skill.is_symlink() 2| TdUdcbVdWdibXdYd

1079 

1080 

1081def test_install_interactive_can_choose_claude_only(tmp_path, monkeypatch): 1abcdefghi

1082 project = write_project(tmp_path, dependencies=["demo-pkg>=1"]) 2tbbecewbdeeejbfege

1083 site_packages = project / ".venv" / "lib" / "python3.12" / "site-packages" 2tbbecewbdeeejbfege

1084 skill_dir = write_distribution_skill( 2tbbecewbdeeejbfege

1085 site_packages, 

1086 dist_name="demo-pkg", 

1087 package_dir="demo_pkg", 

1088 skill_name="demo-skill", 

1089 ) 

1090 monkeypatch.chdir(project) 2tbbecewbdeeejbfege

1091 

1092 with ( 2jb

1093 patch.object(cli, "_select_skills_interactive", lambda skills: skills), 

1094 patch.object( 

1095 cli, 

1096 "_select_targets_interactive", 

1097 lambda project_root, default_targets: [ 

1098 cli.InstallTarget( 

1099 "claude-compatible", project_root / ".claude" / "skills" 

1100 ) 

1101 ], 

1102 ), 

1103 ): 

1104 result = runner.invoke(app, ["install"]) 2tbbecewbdeeejbfege

1105 

1106 agents_skill = project / ".agents" / "skills" / "demo-skill" 2tbbecewbdeeejbfege

1107 claude_skill = project / ".claude" / "skills" / "demo-skill" 2tbbecewbdeeejbfege

1108 assert result.exit_code == 0 2tbbecewbdeeejbfege

1109 assert not agents_skill.exists() 2tbbecewbdeeejbfege

1110 assert claude_skill.is_symlink() 2tbbecewbdeeejbfege

1111 assert claude_skill.resolve() == skill_dir.resolve() 2tbbecewbdeeejbfege

1112 

1113 

1114def test_install_interactive_can_choose_both_targets(tmp_path, monkeypatch): 1abcdefghi

1115 project = write_project(tmp_path, dependencies=["demo-pkg>=1"]) 2} Zd0ddb1d2d@ 3d4d

1116 site_packages = project / ".venv" / "lib" / "python3.12" / "site-packages" 2} Zd0ddb1d2d@ 3d4d

1117 skill_dir = write_distribution_skill( 2} Zd0ddb1d2d@ 3d4d

1118 site_packages, 

1119 dist_name="demo-pkg", 

1120 package_dir="demo_pkg", 

1121 skill_name="demo-skill", 

1122 ) 

1123 monkeypatch.chdir(project) 2} Zd0ddb1d2d@ 3d4d

1124 

1125 with ( 1@

1126 patch.object(cli, "_select_skills_interactive", lambda skills: skills), 

1127 patch.object( 

1128 cli, 

1129 "_select_targets_interactive", 

1130 lambda project_root, default_targets: cli.get_all_target_dirs(project_root), 

1131 ), 

1132 ): 

1133 result = runner.invoke(app, ["install"]) 2} Zd0ddb1d2d@ 3d4d

1134 

1135 agents_skill = project / ".agents" / "skills" / "demo-skill" 2} Zd0ddb1d2d@ 3d4d

1136 claude_skill = project / ".claude" / "skills" / "demo-skill" 2} Zd0ddb1d2d@ 3d4d

1137 assert result.exit_code == 0 2} Zd0ddb1d2d@ 3d4d

1138 assert agents_skill.is_symlink() 2} Zd0ddb1d2d@ 3d4d

1139 assert agents_skill.resolve() == skill_dir.resolve() 2} Zd0ddb1d2d@ 3d4d

1140 assert claude_skill.is_symlink() 2} Zd0ddb1d2d@ 3d4d

1141 assert claude_skill.resolve() == skill_dir.resolve() 2} Zd0ddb1d2d@ 3d4d

1142 

1143 

1144def test_install_with_no_selected_targets_cancels(tmp_path, monkeypatch): 1abcdefghi

1145 project = write_project(tmp_path, dependencies=["demo-pkg>=1"]) 2Vb6e7eZb8e9e2b!e#e

1146 site_packages = project / ".venv" / "lib" / "python3.12" / "site-packages" 2Vb6e7eZb8e9e2b!e#e

1147 write_distribution_skill( 2Vb6e7eZb8e9e2b!e#e

1148 site_packages, 

1149 dist_name="demo-pkg", 

1150 package_dir="demo_pkg", 

1151 skill_name="demo-skill", 

1152 ) 

1153 monkeypatch.chdir(project) 2Vb6e7eZb8e9e2b!e#e

1154 

1155 with patch.object( 2Vb6e7eZb8e9e2b!e#e

1156 cli, "_select_targets_interactive", lambda project_root, default_targets: [] 

1157 ): 

1158 result = runner.invoke(app, ["install", "--all"]) 2Vb6e7eZb8e9e2b!e#e

1159 

1160 assert result.exit_code == 0 2Vb6e7eZb8e9e2b!e#e

1161 assert "No installation targets selected." in result.output 2Vb6e7eZb8e9e2b!e#e

1162 assert not (project / ".agents" / "skills" / "demo-skill").exists() 2Vb6e7eZb8e9e2b!e#e

1163 

1164 

1165def test_default_sync_with_no_selected_targets_cancels(tmp_path, monkeypatch): 1abcdefghi

1166 project = write_project(tmp_path, dependencies=["demo-pkg>=1"]) 2Sb$e%eWb'e(e0b)e*e

1167 site_packages = project / ".venv" / "lib" / "python3.12" / "site-packages" 2Sb$e%eWb'e(e0b)e*e

1168 write_distribution_skill( 2Sb$e%eWb'e(e0b)e*e

1169 site_packages, 

1170 dist_name="demo-pkg", 

1171 package_dir="demo_pkg", 

1172 skill_name="demo-skill", 

1173 ) 

1174 monkeypatch.chdir(project) 2Sb$e%eWb'e(e0b)e*e

1175 

1176 with patch.object( 2Sb$e%eWb'e(e0b)e*e

1177 cli, "_select_targets_interactive", lambda project_root, default_targets: [] 

1178 ): 

1179 result = runner.invoke(app, ["--all"]) 2Sb$e%eWb'e(e0b)e*e

1180 

1181 assert result.exit_code == 0 2Sb$e%eWb'e(e0b)e*e

1182 assert "No installation targets selected." in result.output 2Sb$e%eWb'e(e0b)e*e

1183 assert not (project / ".agents" / "skills" / "demo-skill").exists() 2Sb$e%eWb'e(e0b)e*e

1184 

1185 

1186def test_install_without_selection_and_yes_prints_no_skills_selected( 1abcdefghi

1187 tmp_path, 

1188 monkeypatch, 

1189): 

1190 project = write_project(tmp_path, dependencies=[]) 2nd[f]fsd^f_fxd`f{f

1191 monkeypatch.chdir(project) 2nd[f]fsd^f_fxd`f{f

1192 

1193 result = runner.invoke(app, ["install", "--yes"]) 2nd[f]fsd^f_fxd`f{f

1194 

1195 assert result.exit_code == 0 2nd[f]fsd^f_fxd`f{f

1196 assert "No skills selected." in result.output 2nd[f]fsd^f_fxd`f{f

1197 

1198 

1199def test_remove_named_symlink(tmp_path, monkeypatch): 1abcdefghi

1200 project = write_project(tmp_path, dependencies=["demo-pkg>=1"]) 2ab5d6dfb7d8dlb9d!d

1201 site_packages = project / ".venv" / "lib" / "python3.12" / "site-packages" 2ab5d6dfb7d8dlb9d!d

1202 skill_dir = write_distribution_skill( 2ab5d6dfb7d8dlb9d!d

1203 site_packages, 

1204 dist_name="demo-pkg", 

1205 package_dir="demo_pkg", 

1206 skill_name="demo-skill", 

1207 ) 

1208 installed_dir = project / ".agents" / "skills" 2ab5d6dfb7d8dlb9d!d

1209 installed_dir.mkdir(parents=True) 2ab5d6dfb7d8dlb9d!d

1210 installed = installed_dir / "demo-skill" 2ab5d6dfb7d8dlb9d!d

1211 installed.symlink_to(skill_dir, target_is_directory=True) 2ab5d6dfb7d8dlb9d!d

1212 monkeypatch.chdir(project) 2ab5d6dfb7d8dlb9d!d

1213 

1214 result = runner.invoke(app, ["remove", "demo-skill"]) 2ab5d6dfb7d8dlb9d!d

1215 

1216 assert result.exit_code == 0 2ab5d6dfb7d8dlb9d!d

1217 assert "Removed:" in result.output 2ab5d6dfb7d8dlb9d!d

1218 assert not installed.exists() 2ab5d6dfb7d8dlb9d!d

1219 

1220 

1221def test_list_installed_includes_existing_claude_target_without_flag( 1abcdefghi

1222 tmp_path, monkeypatch 

1223): 

1224 project = write_project(tmp_path, dependencies=["demo-pkg>=1"]) 2) _c`c/ {c|c[ }c~c

1225 site_packages = project / ".venv" / "lib" / "python3.12" / "site-packages" 2) _c`c/ {c|c[ }c~c

1226 skill_dir = write_distribution_skill( 2) _c`c/ {c|c[ }c~c

1227 site_packages, 

1228 dist_name="demo-pkg", 

1229 package_dir="demo_pkg", 

1230 skill_name="demo-skill", 

1231 ) 

1232 claude_dir = project / ".claude" / "skills" 2) _c`c/ {c|c[ }c~c

1233 claude_dir.mkdir(parents=True) 2) _c`c/ {c|c[ }c~c

1234 (claude_dir / "demo-skill").symlink_to(skill_dir, target_is_directory=True) 2) _c`c/ {c|c[ }c~c

1235 monkeypatch.chdir(project) 2) _c`c/ {c|c[ }c~c

1236 

1237 with patch.object(cli, "console", Console(width=1000)): 2) _c`c/ {c|c[ }c~c

1238 result = runner.invoke(app, ["list", "--installed"]) 2) _c`c/ {c|c[ }c~c

1239 

1240 assert result.exit_code == 0 2) _c`c/ {c|c[ }c~c

1241 assert "claude-compatible" in result.output 2) _c`c/ {c|c[ }c~c

1242 assert ".claude" in result.output 2) _c`c/ {c|c[ }c~c

1243 assert "up to date" in result.output 2) _c`c/ {c|c[ }c~c

1244 

1245 

1246def test_remove_includes_existing_claude_target_without_flag(tmp_path, monkeypatch): 1abcdefghi

1247 project = write_project(tmp_path, dependencies=["demo-pkg>=1"]) 2, adbd= cddd_ edfd

1248 site_packages = project / ".venv" / "lib" / "python3.12" / "site-packages" 2, adbd= cddd_ edfd

1249 skill_dir = write_distribution_skill( 2, adbd= cddd_ edfd

1250 site_packages, 

1251 dist_name="demo-pkg", 

1252 package_dir="demo_pkg", 

1253 skill_name="demo-skill", 

1254 ) 

1255 claude_dir = project / ".claude" / "skills" 2, adbd= cddd_ edfd

1256 claude_dir.mkdir(parents=True) 2, adbd= cddd_ edfd

1257 installed = claude_dir / "demo-skill" 2, adbd= cddd_ edfd

1258 installed.symlink_to(skill_dir, target_is_directory=True) 2, adbd= cddd_ edfd

1259 monkeypatch.chdir(project) 2, adbd= cddd_ edfd

1260 

1261 result = runner.invoke(app, ["remove", "demo-skill"]) 2, adbd= cddd_ edfd

1262 

1263 assert result.exit_code == 0 2, adbd= cddd_ edfd

1264 assert "Removed:" in result.output 2, adbd= cddd_ edfd

1265 assert "claude-compatible" in result.output 2, adbd= cddd_ edfd

1266 assert not installed.exists() 2, adbd= cddd_ edfd

1267 

1268 

1269def test_list_installed_and_remove_do_not_create_missing_target_dirs( 1abcdefghi

1270 tmp_path, monkeypatch 

1271): 

1272 project = write_project(tmp_path, dependencies=[]) 2hc+e,ekc-e.enc/e:e

1273 (project / ".claude").mkdir() 2hc+e,ekc-e.enc/e:e

1274 monkeypatch.chdir(project) 2hc+e,ekc-e.enc/e:e

1275 

1276 list_result = runner.invoke(app, ["list", "--installed"]) 2hc+e,ekc-e.enc/e:e

1277 remove_result = runner.invoke(app, ["remove", "--yes"]) 2hc+e,ekc-e.enc/e:e

1278 

1279 assert list_result.exit_code == 0 2hc+e,ekc-e.enc/e:e

1280 assert remove_result.exit_code == 0 2hc+e,ekc-e.enc/e:e

1281 assert not (project / ".agents" / "skills").exists() 2hc+e,ekc-e.enc/e:e

1282 assert not (project / ".claude" / "skills").exists() 2hc+e,ekc-e.enc/e:e

1283 

1284 

1285def test_remove_with_yes_and_no_names_does_not_prompt(tmp_path, monkeypatch): 1abcdefghi

1286 project = write_project(tmp_path, dependencies=[]) 2qd|f}fvd~fagAdbgcg

1287 monkeypatch.chdir(project) 2qd|f}fvd~fagAdbgcg

1288 

1289 result = runner.invoke(app, ["remove", "--yes"]) 2qd|f}fvd~fagAdbgcg

1290 

1291 assert result.exit_code == 0 2qd|f}fvd~fagAdbgcg

1292 assert "No skills selected." in result.output 2qd|f}fvd~fagAdbgcg

1293 

1294 

1295def test_check_exits_non_zero_for_colliding_skill_names(tmp_path, monkeypatch): 1abcdefghi

1296 project = write_project(tmp_path, dependencies=["first-pkg>=1", "second-pkg>=1"]) 2$bAfBf'bCfDf)bEfFf

1297 site_packages = project / ".venv" / "lib" / "python3.12" / "site-packages" 2$bAfBf'bCfDf)bEfFf

1298 write_distribution_skill( 2$bAfBf'bCfDf)bEfFf

1299 site_packages, 

1300 dist_name="first-pkg", 

1301 package_dir="first_pkg", 

1302 skill_name="duplicate-skill", 

1303 ) 

1304 write_distribution_skill( 2$bAfBf'bCfDf)bEfFf

1305 site_packages, 

1306 dist_name="second-pkg", 

1307 package_dir="second_pkg", 

1308 skill_name="duplicate-skill", 

1309 ) 

1310 monkeypatch.chdir(project) 2$bAfBf'bCfDf)bEfFf

1311 

1312 result = runner.invoke(app, ["--check"]) 2$bAfBf'bCfDf)bEfFf

1313 

1314 assert result.exit_code == 1 2$bAfBf'bCfDf)bEfFf

1315 

1316 

1317def test_scan_without_target_environment_prints_warning(tmp_path, monkeypatch): 1abcdefghi

1318 project = tmp_path / "project" 2heiejekelemeneoepe

1319 project.mkdir() 2heiejekelemeneoepe

1320 monkeypatch.chdir(project) 2heiejekelemeneoepe

1321 monkeypatch.delenv("UV_PROJECT_ENVIRONMENT", raising=False) 2heiejekelemeneoepe

1322 monkeypatch.delenv("VIRTUAL_ENV", raising=False) 2heiejekelemeneoepe

1323 monkeypatch.delenv("CONDA_PREFIX", raising=False) 2heiejekelemeneoepe

1324 

1325 result = runner.invoke(app, ["scan"]) 2heiejekelemeneoepe

1326 

1327 assert result.exit_code == 0 2heiejekelemeneoepe

1328 assert "Run from a project root after installing dependencies" in result.output 2heiejekelemeneoepe

1329 assert "uv sync" in result.output 2heiejekelemeneoepe

1330 assert "npm install" in result.output 2heiejekelemeneoepe

1331 

1332 

1333def test_default_check_without_environment_reports_no_discovered_skills( 1abcdefghi

1334 tmp_path, 

1335 monkeypatch, 

1336): 

1337 project = tmp_path / "project" 2;e=e?e@e[e]e^e_e`e

1338 project.mkdir() 2;e=e?e@e[e]e^e_e`e

1339 monkeypatch.chdir(project) 2;e=e?e@e[e]e^e_e`e

1340 monkeypatch.delenv("UV_PROJECT_ENVIRONMENT", raising=False) 2;e=e?e@e[e]e^e_e`e

1341 monkeypatch.delenv("VIRTUAL_ENV", raising=False) 2;e=e?e@e[e]e^e_e`e

1342 monkeypatch.delenv("CONDA_PREFIX", raising=False) 2;e=e?e@e[e]e^e_e`e

1343 

1344 result = runner.invoke(app, ["--check"]) 2;e=e?e@e[e]e^e_e`e

1345 

1346 assert result.exit_code == 0 2;e=e?e@e[e]e^e_e`e

1347 assert "No installed or discovered skills found." in result.output 2;e=e?e@e[e]e^e_e`e

1348 

1349 

1350def test_list_without_installed_option_prints_discovered_skills(tmp_path, monkeypatch): 1abcdefghi

1351 project = write_project(tmp_path, dependencies=["demo-pkg>=1"]) 2%bGfHf(bIfJf*bKfLf

1352 site_packages = project / ".venv" / "lib" / "python3.12" / "site-packages" 2%bGfHf(bIfJf*bKfLf

1353 write_distribution_skill( 2%bGfHf(bIfJf*bKfLf

1354 site_packages, 

1355 dist_name="demo-pkg", 

1356 package_dir="demo_pkg", 

1357 skill_name="demo-skill", 

1358 ) 

1359 monkeypatch.chdir(project) 2%bGfHf(bIfJf*bKfLf

1360 

1361 result = runner.invoke(app, ["list"]) 2%bGfHf(bIfJf*bKfLf

1362 

1363 assert result.exit_code == 0 2%bGfHf(bIfJf*bKfLf

1364 assert "demo-skill" in result.output 2%bGfHf(bIfJf*bKfLf

1365 

1366 

1367def test_install_interactive_selection_installs_selected_skill( 1abcdefghi

1368 tmp_path, 

1369 monkeypatch, 

1370): 

1371 project = write_project(tmp_path, dependencies=["demo-pkg>=1"]) 2Tb{e|eXb}e~eQbafbf

1372 site_packages = project / ".venv" / "lib" / "python3.12" / "site-packages" 2Tb{e|eXb}e~eQbafbf

1373 skill_dir = write_distribution_skill( 2Tb{e|eXb}e~eQbafbf

1374 site_packages, 

1375 dist_name="demo-pkg", 

1376 package_dir="demo_pkg", 

1377 skill_name="demo-skill", 

1378 ) 

1379 monkeypatch.chdir(project) 2Tb{e|eXb}e~eQbafbf

1380 

1381 with ( 2Qb

1382 patch.object(cli, "_select_skills_interactive", lambda skills: skills), 

1383 patch.object( 

1384 cli, 

1385 "_select_targets_interactive", 

1386 lambda project_root, default_targets: default_targets, 

1387 ), 

1388 ): 

1389 result = runner.invoke(app, ["install"]) 2Tb{e|eXb}e~eQbafbf

1390 

1391 installed = project / ".agents" / "skills" / "demo-skill" 2Tb{e|eXb}e~eQbafbf

1392 assert result.exit_code == 0 2Tb{e|eXb}e~eQbafbf

1393 assert installed.is_symlink() 2Tb{e|eXb}e~eQbafbf

1394 assert installed.resolve() == skill_dir.resolve() 2Tb{e|eXb}e~eQbafbf

1395 

1396 

1397def test_default_command_interactive_selection_installs_selected_skill( 1abcdefghi

1398 tmp_path, 

1399 monkeypatch, 

1400): 

1401 project = write_project(tmp_path, dependencies=["demo-pkg>=1"]) 2( gdhd. idjd8 kdld

1402 site_packages = project / ".venv" / "lib" / "python3.12" / "site-packages" 2( gdhd. idjd8 kdld

1403 skill_dir = write_distribution_skill( 2( gdhd. idjd8 kdld

1404 site_packages, 

1405 dist_name="demo-pkg", 

1406 package_dir="demo_pkg", 

1407 skill_name="demo-skill", 

1408 ) 

1409 monkeypatch.chdir(project) 2( gdhd. idjd8 kdld

1410 

1411 with ( 18

1412 patch.object(cli, "_select_skills_interactive", lambda skills: skills), 

1413 patch.object( 

1414 cli, 

1415 "_select_targets_interactive", 

1416 lambda project_root, default_targets: default_targets, 

1417 ), 

1418 patch.object(cli, "_select_tool_skill_interactive", lambda: True), 

1419 ): 

1420 result = runner.invoke(app) 2( gdhd. idjd8 kdld

1421 

1422 installed = project / ".agents" / "skills" / "demo-skill" 2( gdhd. idjd8 kdld

1423 tool_skill = project / ".agents" / "skills" / TOOL_SKILL_NAME 2( gdhd. idjd8 kdld

1424 assert result.exit_code == 0 2( gdhd. idjd8 kdld

1425 assert "Installed 1 skill target(s)." in result.output 2( gdhd. idjd8 kdld

1426 assert "can be committed to Git" in result.output 2( gdhd. idjd8 kdld

1427 assert installed.is_symlink() 2( gdhd. idjd8 kdld

1428 assert installed.resolve() == skill_dir.resolve() 2( gdhd. idjd8 kdld

1429 assert tool_skill.joinpath("SKILL.md").is_file() 2( gdhd. idjd8 kdld

1430 

1431 

1432def test_default_command_copy_installs_selected_skill_as_directory( 1abcdefghi

1433 tmp_path, 

1434 monkeypatch, 

1435): 

1436 project = write_project(tmp_path, dependencies=["demo-pkg>=1"]) 2sbqerevbsetegbueve

1437 site_packages = project / ".venv" / "lib" / "python3.12" / "site-packages" 2sbqerevbsetegbueve

1438 write_distribution_skill( 2sbqerevbsetegbueve

1439 site_packages, 

1440 dist_name="demo-pkg", 

1441 package_dir="demo_pkg", 

1442 skill_name="demo-skill", 

1443 ) 

1444 monkeypatch.chdir(project) 2sbqerevbsetegbueve

1445 

1446 with ( 2gb

1447 patch.object(cli, "_select_skills_interactive", lambda skills: skills), 

1448 patch.object( 

1449 cli, 

1450 "_select_targets_interactive", 

1451 lambda project_root, default_targets: default_targets, 

1452 ), 

1453 patch.object(cli, "_select_tool_skill_interactive", lambda: False), 

1454 ): 

1455 result = runner.invoke(app, ["--copy"]) 2sbqerevbsetegbueve

1456 

1457 installed = project / ".agents" / "skills" / "demo-skill" 2sbqerevbsetegbueve

1458 assert result.exit_code == 0 2sbqerevbsetegbueve

1459 assert "Installed 1 skill target(s)." in result.output 2sbqerevbsetegbueve

1460 assert "can be committed to Git" not in result.output 2sbqerevbsetegbueve

1461 assert installed.is_dir() 2sbqerevbsetegbueve

1462 assert not installed.is_symlink() 2sbqerevbsetegbueve

1463 

1464 

1465def test_default_command_deduplicates_new_skills_across_targets( 1abcdefghi

1466 tmp_path, 

1467 monkeypatch, 

1468): 

1469 project = write_project(tmp_path, dependencies=["demo-pkg>=1"]) 23 GcHc4 IcJcT KcLc

1470 (project / ".agents").mkdir() 23 GcHc4 IcJcT KcLc

1471 (project / ".claude").mkdir() 23 GcHc4 IcJcT KcLc

1472 site_packages = project / ".venv" / "lib" / "python3.12" / "site-packages" 23 GcHc4 IcJcT KcLc

1473 skill_dir = write_distribution_skill( 23 GcHc4 IcJcT KcLc

1474 site_packages, 

1475 dist_name="demo-pkg", 

1476 package_dir="demo_pkg", 

1477 skill_name="demo-skill", 

1478 ) 

1479 prompt_counts: list[int] = [] 23 GcHc4 IcJcT KcLc

1480 monkeypatch.chdir(project) 23 GcHc4 IcJcT KcLc

1481 

1482 def select_skills(skills: list[cli.Skill]) -> list[cli.Skill]: 23 GcHc4 IcJcT KcLc

1483 prompt_counts.append(len(skills)) 23 GcHc4 IcJcT KcLc

1484 return skills 23 GcHc4 IcJcT KcLc

1485 

1486 with ( 1T

1487 patch.object(cli, "_select_skills_interactive", select_skills), 

1488 patch.object( 

1489 cli, 

1490 "_select_targets_interactive", 

1491 lambda project_root, default_targets: default_targets, 

1492 ), 

1493 patch.object(cli, "_select_tool_skill_interactive", lambda: False), 

1494 ): 

1495 result = runner.invoke(app) 23 GcHc4 IcJcT KcLc

1496 

1497 assert result.exit_code == 0 23 GcHc4 IcJcT KcLc

1498 assert prompt_counts == [1] 23 GcHc4 IcJcT KcLc

1499 assert (project / ".agents" / "skills" / "demo-skill").resolve() == skill_dir 23 GcHc4 IcJcT KcLc

1500 assert (project / ".claude" / "skills" / "demo-skill").resolve() == skill_dir 23 GcHc4 IcJcT KcLc

1501 

1502 

1503def test_remove_interactive_selection_removes_selected_skill(tmp_path, monkeypatch): 1abcdefghi

1504 project = write_project(tmp_path, dependencies=["demo-pkg>=1"]) 2~ #d$deb%d'dkb(d)d

1505 site_packages = project / ".venv" / "lib" / "python3.12" / "site-packages" 2~ #d$deb%d'dkb(d)d

1506 skill_dir = write_distribution_skill( 2~ #d$deb%d'dkb(d)d

1507 site_packages, 

1508 dist_name="demo-pkg", 

1509 package_dir="demo_pkg", 

1510 skill_name="demo-skill", 

1511 ) 

1512 installed_dir = project / ".agents" / "skills" 2~ #d$deb%d'dkb(d)d

1513 installed_dir.mkdir(parents=True) 2~ #d$deb%d'dkb(d)d

1514 installed = installed_dir / "demo-skill" 2~ #d$deb%d'dkb(d)d

1515 installed.symlink_to(skill_dir, target_is_directory=True) 2~ #d$deb%d'dkb(d)d

1516 monkeypatch.chdir(project) 2~ #d$deb%d'dkb(d)d

1517 

1518 with patch.object( 2~ #d$deb%d'dkb(d)d

1519 cli, "_select_installed_skills_interactive", lambda statuses: statuses 

1520 ): 

1521 result = runner.invoke(app, ["remove"]) 2~ #d$deb%d'dkb(d)d

1522 

1523 assert result.exit_code == 0 2~ #d$deb%d'dkb(d)d

1524 assert not installed.exists() 2~ #d$deb%d'dkb(d)d

1525 

1526 

1527def test_remove_prints_not_found_when_uninstall_returns_false(tmp_path, monkeypatch): 1abcdefghi

1528 project = write_project(tmp_path, dependencies=["demo-pkg>=1"]) 2ubwexexbyezeybAeBe

1529 site_packages = project / ".venv" / "lib" / "python3.12" / "site-packages" 2ubwexexbyezeybAeBe

1530 skill_dir = write_distribution_skill( 2ubwexexbyezeybAeBe

1531 site_packages, 

1532 dist_name="demo-pkg", 

1533 package_dir="demo_pkg", 

1534 skill_name="demo-skill", 

1535 ) 

1536 installed_dir = project / ".agents" / "skills" 2ubwexexbyezeybAeBe

1537 installed_dir.mkdir(parents=True) 2ubwexexbyezeybAeBe

1538 (installed_dir / "demo-skill").symlink_to(skill_dir, target_is_directory=True) 2ubwexexbyezeybAeBe

1539 monkeypatch.chdir(project) 2ubwexexbyezeybAeBe

1540 

1541 with patch.object(cli, "uninstall_skill", lambda skill_name, target_dir: False): 2ubwexexbyezeybAeBe

1542 result = runner.invoke(app, ["remove", "demo-skill"]) 2ubwexexbyezeybAeBe

1543 

1544 assert result.exit_code == 0 2ubwexexbyezeybAeBe

1545 assert "Not found:" in result.output 2ubwexexbyezeybAeBe

1546 

1547 

1548def test_installed_statuses_classifies_name_mismatch_outdated_and_orphaned(tmp_path): 1abcdefghi

1549 skill = make_cli_skill(tmp_path) 23b4b5b6b7b8b9b!b#b

1550 target = cli.InstallTarget("universal", tmp_path / ".agents" / "skills") 23b4b5b6b7b8b9b!b#b

1551 target.path.mkdir(parents=True) 23b4b5b6b7b8b9b!b#b

1552 (target.path / "wrong-name").symlink_to(skill.skill_dir, target_is_directory=True) 23b4b5b6b7b8b9b!b#b

1553 old_target = tmp_path / "old-target" 23b4b5b6b7b8b9b!b#b

1554 old_target.mkdir() 23b4b5b6b7b8b9b!b#b

1555 (target.path / skill.name).symlink_to(old_target, target_is_directory=True) 23b4b5b6b7b8b9b!b#b

1556 (target.path / "broken-skill").symlink_to( 23b4b5b6b7b8b9b!b#b

1557 tmp_path / "missing-target", target_is_directory=True 

1558 ) 

1559 orphan_target = tmp_path / "orphan-target" 23b4b5b6b7b8b9b!b#b

1560 orphan_target.mkdir() 23b4b5b6b7b8b9b!b#b

1561 (target.path / "orphan-skill").symlink_to(orphan_target, target_is_directory=True) 23b4b5b6b7b8b9b!b#b

1562 hand_authored = target.path / "hand-authored" 23b4b5b6b7b8b9b!b#b

1563 hand_authored.mkdir() 23b4b5b6b7b8b9b!b#b

1564 

1565 statuses = { 23b4b5b6b7b8b9b!b#b

1566 status.name: status.status 

1567 for status in cli._installed_statuses(targets=[target], skills=[skill]) 

1568 } 

1569 

1570 assert statuses["wrong-name"] == "name mismatch" 23b4b5b6b7b8b9b!b#b

1571 assert statuses[skill.name] == "outdated" 23b4b5b6b7b8b9b!b#b

1572 assert statuses["broken-skill"] == "broken" 23b4b5b6b7b8b9b!b#b

1573 assert statuses["orphan-skill"] == "orphaned" 23b4b5b6b7b8b9b!b#b

1574 assert statuses["hand-authored"] == "hand-authored" 23b4b5b6b7b8b9b!b#b

1575 

1576 

1577def test_filter_installable_skills_skips_collisions(tmp_path): 1abcdefghi

1578 first = make_cli_skill(tmp_path / "first", name="duplicate-skill") 2IedgegKefgggMehgig

1579 second = make_cli_skill(tmp_path / "second", name="duplicate-skill") 2IedgegKefgggMehgig

1580 

1581 selected = cli._filter_installable_skills( 2IedgegKefgggMehgig

1582 [first, second], 

1583 selected_names=[], 

1584 include_all=True, 

1585 ) 

1586 

1587 assert selected == [] 2IedgegKefgggMehgig

1588 

1589 

1590def test_select_helpers_use_rich_toolkit(monkeypatch, tmp_path, capsys): 1abcdefghi

1591 skill = make_cli_skill(tmp_path) 2cc?d@ddc[d]dec^d_d

1592 target = cli.InstallTarget("universal", tmp_path / ".agents" / "skills") 2cc?d@ddc[d]dec^d_d

1593 status = cli.InstalledStatus( 2cc?d@ddc[d]dec^d_d

1594 target=target, 

1595 name=skill.name, 

1596 type="symlink", 

1597 path=target.path / skill.name, 

1598 target_path=skill.skill_dir, 

1599 status="up to date", 

1600 skill=skill, 

1601 ) 

1602 monkeypatch.setattr(cli, "_get_rich_toolkit", lambda: FakeToolkit([skill])) 2cc?d@ddc[d]dec^d_d

1603 

1604 assert cli._select_skills_interactive([skill]) == [skill] 2cc?d@ddc[d]dec^d_d

1605 assert "install" in capsys.readouterr().out 2cc?d@ddc[d]dec^d_d

1606 

1607 monkeypatch.setattr(cli, "_get_rich_toolkit", lambda: FakeToolkit([status])) 2cc?d@ddc[d]dec^d_d

1608 

1609 assert cli._select_installed_skills_interactive([status]) == [status] 2cc?d@ddc[d]dec^d_d

1610 assert ( 2cc?d@ddc[d]dec^d_d

1611 cli._select_installed_skills_interactive( 

1612 [ 

1613 cli.InstalledStatus( 

1614 target, 

1615 "dir", 

1616 "directory", 

1617 target.path / "dir", 

1618 None, 

1619 "hand-authored", 

1620 ) 

1621 ] 

1622 ) 

1623 == [] 

1624 ) 

1625 

1626 

1627def test_reconcile_helpers_cover_selection_skip_and_not_found( 1abcdefghi

1628 monkeypatch, tmp_path, capsys 

1629): 

1630 skill = make_cli_skill(tmp_path) 20 IbJb1 KbLb2 MbNb

1631 target = cli.InstallTarget("universal", tmp_path / ".agents" / "skills") 20 IbJb1 KbLb2 MbNb

1632 target.path.mkdir(parents=True) 20 IbJb1 KbLb2 MbNb

1633 status = cli.InstalledStatus( 20 IbJb1 KbLb2 MbNb

1634 target=target, 

1635 name=skill.name, 

1636 type="symlink", 

1637 path=target.path / skill.name, 

1638 target_path=skill.skill_dir, 

1639 status="broken", 

1640 skill=skill, 

1641 ) 

1642 

1643 class FakeToolkitMenu: 20 IbJb1 KbLb2 MbNb

1644 style = None 20 IbJb1 KbLb2 MbNb

1645 console = None 20 IbJb1 KbLb2 MbNb

1646 

1647 class FakeMenu: 20 IbJb1 KbLb2 MbNb

1648 def __init__(self, label, *, options, **kwargs): 20 IbJb1 KbLb2 MbNb

1649 assert "repair" in label 20 IbJb1 KbLb2 MbNb

1650 assert kwargs["multiple"] is True 20 IbJb1 KbLb2 MbNb

1651 assert options[0]["name"] == "demo-skill [Agents]" 20 IbJb1 KbLb2 MbNb

1652 self.options = options 20 IbJb1 KbLb2 MbNb

1653 self.checked = set() 20 IbJb1 KbLb2 MbNb

1654 

1655 def ask(self): 20 IbJb1 KbLb2 MbNb

1656 assert self.checked == {0} 20 IbJb1 KbLb2 MbNb

1657 return [self.options[index]["value"] for index in sorted(self.checked)] 20 IbJb1 KbLb2 MbNb

1658 

1659 monkeypatch.setattr(cli, "_get_rich_toolkit", lambda: FakeToolkitMenu()) 20 IbJb1 KbLb2 MbNb

1660 monkeypatch.setattr(cli, "Menu", FakeMenu) 20 IbJb1 KbLb2 MbNb

1661 

1662 assert cli._select_statuses_interactive([status], action="repair") == [status] 20 IbJb1 KbLb2 MbNb

1663 assert "repair" in capsys.readouterr().out 20 IbJb1 KbLb2 MbNb

1664 assert cli._select_statuses_interactive([], action="repair") == [] 20 IbJb1 KbLb2 MbNb

1665 

1666 skipped = cli.InstalledStatus( 20 IbJb1 KbLb2 MbNb

1667 target=target, 

1668 name="missing-skill", 

1669 type="symlink", 

1670 path=target.path / "missing-skill", 

1671 target_path=None, 

1672 status="broken", 

1673 skill=None, 

1674 ) 

1675 assert cli._repair_selected(statuses=[skipped], project_root=tmp_path) == 0 20 IbJb1 KbLb2 MbNb

1676 

1677 monkeypatch.setattr(cli, "uninstall_skill", lambda skill_name, target_dir: False) 20 IbJb1 KbLb2 MbNb

1678 assert cli._remove_selected(statuses=[status], project_root=tmp_path) == 0 20 IbJb1 KbLb2 MbNb

1679 assert "Not found:" in capsys.readouterr().out 20 IbJb1 KbLb2 MbNb

1680 

1681 

1682def test_select_targets_interactive_preselects_defaults(monkeypatch, tmp_path): 1abcdefghi

1683 default_target = cli.InstallTarget( 2*d+d,d-d.d/d:d;d=d

1684 "claude-compatible", tmp_path / ".claude" / "skills" 

1685 ) 

1686 

1687 class FakeMenu: 2*d+d,d-d.d/d:d;d=d

1688 def __init__(self, _label, *, options, **_kwargs): 2*d+d,d-d.d/d:d;d=d

1689 assert [option["name"] for option in options] == [ 2*d+d,d-d.d/d:d;d=d

1690 "Agents (.agents/skills)", 

1691 "Claude Code (.claude/skills)", 

1692 ] 

1693 self.options = options 2*d+d,d-d.d/d:d;d=d

1694 self.checked = set() 2*d+d,d-d.d/d:d;d=d

1695 

1696 def ask(self): 2*d+d,d-d.d/d:d;d=d

1697 assert self.checked == {1} 2*d+d,d-d.d/d:d;d=d

1698 return [self.options[index]["value"] for index in sorted(self.checked)] 2*d+d,d-d.d/d:d;d=d

1699 

1700 monkeypatch.setattr(cli, "Menu", FakeMenu) 2*d+d,d-d.d/d:d;d=d

1701 

1702 selected = cli._select_targets_interactive( 2*d+d,d-d.d/d:d;d=d

1703 project_root=tmp_path, default_targets=[default_target] 

1704 ) 

1705 

1706 assert [target.name for target in selected] == ["claude-compatible"] 2*d+d,d-d.d/d:d;d=d

1707 

1708 

1709def test_select_tool_skill_interactive_uses_checked_menu(monkeypatch): 1abcdefghi

1710 class FakeToolkitMenu: 2McNcOcPcQcRcScTcUc

1711 style = None 2McNcOcPcQcRcScTcUc

1712 console = None 2McNcOcPcQcRcScTcUc

1713 

1714 class FakeMenu: 2McNcOcPcQcRcScTcUc

1715 def __init__(self, label, *, options, **kwargs): 2McNcOcPcQcRcScTcUc

1716 assert "Library Skills tool skill" in label 2McNcOcPcQcRcScTcUc

1717 assert kwargs["multiple"] is True 2McNcOcPcQcRcScTcUc

1718 self.options = options 2McNcOcPcQcRcScTcUc

1719 self.checked = set() 2McNcOcPcQcRcScTcUc

1720 

1721 def ask(self): 2McNcOcPcQcRcScTcUc

1722 assert self.checked == {0} 2McNcOcPcQcRcScTcUc

1723 return [self.options[index]["value"] for index in sorted(self.checked)] 2McNcOcPcQcRcScTcUc

1724 

1725 monkeypatch.setattr(cli, "_get_rich_toolkit", lambda: FakeToolkitMenu()) 2McNcOcPcQcRcScTcUc

1726 monkeypatch.setattr(cli, "Menu", FakeMenu) 2McNcOcPcQcRcScTcUc

1727 

1728 assert cli._select_tool_skill_interactive() is True 2McNcOcPcQcRcScTcUc

1729 

1730 

1731def test_get_rich_toolkit_returns_toolkit_instance(): 1abcdefghi

1732 assert cli._get_rich_toolkit() is not None 2ygzgAgBgCgDgEgFgGg

1733 

1734 

1735def test_ui_badge_fallback_status_and_summary(capsys): 1abcdefghi

1736 assert ui._badge("unknown", "custom") == "[tag] custom [/]" 2MfNfOfPfQfRfSfTfUf

1737 assert ui._styled_status("custom") == "custom" 2MfNfOfPfQfRfSfTfUf

1738 

1739 ui.print_summary({"repaired": 1, "removed": 2}) 2MfNfOfPfQfRfSfTfUf

1740 

1741 output = capsys.readouterr().out 2MfNfOfPfQfRfSfTfUf

1742 assert "Summary:" in output 2MfNfOfPfQfRfSfTfUf

1743 assert "repaired: 1" in output 2MfNfOfPfQfRfSfTfUf

1744 assert "removed: 2" in output 2MfNfOfPfQfRfSfTfUf

1745 

1746 

1747def test_install_selected_continues_after_install_error(tmp_path): 1abcdefghi

1748 skill = make_cli_skill(tmp_path) 2JejgkgLelgmgNengog

1749 target = cli.InstallTarget("universal", tmp_path / ".agents" / "skills") 2JejgkgLelgmgNengog

1750 (target.path / skill.name).mkdir(parents=True) 2JejgkgLelgmgNengog

1751 

1752 assert ( 2JejgkgLelgmgNengog

1753 cli._install_selected(skills=[skill], targets=[target], project_root=tmp_path) 

1754 == 0 

1755 ) 

1756 

1757 

1758def test_display_path_prefers_project_relative_paths(tmp_path): 1abcdefghi

1759 project = tmp_path / "project" 2lfmfnfofpfqfrfsftf

1760 inside = project / ".agents" / "skills" / "demo" 2lfmfnfofpfqfrfsftf

1761 outside = tmp_path / "outside" 2lfmfnfofpfqfrfsftf

1762 inside.mkdir(parents=True) 2lfmfnfofpfqfrfsftf

1763 outside.mkdir() 2lfmfnfofpfqfrfsftf

1764 

1765 assert cli._display_path(inside, project) == str( 2lfmfnfofpfqfrfsftf

1766 Path(".agents") / "skills" / "demo" 

1767 ) 

1768 assert cli._display_path(outside, project) == str(outside) 2lfmfnfofpfqfrfsftf

1769 assert cli._display_path(None, project) == "" 2lfmfnfofpfqfrfsftf

1770 

1771 

1772def test_target_prompt_labels_are_user_facing(tmp_path): 1abcdefghi

1773 agents = cli.InstallTarget("universal", tmp_path / ".agents" / "skills") 2cfdfefffgfhfifjfkf

1774 claude = cli.InstallTarget("claude-compatible", tmp_path / ".claude" / "skills") 2cfdfefffgfhfifjfkf

1775 custom = cli.InstallTarget("custom", tmp_path / "custom") 2cfdfefffgfhfifjfkf

1776 

1777 assert cli._target_prompt_name(agents) == "Agents (.agents/skills)" 2cfdfefffgfhfifjfkf

1778 assert cli._target_prompt_name(claude) == "Claude Code (.claude/skills)" 2cfdfefffgfhfifjfkf

1779 assert cli._target_prompt_name(custom) == "custom" 2cfdfefffgfhfifjfkf

1780 assert cli._target_short_name(agents) == "Agents" 2cfdfefffgfhfifjfkf

1781 assert cli._target_short_name(claude) == "Claude Code" 2cfdfefffgfhfifjfkf

1782 assert cli._target_short_name(custom) == "custom" 2cfdfefffgfhfifjfkf

1783 

1784 

1785def test_main_module_invokes_cli_main(): 1abcdefghi

1786 import library_skills.cli as cli 2VfWfXfYfZf0f1f2f3f

1787 

1788 called = [] 2VfWfXfYfZf0f1f2f3f

1789 sys.modules.pop("library_skills.__main__", None) 2VfWfXfYfZf0f1f2f3f

1790 

1791 with patch.object(cli, "main", lambda: called.append(True)): 2VfWfXfYfZf0f1f2f3f

1792 importlib.import_module("library_skills.__main__") 2VfWfXfYfZf0f1f2f3f

1793 

1794 assert called == [True] 2VfWfXfYfZf0f1f2f3f

1795 sys.modules.pop("library_skills.__main__", None) 2VfWfXfYfZf0f1f2f3f

1796 

1797 

1798def test_main_invokes_typer_app(): 1abcdefghi

1799 called = [] 2pgqgrgsgtgugvgwgxg

1800 

1801 with patch.object(cli, "app", lambda: called.append(True)): 2pgqgrgsgtgugvgwgxg

1802 cli.main() 2pgqgrgsgtgugvgwgxg

1803 

1804 assert called == [True] 2pgqgrgsgtgugvgwgxg