use std::sync::Arc;
use ratatui::{Terminal, backend::TestBackend};
use uuid::Uuid;
use crate::brain::agent::service::AgentService;
use crate::brain::provider::Provider;
use crate::db::Database;
use crate::services::ServiceContext;
use crate::tests::agent_service_mocks::MockProvider;
use crate::tui::app::{App, DisplayMessage};
use crate::tui::render::{render, theme};
const WIDTH: u16 = 60;
const HEIGHT: u16 = 24;
fn message(role: &str, content: &str) -> DisplayMessage {
DisplayMessage {
id: Uuid::new_v4(),
role: role.to_string(),
content: content.to_string(),
timestamp: chrono::Utc::now(),
token_count: None,
cost: None,
approval: None,
approve_menu: None,
details: None,
expanded: false,
expanded_full: false,
tool_group: None,
duration_secs: None,
}
}
async fn app_with_chat() -> 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")]
let mut app = App::new(
service,
context,
Arc::new(crate::channels::whatsapp::WhatsAppState::new()),
);
#[cfg(not(feature = "whatsapp"))]
let mut app = App::new(service, context);
app.messages.push(message("user", "a question with `code`"));
app.messages
.push(message("assistant", "an answer with **bold** prose"));
app
}
fn draw(terminal: &mut Terminal<TestBackend>, app: &mut App) {
terminal.draw(|f| render(f, app)).unwrap();
}
#[tokio::test]
async fn a_fresh_app_starts_in_sync_with_the_live_generation() {
let app = app_with_chat().await;
assert_eq!(app.render_cache_theme_gen, theme::generation());
}
#[tokio::test]
async fn rendering_fills_the_cache_and_stamps_the_generation() {
let mut app = app_with_chat().await;
let mut terminal = Terminal::new(TestBackend::new(WIDTH, HEIGHT)).unwrap();
draw(&mut terminal, &mut app);
assert!(
!app.render_cache.is_empty(),
"nothing cached, so the invalidation test below would prove nothing"
);
assert_eq!(app.render_cache_theme_gen, theme::generation());
}
#[tokio::test]
async fn a_stale_generation_drops_the_cache() {
let mut app = app_with_chat().await;
let mut terminal = Terminal::new(TestBackend::new(WIDTH, HEIGHT)).unwrap();
draw(&mut terminal, &mut app);
let sentinel = (Uuid::new_v4(), WIDTH);
app.render_cache
.insert(sentinel, vec![ratatui::text::Line::from("stale")]);
draw(&mut terminal, &mut app);
assert!(
app.render_cache.contains_key(&sentinel),
"the cache is being cleared every frame, which defeats its purpose"
);
app.render_cache_theme_gen = theme::generation().wrapping_sub(1);
draw(&mut terminal, &mut app);
assert!(
!app.render_cache.contains_key(&sentinel),
"lines styled under the previous theme survived the switch"
);
assert_eq!(
app.render_cache_theme_gen,
theme::generation(),
"the stamp was not re-synced, so every later frame would clear again"
);
}
#[tokio::test]
async fn a_stale_generation_drops_the_streaming_cache() {
let mut app = app_with_chat().await;
let mut terminal = Terminal::new(TestBackend::new(WIDTH, HEIGHT)).unwrap();
draw(&mut terminal, &mut app);
app.streaming_render_cache = Some((7, vec![ratatui::text::Line::from("stale stream")]));
app.render_cache_theme_gen = theme::generation().wrapping_sub(1);
draw(&mut terminal, &mut app);
assert!(
app.streaming_render_cache.is_none(),
"the streaming answer kept the previous theme's colours"
);
}