There were three answers to "how do I test against a daemon", and the CLI
— the thinnest client — had none, so its formatting and error handling
were exercised by compiling and forking the real binary plus tmux.
Adds crates/maestro-testing with a MockDaemon that serves the real /api/*
surface from maestro-protocol types, backed by a Vec<Session>. It replaces
two ad-hoc stubs: maestro-web's, which returned [{"id":"session-1"}] —
not a valid Session — and maestro-tui's scripted socket with a hardcoded
timeline.
maestro-cli gets its first tests against a daemon: six cases covering the
catalog, that DaemonInfo carries the version and presets its old private
copy dropped, that a structured failure surfaces the daemon's own message,
and that connection-refused stays distinguishable from an HTTP rejection.
maestro-web's proxy suite gains an end-to-end check that an upstream
rejection reaches the browser as a 4502 close carrying the daemon's
message — upstream_close_reason had only unit tests.
e2e keeps the tmux-shaped tests. cli_list_reads_from_daemon still runs the
shipped binary as a subprocess but now against MockDaemon, and
tui_integration.rs is deleted: the real daemon's /api/events contract is
covered by maestro-daemon/tests/local_api.rs and the client reducer by
maestro-tui/tests/catalog_ws.rs. The e2e suite drops from ~32s to ~20s.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
108 lines
3.1 KiB
Rust
108 lines
3.1 KiB
Rust
//! The CLI against a daemon. It had no such tests: `maestro list`'s formatting
|
|
//! was unit-tested, but nothing exercised the client, the error decoding, or
|
|
//! the unreachable-daemon path without compiling and forking a real daemon.
|
|
|
|
use maestro_protocol::{DaemonClient, DaemonInfo};
|
|
use maestro_testing::{session, MockDaemon};
|
|
use uuid::Uuid;
|
|
|
|
#[tokio::test]
|
|
async fn list_reads_the_daemon_catalog() {
|
|
let mock = MockDaemon::start().await;
|
|
mock.add_session(session(Uuid::from_u128(1), "/home/mock/work", "claude"));
|
|
|
|
let sessions = mock.client().list_sessions().await.expect("list");
|
|
|
|
assert_eq!(sessions.len(), 1);
|
|
assert_eq!(sessions[0].command, "claude");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn info_carries_version_and_presets() {
|
|
let mock = MockDaemon::start().await;
|
|
mock.set_info(DaemonInfo {
|
|
hostname: "konishi".into(),
|
|
version: "9.9.9".into(),
|
|
home_dir: "/home/naps".into(),
|
|
presets: vec![maestro_protocol::SpawnPreset {
|
|
id: "claude".into(),
|
|
label: "Claude".into(),
|
|
command: "claude".into(),
|
|
}],
|
|
});
|
|
|
|
let info = mock.client().daemon_info().await.expect("info");
|
|
|
|
// The CLI's own DaemonInfo used to drop both of these fields.
|
|
assert_eq!(info.version, "9.9.9");
|
|
assert_eq!(info.presets.len(), 1);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn a_structured_failure_surfaces_the_daemon_message() {
|
|
let mock = MockDaemon::start().await;
|
|
mock.force_error(502, "tmux_failed", "no server running");
|
|
|
|
let err = mock
|
|
.client()
|
|
.list_sessions()
|
|
.await
|
|
.expect_err("should fail");
|
|
|
|
assert_eq!(
|
|
err.daemon_message(),
|
|
Some("no server running"),
|
|
"the daemon's own message must survive: {err}"
|
|
);
|
|
assert!(!err.is_unreachable());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn an_unreachable_daemon_is_distinguishable_from_a_rejection() {
|
|
// Port 1 has nothing listening on it.
|
|
let client = DaemonClient::new("http://127.0.0.1:1");
|
|
|
|
let err = client.list_sessions().await.expect_err("should fail");
|
|
|
|
assert!(
|
|
err.is_unreachable(),
|
|
"connection refused must not read as an HTTP rejection: {err}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn killing_an_unknown_session_reports_the_daemon_reason() {
|
|
let mock = MockDaemon::start().await;
|
|
|
|
let err = mock
|
|
.client()
|
|
.kill_session(Uuid::new_v4())
|
|
.await
|
|
.expect_err("should fail");
|
|
|
|
assert_eq!(err.daemon_message(), Some("no such session on this daemon"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn spawn_round_trips_through_the_daemon() {
|
|
let mock = MockDaemon::start().await;
|
|
|
|
let created = mock
|
|
.client()
|
|
.create_session(&maestro_protocol::CreateSessionRequest {
|
|
cwd: "/home/mock".into(),
|
|
command: "bash".into(),
|
|
env: Default::default(),
|
|
metadata: Default::default(),
|
|
worktree: None,
|
|
})
|
|
.await
|
|
.expect("create");
|
|
|
|
assert_eq!(created.command, "bash");
|
|
assert_eq!(mock.session_count(), 1);
|
|
|
|
mock.client().kill_session(created.id).await.expect("kill");
|
|
assert_eq!(mock.session_count(), 0);
|
|
}
|