1use 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#[derive(Debug, Deserialize)]
27pub struct HelmlintArgs {
28 #[serde(default)]
30 pub chart: Option<String>,
31
32 #[serde(default)]
34 pub ignore: Vec<String>,
35
36 #[serde(default)]
38 pub threshold: Option<String>,
39}
40
41#[derive(Debug, thiserror::Error)]
43#[error("Helmlint error: {0}")]
44pub struct HelmlintError(String);
45
46#[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 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 fn get_fix_recommendation(code: &str) -> &'static str {
93 match code {
94 "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 "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 "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 "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 "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 fn format_result(result: &LintResult) -> String {
143 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 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 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 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 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 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 let mut config = HelmlintConfig::default();
325
326 for rule in &args.ignore {
328 config = config.ignore(rule.as_str());
329 }
330
331 if let Some(threshold) = &args.threshold {
333 config = config.with_threshold(Self::parse_threshold(threshold));
334 }
335
336 let chart_path = if let Some(chart) = &args.chart {
338 let path = self.project_path.join(chart);
339
340 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 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 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 if !chart_path.join("Chart.yaml").exists() {
388 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 let result = lint_chart(&chart_path, &config);
423
424 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 #[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 assert_eq!(HelmlintTool::parse_threshold("ERROR"), Severity::Error);
450 assert_eq!(HelmlintTool::parse_threshold("Warning"), Severity::Warning);
451 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 assert_eq!(
460 HelmlintTool::get_priority(Severity::Error, RuleCategory::Security),
461 "critical"
462 );
463
464 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 assert_eq!(
484 HelmlintTool::get_priority(Severity::Warning, RuleCategory::Security),
485 "high"
486 );
487
488 assert_eq!(
490 HelmlintTool::get_priority(Severity::Warning, RuleCategory::Template),
491 "high"
492 );
493
494 assert_eq!(
496 HelmlintTool::get_priority(Severity::Warning, RuleCategory::Structure),
497 "medium"
498 );
499
500 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 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 assert_eq!(
526 HelmlintTool::get_priority(Severity::Ignore, RuleCategory::Security),
527 "info"
528 );
529 }
530
531 #[test]
532 fn test_fix_recommendations() {
533 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 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 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 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 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 assert!(HelmlintTool::get_fix_recommendation("HL9999").contains("best practices"));
576 assert!(HelmlintTool::get_fix_recommendation("INVALID").contains("best practices"));
577 }
578
579 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 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 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 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 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 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 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}