use std::sync::Arc;
use uuid::Uuid;
use crate::brain::agent::service::AgentService;
use crate::brain::provider::Provider;
use crate::db::Database;
use crate::db::Session;
use crate::services::ServiceContext;
use crate::tests::agent_service_mocks::MockProvider;
use crate::tui::app::App;
use crate::tui::events::AppMode;
async fn app() -> App {
let db = Database::connect_in_memory().await.unwrap();
db.run_migrations().await.unwrap();
let context = ServiceContext::new(db.pool().clone());
let provider: Arc<dyn Provider> = Arc::new(MockProvider);
let service = Arc::new(AgentService::new_for_test(provider, context.clone()).await);
#[cfg(feature = "whatsapp")]
{
App::new(
service,
context,
Arc::new(crate::channels::whatsapp::WhatsAppState::new()),
)
}
#[cfg(not(feature = "whatsapp"))]
{
App::new(service, context)
}
}
#[tokio::test]
async fn promoting_a_running_session_keeps_it_marked_processing() {
let mut app = app().await;
let sid = Uuid::new_v4();
app.processing_sessions.insert(sid);
app.is_processing = false;
app.streaming_reasoning = Some("weighing the options".to_string());
app.demote_to_background(sid);
app.is_processing = false;
assert!(app.promote_to_foreground(sid), "sidecar entry should exist");
assert!(
app.is_processing,
"a session in processing_sessions must stay marked processing after promotion; \
restoring the snapshot instead is what hid the indicator"
);
}
#[tokio::test]
async fn promoting_an_idle_session_leaves_it_idle() {
let mut app = app().await;
let sid = Uuid::new_v4();
app.processing_sessions.insert(sid);
app.is_processing = true;
app.streaming_reasoning = Some("mid thought".to_string());
app.demote_to_background(sid);
app.processing_sessions.remove(&sid);
app.is_processing = true;
assert!(app.promote_to_foreground(sid));
assert!(
!app.is_processing,
"a session absent from processing_sessions must not come back marked as running"
);
}
#[tokio::test]
async fn the_other_live_fields_still_survive_the_round_trip() {
let mut app = app().await;
let sid = Uuid::new_v4();
app.processing_sessions.insert(sid);
app.streaming_response = Some("partial answer".to_string());
app.streaming_reasoning = Some("thinking out loud".to_string());
app.streaming_output_tokens = 42;
app.demote_to_background(sid);
app.streaming_response = None;
app.streaming_reasoning = None;
app.streaming_output_tokens = 0;
assert!(app.promote_to_foreground(sid));
assert_eq!(app.streaming_response.as_deref(), Some("partial answer"));
assert_eq!(
app.streaming_reasoning.as_deref(),
Some("thinking out loud")
);
assert_eq!(app.streaming_output_tokens, 42);
}
#[tokio::test]
async fn an_idle_session_leaves_no_sidecar_entry() {
let mut app = app().await;
let sid = Uuid::new_v4();
app.demote_to_background(sid);
assert!(
!app.promote_to_foreground(sid),
"nothing live means nothing to promote"
);
}
#[tokio::test]
async fn reopening_the_picker_puts_the_cursor_on_the_current_session() {
let mut app = app().await;
let a = app
.session_service
.create_session(Some("alpha".into()))
.await
.unwrap();
let _b = app
.session_service
.create_session(Some("beta".into()))
.await
.unwrap();
let _c = app
.session_service
.create_session(Some("gamma".into()))
.await
.unwrap();
app.current_session = Some(a.clone());
app.session_service.update_session(&a).await.unwrap();
app.selected_session_index = 99;
app.switch_mode(AppMode::Sessions).await.unwrap();
let landed = app
.sessions
.get(app.selected_session_index)
.expect("cursor must land inside the loaded list");
assert_eq!(
landed.id, a.id,
"the cursor must follow the current session after the list re-sorts; \
keeping the old index is what put Enter on a same-channel twin (#1465)"
);
}
#[tokio::test]
async fn picker_falls_back_to_the_top_when_there_is_no_current_session() {
let mut app = app().await;
app.session_service
.create_session(Some("alpha".into()))
.await
.unwrap();
app.current_session = None;
app.selected_session_index = 42;
app.switch_mode(AppMode::Sessions).await.unwrap();
assert_eq!(app.selected_session_index, 0);
}
#[tokio::test]
async fn cursor_is_resolved_against_the_filtered_view() {
let mut app = app().await;
let keep = app
.session_service
.create_session(Some("keep me".into()))
.await
.unwrap();
app.sessions = vec![
Session {
title: Some("noise one".into()),
..keep.clone()
},
Session {
id: uuid::Uuid::new_v4(),
title: Some("noise two".into()),
..keep.clone()
},
keep.clone(),
];
app.sessions[0].id = uuid::Uuid::new_v4();
app.current_session = Some(keep.clone());
app.session_search = "keep".to_string();
app.focus_current_session();
let visible = app.visible_session_indices();
let idx = visible
.get(app.selected_session_index)
.and_then(|&i| app.sessions.get(i))
.expect("cursor must land inside the filtered view");
assert_eq!(
idx.id, keep.id,
"with a search active the cursor indexes the matches, not the full list"
);
}