use crate::brain::provider::{LLMRequest, LLMResponse, Provider, ProviderError, ProviderStream};
use crate::config::{Config, ProviderConfig};
use crate::tests::agent_service_mocks::create_test_service_with_provider;
use async_trait::async_trait;
use std::sync::Arc;
struct PinnedProvider;
#[async_trait]
impl Provider for PinnedProvider {
async fn complete(
&self,
_request: LLMRequest,
) -> crate::brain::provider::error::Result<LLMResponse> {
Err(ProviderError::RateLimitExceeded(
"pinned mock: daily quota spent".to_string(),
))
}
async fn stream(
&self,
_request: LLMRequest,
) -> crate::brain::provider::error::Result<ProviderStream> {
Err(ProviderError::RateLimitExceeded(
"pinned mock: daily quota spent".to_string(),
))
}
fn name(&self) -> &'static str {
"pinned"
}
fn default_model(&self) -> &'static str {
"pinned-model"
}
fn supported_models(&self) -> Vec<String> {
vec!["pinned-model".to_string()]
}
fn context_window(&self, _model: &str) -> Option<u32> {
Some(4096)
}
fn calculate_cost(&self, _model: &str, _input: u32, _output: u32) -> f64 {
0.0
}
}
fn request() -> LLMRequest {
LLMRequest {
model: "pinned-model".to_string(),
messages: vec![],
system: None,
system_suffix: None,
max_tokens: None,
temperature: None,
tools: None,
stream: false,
metadata: None,
working_directory: None,
session_id: None,
}
}
fn config_with_pinned() -> Config {
let mut cfg = Config::default();
let mut map = std::collections::BTreeMap::new();
map.insert(
"pinned".to_string(),
ProviderConfig {
enabled: true,
api_key: Some("test-key-not-a-real-one".to_string()),
base_url: Some("http://127.0.0.1:1/v1".to_string()),
default_model: Some("pinned-model".to_string()),
..Default::default()
},
);
cfg.providers.custom = Some(map);
cfg
}
async fn sticky_on_fallback() -> (crate::brain::agent::AgentService, uuid::Uuid) {
let (svc, sid) = create_test_service_with_provider(Arc::new(PinnedProvider)).await;
svc.set_fallback_providers_for_test(vec![Arc::new(
crate::tests::agent_service_mocks::MockProvider,
)]);
svc.swap_provider_for_session(sid, Arc::new(PinnedProvider), "pinned-model");
let chain = svc.provider_for_session(sid);
assert!(
chain.is_fallback_chain(),
"the pinned provider must be wrapped in a chain for this test to mean anything"
);
chain
.complete(request())
.await
.expect("the mock fallback answers, so the chain must promote it");
(svc, sid)
}
#[tokio::test]
async fn fast_path_skips_provider_rebuild() {
let _cooldown = crate::tests::telegram_cooldown_lock::guard().await;
let previous = Config::current();
Config::set_current(config_with_pinned());
let (svc, sid) = sticky_on_fallback().await;
assert_eq!(
svc.provider_name_for_session(sid),
"mock",
"the chain should be sticky on the fallback after the pinned primary failed"
);
assert_eq!(
svc.provider_for_session(sid).name(),
"pinned",
"the chain must still report the configured primary"
);
crate::channels::commands::sync_provider_for_session(
&svc,
sid,
Some("pinned"),
Some("pinned-model"),
)
.await;
assert_eq!(
svc.provider_name_for_session(sid),
"mock",
"sync rebuilt the provider chain for a session that had not changed \
provider: the fast path compared against the sticky fallback instead of \
the configured primary (#2184), so every inbound message paid a full \
provider rebuild and threw away the chain's cooldown record"
);
Config::set_current((*previous).clone());
}
#[tokio::test]
async fn a_changed_provider_still_swaps() {
let _cooldown = crate::tests::telegram_cooldown_lock::guard().await;
let previous = Config::current();
let mut cfg = config_with_pinned();
cfg.providers
.custom
.as_mut()
.expect("config_with_pinned sets customs")
.insert(
"other".to_string(),
ProviderConfig {
enabled: true,
api_key: Some("test-key-not-a-real-one".to_string()),
base_url: Some("http://127.0.0.1:2/v1".to_string()),
default_model: Some("other-model".to_string()),
..Default::default()
},
);
Config::set_current(cfg);
let (svc, sid) = sticky_on_fallback().await;
assert_eq!(svc.provider_for_session(sid).name(), "pinned");
crate::channels::commands::sync_provider_for_session(
&svc,
sid,
Some("other"),
Some("other-model"),
)
.await;
assert_eq!(
svc.provider_for_session(sid).name(),
"other",
"a genuine provider change must still be restored, not skipped by the \
fast path"
);
assert_eq!(
svc.provider_name_for_session(sid),
"other",
"the restored chain must start on the new primary, not stay sticky on \
the previous fallback"
);
Config::set_current((*previous).clone());
}