use super::*;
use crate::config::{
default_gonka_model, default_gonka_source_url, default_openai_compatible_base_url,
};
use clap::CommandFactory as _;
#[test]
fn login_cli_defaults_to_bare_tui() {
let cli = Cli::try_parse_from(["link-assistant-router"]).unwrap();
assert!(cli.login_cli_args.is_empty());
assert_eq!(cli.upstream_provider, "auto");
}
#[test]
fn every_declared_default_is_visible_in_its_long_help() {
fn check(command: &clap::Command, path: &str) {
let help = command.clone().render_long_help().to_string();
for argument in command.get_arguments() {
if argument.get_default_values().is_empty() || argument.is_hide_default_value_set() {
continue;
}
let rendered = argument
.get_default_values()
.iter()
.map(|value| value.to_string_lossy())
.collect::<Vec<_>>()
.join(", ");
if rendered.is_empty() {
continue;
}
assert!(
help.contains(&format!("[default: {rendered}]")),
"{path} --{} must render its declared default {rendered:?}",
argument.get_long().unwrap_or("<positional>")
);
}
for subcommand in command.get_subcommands() {
check(subcommand, &format!("{path} {}", subcommand.get_name()));
}
}
check(&Cli::command(), "link-assistant-router");
}
#[test]
fn sensitive_environment_values_are_hidden_from_help() {
fn check(command: &clap::Command) {
for argument in command.get_arguments() {
let Some(environment) = argument.get_env() else {
continue;
};
let environment = environment.to_string_lossy().to_ascii_uppercase();
if (["_TOKEN", "_SECRET", "_PRIVATE_KEY", "_API_KEY"]
.iter()
.any(|marker| environment.ends_with(marker))
|| environment.ends_with("_KEY") && !environment.contains("PUBLIC_KEY"))
&& !environment.ends_with("_ENV")
{
assert!(
argument.is_hide_env_values_set(),
"environment value {environment} must be hidden from help"
);
}
}
for subcommand in command.get_subcommands() {
check(subcommand);
}
}
check(&Cli::command());
}
#[test]
fn cli_defaults_round_trip_to_config() {
let cli = Cli {
command: None,
host: "127.0.0.1".into(),
port: 9090,
verbose: false,
token_secret: Some("k".into()),
claude_code_home: Some("/tmp/c".into()),
upstream_base_url: "https://api.anthropic.com".into(),
api_format: None,
routing_mode: "direct".into(),
storage_policy: "memory".into(),
data_dir: Some(std::path::PathBuf::from("/tmp/d")),
claude_cli_bin: None,
codex_cli_bin: None,
upstream_provider: "anthropic".into(),
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,
request_log: None,
request_log_max_bytes: crate::request_log::DEFAULT_MAX_BYTES,
max_proxy_request_bytes: crate::config::DEFAULT_MAX_PROXY_REQUEST_BYTES,
crater_forgefed_inbox: None,
crater_forgefed_actor: None,
crater_forgefed_target: None,
crater_poll_interval_ms: 1000,
crater_poll_timeout_secs: 120,
openai_compatible_provider_name: "litellm".into(),
openai_compatible_base_url: default_openai_compatible_base_url(),
openai_compatible_api_key: None,
openai_compatible_api_key_env: None,
openai_compatible_model: None,
openai_compatible_models: vec![],
activitypub_actor_base_url: Some("https://router.example".into()),
activitypub_public_key_pem: None,
disable_openai_api: false,
disable_anthropic_api: false,
disable_metrics: false,
additional_account_dirs: vec![],
account_routing_strategy: "round-robin".into(),
account_cooldown_secs: 60,
session_affinity_ttl_secs: 3600,
account_request_limits: vec![],
experimental_compatibility: false,
admin_port: None,
admin_host: "127.0.0.1".into(),
admin_claim_ttl_secs: crate::admin::DEFAULT_CANDIDATE_TTL_SECS,
admin_key: None,
allow_anonymous_admin: false,
telegram_bot_token: None,
vk_bot_token: None,
vk_group_id: None,
chat_admin_secret_ttl_secs: crate::chat_admin::DEFAULT_SECRET_TTL_SECS,
chat_admin_rate_limit_per_minute: crate::chat_admin::DEFAULT_RATE_LIMIT_PER_MINUTE,
mpp_enable: false,
mpp_amount: "0.00".into(),
mpp_currency: "USD".into(),
mpp_recipient: None,
mpp_method: None,
disable_login_api: false,
login_cli_command: "claude".into(),
login_cli_args: vec![],
login_session_ttl_secs: 900,
login_max_sessions: 4,
};
let cfg = cli.into_config().unwrap();
assert_eq!(cfg.listen_addr.port(), 9090);
assert_eq!(cfg.routing_mode, RoutingMode::Direct);
assert_eq!(cfg.storage_policy, StoragePolicy::Memory);
assert!(cfg.enable_openai_api);
assert!(cfg.enable_anthropic_api);
assert!(cfg.enable_metrics);
assert_eq!(
cfg.max_proxy_request_bytes,
crate::config::DEFAULT_MAX_PROXY_REQUEST_BYTES
);
}
#[test]
fn cli_invalid_routing_mode_rejected() {
let cli = Cli {
command: None,
host: "0.0.0.0".into(),
port: 8080,
verbose: false,
token_secret: Some("k".into()),
claude_code_home: Some("/tmp/c".into()),
upstream_base_url: "https://api.anthropic.com".into(),
api_format: None,
routing_mode: "bogus".into(),
storage_policy: "memory".into(),
data_dir: None,
claude_cli_bin: None,
codex_cli_bin: None,
upstream_provider: "anthropic".into(),
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,
request_log: None,
request_log_max_bytes: crate::request_log::DEFAULT_MAX_BYTES,
max_proxy_request_bytes: crate::config::DEFAULT_MAX_PROXY_REQUEST_BYTES,
crater_forgefed_inbox: None,
crater_forgefed_actor: None,
crater_forgefed_target: None,
crater_poll_interval_ms: 1000,
crater_poll_timeout_secs: 120,
openai_compatible_provider_name: "litellm".into(),
openai_compatible_base_url: default_openai_compatible_base_url(),
openai_compatible_api_key: None,
openai_compatible_api_key_env: None,
openai_compatible_model: None,
openai_compatible_models: vec![],
activitypub_actor_base_url: None,
activitypub_public_key_pem: None,
disable_openai_api: false,
disable_anthropic_api: false,
disable_metrics: false,
additional_account_dirs: vec![],
account_routing_strategy: "round-robin".into(),
account_cooldown_secs: 60,
session_affinity_ttl_secs: 3600,
account_request_limits: vec![],
experimental_compatibility: false,
admin_port: None,
admin_host: "127.0.0.1".into(),
admin_claim_ttl_secs: crate::admin::DEFAULT_CANDIDATE_TTL_SECS,
admin_key: None,
allow_anonymous_admin: false,
telegram_bot_token: None,
vk_bot_token: None,
vk_group_id: None,
chat_admin_secret_ttl_secs: crate::chat_admin::DEFAULT_SECRET_TTL_SECS,
chat_admin_rate_limit_per_minute: crate::chat_admin::DEFAULT_RATE_LIMIT_PER_MINUTE,
mpp_enable: false,
mpp_amount: "0.00".into(),
mpp_currency: "USD".into(),
mpp_recipient: None,
mpp_method: None,
disable_login_api: false,
login_cli_command: "claude".into(),
login_cli_args: vec![],
login_session_ttl_secs: 900,
login_max_sessions: 4,
};
let r = cli.into_config();
assert!(matches!(r, Err(ConfigError::InvalidRoutingMode)));
}
fn auth_op_of(args: &[&str]) -> AuthOp {
let cli = Cli::try_parse_from(args).expect("the command line must parse");
match cli.command {
Some(Command::Auth { op, .. }) => op,
other => panic!("expected an auth command, got {other:?}"),
}
}
#[test]
fn auth_gh_accepts_a_target_like_its_siblings() {
let op = auth_op_of(&[
"link-assistant-router",
"auth",
"gh",
"--server",
"http://router.example:8080",
"--status",
]);
let AuthOp::Gh { target, .. } = &op else {
panic!("expected auth gh, got {op:?}");
};
assert_eq!(target.server.as_deref(), Some("http://router.example:8080"));
assert!(!target.local);
}
#[test]
fn only_a_read_only_gh_query_answers_for_a_selected_router() {
let describes = ["--status"];
let refuses: [&[&str]; 3] = [
&["--token-stdin"],
&["--from-gh-config", "/tmp/gh"],
&["--clear"],
];
let op = auth_op_of(&[&["link-assistant-router", "auth", "gh"][..], &describes[..]].concat());
assert_eq!(op.remote_gh(), Some(RemoteGh::DescribeLocal));
for extra in refuses {
let op = auth_op_of(&[&["link-assistant-router", "auth", "gh"][..], extra].concat());
assert_eq!(
op.remote_gh(),
Some(RemoteGh::Refuse),
"a credential must never be stored locally under another target: {extra:?}"
);
}
}
#[test]
fn only_gh_carries_the_remote_restriction() {
for args in [
&["link-assistant-router", "auth", "status"][..],
&["link-assistant-router", "auth", "claude"][..],
&["link-assistant-router", "auth", "codex"][..],
&["link-assistant-router", "auth", "import", "claude"][..],
] {
assert_eq!(
auth_op_of(args).remote_gh(),
None,
"{args:?} must keep its ordinary remote path"
);
}
}