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(
34 &self,
35 skill_name: &str,
36 original_output: String,
37 ) -> Result<String> {
38 if !self.enabled {
39 return Ok(original_output);
40 }
41
42 info!("Auto-verifying skill output for: {}", skill_name);
43
44 if Self::already_verified(&original_output) {
46 debug!("Skill output already contains verification, skipping");
47 return Ok(original_output);
48 }
49
50 let enhanced = self
52 .tracker
53 .enhance_skill_output(original_output.clone())
54 .await?;
55
56 let final_output = if enhanced.len() > original_output.len() {
58 let verification = enhanced
60 .strip_prefix(&format!("{}\n\n", original_output))
61 .unwrap_or(&enhanced);
62
63 format!(
64 "ā Skill '{}' executed\n\n{}{}",
65 skill_name,
66 original_output,
67 if !verification.is_empty() {
68 format!("\n\n{}", verification)
69 } else {
70 String::new()
71 }
72 )
73 } else {
74 enhanced
76 };
77
78 Ok(final_output)
79 }
80
81 pub async fn process_skill_result(&self, skill_name: &str, mut result: Value) -> Result<Value> {
83 if !self.enabled {
84 return Ok(result);
85 }
86
87 let output_text = Self::extract_output_text(&result);
89
90 if let Some(text) = output_text {
91 let enhanced = self.process_skill_output(skill_name, text).await?;
92
93 if let Some(output_field) = result.get_mut("output") {
95 *output_field = Value::String(enhanced);
96 } else if let Some(message_field) = result.get_mut("message") {
97 *message_field = Value::String(enhanced);
98 } else {
99 result["enhanced_output"] = Value::String(enhanced);
100 }
101 }
102
103 Ok(result)
104 }
105
106 fn already_verified(output: &str) -> bool {
108 output.contains("Generated Files:")
109 || output.contains("Missing Files:")
110 || output.contains("ā Generated:")
111 || output.contains("File generated at:")
112 }
113
114 fn extract_output_text(result: &Value) -> Option<String> {
116 if let Some(output) = result.get("output").and_then(|v| v.as_str()) {
118 return Some(output.to_string());
119 }
120
121 if let Some(message) = result.get("message").and_then(|v| v.as_str()) {
122 return Some(message.to_string());
123 }
124
125 if let Some(result_str) = result.get("result").and_then(|v| v.as_str()) {
126 return Some(result_str.to_string());
127 }
128
129 Some(serde_json::to_string_pretty(result).unwrap_or_default())
131 }
132
133 pub async fn create_success_response(
135 &self,
136 skill_name: &str,
137 details: &str,
138 output_hint: Option<&str>,
139 ) -> Result<String> {
140 let mut response = format!(
141 "ā Skill '{}' executed successfully\n\n{}",
142 skill_name, details
143 );
144
145 if let Some(hint) = output_hint {
146 let verification = self.tracker.scan_and_verify_skill_output(hint).await?;
148
149 if !verification.verified_files.is_empty() || !verification.missing_files.is_empty() {
150 response.push_str("\n\n");
151 response.push_str(&verification.summary);
152 }
153 }
154
155 Ok(response)
156 }
157
158 pub fn create_error_response(skill_name: &str, error: &str) -> String {
160 format!(
161 "ā Skill '{}' failed\n\nError: {}\n\nš” Try:\n ⢠Verify the skill is properly installed\n ⢠Check that all dependencies are available\n ⢠Ensure you have the required permissions",
162 skill_name, error
163 )
164 }
165}
166
167#[cfg(test)]
171mod tests {
172 use super::*;
173 use tempfile::TempDir;
174
175 #[tokio::test]
176 async fn test_auto_verifier_creation() {
177 let temp_dir = TempDir::new().unwrap();
178 let verifier = AutoSkillVerifier::new(temp_dir.path().to_path_buf());
179 assert!(verifier.enabled);
180 }
181
182 #[tokio::test]
183 async fn test_process_skill_output() {
184 let temp_dir = TempDir::new().unwrap();
185 let verifier = AutoSkillVerifier::new(temp_dir.path().to_path_buf());
186
187 let output = "Generated report.pdf".to_string();
188 let enhanced = verifier
189 .process_skill_output("test-skill", output)
190 .await
191 .unwrap();
192
193 assert!(enhanced.contains("test-skill"));
194 assert!(enhanced.contains("Generated report.pdf"));
195 }
196
197 #[tokio::test]
198 async fn test_already_verified_detection() {
199 let output = "File generated at: test.pdf";
200 assert!(AutoSkillVerifier::already_verified(output));
201
202 let output = "Some random text";
203 assert!(!AutoSkillVerifier::already_verified(output));
204 }
205
206 #[tokio::test]
207 async fn test_extract_output_text() {
208 let json = serde_json::json!({
209 "output": "Generated file.pdf"
210 });
211 assert_eq!(
212 AutoSkillVerifier::extract_output_text(&json),
213 Some("Generated file.pdf".to_string())
214 );
215
216 let json_str = serde_json::json!("Plain string output");
217 assert!(AutoSkillVerifier::extract_output_text(&json_str).is_some());
218 }
219}