use super::*;
use crate::core::sm::config::SmInferenceConfig;
fn empty_cfg() -> SmInferenceConfig {
SmInferenceConfig {
provider: "auto".to_string(),
sm_model: String::new(),
summary_model: String::new(),
model: String::new(),
fallback: Vec::new(),
temperature: 0.3,
context_token_budget: 24_000,
compaction_model: String::new(),
compressed_context_max_tokens: 4_000,
}
}
#[test]
fn route_anthropic_prefix() {
let (kind, model) =
resolve_provider_and_model("anthropic/claude-sonnet-4-6", ProviderKind::OpenRouter);
assert_eq!(kind, ProviderKind::Anthropic);
assert_eq!(model, "claude-sonnet-4-6");
}
#[test]
fn route_bedrock_prefix() {
let (kind, model) = resolve_provider_and_model(
"bedrock/us.anthropic.claude-sonnet-4-6",
ProviderKind::Anthropic,
);
assert_eq!(kind, ProviderKind::Bedrock);
assert_eq!(model, "us.anthropic.claude-sonnet-4-6");
}
#[test]
fn route_openrouter_prefix() {
let (kind, model) =
resolve_provider_and_model("openrouter/anthropic/claude-haiku", ProviderKind::Bedrock);
assert_eq!(kind, ProviderKind::OpenRouter);
assert_eq!(model, "anthropic/claude-haiku");
}
#[test]
fn route_bare_uses_default() {
let (kind, model) = resolve_provider_and_model("claude-sonnet-4-6", ProviderKind::OpenRouter);
assert_eq!(kind, ProviderKind::OpenRouter);
assert_eq!(model, "claude-sonnet-4-6");
let (kind, _) = resolve_provider_and_model("claude-haiku", ProviderKind::Auto);
assert_eq!(kind, ProviderKind::Auto);
}
#[test]
fn tier_orchestration_uses_sm_model() {
let mut cfg = empty_cfg();
cfg.sm_model = "anthropic/claude-sonnet-4-6".to_string();
let m = resolve_tier_model(&cfg, SmModelTier::Orchestration).unwrap();
assert_eq!(m, "anthropic/claude-sonnet-4-6");
}
#[test]
fn tier_orchestration_alias_fallback() {
let mut cfg = empty_cfg();
cfg.sm_model = String::new();
cfg.model = "legacy/sonnet".to_string();
let m = resolve_tier_model(&cfg, SmModelTier::Orchestration).unwrap();
assert_eq!(m, "legacy/sonnet");
}
#[test]
fn tier_defaults_are_sonnet_and_haiku() {
let cfg = SmInferenceConfig::default();
let orch = resolve_tier_model(&cfg, SmModelTier::Orchestration).unwrap();
let summ = resolve_tier_model(&cfg, SmModelTier::Summary).unwrap();
assert!(
orch.contains("sonnet"),
"orchestration default is Sonnet: {orch}"
);
assert!(summ.contains("haiku"), "summary default is Haiku: {summ}");
}
#[test]
fn tier_summary_default() {
let mut cfg = empty_cfg();
cfg.summary_model = "anthropic/claude-haiku".to_string();
let m = resolve_tier_model(&cfg, SmModelTier::Summary).unwrap();
assert_eq!(m, "anthropic/claude-haiku");
}
#[test]
fn tier_compaction_override() {
let mut cfg = empty_cfg();
cfg.summary_model = "anthropic/claude-haiku".to_string();
cfg.compaction_model = "bedrock/us.anthropic.claude-haiku".to_string();
let m = resolve_tier_model(&cfg, SmModelTier::Compaction).unwrap();
assert_eq!(m, "bedrock/us.anthropic.claude-haiku");
}
#[test]
fn tier_compaction_falls_back_to_summary() {
let mut cfg = empty_cfg();
cfg.summary_model = "anthropic/claude-haiku".to_string();
cfg.compaction_model = String::new();
let m = resolve_tier_model(&cfg, SmModelTier::Compaction).unwrap();
assert_eq!(m, "anthropic/claude-haiku");
}
#[test]
fn tier_empty_is_validation_error() {
let cfg = empty_cfg();
let err = resolve_tier_model(&cfg, SmModelTier::Orchestration).unwrap_err();
assert!(matches!(err, SmLlmError::Validation(_)));
}
#[tokio::test]
async fn registry_rejects_unknown_provider() {
let cfg = SmInferenceConfig {
provider: "totally-bogus".to_string(),
..SmInferenceConfig::default()
};
let registry = ProviderRegistry {
anthropic_api_key: Some("sk-ant".to_string()),
aws_credentials_available: false,
openrouter_api_key: None,
};
let err = registry
.build(&cfg, SmModelTier::Orchestration)
.await
.expect_err("unknown provider rejected");
assert!(matches!(err, SmLlmError::Validation(_)));
assert!(err.is_alarm());
}
#[tokio::test]
async fn registry_routes_explicit_prefix() {
let mut cfg = empty_cfg();
cfg.provider = "openrouter".to_string();
cfg.sm_model = "anthropic/claude-sonnet-4-6".to_string();
let registry = ProviderRegistry {
anthropic_api_key: Some("sk-ant".to_string()),
aws_credentials_available: false,
openrouter_api_key: Some("sk-or".to_string()),
};
let resolved = registry
.build(&cfg, SmModelTier::Orchestration)
.await
.expect("anthropic prefix builds");
assert_eq!(resolved.kind, ProviderKind::Anthropic);
assert_eq!(resolved.model, "claude-sonnet-4-6");
assert_eq!(resolved.provider.name(), "anthropic");
}
#[tokio::test]
async fn registry_auto_precedence() {
let mut cfg = empty_cfg();
cfg.provider = "auto".to_string();
cfg.sm_model = "claude-sonnet-4-6".to_string();
let or_only = ProviderRegistry {
anthropic_api_key: None,
aws_credentials_available: false,
openrouter_api_key: Some("sk-or".to_string()),
};
let resolved = or_only
.build(&cfg, SmModelTier::Orchestration)
.await
.expect("openrouter selected");
assert_eq!(resolved.kind, ProviderKind::OpenRouter);
assert_eq!(resolved.provider.name(), "openrouter");
let both = ProviderRegistry {
anthropic_api_key: Some("sk-ant".to_string()),
aws_credentials_available: false,
openrouter_api_key: Some("sk-or".to_string()),
};
let resolved = both
.build(&cfg, SmModelTier::Orchestration)
.await
.expect("anthropic wins");
assert_eq!(resolved.kind, ProviderKind::Anthropic);
}
#[tokio::test]
async fn registry_degraded_without_creds() {
let mut cfg = empty_cfg();
cfg.provider = "auto".to_string();
cfg.sm_model = "claude-sonnet-4-6".to_string();
let registry = ProviderRegistry::default(); let err = registry
.build(&cfg, SmModelTier::Orchestration)
.await
.expect_err("no creds → degraded");
assert!(err.is_degraded(), "expected degraded, got: {err}");
assert!(!err.is_alarm());
assert!(!err.is_retryable());
}
#[tokio::test]
async fn registry_explicit_anthropic_without_key_degrades() {
let mut cfg = empty_cfg();
cfg.provider = "anthropic".to_string();
cfg.sm_model = "claude-sonnet-4-6".to_string();
let registry = ProviderRegistry {
anthropic_api_key: None,
aws_credentials_available: false,
openrouter_api_key: Some("sk-or".to_string()),
};
let err = registry
.build(&cfg, SmModelTier::Orchestration)
.await
.expect_err("anthropic without key degrades");
assert!(err.is_degraded(), "expected degraded, got: {err}");
}
#[cfg(not(feature = "bedrock"))]
#[tokio::test]
async fn registry_bedrock_prefix_without_feature() {
let mut cfg = empty_cfg();
cfg.provider = "auto".to_string();
cfg.sm_model = "bedrock/us.anthropic.claude-sonnet-4-6".to_string();
let registry = ProviderRegistry {
anthropic_api_key: None,
aws_credentials_available: true,
openrouter_api_key: None,
};
let err = registry
.build(&cfg, SmModelTier::Orchestration)
.await
.expect_err("bedrock without feature errors");
match err {
SmLlmError::Validation(msg) => {
assert!(msg.contains("bedrock"), "msg should mention bedrock: {msg}");
assert!(msg.contains("feature"), "msg should mention feature: {msg}");
}
other => panic!("expected Validation, got {other:?}"),
}
}
#[cfg(feature = "bedrock")]
#[test]
fn registry_auto_prefers_bedrock_when_available() {
let mut cfg = empty_cfg();
cfg.provider = "auto".to_string();
cfg.sm_model = "bedrock/us.anthropic.claude-sonnet-4-6".to_string();
let registry = ProviderRegistry {
anthropic_api_key: None,
aws_credentials_available: true,
openrouter_api_key: Some("sk-or".to_string()),
};
let (kind, model) = registry
.resolve_kind_and_model(&cfg, SmModelTier::Orchestration)
.expect("auto precedence resolves to bedrock");
assert_eq!(kind, ProviderKind::Bedrock);
assert_eq!(model, "us.anthropic.claude-sonnet-4-6");
}