test: add systematic test coverage for 7 bug pattern groups (#852)
Add ~105 test cases across 7 test groups identified in issue #852: TG1 - Provider resolution (27 tests): Factory resolution, alias mapping, custom URLs, auth styles, credential wiring TG2 - Config persistence (18 tests): Config defaults, TOML roundtrip, agent/memory config, workspace dirs TG3 - Channel routing (14 tests): ChannelMessage identity contracts, SendMessage construction, Channel trait send/listen roundtrip TG4 - Agent loop robustness (12 integration + 14 inline tests): Malformed tool calls, failing tools, iteration limits, empty responses, unicode TG5 - Memory restart (14 tests): Dedup on same key, restart persistence, session scoping, recall, concurrent stores, categories TG6 - Channel message splitting (8+8 inline tests): Code blocks at boundary, long words, emoji, CJK chars, whitespace edge cases TG7 - Provider schema (21 tests): ChatMessage/ToolCall/ChatResponse serialization, tool_call_id preservation, auth style variants Also fixes a bug in split_message_for_telegram() where byte-based indexing could panic on multi-byte characters (emoji, CJK). Now uses char_indices() consistent with the Discord split implementation. Closes #852 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
b43e9eb325
commit
7f03ab77a9
9 changed files with 2272 additions and 8 deletions
244
tests/provider_resolution.rs
Normal file
244
tests/provider_resolution.rs
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
//! TG1: Provider End-to-End Resolution Tests
|
||||
//!
|
||||
//! Prevents: Pattern 1 — Provider configuration & resolution bugs (27% of user bugs).
|
||||
//! Issues: #831, #834, #721, #580, #452, #451, #796, #843
|
||||
//!
|
||||
//! Tests the full pipeline from config values through `create_provider_with_url()`
|
||||
//! to provider construction, verifying factory resolution, URL construction,
|
||||
//! credential wiring, and auth header format.
|
||||
|
||||
use zeroclaw::providers::compatible::{AuthStyle, OpenAiCompatibleProvider};
|
||||
use zeroclaw::providers::{create_provider, create_provider_with_url};
|
||||
|
||||
/// Helper: assert provider creation succeeds
|
||||
fn assert_provider_ok(name: &str, key: Option<&str>, url: Option<&str>) {
|
||||
let result = create_provider_with_url(name, key, url);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"{name} provider should resolve: {}",
|
||||
result.err().map(|e| e.to_string()).unwrap_or_default()
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Factory resolution: each major provider name resolves without error
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn factory_resolves_openai_provider() {
|
||||
assert_provider_ok("openai", Some("test-key"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_resolves_anthropic_provider() {
|
||||
assert_provider_ok("anthropic", Some("test-key"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_resolves_deepseek_provider() {
|
||||
assert_provider_ok("deepseek", Some("test-key"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_resolves_mistral_provider() {
|
||||
assert_provider_ok("mistral", Some("test-key"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_resolves_ollama_provider() {
|
||||
assert_provider_ok("ollama", None, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_resolves_groq_provider() {
|
||||
assert_provider_ok("groq", Some("test-key"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_resolves_xai_provider() {
|
||||
assert_provider_ok("xai", Some("test-key"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_resolves_together_provider() {
|
||||
assert_provider_ok("together", Some("test-key"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_resolves_fireworks_provider() {
|
||||
assert_provider_ok("fireworks", Some("test-key"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_resolves_perplexity_provider() {
|
||||
assert_provider_ok("perplexity", Some("test-key"), None);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Factory resolution: alias variants map to same provider
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn factory_grok_alias_resolves_to_xai() {
|
||||
assert_provider_ok("grok", Some("test-key"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_kimi_alias_resolves_to_moonshot() {
|
||||
assert_provider_ok("kimi", Some("test-key"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_zhipu_alias_resolves_to_glm() {
|
||||
assert_provider_ok("zhipu", Some("test-key"), None);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Custom URL provider creation
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn factory_custom_http_url_resolves() {
|
||||
assert_provider_ok("custom:http://localhost:8080", Some("test-key"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_custom_https_url_resolves() {
|
||||
assert_provider_ok("custom:https://api.example.com/v1", Some("test-key"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_custom_ftp_url_rejected() {
|
||||
let result = create_provider_with_url("custom:ftp://example.com", None, None);
|
||||
assert!(result.is_err(), "ftp scheme should be rejected");
|
||||
let err_msg = result.err().unwrap().to_string();
|
||||
assert!(
|
||||
err_msg.contains("http://") || err_msg.contains("https://"),
|
||||
"error should mention valid schemes: {err_msg}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_custom_empty_url_rejected() {
|
||||
let result = create_provider_with_url("custom:", None, None);
|
||||
assert!(result.is_err(), "empty custom URL should be rejected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_unknown_provider_rejected() {
|
||||
let result = create_provider_with_url("nonexistent_provider_xyz", None, None);
|
||||
assert!(result.is_err(), "unknown provider name should be rejected");
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// OpenAiCompatibleProvider: credential and auth style wiring
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn compatible_provider_bearer_auth_style() {
|
||||
// Construction with Bearer auth should succeed
|
||||
let _provider = OpenAiCompatibleProvider::new(
|
||||
"TestProvider",
|
||||
"https://api.test.com",
|
||||
Some("sk-test-key-12345"),
|
||||
AuthStyle::Bearer,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compatible_provider_xapikey_auth_style() {
|
||||
// Construction with XApiKey auth should succeed
|
||||
let _provider = OpenAiCompatibleProvider::new(
|
||||
"TestProvider",
|
||||
"https://api.test.com",
|
||||
Some("sk-test-key-12345"),
|
||||
AuthStyle::XApiKey,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compatible_provider_custom_auth_header() {
|
||||
// Construction with Custom auth should succeed
|
||||
let _provider = OpenAiCompatibleProvider::new(
|
||||
"TestProvider",
|
||||
"https://api.test.com",
|
||||
Some("sk-test-key-12345"),
|
||||
AuthStyle::Custom("X-Custom-Auth".into()),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compatible_provider_no_credential() {
|
||||
// Construction without credential should succeed (for local providers)
|
||||
let _provider = OpenAiCompatibleProvider::new(
|
||||
"TestLocal",
|
||||
"http://localhost:11434",
|
||||
None,
|
||||
AuthStyle::Bearer,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compatible_provider_base_url_trailing_slash_normalized() {
|
||||
// Construction with trailing slash URL should succeed
|
||||
let _provider = OpenAiCompatibleProvider::new(
|
||||
"TestProvider",
|
||||
"https://api.test.com/v1/",
|
||||
Some("key"),
|
||||
AuthStyle::Bearer,
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Provider with api_url override (simulates #721 - Ollama api_url config)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn factory_ollama_with_custom_api_url() {
|
||||
assert_provider_ok("ollama", None, Some("http://192.168.1.100:11434"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_openai_with_custom_api_url() {
|
||||
assert_provider_ok(
|
||||
"openai",
|
||||
Some("test-key"),
|
||||
Some("https://custom-openai-proxy.example.com/v1"),
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Provider default convenience factory
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn convenience_factory_resolves_major_providers() {
|
||||
for provider_name in &[
|
||||
"openai",
|
||||
"anthropic",
|
||||
"deepseek",
|
||||
"mistral",
|
||||
"groq",
|
||||
"xai",
|
||||
"together",
|
||||
"fireworks",
|
||||
"perplexity",
|
||||
] {
|
||||
let result = create_provider(provider_name, Some("test-key"));
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"convenience factory should resolve {provider_name}: {}",
|
||||
result.err().map(|e| e.to_string()).unwrap_or_default()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convenience_factory_ollama_no_key() {
|
||||
let result = create_provider("ollama", None);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"ollama should not require api key: {}",
|
||||
result.err().map(|e| e.to_string()).unwrap_or_default()
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue