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:
Alex Gorevski 2026-02-18 15:28:34 -08:00
parent b43e9eb325
commit 7f03ab77a9
9 changed files with 2272 additions and 8 deletions

View 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()
);
}