syncable_cli/agent/tools/
helmlint.rs

1//! Helmlint tool - Native Helm chart linting using Rig's Tool trait
2//!
3//! Lints Helm **chart structure and templates** (before rendering).
4//! Validates Chart.yaml, values.yaml, Go template syntax, and Helm-specific best practices.
5//!
6//! **Use this for:** Helm chart development, template syntax issues, chart metadata validation.
7//! **Use KubelintTool for:** Security/best practice issues in rendered K8s manifests.
8//!
9//! Output is optimized for AI agent decision-making with:
10//! - Categorized issues (structure, values, template, security, best-practice)
11//! - Priority rankings (critical, high, medium, low)
12//! - Actionable fix recommendations
13//! - Rule documentation
14
15use rig::completion::ToolDefinition;
16use rig::tool::Tool;
17use serde::{Deserialize, Serialize};
18use serde_json::json;
19use std::path::PathBuf;
20
21use super::error::{ErrorCategory, format_error_for_llm};
22use crate::analyzer::helmlint::types::RuleCategory;
23use crate::analyzer::helmlint::{HelmlintConfig, LintResult, Severity, lint_chart};
24
25/// Arguments for the helmlint tool
26#[derive(Debug, Deserialize)]
27pub struct HelmlintArgs {
28    /// Path to Helm chart directory (relative to project root)
29    #[serde(default)]
30    pub chart: Option<String>,
31
32    /// Rules to ignore (e.g., ["HL1007", "HL5001"])
33    #[serde(default)]
34    pub ignore: Vec<String>,
35
36    /// Minimum severity threshold: "error", "warning", "info", "style"
37    #[serde(default)]
38    pub threshold: Option<String>,
39}
40
41/// Error type for helmlint tool
42#[derive(Debug, thiserror::Error)]
43#[error("Helmlint error: {0}")]
44pub struct HelmlintError(String);
45
46/// Tool to lint Helm charts natively
47///
48/// **When to use:**
49/// - Validating Helm chart structure (Chart.yaml, values.yaml)
50/// - Checking Go template syntax issues (unclosed blocks, undefined variables)
51/// - Helm-specific best practices
52///
53/// **When to use KubelintTool instead:**
54/// - Checking security issues in the rendered K8s manifests
55/// - Validating K8s resource configurations (probes, resource limits, RBAC)
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct HelmlintTool {
58    project_path: PathBuf,
59}
60
61impl HelmlintTool {
62    pub fn new(project_path: PathBuf) -> Self {
63        Self { project_path }
64    }
65
66    fn parse_threshold(threshold: &str) -> Severity {
67        match threshold.to_lowercase().as_str() {
68            "error" => Severity::Error,
69            "warning" => Severity::Warning,
70            "info" => Severity::Info,
71            "style" => Severity::Style,
72            _ => Severity::Warning,
73        }
74    }
75
76    /// Get priority based on severity and category
77    fn get_priority(severity: Severity, category: RuleCategory) -> &'static str {
78        match (severity, category) {
79            (Severity::Error, RuleCategory::Security) => "critical",
80            (Severity::Error, _) => "high",
81            (Severity::Warning, RuleCategory::Security) => "high",
82            (Severity::Warning, RuleCategory::Template) => "high",
83            (Severity::Warning, RuleCategory::Structure) => "medium",
84            (Severity::Warning, _) => "medium",
85            (Severity::Info, _) => "low",
86            (Severity::Style, _) => "low",
87            (Severity::Ignore, _) => "info",
88        }
89    }
90
91    /// Get fix recommendation for common rules
92    fn get_fix_recommendation(code: &str) -> &'static str {
93        match code {
94            // Structure rules (HL1xxx)
95            "HL1001" => "Create a Chart.yaml file in the chart root directory.",
96            "HL1002" => "Add 'apiVersion: v2' (for Helm 3) or 'apiVersion: v1' to Chart.yaml.",
97            "HL1003" => "Add a 'name' field to Chart.yaml matching the chart directory name.",
98            "HL1004" => {
99                "Add a 'version' field with semantic versioning (e.g., '1.0.0') to Chart.yaml."
100            }
101            "HL1005" => "Use semantic versioning format (MAJOR.MINOR.PATCH) for the version field.",
102            "HL1006" => "Add a 'description' field explaining what the chart does.",
103            "HL1007" => "Add a 'maintainers' list with name and email for chart ownership.",
104            "HL1008" => "Ensure all dependencies listed in Chart.yaml are available and versioned.",
105
106            // Values rules (HL2xxx)
107            "HL2001" => "Create a values.yaml file with default configuration values.",
108            "HL2002" => "Define this value in values.yaml or provide a default in the template.",
109            "HL2003" => "Remove unused values from values.yaml or use them in templates.",
110            "HL2004" => "Use consistent naming (camelCase or snake_case) for all values.",
111            "HL2005" => "Add comments documenting the purpose and valid options for values.",
112
113            // Template rules (HL3xxx)
114            "HL3001" => "Close the unclosed template block ({{- end }}).",
115            "HL3002" => "Define this template with {{ define \"name\" }} or check for typos.",
116            "HL3003" => "Use {{ .Values.key }} or {{ .Release.Name }} for valid references.",
117            "HL3004" => "Check nesting of if/range/with blocks - each needs matching {{ end }}.",
118            "HL3005" => "Ensure the pipeline uses valid functions and proper syntax.",
119            "HL3006" => "Add whitespace control with {{- and -}} to avoid extra blank lines.",
120
121            // Security rules (HL4xxx)
122            "HL4001" => "Add 'securityContext.runAsNonRoot: true' to container specs.",
123            "HL4002" => "Remove 'privileged: true' or add explicit justification annotation.",
124            "HL4003" => "Add resource limits (cpu, memory) to prevent resource exhaustion.",
125            "HL4004" => "Use 'readOnlyRootFilesystem: true' in securityContext.",
126            "HL4005" => "Drop all capabilities and add only required ones explicitly.",
127
128            // Best practice rules (HL5xxx)
129            "HL5001" => "Add resource requests and limits for all containers.",
130            "HL5002" => "Add liveness and readiness probes for health checking.",
131            "HL5003" => "Use '{{ .Release.Namespace }}' for namespace-aware resources.",
132            "HL5004" => "Include NOTES.txt with post-install instructions.",
133            "HL5005" => "Add labels including 'app.kubernetes.io/name' and 'helm.sh/chart'.",
134            "HL5006" => "Use '{{ include \"chart.fullname\" . }}' for consistent naming.",
135            "HL5007" => "Add selector labels to connect Services with Deployments.",
136
137            _ => "Review the Helm chart best practices documentation.",
138        }
139    }
140
141    /// Format result optimized for agent decision-making
142    fn format_result(result: &LintResult) -> String {
143        // Categorize and enrich failures
144        let enriched_failures: Vec<serde_json::Value> = result
145            .failures
146            .iter()
147            .map(|f| {
148                let code = f.code.as_str();
149                let priority = Self::get_priority(f.severity, f.category);
150
151                json!({
152                    "code": code,
153                    "severity": f.severity.as_str(),
154                    "priority": priority,
155                    "category": f.category.display_name(),
156                    "message": f.message,
157                    "file": f.file.display().to_string(),
158                    "line": f.line,
159                    "column": f.column,
160                    "fixable": f.fixable,
161                    "fix": Self::get_fix_recommendation(code),
162                })
163            })
164            .collect();
165
166        // Group by priority
167        let critical: Vec<_> = enriched_failures
168            .iter()
169            .filter(|f| f["priority"] == "critical")
170            .cloned()
171            .collect();
172        let high: Vec<_> = enriched_failures
173            .iter()
174            .filter(|f| f["priority"] == "high")
175            .cloned()
176            .collect();
177        let medium: Vec<_> = enriched_failures
178            .iter()
179            .filter(|f| f["priority"] == "medium")
180            .cloned()
181            .collect();
182        let low: Vec<_> = enriched_failures
183            .iter()
184            .filter(|f| f["priority"] == "low")
185            .cloned()
186            .collect();
187
188        // Group by category
189        let mut by_category: std::collections::HashMap<&str, usize> =
190            std::collections::HashMap::new();
191        for f in &result.failures {
192            *by_category.entry(f.category.display_name()).or_default() += 1;
193        }
194
195        // Build decision context
196        let decision_context = if critical.is_empty() && high.is_empty() {
197            if medium.is_empty() && low.is_empty() {
198                "Helm chart follows best practices. No issues found."
199            } else if medium.is_empty() {
200                "Minor improvements possible. Low priority issues only."
201            } else {
202                "Good baseline. Medium priority improvements recommended."
203            }
204        } else if !critical.is_empty() {
205            "Critical issues found. Fix template/security issues before deployment."
206        } else {
207            "High priority issues found. Fix template syntax or structure issues."
208        };
209
210        // Build agent-optimized output
211        let mut output = json!({
212            "chart": result.chart_path,
213            "success": !result.has_errors(),
214            "decision_context": decision_context,
215            "tool_guidance": "Use helmlint for chart structure/template issues. Use kubelint for K8s resource security/best practices.",
216            "summary": {
217                "total": result.failures.len(),
218                "files_checked": result.files_checked,
219                "by_priority": {
220                    "critical": critical.len(),
221                    "high": high.len(),
222                    "medium": medium.len(),
223                    "low": low.len(),
224                },
225                "by_severity": {
226                    "errors": result.error_count,
227                    "warnings": result.warning_count,
228                },
229                "by_category": by_category,
230            },
231            "action_plan": {
232                "critical": critical,
233                "high": high,
234                "medium": medium,
235                "low": low,
236            },
237        });
238
239        // Add quick fixes summary
240        if !enriched_failures.is_empty() {
241            let quick_fixes: Vec<String> = enriched_failures
242                .iter()
243                .filter(|f| f["priority"] == "critical" || f["priority"] == "high")
244                .take(5)
245                .map(|f| {
246                    format!(
247                        "{} line {}: {} - {}",
248                        f["file"].as_str().unwrap_or(""),
249                        f["line"],
250                        f["code"].as_str().unwrap_or(""),
251                        f["fix"].as_str().unwrap_or("")
252                    )
253                })
254                .collect();
255
256            if !quick_fixes.is_empty() {
257                output["quick_fixes"] = json!(quick_fixes);
258            }
259        }
260
261        if !result.parse_errors.is_empty() {
262            output["parse_errors"] = json!(result.parse_errors);
263        }
264
265        serde_json::to_string_pretty(&output).unwrap_or_else(|_| "{}".to_string())
266    }
267}
268
269impl Tool for HelmlintTool {
270    const NAME: &'static str = "helmlint";
271
272    type Error = HelmlintError;
273    type Args = HelmlintArgs;
274    type Output = String;
275
276    async fn definition(&self, _prompt: String) -> ToolDefinition {
277        ToolDefinition {
278            name: Self::NAME.to_string(),
279            description: r#"Native Helm chart linting for chart STRUCTURE and TEMPLATES (before rendering).
280
281**What helmlint validates:**
282- Chart.yaml (metadata, versioning, dependencies)
283- values.yaml (schema, unused values, type consistency)
284- Go template syntax (unclosed blocks, undefined variables)
285- Helm-specific best practices (naming, labels, probes)
286
287**Rule Categories:**
288- HL1xxx (Structure): Chart.yaml metadata, directory structure
289- HL2xxx (Values): values.yaml validation, defaults
290- HL3xxx (Template): Go template syntax, undefined references
291- HL4xxx (Security): Security concerns in templates
292- HL5xxx (BestPractice): Helm conventions, standard labels
293
294**Use helmlint for:** Chart development, template syntax issues, metadata validation.
295**Use kubelint for:** Security/best practices in the RENDERED K8s manifests (probes, resources, RBAC).
296
297Returns prioritized issues with fix recommendations grouped by priority (critical/high/medium/low)."#.to_string(),
298            parameters: json!({
299                "type": "object",
300                "properties": {
301                    "chart": {
302                        "type": "string",
303                        "description": "Path to Helm chart directory relative to project root. Must contain Chart.yaml. Examples: 'charts/my-app', 'helm/production', 'deploy/chart'"
304                    },
305                    "ignore": {
306                        "type": "array",
307                        "items": { "type": "string" },
308                        "description": "Rule codes to skip. Format: HL[1-5]xxx. Examples: ['HL1007', 'HL5001']. Categories: 1=Structure, 2=Values, 3=Template, 4=Security, 5=BestPractice"
309                    },
310                    "threshold": {
311                        "type": "string",
312                        "enum": ["error", "warning", "info", "style"],
313                        "default": "warning",
314                        "description": "Minimum severity to report. 'error'=critical only, 'warning'=errors+warnings (default), 'info'=all except style, 'style'=everything"
315                    }
316                },
317                "required": ["chart"]
318            }),
319        }
320    }
321
322    async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
323        // Build configuration
324        let mut config = HelmlintConfig::default();
325
326        // Apply ignored rules
327        for rule in &args.ignore {
328            config = config.ignore(rule.as_str());
329        }
330
331        // Apply threshold
332        if let Some(threshold) = &args.threshold {
333            config = config.with_threshold(Self::parse_threshold(threshold));
334        }
335
336        // Determine chart path
337        let chart_path = if let Some(chart) = &args.chart {
338            let path = self.project_path.join(chart);
339
340            // Check if the path exists at all
341            if !path.exists() {
342                return Ok(format_error_for_llm(
343                    "helmlint",
344                    ErrorCategory::FileNotFound,
345                    &format!("Chart path '{}' does not exist", chart),
346                    Some(vec![
347                        "Verify the chart directory path is correct",
348                        "Use list_directory to explore available paths",
349                        "Helm charts are typically in 'charts/', 'helm/', or 'deploy/' directories",
350                    ]),
351                ));
352            }
353
354            // Check if it's a directory
355            if !path.is_dir() {
356                return Ok(format_error_for_llm(
357                    "helmlint",
358                    ErrorCategory::ValidationFailed,
359                    &format!("'{}' is not a directory", chart),
360                    Some(vec![
361                        "The chart parameter must point to a Helm chart directory",
362                        "The directory should contain Chart.yaml",
363                    ]),
364                ));
365            }
366
367            path
368        } else {
369            // Look for Chart.yaml in project root
370            if self.project_path.join("Chart.yaml").exists() {
371                self.project_path.clone()
372            } else {
373                return Ok(format_error_for_llm(
374                    "helmlint",
375                    ErrorCategory::ValidationFailed,
376                    "No chart specified and no Chart.yaml found in project root",
377                    Some(vec![
378                        "Specify a chart directory with the 'chart' parameter",
379                        "Use list_directory to find Helm charts (look for Chart.yaml files)",
380                        "Common locations: charts/, helm/, deploy/",
381                    ]),
382                ));
383            }
384        };
385
386        // Validate it's a Helm chart (has Chart.yaml)
387        if !chart_path.join("Chart.yaml").exists() {
388            // Check if it's an empty directory
389            let is_empty = std::fs::read_dir(&chart_path)
390                .map(|mut entries| entries.next().is_none())
391                .unwrap_or(false);
392
393            if is_empty {
394                return Ok(format_error_for_llm(
395                    "helmlint",
396                    ErrorCategory::ValidationFailed,
397                    &format!("Directory '{}' is empty", chart_path.display()),
398                    Some(vec![
399                        "The directory must contain Chart.yaml to be a valid Helm chart",
400                        "Run 'helm create <name>' to scaffold a new chart",
401                    ]),
402                ));
403            }
404
405            return Ok(format_error_for_llm(
406                "helmlint",
407                ErrorCategory::ValidationFailed,
408                &format!(
409                    "Not a valid Helm chart: Chart.yaml not found in '{}'",
410                    chart_path.display()
411                ),
412                Some(vec![
413                    "Ensure the path points to a Helm chart directory",
414                    "Chart directory must contain Chart.yaml",
415                    "For K8s manifest linting (not Helm charts), use kubelint instead",
416                    "Use list_directory to explore the directory structure",
417                ]),
418            ));
419        }
420
421        // Lint the chart
422        let result = lint_chart(&chart_path, &config);
423
424        // Check for parse errors
425        if !result.parse_errors.is_empty() {
426            log::warn!("Helm chart parse errors: {:?}", result.parse_errors);
427        }
428
429        Ok(Self::format_result(&result))
430    }
431}
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436    use crate::analyzer::helmlint::types::RuleCategory;
437    use std::fs;
438    use tempfile::TempDir;
439
440    // ==================== Unit Tests ====================
441
442    #[test]
443    fn test_parse_threshold() {
444        assert_eq!(HelmlintTool::parse_threshold("error"), Severity::Error);
445        assert_eq!(HelmlintTool::parse_threshold("warning"), Severity::Warning);
446        assert_eq!(HelmlintTool::parse_threshold("info"), Severity::Info);
447        assert_eq!(HelmlintTool::parse_threshold("style"), Severity::Style);
448        // Case insensitive
449        assert_eq!(HelmlintTool::parse_threshold("ERROR"), Severity::Error);
450        assert_eq!(HelmlintTool::parse_threshold("Warning"), Severity::Warning);
451        // Invalid defaults to warning
452        assert_eq!(HelmlintTool::parse_threshold("invalid"), Severity::Warning);
453        assert_eq!(HelmlintTool::parse_threshold(""), Severity::Warning);
454    }
455
456    #[test]
457    fn test_get_priority() {
458        // Security errors are always critical
459        assert_eq!(
460            HelmlintTool::get_priority(Severity::Error, RuleCategory::Security),
461            "critical"
462        );
463
464        // Non-security errors are high
465        assert_eq!(
466            HelmlintTool::get_priority(Severity::Error, RuleCategory::Structure),
467            "high"
468        );
469        assert_eq!(
470            HelmlintTool::get_priority(Severity::Error, RuleCategory::Template),
471            "high"
472        );
473        assert_eq!(
474            HelmlintTool::get_priority(Severity::Error, RuleCategory::Values),
475            "high"
476        );
477        assert_eq!(
478            HelmlintTool::get_priority(Severity::Error, RuleCategory::BestPractice),
479            "high"
480        );
481
482        // Security warnings are high
483        assert_eq!(
484            HelmlintTool::get_priority(Severity::Warning, RuleCategory::Security),
485            "high"
486        );
487
488        // Template warnings are high
489        assert_eq!(
490            HelmlintTool::get_priority(Severity::Warning, RuleCategory::Template),
491            "high"
492        );
493
494        // Structure warnings are medium
495        assert_eq!(
496            HelmlintTool::get_priority(Severity::Warning, RuleCategory::Structure),
497            "medium"
498        );
499
500        // Other warnings are medium
501        assert_eq!(
502            HelmlintTool::get_priority(Severity::Warning, RuleCategory::BestPractice),
503            "medium"
504        );
505        assert_eq!(
506            HelmlintTool::get_priority(Severity::Warning, RuleCategory::Values),
507            "medium"
508        );
509
510        // Info and Style are low
511        assert_eq!(
512            HelmlintTool::get_priority(Severity::Info, RuleCategory::Structure),
513            "low"
514        );
515        assert_eq!(
516            HelmlintTool::get_priority(Severity::Info, RuleCategory::Security),
517            "low"
518        );
519        assert_eq!(
520            HelmlintTool::get_priority(Severity::Style, RuleCategory::Template),
521            "low"
522        );
523
524        // Ignore is info
525        assert_eq!(
526            HelmlintTool::get_priority(Severity::Ignore, RuleCategory::Security),
527            "info"
528        );
529    }
530
531    #[test]
532    fn test_fix_recommendations() {
533        // Structure rules (HL1xxx)
534        assert!(HelmlintTool::get_fix_recommendation("HL1001").contains("Chart.yaml"));
535        assert!(HelmlintTool::get_fix_recommendation("HL1002").contains("apiVersion"));
536        assert!(HelmlintTool::get_fix_recommendation("HL1003").contains("name"));
537        assert!(HelmlintTool::get_fix_recommendation("HL1004").contains("version"));
538        assert!(HelmlintTool::get_fix_recommendation("HL1005").contains("semantic versioning"));
539        assert!(HelmlintTool::get_fix_recommendation("HL1006").contains("description"));
540        assert!(HelmlintTool::get_fix_recommendation("HL1007").contains("maintainers"));
541        assert!(HelmlintTool::get_fix_recommendation("HL1008").contains("dependencies"));
542
543        // Values rules (HL2xxx)
544        assert!(HelmlintTool::get_fix_recommendation("HL2001").contains("values.yaml"));
545        assert!(HelmlintTool::get_fix_recommendation("HL2002").contains("default"));
546        assert!(HelmlintTool::get_fix_recommendation("HL2003").contains("unused"));
547        assert!(HelmlintTool::get_fix_recommendation("HL2004").contains("naming"));
548        assert!(HelmlintTool::get_fix_recommendation("HL2005").contains("comments"));
549
550        // Template rules (HL3xxx)
551        assert!(HelmlintTool::get_fix_recommendation("HL3001").contains("end"));
552        assert!(HelmlintTool::get_fix_recommendation("HL3002").contains("define"));
553        assert!(HelmlintTool::get_fix_recommendation("HL3003").contains("Values"));
554        assert!(HelmlintTool::get_fix_recommendation("HL3004").contains("nesting"));
555        assert!(HelmlintTool::get_fix_recommendation("HL3005").contains("pipeline"));
556        assert!(HelmlintTool::get_fix_recommendation("HL3006").contains("whitespace"));
557
558        // Security rules (HL4xxx)
559        assert!(HelmlintTool::get_fix_recommendation("HL4001").contains("runAsNonRoot"));
560        assert!(HelmlintTool::get_fix_recommendation("HL4002").contains("privileged"));
561        assert!(HelmlintTool::get_fix_recommendation("HL4003").contains("resource limits"));
562        assert!(HelmlintTool::get_fix_recommendation("HL4004").contains("readOnlyRootFilesystem"));
563        assert!(HelmlintTool::get_fix_recommendation("HL4005").contains("capabilities"));
564
565        // Best practice rules (HL5xxx)
566        assert!(HelmlintTool::get_fix_recommendation("HL5001").contains("resource"));
567        assert!(HelmlintTool::get_fix_recommendation("HL5002").contains("probes"));
568        assert!(HelmlintTool::get_fix_recommendation("HL5003").contains("Namespace"));
569        assert!(HelmlintTool::get_fix_recommendation("HL5004").contains("NOTES.txt"));
570        assert!(HelmlintTool::get_fix_recommendation("HL5005").contains("labels"));
571        assert!(HelmlintTool::get_fix_recommendation("HL5006").contains("fullname"));
572        assert!(HelmlintTool::get_fix_recommendation("HL5007").contains("selector"));
573
574        // Unknown codes return generic message
575        assert!(HelmlintTool::get_fix_recommendation("HL9999").contains("best practices"));
576        assert!(HelmlintTool::get_fix_recommendation("INVALID").contains("best practices"));
577    }
578
579    // ==================== Integration Tests ====================
580
581    fn create_test_chart(dir: &std::path::Path) {
582        fs::create_dir_all(dir.join("templates")).unwrap();
583
584        fs::write(
585            dir.join("Chart.yaml"),
586            r#"apiVersion: v2
587name: test-chart
588version: 1.0.0
589description: A test chart
590"#,
591        )
592        .unwrap();
593
594        fs::write(
595            dir.join("values.yaml"),
596            r#"replicaCount: 1
597image:
598  repository: nginx
599  tag: "1.25"
600"#,
601        )
602        .unwrap();
603
604        fs::write(
605            dir.join("templates/deployment.yaml"),
606            r#"apiVersion: apps/v1
607kind: Deployment
608metadata:
609  name: {{ .Release.Name }}
610spec:
611  replicas: {{ .Values.replicaCount }}
612"#,
613        )
614        .unwrap();
615    }
616
617    #[tokio::test]
618    async fn test_helmlint_valid_chart() {
619        let temp_dir = TempDir::new().unwrap();
620        create_test_chart(temp_dir.path());
621
622        let tool = HelmlintTool::new(temp_dir.path().to_path_buf());
623        let args = HelmlintArgs {
624            chart: Some(".".to_string()),
625            ignore: vec![],
626            threshold: None,
627        };
628
629        let result = tool.call(args).await.unwrap();
630        let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
631
632        assert!(parsed["decision_context"].is_string());
633        assert!(parsed["tool_guidance"].is_string());
634        assert!(parsed["summary"]["files_checked"].is_number());
635    }
636
637    #[tokio::test]
638    async fn test_helmlint_no_chart_returns_error_json() {
639        let temp_dir = TempDir::new().unwrap();
640        // Don't create a chart
641
642        let tool = HelmlintTool::new(temp_dir.path().to_path_buf());
643        let args = HelmlintArgs {
644            chart: None,
645            ignore: vec![],
646            threshold: None,
647        };
648
649        // Now returns Ok with error JSON instead of Err
650        let result = tool.call(args).await.unwrap();
651        let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
652
653        assert_eq!(parsed["error"], true);
654        assert_eq!(parsed["tool"], "helmlint");
655        assert_eq!(parsed["code"], "VALIDATION_FAILED");
656        assert!(
657            parsed["message"]
658                .as_str()
659                .unwrap()
660                .contains("No chart specified")
661        );
662        assert!(parsed["suggestions"].is_array());
663    }
664
665    #[tokio::test]
666    async fn test_helmlint_not_a_chart_returns_error_json() {
667        let temp_dir = TempDir::new().unwrap();
668        // Create a directory without Chart.yaml but with a file so it's not empty
669        fs::create_dir_all(temp_dir.path().join("some-dir")).unwrap();
670        fs::write(temp_dir.path().join("some-dir/README.md"), "test").unwrap();
671
672        let tool = HelmlintTool::new(temp_dir.path().to_path_buf());
673        let args = HelmlintArgs {
674            chart: Some("some-dir".to_string()),
675            ignore: vec![],
676            threshold: None,
677        };
678
679        // Now returns Ok with error JSON instead of Err
680        let result = tool.call(args).await.unwrap();
681        let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
682
683        assert_eq!(parsed["error"], true);
684        assert_eq!(parsed["tool"], "helmlint");
685        assert_eq!(parsed["code"], "VALIDATION_FAILED");
686        assert!(
687            parsed["message"]
688                .as_str()
689                .unwrap()
690                .contains("Chart.yaml not found")
691        );
692        assert!(parsed["suggestions"].is_array());
693    }
694
695    #[tokio::test]
696    async fn test_helmlint_nonexistent_path_returns_error_json() {
697        let temp_dir = TempDir::new().unwrap();
698
699        let tool = HelmlintTool::new(temp_dir.path().to_path_buf());
700        let args = HelmlintArgs {
701            chart: Some("nonexistent-dir".to_string()),
702            ignore: vec![],
703            threshold: None,
704        };
705
706        let result = tool.call(args).await.unwrap();
707        let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
708
709        assert_eq!(parsed["error"], true);
710        assert_eq!(parsed["tool"], "helmlint");
711        assert_eq!(parsed["code"], "FILE_NOT_FOUND");
712        assert!(
713            parsed["message"]
714                .as_str()
715                .unwrap()
716                .contains("does not exist")
717        );
718    }
719
720    #[tokio::test]
721    async fn test_helmlint_file_not_directory_returns_error_json() {
722        let temp_dir = TempDir::new().unwrap();
723        // Create a file instead of a directory
724        fs::write(temp_dir.path().join("not-a-dir"), "content").unwrap();
725
726        let tool = HelmlintTool::new(temp_dir.path().to_path_buf());
727        let args = HelmlintArgs {
728            chart: Some("not-a-dir".to_string()),
729            ignore: vec![],
730            threshold: None,
731        };
732
733        let result = tool.call(args).await.unwrap();
734        let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
735
736        assert_eq!(parsed["error"], true);
737        assert_eq!(parsed["tool"], "helmlint");
738        assert_eq!(parsed["code"], "VALIDATION_FAILED");
739        assert!(
740            parsed["message"]
741                .as_str()
742                .unwrap()
743                .contains("not a directory")
744        );
745    }
746
747    #[tokio::test]
748    async fn test_helmlint_empty_directory_returns_error_json() {
749        let temp_dir = TempDir::new().unwrap();
750        // Create an empty directory
751        fs::create_dir_all(temp_dir.path().join("empty-dir")).unwrap();
752
753        let tool = HelmlintTool::new(temp_dir.path().to_path_buf());
754        let args = HelmlintArgs {
755            chart: Some("empty-dir".to_string()),
756            ignore: vec![],
757            threshold: None,
758        };
759
760        let result = tool.call(args).await.unwrap();
761        let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
762
763        assert_eq!(parsed["error"], true);
764        assert_eq!(parsed["tool"], "helmlint");
765        assert_eq!(parsed["code"], "VALIDATION_FAILED");
766        assert!(parsed["message"].as_str().unwrap().contains("empty"));
767    }
768}