help-probe 0.1.0

CLI tool discovery and automation framework that extracts structured information from command help text
Documentation
use help_probe::model::{ArgumentSpec, ArgumentType, OptionSpec, ProbeResult};
use help_probe::validation::{ValidationErrorType, validate_command};

fn create_test_result() -> ProbeResult {
    ProbeResult {
        command: "mytool".to_string(),
        args: vec![],
        exit_code: Some(0),
        timed_out: false,
        help_flag_detected: false,
        usage_blocks: vec![],
        options: vec![OptionSpec {
            short_flags: vec!["-v".to_string()],
            long_flags: vec!["--verbose".to_string()],
            description: Some("Verbose output".to_string()),
            option_type: help_probe::model::OptionType::Boolean,
            required: false,
            default_value: None,
            takes_argument: false,
            argument_name: None,
            choices: vec![],
        }],
        subcommands: vec![],
        arguments: vec![],
        examples: vec![],
        environment_variables: vec![],
        validation_rules: vec![],
        raw_stdout: String::new(),
        raw_stderr: String::new(),
    }
}

#[test]
fn validate_command_success() {
    let result = create_test_result();

    // Valid command with known option
    let validation = validate_command(&result, "mytool", &["--verbose".to_string()]);
    assert!(validation.is_valid);
    assert!(validation.errors.is_empty());
}

#[test]
fn validate_command_unknown_option() {
    let result = create_test_result();

    // Invalid command with unknown option
    let validation = validate_command(&result, "mytool", &["--unknown".to_string()]);
    assert!(!validation.is_valid);
    assert!(
        validation
            .errors
            .iter()
            .any(|e| matches!(e.error_type, ValidationErrorType::UnknownOption))
    );
}

#[test]
fn validate_command_missing_required_argument() {
    let result = ProbeResult {
        command: "mytool".to_string(),
        args: vec![],
        exit_code: Some(0),
        timed_out: false,
        help_flag_detected: false,
        usage_blocks: vec![],
        options: vec![],
        subcommands: vec![],
        arguments: vec![ArgumentSpec {
            name: "FILE".to_string(),
            description: Some("Input file".to_string()),
            required: true,
            variadic: false,
            arg_type: Some(ArgumentType::Path),
            placeholder: Some("<FILE>".to_string()),
        }],
        examples: vec![],
        environment_variables: vec![],
        validation_rules: vec![],
        raw_stdout: String::new(),
        raw_stderr: String::new(),
    };

    // Missing required argument
    let validation = validate_command(&result, "mytool", &[]);
    assert!(!validation.is_valid);
    assert!(
        validation
            .errors
            .iter()
            .any(|e| matches!(e.error_type, ValidationErrorType::TooFewArguments))
    );
}

#[test]
fn validate_command_option_missing_argument() {
    let result = ProbeResult {
        command: "mytool".to_string(),
        args: vec![],
        exit_code: Some(0),
        timed_out: false,
        help_flag_detected: false,
        usage_blocks: vec![],
        options: vec![OptionSpec {
            short_flags: vec![],
            long_flags: vec!["--file".to_string()],
            description: Some("Input file".to_string()),
            option_type: help_probe::model::OptionType::Path,
            required: false,
            default_value: None,
            takes_argument: true,
            argument_name: Some("FILE".to_string()),
            choices: vec![],
        }],
        subcommands: vec![],
        arguments: vec![],
        examples: vec![],
        environment_variables: vec![],
        validation_rules: vec![],
        raw_stdout: String::new(),
        raw_stderr: String::new(),
    };

    // Option that requires argument but none provided
    let validation = validate_command(&result, "mytool", &["--file".to_string()]);
    assert!(!validation.is_valid);
    assert!(
        validation
            .errors
            .iter()
            .any(|e| matches!(e.error_type, ValidationErrorType::OptionMissingArgument))
    );
}

#[test]
fn validate_command_required_option() {
    let result = ProbeResult {
        command: "mytool".to_string(),
        args: vec![],
        exit_code: Some(0),
        timed_out: false,
        help_flag_detected: false,
        usage_blocks: vec![],
        options: vec![OptionSpec {
            short_flags: vec![],
            long_flags: vec!["--required".to_string()],
            description: Some("Required option (required)".to_string()),
            option_type: help_probe::model::OptionType::Boolean,
            required: true,
            default_value: None,
            takes_argument: false,
            argument_name: None,
            choices: vec![],
        }],
        subcommands: vec![],
        arguments: vec![],
        examples: vec![],
        environment_variables: vec![],
        validation_rules: vec![],
        raw_stdout: String::new(),
        raw_stderr: String::new(),
    };

    // Missing required option
    let validation = validate_command(&result, "mytool", &[]);
    assert!(!validation.is_valid);
    assert!(
        validation
            .errors
            .iter()
            .any(|e| matches!(e.error_type, ValidationErrorType::MissingRequiredOption))
    );
}

#[test]
fn validate_command_too_many_arguments() {
    let result = ProbeResult {
        command: "mytool".to_string(),
        args: vec![],
        exit_code: Some(0),
        timed_out: false,
        help_flag_detected: false,
        usage_blocks: vec![],
        options: vec![],
        subcommands: vec![],
        arguments: vec![ArgumentSpec {
            name: "FILE".to_string(),
            description: Some("Input file".to_string()),
            required: true,
            variadic: false,
            arg_type: Some(ArgumentType::Path),
            placeholder: Some("<FILE>".to_string()),
        }],
        examples: vec![],
        environment_variables: vec![],
        validation_rules: vec![],
        raw_stdout: String::new(),
        raw_stderr: String::new(),
    };

    // Too many arguments
    let validation = validate_command(
        &result,
        "mytool",
        &["file1.txt".to_string(), "file2.txt".to_string()],
    );
    assert!(!validation.is_valid);
    assert!(
        validation
            .errors
            .iter()
            .any(|e| matches!(e.error_type, ValidationErrorType::TooManyArguments))
    );
}

#[test]
fn validate_command_invalid_argument_type() {
    let result = ProbeResult {
        command: "mytool".to_string(),
        args: vec![],
        exit_code: Some(0),
        timed_out: false,
        help_flag_detected: false,
        usage_blocks: vec![],
        options: vec![],
        subcommands: vec![],
        arguments: vec![ArgumentSpec {
            name: "PORT".to_string(),
            description: Some("Port number".to_string()),
            required: true,
            variadic: false,
            arg_type: Some(ArgumentType::Number),
            placeholder: Some("<PORT>".to_string()),
        }],
        examples: vec![],
        environment_variables: vec![],
        validation_rules: vec![],
        raw_stdout: String::new(),
        raw_stderr: String::new(),
    };

    // Invalid argument type (string instead of number)
    let validation = validate_command(&result, "mytool", &["not-a-number".to_string()]);
    // This should generate a warning, not an error
    assert!(
        validation
            .warnings
            .iter()
            .any(|e| matches!(e.error_type, ValidationErrorType::InvalidArgumentType))
    );
}

#[test]
fn validate_command_option_unexpected_argument() {
    let result = ProbeResult {
        command: "mytool".to_string(),
        args: vec![],
        exit_code: Some(0),
        timed_out: false,
        help_flag_detected: false,
        usage_blocks: vec![],
        options: vec![OptionSpec {
            short_flags: vec!["-v".to_string()],
            long_flags: vec!["--verbose".to_string()],
            description: Some("Verbose output".to_string()),
            option_type: help_probe::model::OptionType::Boolean,
            required: false,
            default_value: None,
            takes_argument: false,
            argument_name: None,
            choices: vec![],
        }],
        subcommands: vec![],
        arguments: vec![],
        examples: vec![],
        environment_variables: vec![],
        validation_rules: vec![],
        raw_stdout: String::new(),
        raw_stderr: String::new(),
    };

    // Boolean option with unexpected argument
    let validation = validate_command(&result, "mytool", &["--verbose=yes".to_string()]);
    // This should generate a warning
    assert!(
        validation
            .warnings
            .iter()
            .any(|e| matches!(e.error_type, ValidationErrorType::OptionUnexpectedArgument))
    );
}

#[test]
fn validate_command_subcommand() {
    use help_probe::model::SubcommandSpec;

    let result = ProbeResult {
        command: "mytool".to_string(),
        args: vec![],
        exit_code: Some(0),
        timed_out: false,
        help_flag_detected: false,
        usage_blocks: vec![],
        options: vec![],
        subcommands: vec![SubcommandSpec {
            name: "build".to_string(),
            description: Some("Build the project".to_string()),
            full_path: "build".to_string(),
            parent: None,
            options: Vec::new(),
            arguments: Vec::new(),
            subcommands: Vec::new(),
        }],
        arguments: vec![],
        examples: vec![],
        environment_variables: vec![],
        validation_rules: vec![],
        raw_stdout: String::new(),
        raw_stderr: String::new(),
    };

    // Valid subcommand - "build" is recognized as a subcommand
    // The validation should pass since "build" is a known subcommand
    let validation = validate_command(&result, "mytool", &["build".to_string()]);
    // Subcommand validation should not produce errors for known subcommands
    assert!(
        validation
            .errors
            .iter()
            .all(|e| !matches!(e.error_type, ValidationErrorType::UnknownSubcommand))
    );

    // Test with no subcommand - should also be valid (no required args)
    let validation = validate_command(&result, "mytool", &[]);
    assert!(validation.is_valid);
}