diff --git a/crates/maestro-daemon/tests/acp.rs b/crates/maestro-daemon/tests/acp.rs index 1055499..6af36cb 100644 --- a/crates/maestro-daemon/tests/acp.rs +++ b/crates/maestro-daemon/tests/acp.rs @@ -9,13 +9,15 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::{Duration, Instant}; -use common::{wait_for_async, DaemonFixture}; +use common::{init_bare_repo, init_repo, push_all_to_origin, wait_for_async, DaemonFixture}; use maestro_acp::schema::{ContentBlock, SessionUpdate}; use maestro_acp::{ADAPTER_BIN, INSTALL_COMMAND}; use maestro_daemon::sidecar::{EventCursor, SidecarStore}; use maestro_daemon::worktree::WorktreeManager; use maestro_daemon::SessionManager; -use maestro_protocol::{ActivityState, CreateSessionRequest, SessionStatus, SessionView}; +use maestro_protocol::{ + ActivityState, CreateSessionRequest, SessionStatus, SessionView, WorktreeSpec, +}; use tempfile::TempDir; fn fake_agent() -> PathBuf { @@ -285,6 +287,76 @@ async fn recovery_loads_the_acp_session_without_duplicating_history() { revived.kill(session.id, false).await.expect("kill"); } +#[tokio::test] +async fn shutdown_preserves_a_pristine_structured_worktree_for_recovery() { + let fx = DaemonFixture::new(); + let fake = Fake::new(); + let repo = TempDir::new().unwrap(); + init_repo(repo.path()); + let origin = TempDir::new().unwrap(); + init_bare_repo(origin.path()); + + let branch = "feature/restart"; + let worktree = repo + .path() + .join("worktrees") + .join("feature") + .join("restart"); + let session = fx + .manager + .create(CreateSessionRequest { + view: SessionView::Structured, + cwd: repo.path().to_path_buf(), + command: "claude".into(), + env: fake.env(), + metadata: HashMap::new(), + worktree: Some(WorktreeSpec { + base_repo: repo.path().to_path_buf(), + branch: branch.into(), + create_branch: true, + }), + }) + .await + .expect("create"); + assert_eq!(session.cwd, worktree); + push_all_to_origin(repo.path(), origin.path()); + + fx.manager.shutdown().await; + + assert!( + worktree.is_dir(), + "shutdown must not settle a pristine worktree" + ); + let sidecar: maestro_protocol::Session = serde_json::from_slice( + &std::fs::read(fx.sidecar_dir().join(format!("{}.json", session.id))).unwrap(), + ) + .unwrap(); + assert_eq!( + sidecar.worktree.as_ref().map(|wt| &wt.path), + Some(&worktree) + ); + + let revived = SessionManager::new( + Arc::new(fx.tmux.tmux.clone()), + SidecarStore::new(fx.sidecar_dir()), + WorktreeManager::new(None), + fx.fifo_dir(), + ); + assert_eq!(revived.recover().await.expect("recover"), 1); + assert_eq!( + revived.status(session.id).await, + Some(SessionStatus::Running) + ); + assert!(fake.log().contains("recv session/load"), "{}", fake.log()); + assert!(worktree.is_dir(), "recovery must retain the worktree"); + + revived.kill(session.id, false).await.expect("dismiss"); + assert!( + !worktree.exists(), + "operator dismissal still cleans a pristine tree" + ); +} + #[tokio::test] async fn failed_recovery_cleans_its_fifo() { let fx = DaemonFixture::new();