Maestro is now single-host: one daemon owns the machine's sessions and clients talk to it directly. Removes the registry crate, the daemon's registry client/broker/catalog mirror, JWT attach auth, cookie auth, and the perimeter (T2) transport config. The daemon API is now unauthenticated on its bind address, same trust model as tmux. Session and CreateSessionRequest lose daemon_id; RegistryEvent becomes CatalogEvent with no daemon variants; attach moves to WS /api/attach/:id with no token.
111 lines
3.1 KiB
Rust
111 lines
3.1 KiB
Rust
//! Round-trip and recovery tests for the on-disk sidecar session store.
|
|
|
|
mod common;
|
|
|
|
use std::collections::HashMap;
|
|
use std::path::PathBuf;
|
|
|
|
use chrono::{DateTime, Utc};
|
|
use maestro_daemon::sidecar::SidecarStore;
|
|
use maestro_protocol::{Session, SessionStatus};
|
|
use serde_json::Value;
|
|
use tempfile::TempDir;
|
|
use uuid::Uuid;
|
|
|
|
fn sample(id: Uuid) -> Session {
|
|
Session {
|
|
id,
|
|
cwd: PathBuf::from("/tmp/work"),
|
|
command: "bash".into(),
|
|
env: HashMap::from([("FOO".into(), "bar".into())]),
|
|
metadata: HashMap::from([("name".into(), Value::String("maestro-impl".into()))]),
|
|
status: SessionStatus::Running,
|
|
created_at: DateTime::parse_from_rfc3339("2026-05-24T12:00:00Z")
|
|
.unwrap()
|
|
.with_timezone(&Utc),
|
|
foreground: None,
|
|
activity: Default::default(),
|
|
last_activity_at: chrono::Utc::now(),
|
|
current_cwd: Some(PathBuf::from("/tmp/work/crates")),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn write_then_read_round_trip() {
|
|
let dir = TempDir::new().unwrap();
|
|
let store = SidecarStore::new(dir.path());
|
|
let id = Uuid::new_v4();
|
|
let s = sample(id);
|
|
|
|
store.write(&s).expect("write");
|
|
let loaded = store.read(id).expect("read");
|
|
assert_eq!(loaded.as_ref(), Some(&s));
|
|
|
|
let path = store.path_for(id);
|
|
assert!(path.exists());
|
|
assert!(path.ends_with(format!("{id}.json")));
|
|
}
|
|
|
|
#[test]
|
|
fn read_missing_returns_none() {
|
|
let dir = TempDir::new().unwrap();
|
|
let store = SidecarStore::new(dir.path());
|
|
assert!(store.read(Uuid::new_v4()).unwrap().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn delete_removes_file_and_is_idempotent() {
|
|
let dir = TempDir::new().unwrap();
|
|
let store = SidecarStore::new(dir.path());
|
|
let id = Uuid::new_v4();
|
|
let s = sample(id);
|
|
|
|
store.write(&s).unwrap();
|
|
assert!(store.path_for(id).exists());
|
|
|
|
store.delete(id).unwrap();
|
|
assert!(!store.path_for(id).exists());
|
|
|
|
// second delete is a no-op
|
|
store.delete(id).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn load_all_recovers_every_sidecar_and_skips_garbage() {
|
|
let dir = TempDir::new().unwrap();
|
|
let store = SidecarStore::new(dir.path());
|
|
|
|
let s1 = sample(Uuid::new_v4());
|
|
let s2 = sample(Uuid::new_v4());
|
|
store.write(&s1).unwrap();
|
|
store.write(&s2).unwrap();
|
|
|
|
// garbage files in the directory should be ignored
|
|
std::fs::write(dir.path().join("not-json.txt"), b"hello").unwrap();
|
|
std::fs::write(dir.path().join("broken.json"), b"{not json}").unwrap();
|
|
|
|
let mut loaded = store.load_all().expect("load_all");
|
|
loaded.sort_by_key(|s| s.id);
|
|
let mut expected = vec![s1, s2];
|
|
expected.sort_by_key(|s| s.id);
|
|
assert_eq!(loaded, expected);
|
|
}
|
|
|
|
#[test]
|
|
fn load_all_returns_empty_when_directory_missing() {
|
|
let dir = TempDir::new().unwrap();
|
|
let store = SidecarStore::new(dir.path().join("does-not-exist"));
|
|
assert!(store.load_all().unwrap().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn write_creates_parent_directory() {
|
|
let dir = TempDir::new().unwrap();
|
|
let nested = dir.path().join("a/b/c");
|
|
let store = SidecarStore::new(&nested);
|
|
let s = sample(Uuid::new_v4());
|
|
store.write(&s).expect("write");
|
|
assert!(nested.exists());
|
|
assert!(store.path_for(s.id).exists());
|
|
}
|