320 lines
11 KiB
Rust
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());
|
||
}
|
||
}
|
||
}
|