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/common/mod.rs
T
naps62-yolo ab9a5bf017 feat(maestro): wire daemon to registry + per-session WS stream
Completes the headless daemon stack for NAP-214.

- `config`: daemon TOML config (daemon_id, registry_url, bind_addr,
  token, state_dir, worktree_root) with sensible XDG defaults.
- `jwt`: HS256 verifier; the daemon validates per-attach JWTs locally
  with no callback to the registry. Also exposes `issue()` for tests.
- `pty::PtyBridge`: per-session FIFO + tokio broadcast pump. The FIFO is
  opened r+w so it never sees zero writers — stays alive across
  `pipe-pane` re-issues. Helper `pipe_pane_command()` builds the
  `cat >> <fifo>` shell line.
- `session::SessionManager`: authoritative live-session store. Creates
  worktrees (when requested), spawns tmux, mkfifo + pipe-pane, writes
  sidecar; tears it all down on kill. Emits diff events on a
  broadcast for the registry client. `recover()` adopts surviving
  `maestro-*` tmux sessions on startup and re-pipes them.
- `registry_client::RegistryClient`: persistent outbound WS to the
  registry with exponential backoff. Sends Hello → snapshot, drives a
  select loop that pumps inbound CreateSession/KillSession RPCs into
  the manager (replying with results), emits SessionAdded/Removed/
  Updated diffs from the manager, and heartbeats every 10s.
- `attach`: axum router mounting `WS /sessions/:id/stream?token=<jwt>`.
  Binary frames carry PTY bytes either way (subscribe to broadcast →
  send to client; receive from client → tmux send-keys -l). Text
  frames carry `ClientControl::{Resize,Ping,Pong}`.
- `bin/maestrod`: ties everything together, loads config from
  `MAESTROD_CONFIG` env or `~/.config/maestrod/config.toml`,
  recovers, spawns the registry client, binds the WS server.

Also adds `JwtClaims` to `maestro-protocol` so the daemon and the
future registry JWT issuer (NAP-215) agree on the wire shape.

Integration tests cover session manager round-trip + recovery + PTY
broadcast; attach endpoint JWT (bad/wrong-session/wrong-daemon),
PTY round-trip through real tmux, multi-attach broadcast, ping/pong;
registry client registration + snapshot + diff forwarding +
CreateSession RPC round-trip.

Closes NAP-214.
2026-05-24 15:22:36 +00:00

149 lines
4.3 KiB
Rust

//! Shared helpers for the maestro-daemon integration tests.
//!
//! Each integration test file compiles as its own binary and only uses a
//! subset of these helpers, so `dead_code` warnings are spurious here.
#![allow(dead_code)]
use std::future::Future;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::thread;
use std::time::{Duration, Instant};
use maestro_daemon::sidecar::SidecarStore;
use maestro_daemon::tmux::Tmux;
use maestro_daemon::worktree::WorktreeManager;
use maestro_daemon::SessionManager;
use tempfile::TempDir;
use uuid::Uuid;
/// An isolated tmux instance scoped to a temp `TMUX_TMPDIR` and unique socket
/// name. Drops kill the server so leftover sockets don't accumulate.
pub struct TmuxFixture {
pub tmux: Tmux,
_home: TempDir,
_tmpdir: TempDir,
}
impl TmuxFixture {
pub fn new() -> Self {
let home = TempDir::new().expect("tempdir HOME");
let tmpdir = TempDir::new().expect("tempdir TMUX_TMPDIR");
let socket = format!("maestro-test-{}", Uuid::new_v4().simple());
let tmux = Tmux::new()
.with_socket(socket)
.with_tmpdir(tmpdir.path().to_path_buf());
Self {
tmux,
_home: home,
_tmpdir: tmpdir,
}
}
}
impl Drop for TmuxFixture {
fn drop(&mut self) {
let _ = self.tmux.kill_server();
}
}
/// Wait until `cond` returns `true`, polling every 25ms. Panics on timeout.
pub fn wait_for(timeout: Duration, mut cond: impl FnMut() -> bool) {
let deadline = Instant::now() + timeout;
while Instant::now() < deadline {
if cond() {
return;
}
thread::sleep(Duration::from_millis(25));
}
panic!("timed out after {timeout:?} waiting for condition");
}
/// `git init` a fresh repo in `dir` with one commit so it can host worktrees.
pub fn init_repo(dir: &Path) {
run(Command::new("git")
.arg("-C")
.arg(dir)
.args(["init", "-q", "-b", "main"]));
run(Command::new("git")
.arg("-C")
.arg(dir)
.args(["config", "user.email", "test@example.com"]));
run(Command::new("git")
.arg("-C")
.arg(dir)
.args(["config", "user.name", "Test"]));
std::fs::write(dir.join("README"), b"hello\n").unwrap();
run(Command::new("git").arg("-C").arg(dir).args(["add", "."]));
run(Command::new("git")
.arg("-C")
.arg(dir)
.args(["commit", "-q", "-m", "initial"]));
}
fn run(cmd: &mut Command) {
let out = cmd.output().expect("git spawn");
assert!(
out.status.success(),
"git command failed: {} stderr={}",
out.status,
String::from_utf8_lossy(&out.stderr)
);
}
/// Async polling sibling of [`wait_for`] for tokio tests.
pub async fn wait_for_async<F, Fut>(timeout: Duration, mut cond: F)
where
F: FnMut() -> Fut,
Fut: Future<Output = bool>,
{
let deadline = Instant::now() + timeout;
while Instant::now() < deadline {
if cond().await {
return;
}
tokio::time::sleep(Duration::from_millis(25)).await;
}
panic!("timed out after {timeout:?} waiting for condition");
}
/// Bundle of dirs + a SessionManager hooked to an isolated tmux instance.
/// Holds the `TempDir`s so they survive the test lifetime.
pub struct DaemonFixture {
pub tmux: TmuxFixture,
pub manager: SessionManager,
pub state_dir: TempDir,
pub worktree_root: TempDir,
}
impl DaemonFixture {
pub fn new(daemon_id: &str) -> Self {
let tmux = TmuxFixture::new();
let state_dir = TempDir::new().unwrap();
let worktree_root = TempDir::new().unwrap();
let sidecar = SidecarStore::new(state_dir.path().join("sessions"));
let worktree_mgr = WorktreeManager::new(worktree_root.path().to_path_buf());
let manager = SessionManager::new(
daemon_id.to_string(),
tmux.tmux.clone(),
sidecar,
worktree_mgr,
state_dir.path().join("fifos"),
);
Self {
tmux,
manager,
state_dir,
worktree_root,
}
}
pub fn sidecar_dir(&self) -> PathBuf {
self.state_dir.path().join("sessions")
}
pub fn fifo_dir(&self) -> PathBuf {
self.state_dir.path().join("fifos")
}
}