1pub mod cli;
2pub mod discovery;
3pub mod embedded;
4pub mod installer;
5pub mod lock;
6pub mod providers;
7pub mod types;
8
9pub use cli::{get_command_schema, get_commands, output_commands_json};
10pub use discovery::{discover_skills, discover_skills_with_provider, DiscoveryConfig};
11pub use embedded::{get_embedded_skill, register_embedded_skill};
12pub use installer::{
13 install_skill, install_skill_with_provider, InstallConfig, InstallMode, InstallResult,
14};
15pub use lock::LockManager;
16pub use providers::{MockProvider, SkillProvider};
17pub use types::{Skill, SkillLock, Source, SourceType};
18
19#[cfg(test)]
20mod integration_tests {
21 use super::*;
22 use tempfile::TempDir;
23
24 #[test]
25 fn test_end_to_end_embedded_install() {
26 let temp_dir = TempDir::new().unwrap();
28 let canonical_dir = temp_dir.path().join(".agents/skills");
29 let lock_path = temp_dir.path().join(".agents/.skill-lock.json");
30
31 let source = Source {
33 source_type: SourceType::Self_,
34 url: None,
35 subpath: None,
36 skill_filter: None,
37 ref_: None,
38 };
39
40 let config = DiscoveryConfig::default();
42 let skills = discover_skills(&source, &config).unwrap();
43 assert_eq!(skills.len(), 1);
44 let skill = &skills[0];
45
46 let install_config = InstallConfig::new(canonical_dir.clone());
48 let result = install_skill(skill, &install_config).unwrap();
49
50 assert!(result.path.exists());
52 assert!(result.path.join("SKILL.md").exists());
53
54 let lock_manager = LockManager::new(lock_path);
56 lock_manager
57 .update_entry(&skill.name, &source, &result.path)
58 .unwrap();
59
60 let entry = lock_manager.get_entry(&skill.name).unwrap().unwrap();
62 assert_eq!(entry.source_type, "self");
63 assert!(!entry.skill_folder_hash.is_empty());
64 }
65
66 #[test]
67 fn test_self_and_embedded_are_equivalent() {
68 let config = DiscoveryConfig::default();
69
70 let json_self = r#"{"type":"self"}"#;
72 let source_self: Source = serde_json::from_str(json_self).unwrap();
73 let skills_self = discover_skills(&source_self, &config).unwrap();
74
75 let json_embedded = r#"{"type":"embedded"}"#;
77 let source_embedded: Source = serde_json::from_str(json_embedded).unwrap();
78 let skills_embedded = discover_skills(&source_embedded, &config).unwrap();
79
80 assert_eq!(skills_self.len(), skills_embedded.len());
82 assert_eq!(skills_self[0].name, skills_embedded[0].name);
83 assert_eq!(source_self.source_type, source_embedded.source_type);
84 }
85
86 #[test]
87 fn test_no_external_calls_for_embedded() {
88 let temp_dir = TempDir::new().unwrap();
90 let canonical_dir = temp_dir.path().join(".agents/skills");
91 let lock_path = temp_dir.path().join(".agents/.skill-lock.json");
92
93 let source = Source {
94 source_type: SourceType::Self_,
95 url: None,
96 subpath: None,
97 skill_filter: None,
98 ref_: None,
99 };
100
101 let config = DiscoveryConfig::default();
102 let skills = discover_skills(&source, &config).unwrap();
103
104 let install_config = InstallConfig::new(canonical_dir.clone());
105 let result = install_skill(&skills[0], &install_config).unwrap();
106
107 let lock_manager = LockManager::new(lock_path);
108 lock_manager
109 .update_entry(&skills[0].name, &source, &result.path)
110 .unwrap();
111
112 assert!(result.path.exists());
114 }
115
116 #[test]
117 fn test_end_to_end_embedded_install_with_aux_files() {
118 use std::collections::HashMap;
119 use types::SkillMetadata;
120
121 let temp_dir = TempDir::new().unwrap();
122 let canonical_dir = temp_dir.path().join(".agents/skills");
123
124 let mut auxiliary_files = HashMap::new();
126 auxiliary_files.insert(
127 "scripts/run.py".to_string(),
128 "#!/usr/bin/env python3\nprint('running')".to_string(),
129 );
130 auxiliary_files.insert(
131 "references/overview.md".to_string(),
132 "# Overview\nSkill overview.".to_string(),
133 );
134
135 let skill = Skill {
136 name: "embedded-multi-skill".to_string(),
137 description: "Multi-file embedded skill".to_string(),
138 path: None,
139 raw_content:
140 "---\nname: embedded-multi-skill\ndescription: Multi-file embedded skill\n---\n\n# Skill"
141 .to_string(),
142 metadata: SkillMetadata::default(),
143 auxiliary_files,
144 };
145
146 let install_config = InstallConfig::new(canonical_dir.clone());
148 let result = install_skill(&skill, &install_config).unwrap();
149
150 assert!(result.path.join("SKILL.md").exists());
152 let skill_md = std::fs::read_to_string(result.path.join("SKILL.md")).unwrap();
153 assert_eq!(skill_md, skill.raw_content);
154
155 assert!(result.path.join("scripts/run.py").exists());
156 let run_py = std::fs::read_to_string(result.path.join("scripts/run.py")).unwrap();
157 assert_eq!(run_py, "#!/usr/bin/env python3\nprint('running')");
158
159 assert!(result.path.join("references/overview.md").exists());
160 let overview = std::fs::read_to_string(result.path.join("references/overview.md")).unwrap();
161 assert_eq!(overview, "# Overview\nSkill overview.");
162 }
163
164 #[test]
165 fn test_github_flow_with_mock_provider() {
166 use types::SkillMetadata;
168
169 let temp_dir = TempDir::new().unwrap();
170 let canonical_dir = temp_dir.path().join(".agents/skills");
171 let lock_path = temp_dir.path().join(".agents/.skill-lock.json");
172
173 let github_skill = Skill {
175 name: "github-test-skill".to_string(),
176 description: "A test skill from GitHub".to_string(),
177 path: None,
178 raw_content: r#"---
179name: github-test-skill
180description: A test skill from GitHub
181---
182
183# GitHub Test Skill
184
185This is a test skill.
186"#
187 .to_string(),
188 metadata: SkillMetadata::default(),
189 auxiliary_files: Default::default(),
190 };
191
192 let provider = MockProvider::new(vec![github_skill.clone()])
194 .with_hash("github-hash-abc123".to_string());
195
196 let source = Source {
198 source_type: SourceType::Github,
199 url: Some("https://github.com/example/skills".to_string()),
200 subpath: None,
201 skill_filter: None,
202 ref_: None,
203 };
204
205 let config = DiscoveryConfig::default();
207 let skills = discover_skills_with_provider(&source, &config, Some(&provider)).unwrap();
208 assert_eq!(skills.len(), 1);
209 assert_eq!(skills[0].name, "github-test-skill");
210
211 let install_config = InstallConfig::new(canonical_dir.clone());
213 let result =
214 install_skill_with_provider(&skills[0], &install_config, Some(&provider)).unwrap();
215 assert!(result.path.exists());
216 assert!(result.path.join("SKILL.md").exists());
217
218 let lock_manager = LockManager::new(lock_path.clone());
220 lock_manager
221 .update_entry_with_hash(
222 &skills[0].name,
223 &source,
224 &result.path,
225 "github-hash-abc123".to_string(),
226 )
227 .unwrap();
228
229 let entry = lock_manager.get_entry(&skills[0].name).unwrap().unwrap();
231 assert_eq!(entry.source_type, "github");
232 assert_eq!(
233 entry.source_url,
234 Some("https://github.com/example/skills".to_string())
235 );
236 assert_eq!(entry.skill_folder_hash, "github-hash-abc123");
237
238 let json_content = std::fs::read_to_string(&lock_path).unwrap();
240 assert!(json_content.contains("sourceType"));
241 assert!(json_content.contains("sourceUrl"));
242 assert!(json_content.contains("skillPath"));
243 assert!(json_content.contains("skillFolderHash"));
244 assert!(json_content.contains("installedAt"));
245 assert!(json_content.contains("updatedAt"));
246 assert!(!json_content.contains("source_type"));
248 assert!(!json_content.contains("source_url"));
249 assert!(!json_content.contains("skill_path"));
250 assert!(!json_content.contains("skill_folder_hash"));
251 assert!(!json_content.contains("installed_at"));
252 assert!(!json_content.contains("updated_at"));
253
254 }
256
257 #[test]
258 fn test_github_cli_integration() {
259 use types::SkillMetadata;
261
262 let temp_dir = TempDir::new().unwrap();
263
264 let source = Source {
266 source_type: SourceType::Github,
267 url: Some("https://github.com/mock/skills".to_string()),
268 subpath: None,
269 skill_filter: None,
270 ref_: None,
271 };
272
273 let mock_skill = Skill {
274 name: "cli-github-skill".to_string(),
275 description: "CLI GitHub skill".to_string(),
276 path: None,
277 raw_content: r#"---
278name: cli-github-skill
279description: CLI GitHub skill
280---
281
282# CLI GitHub Skill
283"#
284 .to_string(),
285 metadata: SkillMetadata::default(),
286 auxiliary_files: Default::default(),
287 };
288
289 let provider = MockProvider::new(vec![mock_skill]).with_hash("cli-mock-hash".to_string());
290
291 let config = DiscoveryConfig::default();
293 let skills = discover_skills_with_provider(&source, &config, Some(&provider)).unwrap();
294 assert_eq!(skills.len(), 1);
295
296 let canonical_dir = temp_dir.path().join(".agents/skills");
298 let install_config = InstallConfig::new(canonical_dir);
299 let result =
300 install_skill_with_provider(&skills[0], &install_config, Some(&provider)).unwrap();
301
302 let lock_path = temp_dir.path().join(".agents/.skill-lock.json");
304 let lock_manager = LockManager::new(lock_path.clone());
305 let hash = provider.get_folder_hash(&skills[0]).unwrap();
306 lock_manager
307 .update_entry_with_hash(&skills[0].name, &source, &result.path, hash)
308 .unwrap();
309
310 let entry = lock_manager.get_entry(&skills[0].name).unwrap().unwrap();
312 assert_eq!(entry.source_type, "github");
313 assert_eq!(entry.skill_folder_hash, "cli-mock-hash");
314 assert!(entry.source_url.is_some());
315 }
316}