use std::collections::HashSet;
use clawft_types::routing::{ModelTierConfig, PermissionLevelConfig, RoutingConfig};
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ValidationSeverity {
Error,
Warning,
}
#[derive(Debug, Clone)]
pub struct ValidationError {
pub field: String,
pub message: String,
pub severity: ValidationSeverity,
}
impl std::fmt::Display for ValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let level = match self.severity {
ValidationSeverity::Error => "ERROR",
ValidationSeverity::Warning => "WARN",
};
write!(f, "[{}] {}: {}", level, self.field, self.message)
}
}
impl std::error::Error for ValidationError {}
pub fn validate_routing_config(config: &RoutingConfig) -> Vec<ValidationError> {
let mut errors = Vec::new();
if config.mode == "static" {
return errors;
}
validate_mode(config, &mut errors);
validate_tiers(config, &mut errors);
validate_permissions(config, &mut errors);
validate_escalation(config, &mut errors);
validate_cost_budgets(config, &mut errors);
validate_rate_limiting(config, &mut errors);
validate_fallback_model(config, &mut errors);
errors
}
fn validate_mode(config: &RoutingConfig, errors: &mut Vec<ValidationError>) {
if config.mode != "static" && config.mode != "tiered" {
errors.push(ValidationError {
field: "routing.mode".into(),
message: format!(
"unknown mode '{}', expected 'static' or 'tiered'",
config.mode
),
severity: ValidationSeverity::Error,
});
}
}
fn validate_tiers(config: &RoutingConfig, errors: &mut Vec<ValidationError>) {
if config.mode == "tiered" && config.tiers.is_empty() {
errors.push(ValidationError {
field: "routing.tiers".into(),
message: "tiered mode requires at least one tier definition".into(),
severity: ValidationSeverity::Error,
});
}
let mut seen_names: HashSet<&str> = HashSet::new();
for (i, tier) in config.tiers.iter().enumerate() {
if !seen_names.insert(&tier.name) {
errors.push(ValidationError {
field: format!("routing.tiers[{}].name", i),
message: format!("duplicate tier name '{}'", tier.name),
severity: ValidationSeverity::Error,
});
}
validate_single_tier(i, tier, errors);
}
for i in 0..config.tiers.len() {
for j in (i + 1)..config.tiers.len() {
let a = &config.tiers[i];
let b = &config.tiers[j];
let overlap_min = a.complexity_range[0].max(b.complexity_range[0]);
let overlap_max = a.complexity_range[1].min(b.complexity_range[1]);
if overlap_min < overlap_max {
errors.push(ValidationError {
field: format!("routing.tiers[{}]+[{}]", i, j),
message: format!(
"tiers '{}' and '{}' have overlapping complexity ranges \
[{:.2}, {:.2}]",
a.name, b.name, overlap_min, overlap_max
),
severity: ValidationSeverity::Warning,
});
}
}
}
}
fn validate_single_tier(index: usize, tier: &ModelTierConfig, errors: &mut Vec<ValidationError>) {
let prefix = format!("routing.tiers[{}]", index);
if tier.models.is_empty() {
errors.push(ValidationError {
field: format!("{}.models", prefix),
message: format!(
"tier '{}' has no models -- it is defined but unusable",
tier.name
),
severity: ValidationSeverity::Warning,
});
}
let [min, max] = tier.complexity_range;
if min > max {
errors.push(ValidationError {
field: format!("{}.complexity_range", prefix),
message: format!(
"complexity_range min ({}) > max ({}) for tier '{}'",
min, max, tier.name
),
severity: ValidationSeverity::Error,
});
}
if !(0.0..=1.0).contains(&min) {
errors.push(ValidationError {
field: format!("{}.complexity_range[0]", prefix),
message: format!(
"complexity_range[0] = {} is outside [0.0, 1.0] for tier '{}'",
min, tier.name
),
severity: ValidationSeverity::Error,
});
}
if !(0.0..=1.0).contains(&max) {
errors.push(ValidationError {
field: format!("{}.complexity_range[1]", prefix),
message: format!(
"complexity_range[1] = {} is outside [0.0, 1.0] for tier '{}'",
max, tier.name
),
severity: ValidationSeverity::Error,
});
}
if tier.cost_per_1k_tokens < 0.0 {
errors.push(ValidationError {
field: format!("{}.cost_per_1k_tokens", prefix),
message: format!(
"cost_per_1k_tokens = {} must be non-negative for tier '{}'",
tier.cost_per_1k_tokens, tier.name
),
severity: ValidationSeverity::Error,
});
}
if tier.max_context_tokens == 0 {
errors.push(ValidationError {
field: format!("{}.max_context_tokens", prefix),
message: format!("max_context_tokens must be > 0 for tier '{}'", tier.name),
severity: ValidationSeverity::Error,
});
}
for (j, model) in tier.models.iter().enumerate() {
if !model.contains('/') {
errors.push(ValidationError {
field: format!("{}.models[{}]", prefix, j),
message: format!(
"model '{}' does not contain '/' (expected 'provider/model' format)",
model
),
severity: ValidationSeverity::Warning,
});
}
}
}
fn validate_permissions(config: &RoutingConfig, errors: &mut Vec<ValidationError>) {
let valid_tier_names: HashSet<&str> = config.tiers.iter().map(|t| t.name.as_str()).collect();
validate_permission_level(
&config.permissions.zero_trust,
"routing.permissions.zero_trust",
&valid_tier_names,
errors,
);
validate_permission_level(
&config.permissions.user,
"routing.permissions.user",
&valid_tier_names,
errors,
);
validate_permission_level(
&config.permissions.admin,
"routing.permissions.admin",
&valid_tier_names,
errors,
);
for (user_id, plc) in &config.permissions.users {
validate_permission_level(
plc,
&format!("routing.permissions.users.{}", user_id),
&valid_tier_names,
errors,
);
}
for (channel, plc) in &config.permissions.channels {
validate_permission_level(
plc,
&format!("routing.permissions.channels.{}", channel),
&valid_tier_names,
errors,
);
}
}
fn validate_permission_level(
plc: &PermissionLevelConfig,
field_prefix: &str,
valid_tiers: &HashSet<&str>,
errors: &mut Vec<ValidationError>,
) {
if let Some(level) = plc.level
&& level > 2 {
errors.push(ValidationError {
field: format!("{}.level", field_prefix),
message: format!(
"permission level {} is not recognized (expected 0, 1, or 2)",
level
),
severity: ValidationSeverity::Warning,
});
}
if let Some(threshold) = plc.escalation_threshold
&& (!(0.0..=1.0).contains(&threshold)) {
errors.push(ValidationError {
field: format!("{}.escalation_threshold", field_prefix),
message: format!("escalation_threshold {} must be in [0.0, 1.0]", threshold),
severity: ValidationSeverity::Error,
});
}
if let Some(daily) = plc.cost_budget_daily_usd
&& daily < 0.0 {
errors.push(ValidationError {
field: format!("{}.cost_budget_daily_usd", field_prefix),
message: format!("cost_budget_daily_usd {} must be non-negative", daily),
severity: ValidationSeverity::Error,
});
}
if let Some(monthly) = plc.cost_budget_monthly_usd
&& monthly < 0.0 {
errors.push(ValidationError {
field: format!("{}.cost_budget_monthly_usd", field_prefix),
message: format!("cost_budget_monthly_usd {} must be non-negative", monthly),
severity: ValidationSeverity::Error,
});
}
if let Some(ref max_tier) = plc.max_tier
&& !valid_tiers.is_empty() && !valid_tiers.contains(max_tier.as_str()) {
errors.push(ValidationError {
field: format!("{}.max_tier", field_prefix),
message: format!(
"max_tier '{}' does not match any defined tier (available: {})",
max_tier,
valid_tiers.iter().copied().collect::<Vec<_>>().join(", ")
),
severity: ValidationSeverity::Warning,
});
}
if let Some(ref tool_access) = plc.tool_access {
for entry in tool_access {
if entry.contains('*') && entry != "*" {
errors.push(ValidationError {
field: format!("{}.tool_access", field_prefix),
message: format!(
"glob-like pattern '{}' detected; ensure pattern matching is intended",
entry
),
severity: ValidationSeverity::Warning,
});
}
}
}
}
fn validate_escalation(config: &RoutingConfig, errors: &mut Vec<ValidationError>) {
if config.escalation.enabled && config.escalation.max_escalation_tiers == 0 {
errors.push(ValidationError {
field: "routing.escalation.max_escalation_tiers".into(),
message: "max_escalation_tiers must be > 0 when escalation is enabled".into(),
severity: ValidationSeverity::Error,
});
}
if config.escalation.threshold < 0.0 || config.escalation.threshold > 1.0 {
errors.push(ValidationError {
field: "routing.escalation.threshold".into(),
message: format!(
"escalation threshold {} must be in [0.0, 1.0]",
config.escalation.threshold
),
severity: ValidationSeverity::Error,
});
}
if config.escalation.max_escalation_tiers > config.tiers.len() as u32 {
errors.push(ValidationError {
field: "routing.escalation.max_escalation_tiers".into(),
message: format!(
"max_escalation_tiers ({}) exceeds the number of defined tiers ({})",
config.escalation.max_escalation_tiers,
config.tiers.len()
),
severity: ValidationSeverity::Warning,
});
}
}
fn validate_cost_budgets(config: &RoutingConfig, errors: &mut Vec<ValidationError>) {
if config.cost_budgets.global_daily_limit_usd < 0.0 {
errors.push(ValidationError {
field: "routing.cost_budgets.global_daily_limit_usd".into(),
message: format!(
"global_daily_limit_usd {} must be non-negative",
config.cost_budgets.global_daily_limit_usd
),
severity: ValidationSeverity::Error,
});
}
if config.cost_budgets.global_monthly_limit_usd < 0.0 {
errors.push(ValidationError {
field: "routing.cost_budgets.global_monthly_limit_usd".into(),
message: format!(
"global_monthly_limit_usd {} must be non-negative",
config.cost_budgets.global_monthly_limit_usd
),
severity: ValidationSeverity::Error,
});
}
if config.cost_budgets.reset_hour_utc > 23 {
errors.push(ValidationError {
field: "routing.cost_budgets.reset_hour_utc".into(),
message: format!(
"reset_hour_utc {} must be 0-23",
config.cost_budgets.reset_hour_utc
),
severity: ValidationSeverity::Error,
});
}
}
fn validate_rate_limiting(config: &RoutingConfig, errors: &mut Vec<ValidationError>) {
if config.rate_limiting.window_seconds == 0 {
errors.push(ValidationError {
field: "routing.rate_limiting.window_seconds".into(),
message: "window_seconds must be > 0".into(),
severity: ValidationSeverity::Error,
});
}
let valid_strategies = ["sliding_window", "fixed_window"];
if !valid_strategies.contains(&config.rate_limiting.strategy.as_str()) {
errors.push(ValidationError {
field: "routing.rate_limiting.strategy".into(),
message: format!(
"unknown strategy '{}', expected 'sliding_window' or 'fixed_window'",
config.rate_limiting.strategy
),
severity: ValidationSeverity::Error,
});
}
}
fn validate_fallback_model(config: &RoutingConfig, errors: &mut Vec<ValidationError>) {
if let Some(ref model) = config.fallback_model
&& !model.contains('/') {
errors.push(ValidationError {
field: "routing.fallback_model".into(),
message: format!(
"fallback_model '{}' should match 'provider/model' format",
model
),
severity: ValidationSeverity::Warning,
});
}
}
const DEFAULT_MAX_GRANTABLE_LEVEL: u8 = 1;
pub fn validate_workspace_ceiling(
global: &RoutingConfig,
workspace: &RoutingConfig,
) -> Vec<ValidationError> {
let mut errors = Vec::new();
let max_grantable = DEFAULT_MAX_GRANTABLE_LEVEL;
check_level_ceiling(
&workspace.permissions.zero_trust,
"routing.permissions.zero_trust",
&global.permissions.zero_trust,
max_grantable,
&mut errors,
);
check_level_ceiling(
&workspace.permissions.user,
"routing.permissions.user",
&global.permissions.user,
max_grantable,
&mut errors,
);
check_level_ceiling(
&workspace.permissions.admin,
"routing.permissions.admin",
&global.permissions.admin,
max_grantable,
&mut errors,
);
for (user_id, ws_plc) in &workspace.permissions.users {
let global_plc = global
.permissions
.users
.get(user_id)
.unwrap_or(&global.permissions.user);
check_level_ceiling(
ws_plc,
&format!("routing.permissions.users.{}", user_id),
global_plc,
max_grantable,
&mut errors,
);
}
for (channel, ws_plc) in &workspace.permissions.channels {
let global_plc = global
.permissions
.channels
.get(channel)
.unwrap_or(&global.permissions.user);
check_level_ceiling(
ws_plc,
&format!("routing.permissions.channels.{}", channel),
global_plc,
max_grantable,
&mut errors,
);
}
errors
}
fn check_level_ceiling(
ws_plc: &PermissionLevelConfig,
field_prefix: &str,
global_plc: &PermissionLevelConfig,
max_grantable: u8,
errors: &mut Vec<ValidationError>,
) {
if let Some(ws_level) = ws_plc.level
&& ws_level > max_grantable {
errors.push(ValidationError {
field: format!("{}.level", field_prefix),
message: format!(
"workspace level {} exceeds max_grantable_level {}",
ws_level, max_grantable
),
severity: ValidationSeverity::Error,
});
}
if let (Some(true), Some(false)) = (ws_plc.escalation_allowed, global_plc.escalation_allowed) {
errors.push(ValidationError {
field: format!("{}.escalation_allowed", field_prefix),
message: "workspace cannot enable escalation when global disables it".into(),
severity: ValidationSeverity::Error,
});
}
if let (Some(ws_tools), Some(global_tools)) =
(&ws_plc.tool_access, &global_plc.tool_access)
{
if !global_tools.iter().any(|s| s == "*") {
for ws_tool in ws_tools {
if ws_tool == "*" {
errors.push(ValidationError {
field: format!("{}.tool_access", field_prefix),
message: "workspace cannot grant wildcard tool access \
when global does not"
.into(),
severity: ValidationSeverity::Error,
});
} else if !global_tools.contains(ws_tool) {
errors.push(ValidationError {
field: format!("{}.tool_access", field_prefix),
message: format!("workspace tool '{}' not in global tool_access", ws_tool),
severity: ValidationSeverity::Error,
});
}
}
}
}
if let (Some(ws_rate), Some(global_rate)) = (ws_plc.rate_limit, global_plc.rate_limit)
&& global_rate > 0 && (ws_rate > global_rate || ws_rate == 0) {
errors.push(ValidationError {
field: format!("{}.rate_limit", field_prefix),
message: format!(
"workspace rate_limit {} exceeds global ceiling {}",
ws_rate, global_rate
),
severity: ValidationSeverity::Error,
});
}
if let (Some(ws_daily), Some(global_daily)) = (
ws_plc.cost_budget_daily_usd,
global_plc.cost_budget_daily_usd,
)
&& global_daily > 0.0 && (ws_daily > global_daily || ws_daily == 0.0) {
errors.push(ValidationError {
field: format!("{}.cost_budget_daily_usd", field_prefix),
message: format!(
"workspace daily budget {} exceeds global ceiling {}",
ws_daily, global_daily
),
severity: ValidationSeverity::Error,
});
}
if let (Some(ws_monthly), Some(global_monthly)) = (
ws_plc.cost_budget_monthly_usd,
global_plc.cost_budget_monthly_usd,
)
&& global_monthly > 0.0 && (ws_monthly > global_monthly || ws_monthly == 0.0) {
errors.push(ValidationError {
field: format!("{}.cost_budget_monthly_usd", field_prefix),
message: format!(
"workspace monthly budget {} exceeds global ceiling {}",
ws_monthly, global_monthly
),
severity: ValidationSeverity::Error,
});
}
}
pub fn default_tiers() -> Vec<ModelTierConfig> {
vec![
ModelTierConfig {
name: "free".into(),
models: vec![
"openrouter/meta-llama/llama-3.1-8b-instruct:free".into(),
"groq/llama-3.1-8b".into(),
],
complexity_range: [0.0, 0.3],
cost_per_1k_tokens: 0.0,
max_context_tokens: 8192,
},
ModelTierConfig {
name: "standard".into(),
models: vec![
"anthropic/claude-haiku-3.5".into(),
"openai/gpt-4o-mini".into(),
"groq/llama-3.3-70b".into(),
],
complexity_range: [0.0, 0.7],
cost_per_1k_tokens: 0.001,
max_context_tokens: 16384,
},
ModelTierConfig {
name: "premium".into(),
models: vec![
"anthropic/claude-sonnet-4-20250514".into(),
"openai/gpt-4o".into(),
],
complexity_range: [0.3, 1.0],
cost_per_1k_tokens: 0.01,
max_context_tokens: 200_000,
},
]
}
#[cfg(test)]
mod tests {
use super::*;
use clawft_types::routing::{EscalationConfig, ModelTierConfig, RateLimitConfig};
fn valid_tiered_config() -> RoutingConfig {
RoutingConfig {
mode: "tiered".into(),
tiers: vec![
ModelTierConfig {
name: "free".into(),
models: vec!["groq/llama-3.1-8b".into()],
complexity_range: [0.0, 0.5],
cost_per_1k_tokens: 0.0,
max_context_tokens: 8192,
},
ModelTierConfig {
name: "standard".into(),
models: vec!["anthropic/claude-haiku-3.5".into()],
complexity_range: [0.3, 1.0],
cost_per_1k_tokens: 0.001,
max_context_tokens: 16384,
},
],
..RoutingConfig::default()
}
}
fn valid_static_config() -> RoutingConfig {
RoutingConfig::default()
}
fn count_severity(errors: &[ValidationError], severity: ValidationSeverity) -> usize {
errors.iter().filter(|e| e.severity == severity).count()
}
fn has_error(
errors: &[ValidationError],
field_prefix: &str,
severity: ValidationSeverity,
) -> bool {
errors
.iter()
.any(|e| e.field.starts_with(field_prefix) && e.severity == severity)
}
#[test]
fn valid_static_config_passes() {
let config = valid_static_config();
let errors = validate_routing_config(&config);
assert!(errors.is_empty(), "static config should produce no errors");
}
#[test]
fn valid_tiered_config_passes() {
let config = valid_tiered_config();
let errors = validate_routing_config(&config);
let fatal = count_severity(&errors, ValidationSeverity::Error);
assert_eq!(
fatal, 0,
"valid tiered config should have no fatal errors, got: {:?}",
errors
);
}
#[test]
fn invalid_mode_rejected() {
let mut config = valid_tiered_config();
config.mode = "hybrid".into();
let errors = validate_routing_config(&config);
assert!(
has_error(&errors, "routing.mode", ValidationSeverity::Error),
"unknown mode should produce an error"
);
}
#[test]
fn tiered_with_no_tiers_gives_error() {
let config = RoutingConfig {
mode: "tiered".into(),
tiers: vec![],
..RoutingConfig::default()
};
let errors = validate_routing_config(&config);
assert!(
has_error(&errors, "routing.tiers", ValidationSeverity::Error),
"tiered mode with no tiers should be an error"
);
}
#[test]
fn duplicate_tier_names_detected() {
let mut config = valid_tiered_config();
config.tiers[1].name = "free".into(); let errors = validate_routing_config(&config);
assert!(
has_error(&errors, "routing.tiers[1].name", ValidationSeverity::Error),
"duplicate tier names should produce an error"
);
}
#[test]
fn empty_tier_models_warning() {
let mut config = valid_tiered_config();
config.tiers[0].models.clear();
let errors = validate_routing_config(&config);
assert!(
has_error(
&errors,
"routing.tiers[0].models",
ValidationSeverity::Warning
),
"empty models should produce a warning"
);
}
#[test]
fn complexity_range_inverted() {
let mut config = valid_tiered_config();
config.tiers[0].complexity_range = [0.8, 0.3];
let errors = validate_routing_config(&config);
assert!(
has_error(
&errors,
"routing.tiers[0].complexity_range",
ValidationSeverity::Error
),
"inverted complexity range should produce an error"
);
}
#[test]
fn complexity_out_of_range() {
let mut config = valid_tiered_config();
config.tiers[0].complexity_range = [-0.1, 1.5];
let errors = validate_routing_config(&config);
assert!(
has_error(
&errors,
"routing.tiers[0].complexity_range[0]",
ValidationSeverity::Error
),
"negative min should produce an error"
);
assert!(
has_error(
&errors,
"routing.tiers[0].complexity_range[1]",
ValidationSeverity::Error
),
"max > 1.0 should produce an error"
);
}
#[test]
fn negative_cost_rejected() {
let mut config = valid_tiered_config();
config.tiers[0].cost_per_1k_tokens = -0.001;
let errors = validate_routing_config(&config);
assert!(
has_error(
&errors,
"routing.tiers[0].cost_per_1k_tokens",
ValidationSeverity::Error
),
"negative cost should produce an error"
);
}
#[test]
fn zero_max_context_tokens_rejected() {
let mut config = valid_tiered_config();
config.tiers[0].max_context_tokens = 0;
let errors = validate_routing_config(&config);
assert!(
has_error(
&errors,
"routing.tiers[0].max_context_tokens",
ValidationSeverity::Error
),
"zero max_context_tokens should produce an error"
);
}
#[test]
fn invalid_permission_level_warning() {
let mut config = valid_tiered_config();
config.permissions.zero_trust.level = Some(5);
let errors = validate_routing_config(&config);
assert!(
has_error(
&errors,
"routing.permissions.zero_trust.level",
ValidationSeverity::Warning
),
"invalid level should produce a warning"
);
}
#[test]
fn invalid_escalation_threshold() {
let mut config = valid_tiered_config();
config.permissions.user.escalation_threshold = Some(1.5);
let errors = validate_routing_config(&config);
assert!(
has_error(
&errors,
"routing.permissions.user.escalation_threshold",
ValidationSeverity::Error
),
"escalation_threshold > 1.0 should produce an error"
);
}
#[test]
fn negative_budget_rejected() {
let mut config = valid_tiered_config();
config.permissions.user.cost_budget_daily_usd = Some(-1.0);
let errors = validate_routing_config(&config);
assert!(
has_error(
&errors,
"routing.permissions.user.cost_budget_daily_usd",
ValidationSeverity::Error
),
"negative daily budget should produce an error"
);
}
#[test]
fn max_tier_nonexistent_warns() {
let mut config = valid_tiered_config();
config.permissions.user.max_tier = Some("mythical".into());
let errors = validate_routing_config(&config);
assert!(
has_error(
&errors,
"routing.permissions.user.max_tier",
ValidationSeverity::Warning
),
"undefined max_tier should produce a warning"
);
}
#[test]
fn glob_like_tool_pattern_warning() {
let mut config = valid_tiered_config();
config.permissions.user.tool_access = Some(vec!["file_*".into()]);
let errors = validate_routing_config(&config);
assert!(
has_error(
&errors,
"routing.permissions.user.tool_access",
ValidationSeverity::Warning
),
"glob-like tool_access should produce a warning"
);
}
#[test]
fn exact_wildcard_no_warning() {
let mut config = valid_tiered_config();
config.permissions.admin.tool_access = Some(vec!["*".into()]);
let errors = validate_routing_config(&config);
let glob_warnings = errors.iter().any(|e| {
e.field.contains("tool_access")
&& e.severity == ValidationSeverity::Warning
&& e.message.contains("glob")
});
assert!(
!glob_warnings,
"exact '*' should not produce a glob pattern warning"
);
}
#[test]
fn fallback_model_format_warning() {
let mut config = valid_tiered_config();
config.fallback_model = Some("gpt-4o".into());
let errors = validate_routing_config(&config);
assert!(
has_error(
&errors,
"routing.fallback_model",
ValidationSeverity::Warning
),
"fallback model without '/' should produce a warning"
);
}
#[test]
fn workspace_ceiling_level_escalation_blocked() {
let global = valid_tiered_config();
let mut workspace = RoutingConfig::default();
workspace.permissions.user.level = Some(2); let errors = validate_workspace_ceiling(&global, &workspace);
assert!(
has_error(
&errors,
"routing.permissions.user.level",
ValidationSeverity::Error
),
"workspace level exceeding max_grantable should produce an error"
);
}
#[test]
fn workspace_ceiling_rate_limit_increase_blocked() {
let mut global = valid_tiered_config();
global.permissions.user.rate_limit = Some(60);
let mut workspace = RoutingConfig::default();
workspace.permissions.user.rate_limit = Some(120);
let errors = validate_workspace_ceiling(&global, &workspace);
assert!(
has_error(
&errors,
"routing.permissions.user.rate_limit",
ValidationSeverity::Error
),
"workspace rate_limit exceeding global should produce an error"
);
}
#[test]
fn workspace_ceiling_budget_increase_blocked() {
let mut global = valid_tiered_config();
global.permissions.user.cost_budget_daily_usd = Some(5.0);
let mut workspace = RoutingConfig::default();
workspace.permissions.user.cost_budget_daily_usd = Some(10.0);
let errors = validate_workspace_ceiling(&global, &workspace);
assert!(
has_error(
&errors,
"routing.permissions.user.cost_budget_daily_usd",
ValidationSeverity::Error
),
"workspace daily budget exceeding global should produce an error"
);
}
#[test]
fn workspace_ceiling_tool_access_expansion_blocked() {
let mut global = valid_tiered_config();
global.permissions.user.tool_access = Some(vec!["read_file".into(), "list_dir".into()]);
let mut workspace = RoutingConfig::default();
workspace.permissions.user.tool_access = Some(vec!["exec_shell".into()]);
let errors = validate_workspace_ceiling(&global, &workspace);
assert!(
has_error(
&errors,
"routing.permissions.user.tool_access",
ValidationSeverity::Error
),
"workspace adding tool not in global should produce an error"
);
}
#[test]
fn workspace_within_bounds_passes() {
let mut global = valid_tiered_config();
global.permissions.user.level = Some(1);
global.permissions.user.rate_limit = Some(60);
global.permissions.user.cost_budget_daily_usd = Some(5.0);
let mut workspace = RoutingConfig::default();
workspace.permissions.user.level = Some(1);
workspace.permissions.user.rate_limit = Some(30);
workspace.permissions.user.cost_budget_daily_usd = Some(3.0);
let errors = validate_workspace_ceiling(&global, &workspace);
let fatal = count_severity(&errors, ValidationSeverity::Error);
assert_eq!(
fatal, 0,
"workspace within bounds should have no errors, got: {:?}",
errors
);
}
#[test]
fn default_tiers_generation() {
let tiers = default_tiers();
assert_eq!(tiers.len(), 3, "should produce 3 default tiers");
assert_eq!(tiers[0].name, "free");
assert_eq!(tiers[1].name, "standard");
assert_eq!(tiers[2].name, "premium");
for tier in &tiers {
assert!(
!tier.models.is_empty(),
"tier '{}' should have models",
tier.name
);
}
assert_eq!(tiers[0].cost_per_1k_tokens, 0.0);
for tier in &tiers {
assert!(tier.complexity_range[0] >= 0.0);
assert!(tier.complexity_range[1] <= 1.0);
assert!(tier.complexity_range[0] <= tier.complexity_range[1]);
}
}
#[test]
fn default_tiers_pass_validation() {
let config = RoutingConfig {
mode: "tiered".into(),
tiers: default_tiers(),
..RoutingConfig::default()
};
let errors = validate_routing_config(&config);
let fatal = count_severity(&errors, ValidationSeverity::Error);
assert_eq!(
fatal, 0,
"default tiers should pass validation, got: {:?}",
errors
);
}
#[test]
fn escalation_global_threshold_out_of_range() {
let mut config = valid_tiered_config();
config.escalation = EscalationConfig {
enabled: true,
threshold: 1.5,
max_escalation_tiers: 1,
};
let errors = validate_routing_config(&config);
assert!(
has_error(
&errors,
"routing.escalation.threshold",
ValidationSeverity::Error
),
"escalation threshold > 1.0 should produce an error"
);
}
#[test]
fn zero_window_seconds_rejected() {
let mut config = valid_tiered_config();
config.rate_limiting.window_seconds = 0;
let errors = validate_routing_config(&config);
assert!(
has_error(
&errors,
"routing.rate_limiting.window_seconds",
ValidationSeverity::Error
),
"zero window_seconds should produce an error"
);
}
#[test]
fn unknown_rate_limit_strategy_rejected() {
let mut config = valid_tiered_config();
config.rate_limiting.strategy = "token_bucket".into();
let errors = validate_routing_config(&config);
assert!(
has_error(
&errors,
"routing.rate_limiting.strategy",
ValidationSeverity::Error
),
"unknown rate limit strategy should produce an error"
);
}
#[test]
fn reset_hour_out_of_range() {
let mut config = valid_tiered_config();
config.cost_budgets.reset_hour_utc = 25;
let errors = validate_routing_config(&config);
assert!(
has_error(
&errors,
"routing.cost_budgets.reset_hour_utc",
ValidationSeverity::Error
),
"reset_hour_utc > 23 should produce an error"
);
}
#[test]
fn negative_global_daily_limit() {
let mut config = valid_tiered_config();
config.cost_budgets.global_daily_limit_usd = -10.0;
let errors = validate_routing_config(&config);
assert!(
has_error(
&errors,
"routing.cost_budgets.global_daily_limit_usd",
ValidationSeverity::Error
),
"negative global daily limit should produce an error"
);
}
#[test]
fn negative_global_monthly_limit() {
let mut config = valid_tiered_config();
config.cost_budgets.global_monthly_limit_usd = -100.0;
let errors = validate_routing_config(&config);
assert!(
has_error(
&errors,
"routing.cost_budgets.global_monthly_limit_usd",
ValidationSeverity::Error
),
"negative global monthly limit should produce an error"
);
}
#[test]
fn multiple_errors_collected() {
let config = RoutingConfig {
mode: "tiered".into(),
tiers: vec![
ModelTierConfig {
name: "bad".into(),
models: vec![],
complexity_range: [0.8, 0.3], cost_per_1k_tokens: -1.0, max_context_tokens: 0, },
ModelTierConfig {
name: "bad".into(), models: vec![],
complexity_range: [0.0, 1.0],
cost_per_1k_tokens: 0.0,
max_context_tokens: 8192,
},
],
rate_limiting: RateLimitConfig {
window_seconds: 0, strategy: "sliding_window".into(),
..RateLimitConfig::default()
},
..RoutingConfig::default()
};
let errors = validate_routing_config(&config);
let fatal_count = count_severity(&errors, ValidationSeverity::Error);
assert!(
fatal_count >= 4,
"should collect at least 4 errors, got {}: {:?}",
fatal_count,
errors
);
}
#[test]
fn static_mode_skips_validation() {
let config = RoutingConfig {
mode: "static".into(),
tiers: vec![ModelTierConfig {
name: "broken".into(),
models: vec![],
complexity_range: [0.8, 0.3], cost_per_1k_tokens: -1.0,
max_context_tokens: 0,
}],
..RoutingConfig::default()
};
let errors = validate_routing_config(&config);
assert!(
errors.is_empty(),
"static mode should skip all validation, got: {:?}",
errors
);
}
#[test]
fn overlapping_complexity_ranges_warning() {
let config = RoutingConfig {
mode: "tiered".into(),
tiers: vec![
ModelTierConfig {
name: "a".into(),
models: vec!["provider/model-a".into()],
complexity_range: [0.0, 0.7],
cost_per_1k_tokens: 0.0,
max_context_tokens: 8192,
},
ModelTierConfig {
name: "b".into(),
models: vec!["provider/model-b".into()],
complexity_range: [0.3, 1.0],
cost_per_1k_tokens: 0.01,
max_context_tokens: 16384,
},
],
..RoutingConfig::default()
};
let errors = validate_routing_config(&config);
let overlap_warnings = errors.iter().any(|e| {
e.severity == ValidationSeverity::Warning && e.message.contains("overlapping")
});
assert!(
overlap_warnings,
"overlapping ranges should produce a warning"
);
}
#[test]
fn model_format_warning() {
let mut config = valid_tiered_config();
config.tiers[0].models = vec!["gpt-4o".into()]; let errors = validate_routing_config(&config);
assert!(
has_error(
&errors,
"routing.tiers[0].models[0]",
ValidationSeverity::Warning
),
"model without '/' should produce a warning"
);
}
#[test]
fn per_user_override_validated() {
let mut config = valid_tiered_config();
config.permissions.users.insert(
"alice".into(),
PermissionLevelConfig {
cost_budget_monthly_usd: Some(-5.0),
..PermissionLevelConfig::default()
},
);
let errors = validate_routing_config(&config);
assert!(
has_error(
&errors,
"routing.permissions.users.alice.cost_budget_monthly_usd",
ValidationSeverity::Error
),
"per-user negative budget should produce an error"
);
}
#[test]
fn per_channel_override_validated() {
let mut config = valid_tiered_config();
config.permissions.channels.insert(
"telegram".into(),
PermissionLevelConfig {
escalation_threshold: Some(-0.5),
..PermissionLevelConfig::default()
},
);
let errors = validate_routing_config(&config);
assert!(
has_error(
&errors,
"routing.permissions.channels.telegram.escalation_threshold",
ValidationSeverity::Error
),
"per-channel invalid threshold should produce an error"
);
}
#[test]
fn escalation_max_tiers_exceeds_defined_count_warns() {
let mut config = valid_tiered_config();
config.escalation = EscalationConfig {
enabled: true,
threshold: 0.6,
max_escalation_tiers: 10, };
let errors = validate_routing_config(&config);
assert!(
has_error(
&errors,
"routing.escalation.max_escalation_tiers",
ValidationSeverity::Warning
),
"max_escalation_tiers > tier count should warn"
);
}
#[test]
fn workspace_ceiling_monthly_budget_increase_blocked() {
let mut global = valid_tiered_config();
global.permissions.user.cost_budget_monthly_usd = Some(50.0);
let mut workspace = RoutingConfig::default();
workspace.permissions.user.cost_budget_monthly_usd = Some(100.0);
let errors = validate_workspace_ceiling(&global, &workspace);
assert!(
has_error(
&errors,
"routing.permissions.user.cost_budget_monthly_usd",
ValidationSeverity::Error
),
"workspace monthly budget exceeding global should produce an error"
);
}
#[test]
fn workspace_ceiling_escalation_enable_blocked() {
let mut global = valid_tiered_config();
global.permissions.user.escalation_allowed = Some(false);
let mut workspace = RoutingConfig::default();
workspace.permissions.user.escalation_allowed = Some(true);
let errors = validate_workspace_ceiling(&global, &workspace);
assert!(
has_error(
&errors,
"routing.permissions.user.escalation_allowed",
ValidationSeverity::Error
),
"workspace enabling escalation when global disables should error"
);
}
#[test]
fn workspace_wildcard_tool_access_blocked() {
let mut global = valid_tiered_config();
global.permissions.user.tool_access = Some(vec!["read_file".into()]);
let mut workspace = RoutingConfig::default();
workspace.permissions.user.tool_access = Some(vec!["*".into()]);
let errors = validate_workspace_ceiling(&global, &workspace);
assert!(
has_error(
&errors,
"routing.permissions.user.tool_access",
ValidationSeverity::Error
),
"workspace wildcard when global does not should error"
);
}
#[test]
fn workspace_per_user_override_ceiling() {
let global = valid_tiered_config();
let mut workspace = RoutingConfig::default();
workspace.permissions.users.insert(
"attacker".into(),
PermissionLevelConfig {
level: Some(2), ..PermissionLevelConfig::default()
},
);
let errors = validate_workspace_ceiling(&global, &workspace);
assert!(
has_error(
&errors,
"routing.permissions.users.attacker.level",
ValidationSeverity::Error
),
"workspace per-user admin level should be blocked"
);
}
#[test]
fn display_impl_formats_correctly() {
let err = ValidationError {
field: "routing.tiers[0].name".into(),
message: "duplicate tier name 'free'".into(),
severity: ValidationSeverity::Error,
};
let displayed = format!("{}", err);
assert!(displayed.contains("[ERROR]"));
assert!(displayed.contains("routing.tiers[0].name"));
assert!(displayed.contains("duplicate tier name 'free'"));
let warn = ValidationError {
field: "routing.fallback_model".into(),
message: "missing provider prefix".into(),
severity: ValidationSeverity::Warning,
};
let displayed = format!("{}", warn);
assert!(displayed.contains("[WARN]"));
}
#[test]
fn escalation_enabled_zero_max_tiers() {
let mut config = valid_tiered_config();
config.escalation = EscalationConfig {
enabled: true,
threshold: 0.6,
max_escalation_tiers: 0,
};
let errors = validate_routing_config(&config);
assert!(
has_error(
&errors,
"routing.escalation.max_escalation_tiers",
ValidationSeverity::Error
),
"max_escalation_tiers=0 with enabled escalation should error"
);
}
#[test]
fn valid_fallback_model_no_warning() {
let mut config = valid_tiered_config();
config.fallback_model = Some("groq/llama-3.1-8b".into());
let errors = validate_routing_config(&config);
let fb_warnings = errors.iter().any(|e| {
e.field.contains("fallback_model") && e.severity == ValidationSeverity::Warning
});
assert!(!fb_warnings, "valid fallback model should not warn");
}
#[test]
fn workspace_tools_within_global_wildcard_ok() {
let mut global = valid_tiered_config();
global.permissions.user.tool_access = Some(vec!["*".into()]);
let mut workspace = RoutingConfig::default();
workspace.permissions.user.tool_access =
Some(vec!["read_file".into(), "exec_shell".into()]);
let errors = validate_workspace_ceiling(&global, &workspace);
let tool_errors = errors
.iter()
.any(|e| e.field.contains("tool_access") && e.severity == ValidationSeverity::Error);
assert!(
!tool_errors,
"workspace tools under global wildcard should pass"
);
}
}