Skip to main content

vtcode_core/skills/
enhanced_harness.rs

1//! # Enhanced Skill Harness
2//!
3//! Provides an improved skill execution harness that automatically tracks and reports
4//! generated files, eliminating the "where is it?" problem identified in session logs.
5
6use crate::tools::generation_helpers::GenerationHelper;
7use anyhow::Result;
8use serde_json::Value;
9use std::path::PathBuf;
10
11/// Enhanced skill result with automatic file tracking
12pub struct EnhancedSkillResult {
13    pub message: String,
14    pub generated_files: Vec<String>,
15    pub metadata: Value,
16}
17
18/// Enhanced skill harness that provides automatic file verification
19pub struct EnhancedSkillHarness {
20    workspace_root: PathBuf,
21    helper: GenerationHelper,
22}
23
24impl EnhancedSkillHarness {
25    pub fn new(workspace_root: PathBuf) -> Self {
26        Self {
27            workspace_root: workspace_root.clone(),
28            helper: GenerationHelper::new(workspace_root),
29        }
30    }
31
32    /// Execute a skill and automatically verify generated files
33    pub async fn execute_skill_with_verification(
34        &self,
35        skill_name: &str,
36        output_filename: Option<&str>,
37        skill_logic: impl FnOnce() -> Result<()>,
38    ) -> Result<EnhancedSkillResult> {
39        // Execute the skill logic
40        skill_logic()?;
41
42        // If output filename is provided, verify it
43        let mut verification_messages = Vec::new();
44        let mut generated_files = Vec::new();
45
46        if let Some(filename) = output_filename {
47            let verification = self.helper.verify_and_report(filename).await?;
48            verification_messages.push(verification.clone());
49            generated_files.push(filename.to_string());
50        }
51
52        // Create result message
53        let message = if verification_messages.is_empty() {
54            format!("✓ Skill '{}' executed successfully", skill_name)
55        } else {
56            format!(
57                "✓ Skill '{}' executed successfully\n\n{}",
58                skill_name,
59                verification_messages.join("\n")
60            )
61        };
62
63        Ok(EnhancedSkillResult {
64            message,
65            generated_files,
66            metadata: serde_json::json!({
67                "skill_name": skill_name,
68                "workspace_root": self.workspace_root.display().to_string(),
69            }),
70        })
71    }
72
73    /// Creates a standardized response for file generation skills
74    pub async fn create_file_generation_response(
75        &self,
76        file_type: &str,
77        filename: &str,
78        generation_details: Option<&str>,
79    ) -> Result<String> {
80        let verification = self.helper.verify_and_report(filename).await?;
81
82        let details_section = if let Some(details) = generation_details {
83            format!("\n\n{}", details)
84        } else {
85            String::new()
86        };
87
88        Ok(format!(
89            "✓ Generated {}: {}\n\n{}{}",
90            file_type, filename, verification, details_section
91        ))
92    }
93
94    /// Creates a quick sample response with immediate file verification
95    pub async fn create_sample_response(
96        &self,
97        file_type: &str,
98        generator_code: impl FnOnce(&PathBuf) -> Result<()>,
99    ) -> Result<String> {
100        // Generate a timestamped filename
101        let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
102        let filename = format!("sample_{}_{}", file_type.to_lowercase(), timestamp);
103        let filepath = self.workspace_root.join(&filename);
104
105        // Execute the generator
106        generator_code(&filepath)?;
107
108        // Verify and report
109        self.create_file_generation_response(
110            file_type,
111            &filename,
112            Some("This is a sample file. You can modify the content as needed."),
113        )
114        .await
115    }
116}
117
118/// Quick execution helper for common patterns
119pub async fn execute_file_generation_skill(
120    workspace_root: PathBuf,
121    skill_name: &str,
122    filename: &str,
123    success_message: Option<&str>,
124) -> Result<String> {
125    let harness = EnhancedSkillHarness::new(workspace_root);
126
127    let verification = harness.helper.verify_and_report(filename).await?;
128
129    let message = if let Some(custom_msg) = success_message {
130        format!("{}\n\n{}", custom_msg, verification)
131    } else {
132        format!("✓ Skill '{}' completed.\n\n{}", skill_name, verification)
133    };
134
135    Ok(message)
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use tempfile::TempDir;
142
143    #[tokio::test]
144    async fn test_enhanced_harness_creation() {
145        let temp_dir = TempDir::new().unwrap();
146        let harness = EnhancedSkillHarness::new(temp_dir.path().to_path_buf());
147
148        // Test basic creation
149        assert_eq!(harness.workspace_root, temp_dir.path().to_path_buf());
150    }
151
152    #[tokio::test]
153    async fn test_execute_skill_with_verification() {
154        let temp_dir = TempDir::new().unwrap();
155        let harness = EnhancedSkillHarness::new(temp_dir.path().to_path_buf());
156
157        // Test with a skill that doesn't generate files
158        let result = harness
159            .execute_skill_with_verification("test-skill", None, || Ok(()))
160            .await
161            .unwrap();
162
163        assert!(result.message.contains("test-skill"));
164        assert!(result.message.contains("executed successfully"));
165        assert!(result.generated_files.is_empty());
166    }
167
168    #[tokio::test]
169    async fn test_create_file_generation_response() {
170        let temp_dir = TempDir::new().unwrap();
171        let harness = EnhancedSkillHarness::new(temp_dir.path().to_path_buf());
172
173        let response = harness
174            .create_file_generation_response("PDF", "nonexistent.pdf", None)
175            .await
176            .unwrap();
177
178        assert!(response.contains("Generated PDF"));
179        assert!(response.contains("nonexistent.pdf"));
180    }
181
182    #[tokio::test]
183    async fn test_quick_execution_helper() {
184        let temp_dir = TempDir::new().unwrap();
185        let result = execute_file_generation_skill(
186            temp_dir.path().to_path_buf(),
187            "test-skill",
188            "test.pdf",
189            Some("Custom success message"),
190        )
191        .await
192        .unwrap();
193
194        assert!(result.contains("Custom success message"));
195    }
196}