Replace the freeform `metadata.label` session grouping with nesting derived
from each session's working directory. The daemon's activity watcher already
polls each tmux pane every tick, so it now also captures
`#{pane_current_path}` and surfaces it as a new `Session.current_cwd`
(serde-default, back-compat); `cwd` stays the spawn dir for worktree
inference, spawn-similar, and recents.
Web catalog builds the tree from `current_cwd ?? cwd`, collapses the home
dir to `~`, and folds single-child folder chains so `~/git/maestro` reads as
one node. CLI swaps its LABEL column for an abbreviated DIR; the spawn forms
(web + TUI) and `--label` flag are removed.
The pane format stays space-delimited (parser splits the first three tokens
and keeps the remainder as the path) so a cwd containing spaces survives.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
112 lines
3.3 KiB
Rust
112 lines
3.3 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, daemon_id: &str) -> Session {
|
|
Session {
|
|
id,
|
|
daemon_id: daemon_id.to_string(),
|
|
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, "laptop");
|
|
|
|
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, "laptop");
|
|
|
|
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(), "laptop");
|
|
let s2 = sample(Uuid::new_v4(), "dev-vm");
|
|
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.daemon_id.clone());
|
|
let mut expected = vec![s1, s2];
|
|
expected.sort_by_key(|s| s.daemon_id.clone());
|
|
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(), "laptop");
|
|
store.write(&s).expect("write");
|
|
assert!(nested.exists());
|
|
assert!(store.path_for(s.id).exists());
|
|
}
|