use super::support::{CONTENT, SYSTEM, server_returning_json, server_returning_prose};
use crate::test_support::{
cfg_for, fast_retry_chain, request_count, server_failing_with, temp_cache,
};
#[tokio::test]
async fn a_demoted_provider_is_not_contacted_again() {
let dead = server_failing_with(500).await;
let healthy = server_returning_json().await;
let (cache, _dir) = temp_cache();
let chain = fast_retry_chain(&[cfg_for(&dead, "a", 1), cfg_for(&healthy, "b", 1)]);
let first = chain
.complete_json(SYSTEM, "file one", &cache)
.await
.expect("provider 2 answers");
assert_eq!(first.provider, 1);
assert!(
chain.providers()[0].is_down(),
"the head is demoted after failing over"
);
assert_eq!(request_count(&dead).await, 1);
let second = chain
.complete_json(SYSTEM, "file two", &cache)
.await
.expect("provider 2 answers again");
assert_eq!(second.provider, 1);
assert_eq!(
request_count(&dead).await,
1,
"the demoted head must not be contacted a second time"
);
assert_eq!(request_count(&healthy).await, 2);
}
#[tokio::test]
async fn a_skipped_provider_is_reported_with_the_reason_it_went_down() {
let dead = server_failing_with(500).await;
let broken = server_returning_prose().await;
let (cache, _dir) = temp_cache();
let chain = fast_retry_chain(&[cfg_for(&dead, "a", 1), cfg_for(&broken, "b", 1)]);
chain
.complete_json(SYSTEM, "file one", &cache)
.await
.expect_err("the fallback cannot parse");
assert!(chain.providers()[0].is_down());
let err = chain
.complete_json(SYSTEM, "file two", &cache)
.await
.expect_err("both providers still fail");
assert_eq!(err.attempts.len(), 2, "both providers are accounted for");
assert!(
err.attempts[0].skipped,
"the head was skipped, not contacted"
);
assert_eq!(
err.attempts[0].error.status(),
Some(500),
"the recorded reason is the one that demoted it"
);
assert!(!err.attempts[1].skipped);
assert_eq!(
request_count(&dead).await,
1,
"the demoted head was contacted once, on the first file"
);
}
#[tokio::test]
async fn a_fully_demoted_chain_reports_every_provider_without_contacting_any() {
let first = server_failing_with(500).await;
let second = server_failing_with(503).await;
let (cache, _dir) = temp_cache();
let chain = fast_retry_chain(&[cfg_for(&first, "a", 1), cfg_for(&second, "b", 1)]);
chain
.complete_json(SYSTEM, "file one", &cache)
.await
.expect_err("both fail");
assert!(chain.providers()[0].is_down() && chain.providers()[1].is_down());
let err = chain
.complete_json(SYSTEM, "file two", &cache)
.await
.expect_err("both are down");
assert_eq!(err.attempts.len(), 2);
assert!(err.attempts.iter().all(|a| a.skipped));
assert_eq!(request_count(&first).await, 1);
assert_eq!(request_count(&second).await, 1);
}
#[tokio::test]
async fn a_demoted_provider_can_serve_its_cached_answer() {
let dead = server_failing_with(500).await;
let healthy = server_returning_json().await;
let (cache, _dir) = temp_cache();
let chain = fast_retry_chain(&[
cfg_for(&dead, "model-a", 1),
cfg_for(&healthy, "model-b", 1),
]);
chain
.complete_json(SYSTEM, "file one", &cache)
.await
.expect("the fallback answers");
assert!(chain.providers()[0].is_down());
let planted = serde_json::json!({"issues": [], "summary": "clean"});
cache
.put(
&chain.providers()[0].cache_key(&cache, SYSTEM, "file two"),
&planted,
)
.expect("cache write");
let served = chain
.complete_json(SYSTEM, "file two", &cache)
.await
.expect("the fallback answers again");
assert_eq!(
served.provider, 0,
"a cache hit must be served before the provider's demotion is replayed"
);
assert!(served.from_cache);
assert_eq!(
request_count(&healthy).await,
1,
"the fallback is not contacted when the preferred provider has a cache hit"
);
}
#[tokio::test]
async fn a_file_waiting_on_the_limiter_is_skipped_once_the_provider_goes_down() {
let dead = server_failing_with(500).await;
let healthy = server_returning_json().await;
let (cache, _dir) = temp_cache();
let mut head = cfg_for(&dead, "a", 1);
head.max_concurrent = 1;
let chain = fast_retry_chain(&[head, cfg_for(&healthy, "b", 1)]);
let (first, second) = tokio::join!(
chain.complete_json(SYSTEM, "file one", &cache),
chain.complete_json(SYSTEM, "file two", &cache),
);
assert_eq!(first.expect("served").provider, 1);
assert_eq!(second.expect("served").provider, 1);
assert_eq!(
request_count(&dead).await,
1,
"the second file waited on the head's only permit and must be skipped, \
not sent, once the first file demoted it"
);
}
#[tokio::test]
async fn a_request_level_4xx_does_not_demote_the_provider() {
let picky = server_failing_with(400).await;
let healthy = server_returning_json().await;
let (cache, _dir) = temp_cache();
let chain = fast_retry_chain(&[cfg_for(&picky, "a", 1), cfg_for(&healthy, "b", 1)]);
chain
.complete_json(SYSTEM, "file one", &cache)
.await
.expect_err("the 400 fails this file");
assert!(
!chain.providers()[0].is_down(),
"a 400 is about the request, not the endpoint"
);
chain
.complete_json(SYSTEM, "file two", &cache)
.await
.expect_err("and this one is judged on its own merits");
assert_eq!(
request_count(&picky).await,
2,
"the second file must be offered to the head, not stopped by the first file's 400"
);
}
#[tokio::test]
async fn a_request_level_4xx_still_does_not_reach_the_fallback() {
let picky = server_failing_with(400).await;
let healthy = server_returning_json().await;
let (cache, _dir) = temp_cache();
let chain = fast_retry_chain(&[cfg_for(&picky, "a", 1), cfg_for(&healthy, "b", 1)]);
chain
.complete_json(SYSTEM, CONTENT, &cache)
.await
.expect_err("the 400 fails the file");
assert_eq!(request_count(&healthy).await, 0);
}