use super::*;
fn build_default(secret: Option<&'static str>) -> Result<Config, ConfigError> {
Config::build(default_args(secret))
}
fn default_args(secret: Option<&'static str>) -> BuildArgs<'static> {
BuildArgs {
host: "0.0.0.0",
port: "8080",
token_secret: secret,
claude_code_home: "/tmp/claude",
upstream_base_url: "https://api.anthropic.com",
verbose: false,
max_proxy_request_bytes: DEFAULT_MAX_PROXY_REQUEST_BYTES,
api_format: None,
routing_mode: RoutingMode::Direct,
storage_policy: StoragePolicy::Memory,
data_dir: PathBuf::from("/tmp/test-data"),
claude_cli_bin: None,
upstream_provider: UpstreamProvider::Auto,
gonka_private_key: None,
gonka_source_url: default_gonka_source_url(),
gonka_model: default_gonka_model(),
bridge_model: None,
bridge_model_policy: None,
audit_log: None,
crater: default_crater_config("https://router.example"),
openai_compatible: default_openai_compatible_config(),
activitypub_actor_base_url: "https://router.example".into(),
activitypub_public_key_pem: default_activitypub_public_key_pem(),
enable_openai_api: true,
enable_anthropic_api: true,
enable_metrics: true,
additional_account_dirs: vec![],
account_routing_strategy: SelectionStrategy::default(),
account_cooldown_secs: 60,
session_affinity_ttl_secs: 3600,
account_request_limits: vec![],
experimental_compatibility: false,
admin_key: None,
allow_anonymous_admin: false,
mpp: default_mpp_config(),
login: crate::login::LoginConfig::default(),
admin_ui: crate::admin::AdminUiConfig::default(),
chat_admin: crate::chat_admin::ChatAdminConfig::default(),
}
}
#[test]
fn test_config_missing_token_secret() {
let result = build_default(None);
assert!(result.is_err());
}
#[test]
fn test_config_empty_token_secret() {
let result = build_default(Some(""));
assert!(result.is_err());
}
#[test]
fn test_config_with_valid_values() {
let config = build_default(Some("test-secret-key")).expect("Config should build");
assert_eq!(config.listen_addr.port(), 8080);
assert_eq!(config.token_secret, "test-secret-key");
assert_eq!(config.claude_code_home, "/tmp/claude");
assert_eq!(config.upstream_base_url, "https://api.anthropic.com");
assert_eq!(config.upstream_provider, UpstreamProvider::Auto);
assert!(!config.verbose);
assert_eq!(config.routing_mode, RoutingMode::Direct);
}
#[test]
fn default_provider_routes_across_subscriptions() {
let config = build_default(Some("secret")).expect("should build");
assert_eq!(config.upstream_provider, UpstreamProvider::Auto);
}
#[test]
fn account_limits_must_align_with_the_configured_pool() {
let mut args = gonka_args(None);
args.upstream_provider = UpstreamProvider::Anthropic;
args.additional_account_dirs = vec![PathBuf::from("/tmp/second")];
args.account_request_limits = vec![100];
assert!(matches!(
Config::build(args),
Err(ConfigError::MismatchedAccountRequestLimits)
));
}
#[test]
fn account_limit_parser_accepts_zero_as_unlimited() {
assert_eq!(parse_usize_csv("100, 0,250").unwrap(), vec![100, 0, 250]);
assert!(matches!(
parse_usize_csv("100,nope"),
Err(ConfigError::InvalidAccountRequestLimits)
));
}
#[test]
fn gonka_provider_requires_private_key() {
let result = Config::build(gonka_args(None));
assert!(matches!(result, Err(ConfigError::MissingGonkaPrivateKey)));
}
#[test]
fn gonka_provider_builds_with_private_key_and_defaults() {
let config =
Config::build(gonka_args(Some("gonka-private-key"))).expect("gonka config should build");
assert_eq!(config.upstream_provider, UpstreamProvider::Gonka);
assert_eq!(config.gonka_source_url, default_gonka_source_url());
assert_eq!(config.gonka_model, default_gonka_model());
assert_eq!(
config.gonka_private_key.as_deref(),
Some("gonka-private-key")
);
}
#[test]
fn crater_provider_requires_forgefed_inbox() {
let mut args = gonka_args(None);
args.upstream_provider = UpstreamProvider::Crater;
args.gonka_private_key = None;
let result = Config::build(args);
assert!(matches!(
result,
Err(ConfigError::MissingCraterForgeFedInbox)
));
}
#[test]
fn crater_provider_builds_with_forgefed_inbox() {
let mut args = gonka_args(None);
args.upstream_provider = UpstreamProvider::Crater;
args.gonka_private_key = None;
args.crater.inbox = Some("https://tracker.example/inbox".into());
let config = Config::build(args).expect("crater config should build");
assert_eq!(config.upstream_provider, UpstreamProvider::Crater);
assert_eq!(
config.crater.inbox.as_deref(),
Some("https://tracker.example/inbox")
);
}
fn gonka_args(private_key: Option<&str>) -> BuildArgs<'static> {
BuildArgs {
host: "0.0.0.0",
port: "8080",
token_secret: Some("secret"),
claude_code_home: "/tmp/claude",
upstream_base_url: "https://api.anthropic.com",
verbose: false,
max_proxy_request_bytes: DEFAULT_MAX_PROXY_REQUEST_BYTES,
api_format: None,
routing_mode: RoutingMode::Direct,
storage_policy: StoragePolicy::Memory,
data_dir: PathBuf::from("/tmp/test-data"),
claude_cli_bin: None,
upstream_provider: UpstreamProvider::Gonka,
gonka_private_key: private_key.map(str::to_string),
gonka_source_url: default_gonka_source_url(),
gonka_model: default_gonka_model(),
bridge_model: None,
bridge_model_policy: None,
audit_log: None,
crater: default_crater_config("https://router.example"),
openai_compatible: default_openai_compatible_config(),
activitypub_actor_base_url: "https://router.example".into(),
activitypub_public_key_pem: default_activitypub_public_key_pem(),
enable_openai_api: true,
enable_anthropic_api: true,
enable_metrics: true,
additional_account_dirs: vec![],
account_routing_strategy: SelectionStrategy::default(),
account_cooldown_secs: 60,
session_affinity_ttl_secs: 3600,
account_request_limits: vec![],
experimental_compatibility: false,
admin_key: None,
allow_anonymous_admin: false,
mpp: default_mpp_config(),
login: crate::login::LoginConfig::default(),
admin_ui: crate::admin::AdminUiConfig::default(),
chat_admin: crate::chat_admin::ChatAdminConfig::default(),
}
}
#[test]
fn test_config_invalid_port() {
let mut args = default_args(Some("secret"));
args.port = "not-a-number";
assert!(Config::build(args).is_err());
}
#[test]
fn test_config_default_port() {
let config = build_default(Some("secret")).expect("should build");
assert_eq!(config.listen_addr.port(), 8080);
}
#[test]
fn test_api_format_parsing() {
assert_eq!(
ApiFormat::from_str_opt("anthropic"),
Some(ApiFormat::Anthropic)
);
assert_eq!(
ApiFormat::from_str_opt("messages"),
Some(ApiFormat::Anthropic)
);
assert_eq!(ApiFormat::from_str_opt("bedrock"), Some(ApiFormat::Bedrock));
assert_eq!(ApiFormat::from_str_opt("invoke"), Some(ApiFormat::Bedrock));
assert_eq!(ApiFormat::from_str_opt("vertex"), Some(ApiFormat::Vertex));
assert_eq!(
ApiFormat::from_str_opt("rawpredict"),
Some(ApiFormat::Vertex)
);
assert_eq!(
ApiFormat::from_str_opt("ANTHROPIC"),
Some(ApiFormat::Anthropic)
);
assert!(ApiFormat::from_str_opt("unknown").is_none());
}
#[test]
fn test_routing_mode_parsing() {
assert_eq!(
RoutingMode::from_str_opt("direct"),
Some(RoutingMode::Direct)
);
assert_eq!(RoutingMode::from_str_opt("cli"), Some(RoutingMode::Cli));
assert_eq!(
RoutingMode::from_str_opt("hybrid"),
Some(RoutingMode::Hybrid)
);
assert_eq!(RoutingMode::from_str_opt("auto"), Some(RoutingMode::Hybrid));
assert_eq!(
RoutingMode::from_str_opt("subprocess"),
Some(RoutingMode::Cli)
);
assert!(RoutingMode::from_str_opt("nope").is_none());
}
#[test]
fn test_storage_policy_parsing() {
assert_eq!(
StoragePolicy::from_str_opt("memory"),
Some(StoragePolicy::Memory)
);
assert_eq!(
StoragePolicy::from_str_opt("text"),
Some(StoragePolicy::Text)
);
assert_eq!(
StoragePolicy::from_str_opt("binary"),
Some(StoragePolicy::Binary)
);
assert_eq!(
StoragePolicy::from_str_opt("both"),
Some(StoragePolicy::Both)
);
assert!(StoragePolicy::from_str_opt("nope").is_none());
}
#[test]
fn test_verbose_default_false() {
let config = build_default(Some("secret")).expect("should build");
assert!(!config.verbose);
}
#[test]
fn the_bridge_model_policy_is_configurable_and_validated() {
use crate::bridge_selection::BridgeModelPolicy;
let default = Config::build(default_args(Some("secret"))).expect("default config");
assert_eq!(
default.bridge_model_policy,
BridgeModelPolicy::FirstAdvertised,
"an unset policy keeps the deterministic default"
);
let mut args = default_args(Some("secret"));
args.bridge_model_policy = Some("last-advertised".to_string());
assert_eq!(
Config::build(args)
.expect("explicit policy")
.bridge_model_policy,
BridgeModelPolicy::LastAdvertised
);
let mut args = default_args(Some("secret"));
args.bridge_model_policy = Some("cheapest-possible".to_string());
let error = Config::build(args).expect_err("unknown policy must fail startup");
assert!(error.to_string().contains("cheapest-possible"), "{error}");
let mut args = default_args(Some("secret"));
args.bridge_model_policy = Some(String::new());
assert_eq!(
Config::build(args)
.expect("empty policy")
.bridge_model_policy,
BridgeModelPolicy::FirstAdvertised
);
}
#[test]
fn csv_parsing_trims_and_drops_blank_entries() {
assert_eq!(parse_csv("a, b ,,c"), vec!["a", "b", "c"]);
assert!(parse_csv("").is_empty());
assert!(parse_csv(" , ").is_empty());
}
#[test]
fn usize_csv_parsing_rejects_non_numeric_entries() {
assert_eq!(parse_usize_csv("1, 2,3").expect("numbers"), vec![1, 2, 3]);
assert!(parse_usize_csv("").expect("empty").is_empty());
assert!(parse_usize_csv("1,not-a-number").is_err());
}
#[test]
fn enum_parsing_accepts_documented_spellings() {
assert_eq!(
ApiFormat::from_str_opt("ANTHROPIC"),
Some(ApiFormat::Anthropic)
);
assert_eq!(ApiFormat::from_str_opt("nonsense"), None);
assert_eq!(
RoutingMode::from_str_opt("direct"),
Some(RoutingMode::Direct)
);
assert_eq!(RoutingMode::from_str_opt("nonsense"), None);
assert_eq!(
StoragePolicy::from_str_opt("memory"),
Some(StoragePolicy::Memory)
);
assert_eq!(StoragePolicy::from_str_opt("nonsense"), None);
assert_eq!(
UpstreamProvider::from_str_opt("codex"),
Some(UpstreamProvider::Codex)
);
assert_eq!(UpstreamProvider::from_str_opt("nonsense"), None);
}