Skip to main content

skm_cli/commands/
test.rs

1//! Test command: run trigger test suites.
2
3use clap::Args;
4use std::path::PathBuf;
5
6use skm_core::SkillRegistry;
7use skm_learn::{TestSuite, TriggerTestHarness};
8use skm_select::{CascadeSelector, TriggerStrategy};
9
10#[derive(Args)]
11pub struct TestArgs {
12    /// Test suite file (YAML)
13    suite: PathBuf,
14
15    /// Skill directories
16    #[arg(short, long, default_value = ".")]
17    skills: Vec<PathBuf>,
18
19    /// Verbose output (show each test)
20    #[arg(short, long)]
21    verbose: bool,
22
23    /// Output format
24    #[arg(long, default_value = "text")]
25    format: String,
26}
27
28pub async fn test(args: TestArgs) -> anyhow::Result<()> {
29    // Load test suite
30    let suite = TestSuite::from_file(&args.suite)?;
31    println!("Loaded test suite: {} ({} cases)", suite.name, suite.cases.len());
32
33    // Load skills
34    let registry = SkillRegistry::new(&args.skills).await?;
35    let trigger = TriggerStrategy::from_registry(&registry).await?;
36    let selector = CascadeSelector::builder().with_triggers(trigger).build();
37
38    // Run tests
39    let harness = TriggerTestHarness::new();
40    let report = harness.run(&suite, &selector, &registry).await?;
41
42    match args.format.as_str() {
43        "json" => {
44            let output = serde_json::json!({
45                "suite": report.suite_name,
46                "total": report.total,
47                "passed": report.passed,
48                "accuracy": report.accuracy(),
49                "avg_latency_ms": report.avg_latency_ms,
50                "results": report.results.iter().map(|r| {
51                    serde_json::json!({
52                        "name": r.name,
53                        "passed": r.passed,
54                        "selected": r.selected.as_ref().map(|s| s.as_str()),
55                        "score": r.score,
56                        "latency_ms": r.latency_ms
57                    })
58                }).collect::<Vec<_>>()
59            });
60            println!("{}", serde_json::to_string_pretty(&output)?);
61        }
62        _ => {
63            if args.verbose {
64                println!("\nResults:");
65                for r in &report.results {
66                    let status = if r.passed { "✓" } else { "✗" };
67                    let selected = r
68                        .selected
69                        .as_ref()
70                        .map(|s| s.as_str())
71                        .unwrap_or("(none)");
72                    println!(
73                        "  {} {} -> {} ({}ms)",
74                        status, r.name, selected, r.latency_ms
75                    );
76                }
77                println!();
78            }
79
80            // Summary
81            println!("Test Results: {}/{} passed ({:.1}%)",
82                report.passed,
83                report.total,
84                report.accuracy() * 100.0
85            );
86            println!("Average latency: {:.1}ms", report.avg_latency_ms);
87
88            // Per-skill breakdown
89            if !report.per_skill.is_empty() {
90                println!("\nPer-skill metrics:");
91                for (skill, metrics) in &report.per_skill {
92                    println!(
93                        "  {}: P={:.1}% R={:.1}% F1={:.1}%",
94                        skill,
95                        metrics.precision() * 100.0,
96                        metrics.recall() * 100.0,
97                        metrics.f1() * 100.0
98                    );
99                }
100            }
101        }
102    }
103
104    // Exit with error if tests failed
105    if report.passed < report.total {
106        std::process::exit(1);
107    }
108
109    Ok(())
110}