Drops the registry subprocess and the T2 broker scenarios; the harness spawns one daemon and the client talks straight to its /api/* surface.
145 lines
4.3 KiB
Rust
145 lines
4.3 KiB
Rust
//! TUI integration tests (NAP-282).
|
|
//!
|
|
//! Exercises `CatalogStore` and the per-session `SessionTask` against a real
|
|
//! daemon subprocess. We don't spin the full ratatui frontend
|
|
//! here — those interactions are best tested with `TestBackend` unit tests in
|
|
//! `crates/maestro-tui/src/*.rs`. This file fills the gap between those
|
|
//! and the headless e2e suite by verifying that the TUI's wire layer plays
|
|
//! nicely with a real daemon.
|
|
//!
|
|
//! Requires `tmux` and `git` on `$PATH`.
|
|
|
|
#![allow(clippy::result_large_err)]
|
|
|
|
use std::path::PathBuf;
|
|
use std::time::{Duration, Instant};
|
|
|
|
use maestro_e2e::Daemon;
|
|
use maestro_protocol::CreateSessionRequest;
|
|
use maestro_tui::catalog::CatalogStore;
|
|
|
|
async fn wait_until<F, Fut>(label: &str, deadline: Duration, mut check: F)
|
|
where
|
|
F: FnMut() -> Fut,
|
|
Fut: std::future::Future<Output = bool>,
|
|
{
|
|
let start = Instant::now();
|
|
loop {
|
|
if check().await {
|
|
return;
|
|
}
|
|
if start.elapsed() >= deadline {
|
|
panic!("timeout waiting for {label}");
|
|
}
|
|
tokio::time::sleep(Duration::from_millis(25)).await;
|
|
}
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn catalog_bootstrap_against_real_daemon() {
|
|
let daemon = Daemon::spawn().await;
|
|
let ws_url = format!("{}/api/events", daemon.ws_base());
|
|
|
|
let store = CatalogStore::default();
|
|
let bg = store.clone();
|
|
tokio::spawn(async move { bg.run_events_loop(ws_url).await });
|
|
|
|
tokio::time::timeout(Duration::from_secs(3), store.wait_for_initial_snapshot())
|
|
.await
|
|
.expect("initial snapshot");
|
|
|
|
assert!(store.snapshot().sessions.is_empty());
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn post_session_propagates_through_events_ws() {
|
|
let daemon = Daemon::spawn().await;
|
|
let ws_url = format!("{}/api/events", daemon.ws_base());
|
|
let http_base = daemon.http_base();
|
|
|
|
let store = CatalogStore::default();
|
|
let bg = store.clone();
|
|
tokio::spawn(async move { bg.run_events_loop(ws_url).await });
|
|
|
|
tokio::time::timeout(Duration::from_secs(3), store.wait_for_initial_snapshot())
|
|
.await
|
|
.expect("initial snapshot");
|
|
|
|
let cwd = PathBuf::from(daemon.home());
|
|
let req = CreateSessionRequest {
|
|
cwd,
|
|
command: "bash".into(),
|
|
env: Default::default(),
|
|
metadata: Default::default(),
|
|
worktree: None,
|
|
};
|
|
let res = reqwest::Client::new()
|
|
.post(format!("{http_base}/api/sessions"))
|
|
.json(&req)
|
|
.send()
|
|
.await
|
|
.expect("post sessions");
|
|
assert!(res.status().is_success(), "POST /api/sessions: {res:?}");
|
|
|
|
wait_until(
|
|
"session to surface in catalog",
|
|
Duration::from_secs(2),
|
|
|| async { store.snapshot().sessions.len() == 1 },
|
|
)
|
|
.await;
|
|
|
|
let snap = store.snapshot();
|
|
assert_eq!(snap.sessions.len(), 1);
|
|
assert_eq!(snap.sessions[0].command, "bash");
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn delete_session_propagates_removal() {
|
|
let daemon = Daemon::spawn().await;
|
|
let ws_url = format!("{}/api/events", daemon.ws_base());
|
|
let http_base = daemon.http_base();
|
|
|
|
let store = CatalogStore::default();
|
|
let bg = store.clone();
|
|
tokio::spawn(async move { bg.run_events_loop(ws_url).await });
|
|
|
|
tokio::time::timeout(Duration::from_secs(3), store.wait_for_initial_snapshot())
|
|
.await
|
|
.expect("initial snapshot");
|
|
|
|
let cwd = PathBuf::from(daemon.home());
|
|
let req = CreateSessionRequest {
|
|
cwd,
|
|
command: "bash".into(),
|
|
env: Default::default(),
|
|
metadata: Default::default(),
|
|
worktree: None,
|
|
};
|
|
let client = reqwest::Client::new();
|
|
let res = client
|
|
.post(format!("{http_base}/api/sessions"))
|
|
.json(&req)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert!(res.status().is_success());
|
|
|
|
wait_until("creation", Duration::from_secs(2), || async {
|
|
store.snapshot().sessions.len() == 1
|
|
})
|
|
.await;
|
|
|
|
let sid = store.snapshot().sessions[0].id;
|
|
let res = client
|
|
.delete(format!("{http_base}/api/sessions/{sid}"))
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert!(res.status().is_success(), "DELETE /api/sessions: {res:?}");
|
|
|
|
wait_until("removal", Duration::from_secs(2), || async {
|
|
store.snapshot().sessions.is_empty()
|
|
})
|
|
.await;
|
|
}
|