use super::*;
#[cfg(test)]
#[path = "learn_validate_tests.rs"]
mod validate;
#[test]
fn test_strip_fences_toml() {
let input = "```toml\ncommand_match = \"test\"\n```";
assert_eq!(strip_fences(input), "command_match = \"test\"");
}
#[test]
fn test_strip_fences_plain() {
let input = "```\ncommand_match = \"test\"\n```";
assert_eq!(strip_fences(input), "command_match = \"test\"");
}
#[test]
fn test_strip_fences_none() {
let input = "command_match = \"test\"";
assert_eq!(strip_fences(input), "command_match = \"test\"");
}
#[test]
fn test_strip_fences_whitespace_preserved() {
let input = "```toml\n\ncommand_match = \"test\"\n\n```";
let result = strip_fences(input);
assert!(
result.contains("command_match"),
"content must be preserved"
);
}
#[test]
fn test_validate_valid_toml() {
let toml = r#"
command_match = "^mytest"
[success]
pattern = '(?P<n>\d+) passed'
summary = "{n} passed"
"#;
assert!(validate_pattern_toml(toml).is_ok());
}
#[test]
fn test_validate_invalid_regex() {
let toml = r#"
command_match = "[invalid"
"#;
assert!(validate_pattern_toml(toml).is_err());
}
#[test]
fn test_validate_valid_toml_no_success() {
let toml = r#"command_match = "^cargo""#;
assert!(validate_pattern_toml(toml).is_ok());
}
#[test]
fn test_validate_invalid_toml_syntax() {
let toml = "this is not valid = [toml";
assert!(validate_pattern_toml(toml).is_err());
}
#[test]
fn test_validate_missing_command_match() {
let toml = r#"
[success]
pattern = "ok"
summary = "done"
"#;
assert!(validate_pattern_toml(toml).is_err());
}
#[test]
fn test_validate_invalid_command_match_regex() {
let toml = r#"command_match = "[invalid_regex""#;
assert!(validate_pattern_toml(toml).is_err());
}
#[test]
fn test_validate_invalid_success_pattern_regex() {
let toml = r#"
command_match = "^cargo"
[success]
pattern = "[invalid"
summary = "done"
"#;
assert!(validate_pattern_toml(toml).is_err());
}
#[test]
fn test_validate_toml_with_valid_success_pattern() {
let toml = "command_match = \"^pytest\"\n[success]\npattern = '(?P<n>\\d+) passed'\nsummary = \"{n} passed\"";
assert!(validate_pattern_toml(toml).is_ok());
}
#[test]
fn test_truncate_for_prompt() {
let short = "hello";
assert_eq!(truncate_for_prompt(short), "hello");
let long = "x".repeat(5000);
assert_eq!(truncate_for_prompt(&long).len(), 4000);
}
#[test]
fn test_truncate_for_prompt_boundary() {
let exact = "a".repeat(4000);
assert_eq!(truncate_for_prompt(&exact).len(), 4000);
let over = "a".repeat(4001);
assert_eq!(truncate_for_prompt(&over).len(), 4000);
}
#[test]
fn test_truncate_utf8_multibyte_boundary() {
let s = "££££"; let result = truncate_utf8(s, 5);
assert_eq!(result.len(), 4);
assert!(result.is_ascii() || std::str::from_utf8(result.as_bytes()).is_ok());
assert_eq!(result, "££");
}
#[test]
fn test_truncate_utf8_exact_boundary() {
let s = "hello"; assert_eq!(truncate_utf8(s, 5), "hello");
assert_eq!(truncate_utf8(s, 10), "hello");
}
#[test]
fn test_label_extraction() {
assert_eq!(label("pytest -x"), "pytest");
assert_eq!(label("/usr/bin/cargo test"), "cargo-test");
}
#[test]
fn test_label_path_extraction() {
assert_eq!(label("/usr/local/bin/rustc"), "rustc");
assert_eq!(label("./target/release/oo"), "oo");
}
#[test]
fn test_label_empty_command() {
assert_eq!(label(""), "unknown");
}
#[test]
fn test_label_subcommand_included() {
assert_eq!(label("cargo fmt --check"), "cargo-fmt");
assert_eq!(label("cargo clippy -- -D warnings"), "cargo-clippy");
assert_eq!(label("npm run build"), "npm-run");
assert_eq!(label("cargo test"), "cargo-test");
}
#[test]
fn test_label_flag_excluded() {
assert_eq!(label("pytest -x"), "pytest");
assert_eq!(label("cargo --verbose test"), "cargo");
}
#[test]
fn test_label_sanitizes_unsafe_chars() {
assert_eq!(label("git some/path/arg"), "git-somepatharg");
assert_eq!(label("cargo /absolute/path"), "cargo-absolutepath");
assert_eq!(label("git subcommand=val"), "git-subcommandval");
assert_eq!(label("cmd ../etc/passwd"), "cmd-etcpasswd");
assert_eq!(label("rustc --foo --bar"), "rustc");
}
#[test]
fn test_default_config_has_valid_fields() {
let config = LearnConfig::default();
assert!(!config.provider.is_empty(), "provider must not be empty");
assert!(!config.model.is_empty(), "model must not be empty");
assert!(
!config.api_key_env.is_empty(),
"api_key_env must not be empty"
);
}
#[test]
fn test_detect_provider_no_keys_defaults_to_anthropic() {
let config = detect_provider_with(|_| None);
assert_eq!(config.provider, "anthropic");
assert_eq!(config.api_key_env, "ANTHROPIC_API_KEY");
assert_eq!(config.model, "claude-haiku-4-5");
}
#[test]
fn test_detect_provider_anthropic_model_name() {
let config = detect_provider_with(|key| {
if key == "ANTHROPIC_API_KEY" {
Some("test-key".into())
} else {
None
}
});
assert_eq!(config.provider, "anthropic");
assert_eq!(
config.model, "claude-haiku-4-5",
"Anthropic model must use the stable alias, not the dated snapshot"
);
}
#[test]
fn test_load_learn_config_no_file_returns_default() {
match load_learn_config() {
Ok(c) => {
assert!(!c.provider.is_empty());
assert!(!c.model.is_empty());
assert!(!c.api_key_env.is_empty());
}
Err(_) => {} }
}
#[test]
fn test_patterns_dir_is_under_config_dir() {
let dir = patterns_dir();
let s = dir.to_string_lossy();
assert!(s.ends_with("oo/patterns"), "got: {s}");
}
#[test]
fn test_run_background_missing_file_returns_err() {
assert!(run_background("/tmp/__oo_no_such_file_xyz__.json").is_err());
}
#[test]
fn test_run_background_invalid_json_returns_err() {
let tmp = tempfile::NamedTempFile::new().expect("tempfile");
std::fs::write(tmp.path(), b"not valid json {{{").expect("write");
assert!(run_background(tmp.path().to_str().expect("utf8 path")).is_err());
}
#[test]
fn test_run_background_valid_json_no_api_key() {
let tmp = tempfile::NamedTempFile::new().expect("tempfile");
let data = serde_json::json!({"command": "echo hello", "output": "hello", "exit_code": 0});
std::fs::write(tmp.path(), data.to_string()).expect("write");
let _ = run_background(tmp.path().to_str().expect("utf8 path"));
}
#[test]
fn test_anthropic_url_validation() {
assert!(validate_anthropic_url("https://api.anthropic.com/v1/messages").is_ok());
assert!(validate_anthropic_url("https://custom.example.com/api").is_ok());
assert!(validate_anthropic_url("http://localhost:8000/api").is_ok());
assert!(validate_anthropic_url("http://localhost/api").is_ok());
assert!(validate_anthropic_url("http://127.0.0.1:8000/api").is_ok());
assert!(validate_anthropic_url("http://127.0.0.1/api").is_ok());
let result = validate_anthropic_url("http://api.example.com/v1/messages");
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("ANTHROPIC_API_URL must use HTTPS"));
assert!(err_msg.contains("got: http://api.example.com/v1/messages"));
let result = validate_anthropic_url("http://192.168.1.1/api");
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("got: http://192.168.1.1/api"));
let result = validate_anthropic_url("http://example.com");
assert!(result.is_err());
let result = validate_anthropic_url("http://localhost.evil.com/api");
assert!(result.is_err());
}
#[test]
fn test_learn_status_written_on_failure() {
let dir = tempfile::TempDir::new().expect("tempdir");
let status_path = dir.path().join("learn-status.log");
crate::commands::write_learn_status_failure(&status_path, "cargo-test", "no API key set")
.expect("write must succeed");
let content = std::fs::read_to_string(&status_path).expect("read");
assert!(
content.starts_with("FAILED cargo-test:"),
"status must start with FAILED prefix: {content}"
);
assert!(
content.contains("no API key set"),
"status must contain error message: {content}"
);
let tmp = tempfile::NamedTempFile::new().expect("tempfile");
let data = serde_json::json!({"command": "echo hello", "output": "hello", "exit_code": 0});
std::fs::write(tmp.path(), data.to_string()).expect("write");
let _ = run_background(tmp.path().to_str().expect("utf8 path"));
}