use super::*;
#[test]
#[serial_test::serial]
fn resolve_secret_reads_the_bot_token_from_the_process_environment() {
use crate::secret_source::test_env::{EnvVarGuard, assert_secret_eq};
use trusty_common::credentials::MemoryKeyStore;
let _env = EnvVarGuard::set("TELEGRAM_BOT_TOKEN", "123:SYNTHETIC-NOT-A-TOKEN");
let value = crate::secret_source::resolve_secret_with(
"TELEGRAM_BOT_TOKEN",
std::sync::Arc::new(MemoryKeyStore::new()),
std::time::Duration::from_millis(500),
);
assert_secret_eq(
value.as_deref(),
Some("123:SYNTHETIC-NOT-A-TOKEN"),
"the bot token did not come from the process environment",
);
}
#[test]
fn resolve_token_is_none_when_the_credential_is_unresolvable() {
assert!(resolve_token("TRUSTY_MPM_TEST_TELEGRAM_UNREGISTERED").is_none());
}
#[test]
fn resolve_token_missing_is_none() {
assert!(resolve_token("TRUSTY_MPM_TEST_TELEGRAM_ABSENT").is_none());
}
#[test]
fn resolve_username_uses_env_when_set() {
assert_eq!(resolve_username(Some("my_bot".into())), "my_bot");
assert_eq!(resolve_username(Some(" my_bot ".into())), "my_bot");
}
#[test]
fn resolve_username_falls_back_when_unset() {
assert_eq!(resolve_username(None), DEFAULT_BOT_USERNAME);
assert_eq!(resolve_username(None), "t_sess_bot");
}
#[test]
fn resolve_username_falls_back_when_empty() {
assert_eq!(resolve_username(Some(String::new())), DEFAULT_BOT_USERNAME);
assert_eq!(resolve_username(Some(" ".into())), DEFAULT_BOT_USERNAME);
}
#[test]
fn authorization_respects_allowed_user() {
let unrestricted = BotOptions::default();
assert!(is_authorized(&unrestricted, Some(7)));
assert!(is_authorized(&unrestricted, None));
let restricted = BotOptions {
allowed_user_id: Some(42),
alert_chat_id: None,
};
assert!(is_authorized(&restricted, Some(42)));
assert!(!is_authorized(&restricted, Some(99)));
assert!(!is_authorized(&restricted, None));
}
async fn spawn_test_daemon() -> (std::sync::Arc<crate::daemon::state::DaemonState>, String) {
use crate::daemon::{api, state::DaemonState};
use std::future::IntoFuture;
let root = tempfile::tempdir().unwrap().keep();
let state = std::sync::Arc::new(DaemonState::with_root(root));
let router = api::router(std::sync::Arc::clone(&state));
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(axum::serve(listener, router).into_future());
(state, format!("http://{addr}"))
}
async fn spawn_test_daemon_degraded_sm() -> (
std::sync::Arc<crate::daemon::state::DaemonState>,
String,
tempfile::TempDir,
) {
use crate::core::sm::SessionManagerAgent;
use crate::core::sm::agent::mock::MockResolver;
use crate::core::sm::config::SessionManagerConfig;
use crate::daemon::{api, state::DaemonState};
use std::future::IntoFuture;
let tmp = tempfile::tempdir().unwrap();
let enabled_cfg = SessionManagerConfig {
enabled: true,
..SessionManagerConfig::default()
};
let agent = std::sync::Arc::new(SessionManagerAgent::for_test(
enabled_cfg,
std::sync::Arc::new(MockResolver::degraded()),
tmp.path().to_path_buf(),
));
let state = std::sync::Arc::new(DaemonState::with_session_manager_agent(agent));
let router = api::router(std::sync::Arc::clone(&state));
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(axum::serve(listener, router).into_future());
(state, format!("http://{addr}"), tmp)
}
#[tokio::test]
async fn action_chat_reply_reports_unconfigured() {
let (_state, url, _tmp) = spawn_test_daemon_degraded_sm().await;
let executor = CommandExecutor::new(url);
let histories: ChatHistories = Arc::new(Mutex::new(HashMap::new()));
let reply = action_chat_reply(&executor, &histories, 42, "hello there").await;
assert!(
reply.contains("inference"),
"reply must mention 'inference', got: {reply:?}"
);
assert!(
reply.contains("OPENROUTER_API_KEY")
|| reply.contains("configured")
|| reply.contains("credentials"),
"reply must hint at configuring credentials, got: {reply:?}"
);
assert!(
histories.lock().unwrap().get(&42).is_some(),
"explanatory turn must be stored in chat history"
);
}
#[test]
fn record_chat_turn_caps_history() {
let mut entry: Vec<ChatMessage> = Vec::new();
let turns = MAX_CHAT_HISTORY_TURNS * 2; for i in 0..turns {
record_chat_turn(&mut entry, &format!("user {i}"), &format!("reply {i}"));
}
assert_eq!(entry.len(), MAX_CHAT_HISTORY_TURNS);
assert!(
!entry.iter().any(|m| m.content == "user 0"),
"oldest turn must be evicted"
);
let last = turns - 1;
assert_eq!(entry.last().unwrap().content, format!("reply {last}"));
}
#[test]
fn record_chat_turn_skips_empty_reply() {
let mut entry: Vec<ChatMessage> = Vec::new();
record_chat_turn(&mut entry, "hello", "");
assert_eq!(entry.len(), 1);
assert_eq!(entry[0].role, "user");
assert_eq!(entry[0].content, "hello");
record_chat_turn(&mut entry, "again", "there");
assert_eq!(entry.len(), 3);
assert_eq!(entry[2].role, "assistant");
assert_eq!(entry[2].content, "there");
}
#[test]
fn action_footer_lists_verbs() {
let verbs = vec!["sessions.health".to_string(), "sessions.list".to_string()];
let footer = action_footer(Some(&verbs)).expect("footer for non-empty verbs");
assert_eq!(footer, "\n\n<i>ran: sessions.health, sessions.list</i>");
}
#[test]
fn action_footer_absent_when_empty() {
assert!(action_footer(None).is_none());
assert!(action_footer(Some(&[])).is_none());
}
#[tokio::test]
async fn dispatch_help_returns_help() {
let executor = CommandExecutor::new("http://unused");
let result = dispatch_command(TelegramCommand::Help, &executor, 1).await;
assert!(matches!(result, CommandResult::Help(_)));
}
#[tokio::test]
async fn dispatch_start_with_no_code_queries_state() {
let (_state, url) = spawn_test_daemon().await;
let executor = CommandExecutor::new(url);
let result = dispatch_command(TelegramCommand::Start(String::new()), &executor, 1).await;
match result {
CommandResult::PairState { paired } => assert!(!paired),
other => panic!("expected PairState, got {other:?}"),
}
}
#[tokio::test]
async fn dispatch_start_with_deep_link_code_confirms() {
let (state, url) = spawn_test_daemon().await;
let code = state.generate_pair_code();
let executor = CommandExecutor::new(url);
let result = dispatch_command(TelegramCommand::Start(code), &executor, 555).await;
match result {
CommandResult::PairSuccess { chat_info } => assert!(chat_info.contains("555")),
other => panic!("expected PairSuccess, got {other:?}"),
}
}
#[tokio::test]
async fn dispatch_pair_with_bad_code_errors() {
let (_state, url) = spawn_test_daemon().await;
let executor = CommandExecutor::new(url);
let result = dispatch_command(TelegramCommand::Pair("ZZZZZZ".into()), &executor, 1).await;
assert!(matches!(result, CommandResult::Error(_)));
}