This repository has been archived on 2026-10-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
maestro/crates/maestro-e2e/tests/tui_integration.rs
T
Miguel PalhasandClaude Opus 5 02356f84ab
ci / rust (pull_request) Successful in 3m0s
refactor(maestro): route every client through the shared DaemonClient
The CLI, TUI and end-to-end harness each carried their own client: three
reqwest builders with three timeout policies, three copies of the
success-or-body check, and route strings at every call site. Two of them
discarded the daemon's structured error and reported only a status code.

Deletes `maestro-cli/src/http.rs`, both `ws_base` copies in the TUI, the
CLI's partial `DaemonInfo` (it dropped `version` and `presets`), and the
e2e harness's `check_ok`. `maestro-web` gains a protocol dependency and
stops building the error envelope with a format string and reading it back
as untyped JSON; the daemon and the proxy now register routes from the same
constants.

`maestro_web::router` returns `Result` — an unusable daemon URL fails at
startup instead of at first connect.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 12:11:43 +01:00

130 lines
3.8 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 = daemon.client().events_url().unwrap();
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 = daemon.client().events_url().unwrap();
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,
};
daemon
.client()
.create_session(&req)
.await
.expect("create session");
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 = daemon.client().events_url().unwrap();
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 = daemon.client();
client.create_session(&req).await.expect("create session");
wait_until("creation", Duration::from_secs(2), || async {
store.snapshot().sessions.len() == 1
})
.await;
let sid = store.snapshot().sessions[0].id;
client.kill_session(sid).await.expect("kill session");
wait_until("removal", Duration::from_secs(2), || async {
store.snapshot().sessions.is_empty()
})
.await;
}