brokecrew/tests/fallback_tests.rs

174 lines
5.1 KiB
Rust

use async_trait::async_trait;
use brokecrew::client::{ClientError, ModelClient};
use brokecrew::persona::{ModelCandidate, Persona, PersonaRegistry, Tier};
use brokecrew::pipeline::PipelineExecutor;
use brokecrew::registry::ModelRegistry;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use tokio::sync::mpsc;
struct FaultInjectingClient {
candidate1_error: Option<ClientError>,
calls: Arc<AtomicUsize>,
}
#[async_trait]
impl ModelClient for FaultInjectingClient {
async fn complete(&self, model: &str, system: &str, user: &str) -> Result<String, ClientError> {
self.calls.fetch_add(1, Ordering::SeqCst);
if model == "candidate-1" {
if let Some(ref err) = self.candidate1_error {
return match err {
ClientError::RateLimited(s) => Err(ClientError::RateLimited(*s)),
ClientError::ServerError { status, message } => Err(ClientError::ServerError {
status: *status,
message: message.clone(),
}),
ClientError::Unauthorized => Err(ClientError::Unauthorized),
ClientError::PaymentRequired => Err(ClientError::PaymentRequired),
_ => Err(ClientError::ModelUnavailable("simulated".into())),
};
}
}
Ok(format!(
"Success from {model} for prompt '{user}' (system: {system})"
))
}
async fn complete_stream(
&self,
model: &str,
system: &str,
user: &str,
chunk_sender: mpsc::Sender<String>,
) -> Result<String, ClientError> {
let reply = self.complete(model, system, user).await?;
let _ = chunk_sender.send(reply.clone()).await;
Ok(reply)
}
}
fn create_resilience_persona() -> Persona {
Persona {
name: "resilient-coder".into(),
system_prompt: "You are coder".into(),
trigger_words: vec!["code".into()],
candidates: vec![
ModelCandidate {
id: "candidate-1".into(),
tier: Tier::Free,
is_stealth: false,
},
ModelCandidate {
id: "candidate-2".into(),
tier: Tier::Free,
is_stealth: false,
},
],
}
}
#[tokio::test]
async fn test_candidate_cascade_on_503_service_unavailable() {
let persona = create_resilience_persona();
let persona_reg = PersonaRegistry::new(vec![persona]);
let model_reg = ModelRegistry::new();
let calls = Arc::new(AtomicUsize::new(0));
let client = FaultInjectingClient {
candidate1_error: Some(ClientError::ServerError {
status: 503,
message: "Overloaded".into(),
}),
calls: calls.clone(),
};
let result = PipelineExecutor::dispatch_persona(
"resilient-coder",
&persona_reg,
&model_reg,
&client,
"write rust code",
false,
false,
None,
)
.await
.expect("fallback to candidate-2 should succeed");
assert_eq!(result.persona, "resilient-coder");
assert_eq!(result.model_used, "candidate-2");
assert!(result.fell_back, "Result must indicate fallback occurred");
assert_eq!(result.attempts.len(), 1);
assert_eq!(result.attempts[0].model_id, "candidate-1");
assert_eq!(
calls.load(Ordering::SeqCst),
2,
"Both candidates must have been called"
);
}
#[tokio::test]
async fn test_candidate_cascade_on_429_rate_limited() {
let persona = create_resilience_persona();
let persona_reg = PersonaRegistry::new(vec![persona]);
let model_reg = ModelRegistry::new();
let calls = Arc::new(AtomicUsize::new(0));
let client = FaultInjectingClient {
candidate1_error: Some(ClientError::RateLimited(15)),
calls: calls.clone(),
};
let result = PipelineExecutor::dispatch_persona(
"resilient-coder",
&persona_reg,
&model_reg,
&client,
"write python code",
false,
false,
None,
)
.await
.expect("fallback to candidate-2 should succeed");
assert_eq!(result.model_used, "candidate-2");
assert!(result.fell_back);
assert_eq!(calls.load(Ordering::SeqCst), 2);
}
#[tokio::test]
async fn test_fail_fast_on_401_unauthorized() {
let persona = create_resilience_persona();
let persona_reg = PersonaRegistry::new(vec![persona]);
let model_reg = ModelRegistry::new();
let calls = Arc::new(AtomicUsize::new(0));
let client = FaultInjectingClient {
candidate1_error: Some(ClientError::Unauthorized),
calls: calls.clone(),
};
let err = PipelineExecutor::dispatch_persona(
"resilient-coder",
&persona_reg,
&model_reg,
&client,
"test query",
false,
false,
None,
)
.await
.unwrap_err();
match err {
ClientError::Unauthorized => (),
_ => panic!("Expected Unauthorized fail-fast, got {:?}", err),
}
// Must NOT call candidate-2 when 401 Unauthorized occurs!
assert_eq!(calls.load(Ordering::SeqCst), 1);
}