use super::*;
#[test]
fn vlm_solver_defaults_when_env_returns_none() {
let s = VlmCaptchaSolver::new_with_env(|_| None);
assert_eq!(s.endpoint, "http://localhost:11434");
assert_eq!(s.model, "qwen3-vl:8b-thinking");
}
#[test]
fn vlm_solver_env_overrides_defaults() {
let s = VlmCaptchaSolver::new_with_env(|k| match k {
"CAPTCHAFORGE_VLM_ENDPOINT" => Some("http://env-host:11434".into()),
"CAPTCHAFORGE_VLM_MODEL" => Some("env-model:latest".into()),
_ => None,
});
assert_eq!(s.endpoint, "http://env-host:11434");
assert_eq!(s.model, "env-model:latest");
}
#[test]
fn vlm_solver_builder_overrides_env() {
let s = VlmCaptchaSolver::new_with_env(|k| match k {
"CAPTCHAFORGE_VLM_ENDPOINT" => Some("http://env-host:11434".into()),
"CAPTCHAFORGE_VLM_MODEL" => Some("env-model:latest".into()),
_ => None,
})
.with_endpoint("http://builder:11434")
.with_model("builder-model:7b");
assert_eq!(s.endpoint, "http://builder:11434");
assert_eq!(s.model, "builder-model:7b");
}
#[test]
fn vlm_solver_empty_env_falls_through_to_defaults() {
let s = VlmCaptchaSolver::new_with_env(|_| Some(String::new()));
assert_eq!(s.endpoint, "http://localhost:11434");
assert_eq!(s.model, "qwen3-vl:8b-thinking");
}
#[test]
fn vlm_solver_env_constants_match_documented_names() {
assert_eq!(ENV_VLM_ENDPOINT, "CAPTCHAFORGE_VLM_ENDPOINT");
assert_eq!(ENV_VLM_MODEL, "CAPTCHAFORGE_VLM_MODEL");
assert_eq!(ENV_ANTHROPIC_KEY, "ANTHROPIC_API_KEY");
assert_eq!(ENV_OPENAI_KEY, "OPENAI_API_KEY");
}
#[test]
fn vlm_solver_no_api_key_picks_ollama() {
let s = VlmCaptchaSolver::new_with_env(|_| None);
assert_eq!(s.provider, VlmProvider::Ollama);
assert!(s.api_key.is_none());
assert_eq!(s.endpoint, "http://localhost:11434");
assert_eq!(s.model, "qwen3-vl:8b-thinking");
}
#[test]
fn vlm_solver_anthropic_api_key_picks_anthropic() {
let s = VlmCaptchaSolver::new_with_env(|k| match k {
"ANTHROPIC_API_KEY" => Some("sk-ant-test".into()),
_ => None,
});
assert_eq!(s.provider, VlmProvider::Anthropic);
assert_eq!(s.api_key.as_deref(), Some("sk-ant-test"));
assert_eq!(s.endpoint, "https://api.anthropic.com");
assert_eq!(s.model, "claude-haiku-4-5");
}
#[test]
fn vlm_solver_openai_api_key_picks_openai_when_no_anthropic() {
let s = VlmCaptchaSolver::new_with_env(|k| match k {
"OPENAI_API_KEY" => Some("sk-openai-test".into()),
_ => None,
});
assert_eq!(s.provider, VlmProvider::OpenAI);
assert_eq!(s.api_key.as_deref(), Some("sk-openai-test"));
assert_eq!(s.endpoint, "https://api.openai.com");
assert_eq!(s.model, "gpt-4o-mini");
}
#[test]
fn vlm_solver_anthropic_wins_over_openai_when_both_set() {
let s = VlmCaptchaSolver::new_with_env(|k| match k {
"ANTHROPIC_API_KEY" => Some("sk-ant-test".into()),
"OPENAI_API_KEY" => Some("sk-openai-test".into()),
_ => None,
});
assert_eq!(s.provider, VlmProvider::Anthropic);
assert_eq!(s.api_key.as_deref(), Some("sk-ant-test"));
}
#[test]
fn vlm_solver_with_provider_overrides_env_detection() {
let s = VlmCaptchaSolver::new_with_env(|k| match k {
"ANTHROPIC_API_KEY" => Some("sk-ant-test".into()),
_ => None,
})
.with_provider(VlmProvider::Ollama);
assert_eq!(s.provider, VlmProvider::Ollama);
}
#[test]
fn vlm_solver_with_api_key_overrides_env_detection() {
let s = VlmCaptchaSolver::new_with_env(|_| None).with_api_key("override-key");
assert_eq!(s.api_key.as_deref(), Some("override-key"));
}
#[test]
fn vlm_solver_empty_anthropic_key_falls_through_to_ollama() {
let s = VlmCaptchaSolver::new_with_env(|k| match k {
"ANTHROPIC_API_KEY" => Some(String::new()),
_ => None,
});
assert_eq!(s.provider, VlmProvider::Ollama);
}
#[test]
fn vlm_provider_default_endpoints_and_models_consistent() {
assert_eq!(VlmProvider::Ollama.default_endpoint(), DEFAULT_OLLAMA_BASE);
assert_eq!(VlmProvider::Ollama.default_model(), DEFAULT_OLLAMA_MODEL);
assert_eq!(
VlmProvider::Anthropic.default_endpoint(),
DEFAULT_ANTHROPIC_BASE
);
assert_eq!(
VlmProvider::Anthropic.default_model(),
DEFAULT_ANTHROPIC_MODEL
);
assert_eq!(VlmProvider::OpenAI.default_endpoint(), DEFAULT_OPENAI_BASE);
assert_eq!(VlmProvider::OpenAI.default_model(), DEFAULT_OPENAI_MODEL);
assert_eq!(
VlmProvider::LlamaCpp.default_endpoint(),
DEFAULT_LLAMACPP_BASE
);
assert_eq!(
VlmProvider::LlamaCpp.default_model(),
DEFAULT_LLAMACPP_MODEL
);
}
#[test]
fn vlm_solver_llamacpp_endpoint_picks_llamacpp() {
let s = VlmCaptchaSolver::new_with_env(|k| match k {
"LLAMACPP_VLM_ENDPOINT" => Some("http://gpu:8000".into()),
_ => None,
});
assert_eq!(s.provider, VlmProvider::LlamaCpp);
assert!(s.api_key.is_none());
assert_eq!(s.endpoint, "http://gpu:8000");
assert_eq!(s.model, "qwen3.6-35b-a3b");
}
#[test]
fn vlm_solver_llamacpp_env_constant_matches_documented_name() {
assert_eq!(ENV_LLAMACPP_ENDPOINT, "LLAMACPP_VLM_ENDPOINT");
}
#[test]
fn vlm_solver_custom_endpoint() {
let s = VlmCaptchaSolver::new().with_endpoint("http://ollama.internal:11434");
assert_eq!(s.endpoint, "http://ollama.internal:11434");
}
#[test]
fn vlm_solver_custom_model() {
let s = VlmCaptchaSolver::new().with_model("llava:13b");
assert_eq!(s.model, "llava:13b");
}
#[test]
fn vlm_solver_chained_builders() {
let s = VlmCaptchaSolver::new()
.with_endpoint("http://gpu-box:11434")
.with_model("qwen3-vl:8b-thinking");
assert_eq!(s.endpoint, "http://gpu-box:11434");
assert_eq!(s.model, "qwen3-vl:8b-thinking");
}
#[test]
fn grid_prompt_includes_task() {
let prompt = VlmCaptchaSolver::grid_prompt("Select all traffic lights");
assert!(prompt.contains("Select all traffic lights"));
assert!(prompt.contains("selected"));
assert!(prompt.contains("confidence"));
}
#[test]
fn text_captcha_prompt_is_valid() {
let prompt = VlmCaptchaSolver::text_captcha_prompt();
assert!(prompt.contains("text"));
assert!(prompt.contains("confidence"));
}
#[test]
fn click_target_prompt_includes_target() {
let prompt = VlmCaptchaSolver::click_target_prompt("submit button");
assert!(prompt.contains("submit button"));
assert!(prompt.contains('"'));
}
#[test]
fn extract_json_from_plain_text() {
let s = r#"Some preamble {"key": "value"} trailing"#;
assert_eq!(extract_json(s), Some(r#"{"key": "value"}"#));
}
#[test]
fn extract_json_from_markdown_fence() {
let s = "```json\n{\"answer\": 42}\n```";
assert_eq!(extract_json(s), Some("{\"answer\": 42}"));
}
#[test]
fn extract_json_no_json_returns_none() {
assert_eq!(extract_json("no json here"), None);
}
#[test]
fn openai_body_disable_thinking_adds_chat_template_kwargs() {
let body = openai_request_body("qwen3.6-35b-a3b", "QUJD", "solve", true);
assert_eq!(
body["chat_template_kwargs"]["enable_thinking"],
serde_json::json!(false)
);
assert_eq!(body["model"], "qwen3.6-35b-a3b");
assert!(body["messages"][0]["content"][1]["image_url"]["url"]
.as_str()
.unwrap()
.starts_with("data:image/png;base64,QUJD"));
}
#[test]
fn openai_body_without_disable_thinking_omits_kwargs() {
let body = openai_request_body("gpt-4o-mini", "QUJD", "solve", false);
assert!(body.get("chat_template_kwargs").is_none());
}