Files
zeroclaw/tests/quota_tools_live.rs
T

320 lines
11 KiB
Rust

//! Live E2E tests for quota tools with real auth profiles.
//!
//! These tests require real auth-profiles.json at ~/.zeroclaw/auth-profiles.json
//! Run with: cargo test --test quota_tools_live -- --nocapture
//! Or: cargo test --test quota_tools_live -- --nocapture --ignored (for ignored tests)
use serde_json::json;
use std::path::PathBuf;
use std::sync::Arc;
use zeroclaw::config::Config;
use zeroclaw::tools::quota_tools::{
CheckProviderQuotaTool, EstimateQuotaCostTool, SwitchProviderTool,
};
use zeroclaw::tools::Tool;
fn zeroclaw_dir() -> PathBuf {
PathBuf::from(std::env::var("HOME").expect("HOME not set")).join(".zeroclaw")
}
fn has_auth_profiles() -> bool {
zeroclaw_dir().join("auth-profiles.json").exists()
}
fn live_config() -> Config {
Config {
workspace_dir: zeroclaw_dir(),
config_path: zeroclaw_dir().join("config.toml"),
..Config::default()
}
}
// ============================================================================
// Test 1: Какие модели доступны?
// ============================================================================
#[tokio::test]
async fn live_check_all_providers_status() {
if !has_auth_profiles() {
eprintln!("SKIP: no auth-profiles.json");
return;
}
let tool = CheckProviderQuotaTool::new(Arc::new(live_config()));
let result = tool.execute(json!({})).await.unwrap();
println!("\n=== Test: Какие модели доступны? ===");
println!("{}", result.output);
assert!(result.success, "Tool execution failed");
assert!(
result.output.contains("Quota Status"),
"Missing 'Quota Status' header"
);
}
// ============================================================================
// Test 2: Gemini провайдер
// ============================================================================
#[tokio::test]
async fn live_check_gemini_quota() {
if !has_auth_profiles() {
eprintln!("SKIP: no auth-profiles.json");
return;
}
let tool = CheckProviderQuotaTool::new(Arc::new(live_config()));
let result = tool.execute(json!({"provider": "gemini"})).await.unwrap();
println!("\n=== Test: Gemini Quota ===");
println!("{}", result.output);
assert!(result.success, "Tool execution failed");
assert!(
result.output.contains("Quota Status"),
"Missing quota header"
);
}
// ============================================================================
// Test 3: OpenAI Codex провайдер
// ============================================================================
#[tokio::test]
async fn live_check_openai_codex_quota() {
if !has_auth_profiles() {
eprintln!("SKIP: no auth-profiles.json");
return;
}
let tool = CheckProviderQuotaTool::new(Arc::new(live_config()));
let result = tool
.execute(json!({"provider": "openai-codex"}))
.await
.unwrap();
println!("\n=== Test: OpenAI Codex Quota ===");
println!("{}", result.output);
assert!(result.success, "Tool execution failed");
}
// ============================================================================
// Test 4: Переключение провайдера
// ============================================================================
#[tokio::test]
async fn live_switch_provider() {
// Use a temp dir so we don't mutate the real config
let tmp = tempfile::TempDir::new().unwrap();
let config = Config {
workspace_dir: tmp.path().to_path_buf(),
config_path: tmp.path().join("config.toml"),
..Config::default()
};
config.save().await.unwrap();
let tool = SwitchProviderTool::new(Arc::new(config));
// Switch to gemini
let result = tool
.execute(json!({
"provider": "gemini",
"model": "gemini-2.5-flash",
"reason": "openai-codex rate limited"
}))
.await
.unwrap();
println!("\n=== Test: Переключение на Gemini ===");
println!("{}", result.output);
assert!(result.success);
assert!(result.output.contains("gemini"));
assert!(result.output.contains("rate limited"));
// Switch to openai-codex
let result = tool
.execute(json!({
"provider": "openai-codex",
"model": "o3-mini",
"reason": "gemini quota exhausted"
}))
.await
.unwrap();
println!("\n=== Test: Переключение на OpenAI Codex ===");
println!("{}", result.output);
assert!(result.success);
assert!(result.output.contains("openai-codex"));
}
// ============================================================================
// Test 5: Оценка затрат
// ============================================================================
#[tokio::test]
async fn live_estimate_quota_cost() {
let tool = EstimateQuotaCostTool;
let result = tool
.execute(json!({
"operation": "chat_response",
"estimated_tokens": 10000,
"parallel_count": 3
}))
.await
.unwrap();
println!("\n=== Test: Оценка затрат (10k tokens x 3) ===");
println!("{}", result.output);
assert!(result.success);
assert!(result.output.contains("30000")); // 10000 * 3
assert!(result.output.contains("$"));
}
// ============================================================================
// Test 6: Все 3 инструмента зарегистрированы с правильными именами
// ============================================================================
#[test]
fn tools_have_correct_names() {
let quota_tool = CheckProviderQuotaTool::new(Arc::new(Config::default()));
let switch_tool = SwitchProviderTool::new(Arc::new(Config::default()));
let estimate_tool = EstimateQuotaCostTool;
assert_eq!(quota_tool.name(), "check_provider_quota");
assert_eq!(switch_tool.name(), "switch_provider");
assert_eq!(estimate_tool.name(), "estimate_quota_cost");
}
// ============================================================================
// Test 7: Schemas are valid JSON with required fields
// ============================================================================
#[test]
fn tools_have_valid_schemas() {
let quota_tool = CheckProviderQuotaTool::new(Arc::new(Config::default()));
let switch_tool = SwitchProviderTool::new(Arc::new(Config::default()));
let estimate_tool = EstimateQuotaCostTool;
// All tools should have object schemas with properties
for (name, schema) in [
("check_provider_quota", quota_tool.parameters_schema()),
("switch_provider", switch_tool.parameters_schema()),
("estimate_quota_cost", estimate_tool.parameters_schema()),
] {
assert!(
schema["type"] == "object",
"{name}: schema type should be 'object'"
);
assert!(
schema["properties"].is_object(),
"{name}: schema should have properties"
);
}
// switch_provider requires "provider"
let switch_schema = switch_tool.parameters_schema();
let required = switch_schema["required"].as_array().unwrap();
assert!(
required.contains(&json!("provider")),
"switch_provider should require 'provider'"
);
}
// ============================================================================
// Test 8: Error parser works with real-world error payloads
// ============================================================================
#[test]
fn error_parser_real_world_payloads() {
use zeroclaw::providers::error_parser;
// Real OpenAI Codex usage_limit_reached error
let payload_1 = r#"{
"error": {
"type": "usage_limit_reached",
"message": "The usage limit has been reached for this organization on o3-mini-2025-01-31 in the current billing period. Upgrade to the next usage tier by adding more funds to your account.",
"plan_type": "enterprise",
"resets_at": 1772087057
}
}"#;
let info = error_parser::parse_openai_codex_error(payload_1).unwrap();
assert_eq!(info.error_type, "usage_limit_reached");
assert_eq!(info.plan_type, Some("enterprise".to_string()));
assert!(info.resets_at.is_some());
let reset_time = info.resets_at.unwrap();
println!(
"\n=== Error Parser: resets_at decoded ===\nTimestamp: {}\nHuman: {}",
reset_time.timestamp(),
reset_time.format("%Y-%m-%d %H:%M:%S UTC")
);
// Real OpenAI rate_limit_exceeded error (without resets_at)
let payload_2 = r#"{
"error": {
"type": "rate_limit_exceeded",
"message": "Rate limit reached for default-model in organization org-xxx on requests per min (RPM): Limit 3, Used 3, Requested 1. Please try again in 20s."
}
}"#;
let info = error_parser::parse_openai_codex_error(payload_2).unwrap();
assert_eq!(info.error_type, "rate_limit_exceeded");
assert!(info.plan_type.is_none());
assert!(info.resets_at.is_none());
println!("Rate limit message: {}", info.message);
// Non-JSON error
let payload_3 = "Internal Server Error";
assert!(error_parser::parse_openai_codex_error(payload_3).is_none());
}
// ============================================================================
// Test 9: tool descriptions mention key capabilities
// ============================================================================
#[test]
fn tool_descriptions_mention_key_capabilities() {
let quota_tool = CheckProviderQuotaTool::new(Arc::new(Config::default()));
let desc = quota_tool.description();
// Should mention rate limit checking
assert!(
desc.contains("rate limit") || desc.contains("quota"),
"Description should mention rate limit or quota"
);
// Should mention model availability
assert!(
desc.contains("available") || desc.contains("model availability"),
"Description should mention model availability"
);
}
// ============================================================================
// Test 10: metadata JSON in output is valid
// ============================================================================
#[tokio::test]
async fn output_contains_valid_metadata_json() {
let tmp = tempfile::TempDir::new().unwrap();
let config = Config {
workspace_dir: tmp.path().to_path_buf(),
config_path: tmp.path().join("config.toml"),
..Config::default()
};
let tool = CheckProviderQuotaTool::new(Arc::new(config));
let result = tool.execute(json!({})).await.unwrap();
// Extract metadata JSON from output
if let Some(start) = result.output.find("<!-- metadata: ") {
let json_start = start + "<!-- metadata: ".len();
if let Some(end) = result.output[json_start..].find(" -->") {
let json_str = &result.output[json_start..json_start + end];
let parsed: serde_json::Value =
serde_json::from_str(json_str).expect("Metadata JSON should be valid");
println!("\n=== Metadata JSON ===");
println!("{}", serde_json::to_string_pretty(&parsed).unwrap());
assert!(parsed["available_providers"].is_array());
assert!(parsed["rate_limited_providers"].is_array());
assert!(parsed["circuit_open_providers"].is_array());
}
}
}