agpm_cli/resolver/
transitive_extractor.rs1use anyhow::{Context, Result};
8use std::collections::HashMap;
9use std::path::Path;
10
11use crate::core::ResourceType;
12use crate::manifest::DependencySpec;
13use crate::metadata::MetadataExtractor;
14
15pub async fn extract_transitive_deps(
49 worktree_path: &Path,
50 resource_path: &str,
51 variant_inputs: Option<&serde_json::Value>,
52) -> Result<HashMap<ResourceType, Vec<DependencySpec>>> {
53 let file_path = worktree_path.join(resource_path);
55
56 let content = tokio::fs::read_to_string(&file_path)
58 .await
59 .with_context(|| format!("Failed to read resource file: {}", file_path.display()))?;
60
61 let metadata = MetadataExtractor::extract(&file_path, &content, variant_inputs, None)
63 .with_context(|| format!("Failed to extract metadata from: {}", file_path.display()))?;
64
65 let deps = metadata.get_dependencies_typed().unwrap_or_default();
67
68 for (resource_type, specs) in &deps {
70 for spec in specs {
71 tracing::debug!(
72 "EXTRACT: {} extracted from '{}' -> path='{}' version='{}'",
73 resource_type,
74 resource_path,
75 spec.path,
76 spec.version.as_deref().unwrap_or("HEAD")
77 );
78 }
79 }
80
81 Ok(deps)
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 use tempfile::TempDir;
89
90 #[tokio::test]
91 async fn test_extract_from_markdown_with_frontmatter() {
92 let temp_dir = TempDir::new().unwrap();
93 let file_path = temp_dir.path().join("test.md");
94
95 let content = r#"---
96dependencies:
97 agents:
98 - path: agents/helper.md
99 version: v1.0.0
100 snippets:
101 - path: snippets/guide.md
102---
103# Test Agent
104"#;
105
106 tokio::fs::write(&file_path, content).await.unwrap();
107
108 let deps = extract_transitive_deps(temp_dir.path(), "test.md", None).await.unwrap();
109
110 assert_eq!(deps.len(), 2);
111 assert!(deps.contains_key(&ResourceType::Agent));
112 assert!(deps.contains_key(&ResourceType::Snippet));
113
114 let agents = &deps[&ResourceType::Agent];
115 assert_eq!(agents.len(), 1);
116 assert_eq!(agents[0].path, "agents/helper.md");
117 assert_eq!(agents[0].version.as_deref(), Some("v1.0.0"));
118 }
119
120 #[tokio::test]
121 async fn test_extract_from_file_without_dependencies() {
122 let temp_dir = TempDir::new().unwrap();
123 let file_path = temp_dir.path().join("test.md");
124
125 let content = "# Simple Agent\n\nNo dependencies here.";
126 tokio::fs::write(&file_path, content).await.unwrap();
127
128 let deps = extract_transitive_deps(temp_dir.path(), "test.md", None).await.unwrap();
129
130 assert_eq!(deps.len(), 0);
131 }
132
133 #[tokio::test]
134 async fn test_extract_from_json() {
135 let temp_dir = TempDir::new().unwrap();
136 let file_path = temp_dir.path().join("test.json");
137
138 let content = r#"{
139 "name": "test",
140 "dependencies": {
141 "agents": [
142 {
143 "path": "agents/helper.md",
144 "version": "v1.0.0"
145 }
146 ]
147 }
148}"#;
149
150 tokio::fs::write(&file_path, content).await.unwrap();
151
152 let deps = extract_transitive_deps(temp_dir.path(), "test.json", None).await.unwrap();
153
154 assert_eq!(deps.len(), 1);
155 assert!(deps.contains_key(&ResourceType::Agent));
156
157 let agents = &deps[&ResourceType::Agent];
158 assert_eq!(agents.len(), 1);
159 assert_eq!(agents[0].path, "agents/helper.md");
160 }
161
162 #[tokio::test]
163 async fn test_extract_nonexistent_file() {
164 let temp_dir = TempDir::new().unwrap();
165
166 let result = extract_transitive_deps(temp_dir.path(), "nonexistent.md", None).await;
167
168 assert!(result.is_err());
169 }
170}