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/sidecar.rs
T
Miguel Palhas e9368bb242 fix(maestro): fourth-review findings across daemon, tui and web
The existing-branch picker could never succeed in either client: both
treated the main checkout's HEAD as available, and since that is usually
`main` it became the default pick — git then refuses the second checkout.
A TUI test had baked the bug in by asserting the selection lands on the
`is_head` entry. Both pickers now treat "checked out anywhere" as held.

Worktree paths were still not injective. Flattening `/` and `.` to `-`
made `feat/x`, `feat.x` and `feat-x` share one directory, and the
override root keyed on the repo basename alone, so `~/a/maestro` and
`~/b/maestro` collided. A branch name is already a valid relative path —
git stores it as refs/heads/feat/example — so it is used verbatim and
nests under `worktrees/feat/`; the override root gains a digest of the
full repo path. Git forbids holding both `feat` and `feat/x`, so the
nesting cannot collide for valid refs.

Also:

- Recovered sessions dropped their worktree association, so after a
  restart a kill cleaned up nothing and the leftover directory blocked
  that branch forever. It is persisted on Session now.
- The nesting guard compared git's resolved paths against an unresolved
  one, so it silently passed on any symlinked repo path — and that guard
  is what stands between a kill and an inner tree's uncommitted work.
- Draft loads ran on the TUI event loop: /api/projects on a cold cache
  walks all of $HOME, freezing keys, frames and attached panes for up to
  the request timeout. They are detached tasks posting to a channel now.
- Fast-forward in the web client created the session and never mounted
  it, so `N` looked like a dead key while the TUI attached.
- The fast-forward fallback seed forced the worktree toggle off, against
  both a normal draft and the TUI.
- The projects background refresh honoured the TTL, so two of every three
  ticks were no-ops and requests paid for the rescan instead.
2026-08-24 00:10:35 +01:00

112 lines
3.2 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()))]),
worktree: None,
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());
}