Skip to main content

agent_skills_rs/
lib.rs

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        // Setup
27        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        // Create source
32        let source = Source {
33            source_type: SourceType::Self_,
34            url: None,
35            subpath: None,
36            skill_filter: None,
37            ref_: None,
38        };
39
40        // Discover skills
41        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        // Install skill
47        let install_config = InstallConfig::new(canonical_dir.clone());
48        let result = install_skill(skill, &install_config).unwrap();
49
50        // Verify installation
51        assert!(result.path.exists());
52        assert!(result.path.join("SKILL.md").exists());
53
54        // Update lock
55        let lock_manager = LockManager::new(lock_path);
56        lock_manager
57            .update_entry(&skill.name, &source, &result.path)
58            .unwrap();
59
60        // Verify lock entry
61        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        // Test with source_type parsed from "self" JSON
71        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        // Test with source_type parsed from "embedded" JSON (should deserialize to Self_ due to alias)
76        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        // Should produce same results (both deserialize to Self_)
81        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        // This test verifies the entire flow works without external network access
89        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        // All operations should succeed without network access
113        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        // Register a skill with auxiliary files
125        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        // Install the skill
147        let install_config = InstallConfig::new(canonical_dir.clone());
148        let result = install_skill(&skill, &install_config).unwrap();
149
150        // Verify all files exist with correct content
151        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        // Test the complete flow: discover -> install -> lock for GitHub source
167        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        // Create a mock GitHub skill
174        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        // Create mock provider
193        let provider = MockProvider::new(vec![github_skill.clone()])
194            .with_hash("github-hash-abc123".to_string());
195
196        // Setup source
197        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        // Discover skills
206        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        // Install skill
212        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        // Update lock
219        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        // Verify lock entry
230        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        // Verify JSON file uses camelCase
239        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        // Should NOT contain snake_case versions
247        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        // This test should complete without any external network calls
255    }
256
257    #[test]
258    fn test_github_cli_integration() {
259        // Test that the CLI flow works end-to-end for GitHub source
260        use types::SkillMetadata;
261
262        let temp_dir = TempDir::new().unwrap();
263
264        // Simulate the CLI flow
265        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        // Discovery
292        let config = DiscoveryConfig::default();
293        let skills = discover_skills_with_provider(&source, &config, Some(&provider)).unwrap();
294        assert_eq!(skills.len(), 1);
295
296        // Installation
297        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        // Lock update (global)
303        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        // Verify
311        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}