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-daemon/tests/registry_client.rs
T
naps62-yoloandClaude Opus 4.8 6d7cd0b4c8 feat(maestro): spawn form defaults command to claude + prefills cwd to daemon home dir (NAP-328)
Two web spawn QoL improvements plus the wire support they need:

- Command field now prefills `claude` instead of an empty box.
- Working directory prefills the selected daemon's home dir, updating on
  daemon change unless the user has typed their own value.

The daemon resolves its `$HOME` via `dirs::home_dir()` at startup and
advertises it end-to-end: a new `home_dir` field on `DaemonOutbound::Hello`
flows through the registry's `register` into `DaemonInfo` (served on
`/api/daemons` and `/api/events`). The field is `#[serde(default)]`, so older
daemons that omit it deserialize to an empty string — backward compatible, and
the UI simply skips prefill when it's empty.

Hierarchical `/`-separated labels in the web sidebar already worked
(web/src/catalog.ts); no change there.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 18:07:53 +00:00

215 lines
6.3 KiB
Rust

//! End-to-end tests for the daemon's outbound registry client.
//!
//! Uses the `maestro-registry` crate as the real registry under test. Verifies
//! the daemon connects, sends Hello + snapshot, forwards diffs, replies to
//! inbound CreateSession/KillSession, and reconnects after a registry kill.
mod common;
use std::collections::HashMap;
use std::net::{Ipv4Addr, SocketAddr};
use std::time::Duration;
use common::{wait_for_async, DaemonFixture};
use maestro_daemon::RegistryClient;
use maestro_protocol::CreateSessionRequest;
use maestro_registry::{router, Registry};
use tempfile::TempDir;
use tokio::net::TcpListener;
use tokio::sync::oneshot;
const TOKEN: &str = "shared-registry-token";
struct RegistryHarness {
url: String,
state: Registry,
shutdown: Option<oneshot::Sender<()>>,
}
async fn spawn_registry() -> RegistryHarness {
let listener = TcpListener::bind(SocketAddr::from((Ipv4Addr::LOCALHOST, 0)))
.await
.unwrap();
let addr = listener.local_addr().unwrap();
let state = Registry::new();
let app = router(state.clone(), TOKEN.to_string());
let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
tokio::spawn(async move {
let _ = axum::serve(listener, app)
.with_graceful_shutdown(async move {
let _ = shutdown_rx.await;
})
.await;
});
RegistryHarness {
url: format!("ws://{addr}/daemon"),
state,
shutdown: Some(shutdown_tx),
}
}
#[tokio::test]
async fn daemon_registers_and_streams_snapshot() {
let reg = spawn_registry().await;
let fx = DaemonFixture::new("laptop");
let client = RegistryClient::new(
reg.url.clone(),
"laptop",
"naps-laptop",
"ws://127.0.0.1:8081",
"/home/naps62",
TOKEN,
fx.manager.clone(),
);
tokio::spawn(async move { client.run().await });
// wait for the Hello + snapshot to land
let state = reg.state.clone();
wait_for_async(Duration::from_secs(5), || {
let r = state.clone();
async move {
let daemons = r.daemons().await;
daemons.len() == 1 && daemons[0].daemon_id == "laptop"
}
})
.await;
// create a session via the manager — should trigger a SessionAdded diff
let cwd = TempDir::new().unwrap();
let session = fx
.manager
.create(CreateSessionRequest {
daemon_id: "laptop".into(),
cwd: cwd.path().to_path_buf(),
command: "sleep 30".into(),
env: HashMap::new(),
metadata: HashMap::new(),
worktree: None,
})
.await
.unwrap();
let state = reg.state.clone();
let want_id = session.id;
wait_for_async(Duration::from_secs(5), || {
let r = state.clone();
async move {
let s = r.sessions().await;
s.len() == 1 && s[0].id == want_id
}
})
.await;
let _ = fx.manager.kill(session.id).await;
}
#[tokio::test]
async fn daemon_reconnects_after_registry_drop() {
// first registry: accept connection, then drop
let mut reg1 = spawn_registry().await;
let url = reg1.url.clone();
let fx = DaemonFixture::new("laptop");
let client = RegistryClient::new(
url.clone(),
"laptop",
"naps-laptop",
"ws://127.0.0.1:8081",
"/home/naps62",
TOKEN,
fx.manager.clone(),
);
tokio::spawn(async move { client.run().await });
// wait for daemon to register on the first registry
let state1 = reg1.state.clone();
wait_for_async(Duration::from_secs(5), || {
let r = state1.clone();
async move { !r.daemons().await.is_empty() }
})
.await;
// drop registry1; daemon should reconnect once we bring another up on the
// same port. Easier: bring up a second registry on a *different* port
// requires changing url, which RegistryClient doesn't support — instead,
// just verify the daemon's task survives the drop and the original
// registry's state becomes empty (the daemon connection died).
let _ = reg1.shutdown.take().unwrap().send(());
// give some time for the daemon to notice
tokio::time::sleep(Duration::from_millis(500)).await;
// RegistryClient::run loops forever, so we can't easily assert
// reconnection without standing up the registry on the same port again.
// The hard part — graceful failure of the inner connection — is what we're
// really testing: the daemon must not panic.
// (A full reconnect-against-fresh-server test belongs in the e2e crate
// NAP-217.)
let _ = fx;
}
#[tokio::test]
async fn registry_create_session_round_trips() {
let reg = spawn_registry().await;
let fx = DaemonFixture::new("laptop");
let client = RegistryClient::new(
reg.url.clone(),
"laptop",
"naps-laptop",
"ws://127.0.0.1:8081",
"/home/naps62",
TOKEN,
fx.manager.clone(),
);
tokio::spawn(async move { client.run().await });
let state = reg.state.clone();
wait_for_async(Duration::from_secs(5), || {
let r = state.clone();
async move { !r.daemons().await.is_empty() }
})
.await;
let cwd = TempDir::new().unwrap();
let request = CreateSessionRequest {
daemon_id: "laptop".into(),
cwd: cwd.path().to_path_buf(),
command: "sleep 30".into(),
env: HashMap::new(),
metadata: HashMap::new(),
worktree: None,
};
let result = reg
.state
.create_session("laptop", request)
.await
.expect("transport ok");
let session = result.expect("rpc ok");
assert_eq!(session.command, "sleep 30");
// the registry should also see the SessionAdded diff in its catalog
let state = reg.state.clone();
wait_for_async(Duration::from_secs(5), || {
let r = state.clone();
async move { r.sessions().await.len() == 1 }
})
.await;
// and KillSession should round-trip too
let kill = reg
.state
.kill_session("laptop", session.id)
.await
.expect("transport ok");
kill.expect("rpc ok");
let state = reg.state.clone();
wait_for_async(Duration::from_secs(5), || {
let r = state.clone();
async move { r.sessions().await.is_empty() }
})
.await;
}