vtcode_core/skills/
auto_verification.rs1use crate::skills::skill_file_tracker::SkillFileTracker;
8use anyhow::Result;
9use serde_json::Value;
10use std::path::PathBuf;
11use tracing::{debug, info};
12
13pub struct AutoSkillVerifier {
15 tracker: SkillFileTracker,
16 enabled: bool,
17}
18
19impl AutoSkillVerifier {
20 pub fn new(workspace_root: PathBuf) -> Self {
21 Self {
22 tracker: SkillFileTracker::new(workspace_root),
23 enabled: true,
24 }
25 }
26
27 pub fn set_enabled(&mut self, enabled: bool) {
29 self.enabled = enabled;
30 }
31
32 pub async fn process_skill_output(&self, skill_name: &str, original_output: String) -> Result<String> {
34 if !self.enabled {
35 return Ok(original_output);
36 }
37
38 info!("Auto-verifying skill output for: {}", skill_name);
39
40 if Self::already_verified(&original_output) {
42 debug!("Skill output already contains verification, skipping");
43 return Ok(original_output);
44 }
45
46 let enhanced = self.tracker.enhance_skill_output(original_output.clone()).await?;
48
49 let final_output = if enhanced.len() > original_output.len() {
51 let verification = enhanced.strip_prefix(&format!("{original_output}\n\n")).unwrap_or(&enhanced);
53
54 format!(
55 "ā Skill '{}' executed\n\n{}{}",
56 skill_name,
57 original_output,
58 if !verification.is_empty() {
59 format!("\n\n{verification}")
60 } else {
61 String::new()
62 }
63 )
64 } else {
65 enhanced
67 };
68
69 Ok(final_output)
70 }
71
72 pub async fn process_skill_result(&self, skill_name: &str, mut result: Value) -> Result<Value> {
74 if !self.enabled {
75 return Ok(result);
76 }
77
78 let output_text = Self::extract_output_text(&result);
80
81 if let Some(text) = output_text {
82 let enhanced = self.process_skill_output(skill_name, text).await?;
83
84 if let Some(output_field) = result.get_mut("output") {
86 *output_field = Value::String(enhanced);
87 } else if let Some(message_field) = result.get_mut("message") {
88 *message_field = Value::String(enhanced);
89 } else {
90 result["enhanced_output"] = Value::String(enhanced);
91 }
92 }
93
94 Ok(result)
95 }
96
97 fn already_verified(output: &str) -> bool {
99 output.contains("Generated Files:")
100 || output.contains("Missing Files:")
101 || output.contains("ā Generated:")
102 || output.contains("File generated at:")
103 }
104
105 fn extract_output_text(result: &Value) -> Option<String> {
107 if let Some(output) = result.get("output").and_then(|v| v.as_str()) {
109 return Some(output.to_string());
110 }
111
112 if let Some(message) = result.get("message").and_then(|v| v.as_str()) {
113 return Some(message.to_string());
114 }
115
116 if let Some(result_str) = result.get("result").and_then(|v| v.as_str()) {
117 return Some(result_str.to_string());
118 }
119
120 Some(serde_json::to_string_pretty(result).unwrap_or_default())
122 }
123
124 pub async fn create_success_response(
126 &self,
127 skill_name: &str,
128 details: &str,
129 output_hint: Option<&str>,
130 ) -> Result<String> {
131 let mut response = format!("ā Skill '{skill_name}' executed successfully\n\n{details}");
132
133 if let Some(hint) = output_hint {
134 let verification = self.tracker.scan_and_verify_skill_output(hint).await?;
136
137 if !verification.verified_files.is_empty() || !verification.missing_files.is_empty() {
138 response.push_str("\n\n");
139 response.push_str(&verification.summary);
140 }
141 }
142
143 Ok(response)
144 }
145
146 pub fn create_error_response(skill_name: &str, error: &str) -> String {
148 format!(
149 "ā Skill '{skill_name}' failed\n\nError: {error}\n\nš” Try:\n ⢠Verify the skill is properly installed\n ⢠Check that all dependencies are available\n ⢠Ensure you have the required permissions"
150 )
151 }
152}
153
154#[cfg(test)]
158mod tests {
159 use super::*;
160 use tempfile::TempDir;
161
162 #[tokio::test]
163 async fn test_auto_verifier_creation() {
164 let temp_dir = TempDir::new().unwrap();
165 let verifier = AutoSkillVerifier::new(temp_dir.path().to_path_buf());
166 assert!(verifier.enabled);
167 }
168
169 #[tokio::test]
170 async fn test_process_skill_output() {
171 let temp_dir = TempDir::new().unwrap();
172 let verifier = AutoSkillVerifier::new(temp_dir.path().to_path_buf());
173
174 let output = "Generated report.pdf".to_string();
175 let enhanced = verifier.process_skill_output("test-skill", output).await.unwrap();
176
177 assert!(enhanced.contains("test-skill"));
178 assert!(enhanced.contains("Generated report.pdf"));
179 }
180
181 #[tokio::test]
182 async fn test_already_verified_detection() {
183 let output = "File generated at: test.pdf";
184 assert!(AutoSkillVerifier::already_verified(output));
185
186 let output = "Some random text";
187 assert!(!AutoSkillVerifier::already_verified(output));
188 }
189
190 #[tokio::test]
191 async fn test_extract_output_text() {
192 let json = serde_json::json!({
193 "output": "Generated file.pdf"
194 });
195 assert_eq!(AutoSkillVerifier::extract_output_text(&json), Some("Generated file.pdf".to_string()));
196
197 let json_str = serde_json::json!("Plain string output");
198 assert!(AutoSkillVerifier::extract_output_text(&json_str).is_some());
199 }
200}