diff --git a/crates/maestro-e2e/tests/e2e.rs b/crates/maestro-e2e/tests/e2e.rs index 217824a..1755c55 100644 --- a/crates/maestro-e2e/tests/e2e.rs +++ b/crates/maestro-e2e/tests/e2e.rs @@ -694,6 +694,103 @@ async fn daemon_events_ws_streams_snapshot_then_session_added() { client.kill_session(session.id).await.expect("kill"); } +// --------------------------------------------------------------------------- +// NAP-244 — CLI rewired through the home-daemon broker, with registry fallback +// --------------------------------------------------------------------------- + +/// Run the real `maestro` binary with an isolated env and capture its output. +fn run_cli(args: &[&str], envs: &[(&str, &str)]) -> std::process::Output { + let bin = maestro_e2e::workspace_target_bin("maestro", "maestro"); + let mut cmd = Command::new(&bin); + cmd.args(args); + cmd.env_clear().env( + "PATH", + std::env::var("PATH").unwrap_or_else(|_| "/usr/bin:/bin".into()), + ); + for (k, v) in envs { + cmd.env(k, v); + } + cmd.output().expect("run maestro cli") +} + +#[tokio::test(flavor = "multi_thread")] +async fn cli_list_reads_from_home_daemon() { + // Broker path: point the CLI at the daemon's local API and DON'T configure + // a registry. Success proves the catalog read came from the home daemon — + // the registry path would have errored with NoRegistryUrl. + let (_reg, daemons, client) = bring_up(&["alpha"]).await; + let cwd = TempDir::new().unwrap(); + let session = client + .spawn_session(&create_req("alpha", cwd.path(), "sleep 30")) + .await + .expect("spawn"); + + let daemon_url = format!("http://{}", daemons[0].bind_addr); + let home = TempDir::new().unwrap(); + let cfg_path = home.path().join("config.toml"); + let out = run_cli( + &["list", "--json"], + &[ + ("HOME", home.path().to_str().unwrap()), + ("MAESTRO_CONFIG", cfg_path.to_str().unwrap()), + ("MAESTRO_HOME_DAEMON_URL", &daemon_url), + ], + ); + let stdout = String::from_utf8_lossy(&out.stdout); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!(out.status.success(), "list failed: {stderr}\n{stdout}"); + assert!( + stdout.contains(&session.id.to_string()), + "session not listed via home daemon: {stdout}" + ); + assert!( + !stderr.contains("falling back"), + "unexpected registry fallback: {stderr}" + ); + + client.kill_session(session.id).await.ok(); +} + +#[tokio::test(flavor = "multi_thread")] +async fn cli_list_falls_back_to_registry_when_daemon_down() { + // Compat path: home daemon URL points at a dead port (connection refused). + // The CLI must warn and fall back to the configured registry, still + // returning the session. + let (reg, _daemons, client) = bring_up(&["alpha"]).await; + let cwd = TempDir::new().unwrap(); + let session = client + .spawn_session(&create_req("alpha", cwd.path(), "sleep 30")) + .await + .expect("spawn"); + + let dead_daemon = format!("http://127.0.0.1:{}", maestro_e2e::util::free_port()); + let registry_url = reg.http_base(); + let home = TempDir::new().unwrap(); + let cfg_path = home.path().join("config.toml"); + let out = run_cli( + &["list", "--json"], + &[ + ("HOME", home.path().to_str().unwrap()), + ("MAESTRO_CONFIG", cfg_path.to_str().unwrap()), + ("MAESTRO_HOME_DAEMON_URL", &dead_daemon), + ("MAESTRO_REGISTRY_URL", ®istry_url), + ], + ); + let stdout = String::from_utf8_lossy(&out.stdout); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!(out.status.success(), "list failed: {stderr}\n{stdout}"); + assert!( + stdout.contains(&session.id.to_string()), + "session not listed via registry fallback: {stdout}" + ); + assert!( + stderr.contains("falling back"), + "expected a fallback warning on stderr: {stderr}" + ); + + client.kill_session(session.id).await.ok(); +} + async fn next_event(ws: &mut S) -> maestro_protocol::RegistryEvent where S: StreamExt> + Unpin,