use crate::channels::telegram::TelegramState;
use crate::channels::telegram::session_resolve::{
ResolveSource, build_session_title, chat_id_suffix, choose_resolve_source,
session_idle_expired, should_refresh_label, topic_session_id,
};
use crate::db::Database;
use crate::db::models::Session;
use crate::db::repository::SessionRepository;
use crate::services::{ServiceContext, SessionService};
use uuid::Uuid;
async fn fresh_repo() -> (Database, SessionRepository) {
let db = Database::connect_in_memory()
.await
.expect("in-memory DB connect");
db.run_migrations().await.expect("migrations");
let repo = SessionRepository::new(db.pool().clone());
(db, repo)
}
#[test]
fn resolve_policy_prefers_chat_bound_over_suffix_winner() {
let bound = Uuid::new_v4();
let suffix = Uuid::new_v4();
assert_eq!(
choose_resolve_source(Some(bound), false, Some(suffix)),
ResolveSource::ChatBound
);
}
#[tokio::test]
async fn telegram_state_chat_map_survives_suffix_competition() {
let state = TelegramState::new();
let chat_id = 4242_i64;
let bound = Uuid::new_v4();
let suffix_winner = Uuid::new_v4();
state.register_session_chat(bound, chat_id, None).await;
assert_eq!(state.chat_session(chat_id, None).await, Some(bound));
assert_eq!(
choose_resolve_source(
state.chat_session(chat_id, None).await,
false,
Some(suffix_winner)
),
ResolveSource::ChatBound
);
}
#[test]
fn should_not_clobber_auto_titled_dm_title() {
let auto = "Telegram: Fix deploy pipeline [chat:133526395]";
let template = build_session_title(true, "Alexey", 133526395, "", 133526395, None, None);
assert!(
!should_refresh_label(auto, &template),
"auto-titled DM must not revert to default template"
);
}
#[test]
fn group_rename_still_refreshes() {
let old = "Telegram: Old Group [chat:-5246593256]";
let new = "Telegram: New Group [chat:-5246593256]";
assert!(should_refresh_label(old, new));
}
#[tokio::test]
async fn suffix_lookup_after_switch_touch_picks_switched_row() {
let (_db, repo) = fresh_repo().await;
let chat_id = 42_i64;
let suffix = chat_id_suffix(chat_id, None);
let title = build_session_title(true, "U", 1, "", chat_id, None, None);
let older = Session::new(Some(title.clone()), None, None);
repo.create(&older).await.expect("create older");
let mut newer = Session::new(Some(title), None, None);
newer.updated_at = older.updated_at + chrono::Duration::seconds(1);
repo.create(&newer).await.expect("create newer");
let mut switched = older.clone();
switched.updated_at = newer.updated_at + chrono::Duration::seconds(1);
repo.update(&switched).await.expect("touch older");
let hit = repo
.find_by_title_suffix(&suffix)
.await
.expect("query")
.expect("hit");
assert_eq!(hit.id, older.id);
}
#[tokio::test]
async fn auto_titled_title_survives_should_refresh_check() {
let template = build_session_title(true, "Alice", 1, "", 99, None, None);
let auto_titled = format!("Telegram: Deploy fix {}", chat_id_suffix(99, None));
assert!(!should_refresh_label(&auto_titled, &template));
}
#[tokio::test]
async fn register_session_chat_binds_guest_dm() {
let state = TelegramState::new();
let guest_chat_id = 9988_i64;
let session_id = Uuid::new_v4();
state
.register_session_chat(session_id, guest_chat_id, None)
.await;
assert_eq!(
state.chat_session(guest_chat_id, None).await,
Some(session_id)
);
assert_eq!(
choose_resolve_source(state.chat_session(guest_chat_id, None).await, false, None),
ResolveSource::ChatBound
);
}
#[tokio::test]
async fn archived_chat_map_entry_uses_suffix_not_bound() {
let bound = Uuid::new_v4();
let suffix = Uuid::new_v4();
assert_eq!(
choose_resolve_source(Some(bound), true, Some(suffix)),
ResolveSource::Suffix
);
}
#[tokio::test]
async fn suffix_path_when_chat_map_empty() {
let suffix = Uuid::new_v4();
assert_eq!(
choose_resolve_source(None, false, Some(suffix)),
ResolveSource::Suffix
);
assert_eq!(
choose_resolve_source(None, false, None),
ResolveSource::Create
);
}
#[tokio::test]
async fn chat_bound_idle_archives_and_creates_new_session() {
let (db, repo) = fresh_repo().await;
let ctx = ServiceContext::new(db.pool().clone());
let svc = SessionService::new(ctx.clone());
let chat_id = 77_i64;
let title = build_session_title(true, "U", 1, "", chat_id, None, None);
let mut bound = Session::new(Some(title.clone()), None, None);
bound.updated_at = chrono::Utc::now() - chrono::Duration::hours(48);
repo.create(&bound).await.expect("create bound");
assert!(session_idle_expired(bound.updated_at, Some(1.0)));
repo.archive(bound.id).await.expect("archive idle bound");
let new_session = svc
.create_session(Some(title))
.await
.expect("create replacement");
assert_ne!(new_session.id, bound.id);
let archived = svc.get_session(bound.id).await.expect("get").expect("row");
assert!(archived.is_archived());
}
#[test]
fn chat_id_suffix_topic_vs_base_format() {
assert_eq!(chat_id_suffix(-100, None), "[chat:-100]");
assert_eq!(chat_id_suffix(-100, Some(42)), "[chat:-100:topic:42]");
}
#[test]
fn build_session_title_carries_topic_suffix() {
let base = build_session_title(false, "", 0, "Build", -100, None, None);
let topic = build_session_title(false, "", 0, "Build", -100, Some(42), None);
assert_eq!(base, "Telegram: Build [chat:-100]");
assert_eq!(topic, "Telegram: Build [chat:-100:topic:42]");
}
#[test]
fn build_session_title_shows_topic_name_keeps_numeric_suffix() {
let named = build_session_title(false, "", 0, "Build", -100, Some(2), Some("Devops"));
assert_eq!(named, "Telegram: Build / Devops [chat:-100:topic:2]");
assert!(
named.ends_with("[chat:-100:topic:2]"),
"suffix must stay numeric"
);
let blank = build_session_title(false, "", 0, "Build", -100, Some(2), Some(" "));
assert_eq!(blank, "Telegram: Build [chat:-100:topic:2]");
let no_topic = build_session_title(false, "", 0, "Build", -100, None, Some("Devops"));
assert_eq!(no_topic, "Telegram: Build [chat:-100]");
}
#[test]
fn refresh_label_promotes_id_titled_topic_session_to_named() {
let id_titled = "Telegram: Build [chat:-100:topic:2]";
let named = build_session_title(false, "", 0, "Build", -100, Some(2), Some("Devops"));
assert!(
should_refresh_label(id_titled, &named),
"id-titled topic session should be promoted to the named form"
);
}
#[test]
fn topic_session_id_gates_on_is_topic_message() {
assert_eq!(topic_session_id(true, Some(7)), Some(7));
assert_eq!(topic_session_id(false, Some(7)), None);
assert_eq!(topic_session_id(false, None), None);
assert_eq!(topic_session_id(true, None), None);
}
#[tokio::test]
async fn chat_map_isolates_topic_from_base_session() {
let state = TelegramState::new();
let chat_id = -1009_i64;
let base = Uuid::new_v4();
let topic = Uuid::new_v4();
state.register_session_chat(base, chat_id, None).await;
state.register_session_chat(topic, chat_id, Some(5)).await;
assert_eq!(state.chat_session(chat_id, None).await, Some(base));
assert_eq!(state.chat_session(chat_id, Some(5)).await, Some(topic));
assert_eq!(state.chat_session(chat_id, Some(99)).await, None);
}
#[tokio::test]
async fn base_and_topic_titles_do_not_cross_resolve() {
let (_db, repo) = fresh_repo().await;
let chat_id = -100_i64;
let base_title = build_session_title(false, "", 0, "G", chat_id, None, None);
let topic_title = build_session_title(false, "", 0, "G", chat_id, Some(7), None);
let base = Session::new(Some(base_title), None, None);
let topic = Session::new(Some(topic_title), None, None);
repo.create(&base).await.expect("create base");
repo.create(&topic).await.expect("create topic");
let base_hit = repo
.find_by_title_suffix(&chat_id_suffix(chat_id, None))
.await
.expect("query base")
.expect("base hit");
assert_eq!(base_hit.id, base.id, "base suffix must not match topic row");
let topic_hit = repo
.find_by_title_suffix(&chat_id_suffix(chat_id, Some(7)))
.await
.expect("query topic")
.expect("topic hit");
assert_eq!(
topic_hit.id, topic.id,
"topic suffix must not match base row"
);
}
#[tokio::test]
async fn service_update_session_title_preserves_suffix() {
let db = Database::connect_in_memory().await.expect("connect");
db.run_migrations().await.expect("migrations");
let ctx = ServiceContext::new(db.pool().clone());
let svc = SessionService::new(ctx);
let title = build_session_title(true, "U", 1, "", 77, None, None);
let session = svc
.create_session(Some(title.clone()))
.await
.expect("create");
let new_title = format!("Telegram: Custom topic {}", chat_id_suffix(77, None));
svc.update_session_title(session.id, Some(new_title.clone()))
.await
.expect("rename");
let loaded = svc
.get_session(session.id)
.await
.expect("get")
.expect("row");
assert_eq!(loaded.title.as_deref(), Some(new_title.as_str()));
assert!(
loaded.title.as_ref().unwrap().ends_with("[chat:77]"),
"suffix must remain for lookup"
);
}