use std::path::{Path, PathBuf};
use super::ManagedTmuxDriver;
use super::manager::ManagedError;
use super::record::{ManagedSessionId, SessionRecord};
pub(super) async fn resolve_existing_workdir(
id: &ManagedSessionId,
record: &SessionRecord,
) -> Result<PathBuf, ManagedError> {
let (last_cwd, workspace_path, cwd) = (
record.last_cwd.as_deref(),
record.workspace_path.as_deref(),
record.cwd.as_path(),
);
for candidate in [last_cwd, workspace_path, Some(cwd)].into_iter().flatten() {
if tokio::fs::try_exists(candidate).await.unwrap_or(false) {
return Ok(candidate.to_path_buf());
}
}
let missing = workspace_path.unwrap_or(cwd);
Err(ManagedError::WorkspaceMissing(
id.to_string(),
missing.display().to_string(),
))
}
pub(super) fn verify_pane_cwd(
driver: &dyn ManagedTmuxDriver,
name: &str,
expected: &Path,
) -> Result<(), ManagedError> {
let Some(actual) = driver.get_pane_cwd(name) else {
return Ok(());
};
let expected_c = expected
.canonicalize()
.unwrap_or_else(|_| expected.to_path_buf());
let actual_c = actual.canonicalize().unwrap_or_else(|_| actual.clone());
if expected_c == actual_c {
Ok(())
} else {
Err(ManagedError::TmuxUnavailable(format!(
"tmux session '{name}' pane landed at '{}' instead of the requested workspace \
'{}' — tmux silently fell back (likely $HOME) because the target directory was \
unusable; refusing to type the resume command into a mis-rooted pane",
actual.display(),
expected.display()
)))
}
}
pub(super) fn create_and_verify_pane(
driver: &dyn ManagedTmuxDriver,
name: &str,
workdir: &str,
) -> Result<(), ManagedError> {
driver
.create_session(name, workdir)
.map_err(|e| ManagedError::TmuxUnavailable(e.to_string()))?;
verify_pane_cwd(driver, name, Path::new(workdir))
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use std::sync::Mutex;
use chrono::Utc;
use tempfile::TempDir;
use super::super::record::ManagedSessionState;
use super::*;
fn make_record(
cwd: PathBuf,
workspace_path: Option<PathBuf>,
last_cwd: Option<PathBuf>,
) -> SessionRecord {
SessionRecord {
id: ManagedSessionId::new(),
tmux_name: "tmpm-resume-workdir-test".into(),
cwd,
task: "test".into(),
state: ManagedSessionState::Stopped,
created_at: Utc::now(),
last_activity_at: None,
workspace_path,
repo_url: None,
branch: None,
pending_decision: None,
proposed_default: None,
correlation: Default::default(),
runtime: Default::default(),
ephemeral: false,
workspace_owned: false,
source_id: None,
claude_session_id: None,
scrollback_path: None,
last_cwd,
}
}
#[tokio::test]
async fn resolve_existing_workdir_prefers_first_existing() {
let last = TempDir::new().unwrap();
let workspace = TempDir::new().unwrap();
let id = ManagedSessionId::new();
let record = make_record(
last.path().to_owned(),
Some(workspace.path().to_owned()),
Some(last.path().to_owned()),
);
let resolved = resolve_existing_workdir(&id, &record)
.await
.expect("last_cwd exists, must be chosen first");
assert_eq!(resolved, last.path());
}
#[tokio::test]
async fn resolve_existing_workdir_falls_back_through_candidates() {
let workspace = TempDir::new().unwrap();
let cwd = TempDir::new().unwrap();
let id = ManagedSessionId::new();
let missing_last = PathBuf::from("/nonexistent/last-cwd-2250");
let record = make_record(
cwd.path().to_owned(),
Some(workspace.path().to_owned()),
Some(missing_last.clone()),
);
let resolved = resolve_existing_workdir(&id, &record)
.await
.expect("workspace_path exists, must be chosen over missing last_cwd");
assert_eq!(resolved, workspace.path());
let missing_workspace = PathBuf::from("/nonexistent/workspace-2250");
let record = make_record(
cwd.path().to_owned(),
Some(missing_workspace),
Some(missing_last),
);
let resolved = resolve_existing_workdir(&id, &record)
.await
.expect("cwd exists, must be the final fallback");
assert_eq!(resolved, cwd.path());
}
#[tokio::test]
async fn resolve_existing_workdir_errors_when_all_missing() {
let id = ManagedSessionId::new();
let missing_workspace = PathBuf::from("/nonexistent/workspace-2250");
let record = make_record(
PathBuf::from("/nonexistent/cwd-2250"),
Some(missing_workspace.clone()),
Some(PathBuf::from("/nonexistent/last-cwd-2250")),
);
let err = resolve_existing_workdir(&id, &record).await.expect_err(
"no candidate exists on disk — must error, not silently fall back to $HOME",
);
match err {
ManagedError::WorkspaceMissing(err_id, path) => {
assert_eq!(err_id, id.to_string());
assert_eq!(path, missing_workspace.display().to_string());
}
other => panic!("expected WorkspaceMissing, got {other:?}"),
}
}
struct StubDriver {
pane_cwd: Mutex<Option<PathBuf>>,
}
impl ManagedTmuxDriver for StubDriver {
fn create_session(&self, _name: &str, _workdir: &str) -> Result<(), ManagedError> {
Ok(())
}
fn kill_session(&self, _name: &str) -> Result<(), ManagedError> {
Ok(())
}
fn send_line(&self, _name: &str, _text: &str) -> Result<(), ManagedError> {
Ok(())
}
fn capture(&self, _name: &str, _lines: usize) -> Result<String, ManagedError> {
Ok(String::new())
}
fn list_sessions(&self) -> Result<Vec<String>, ManagedError> {
Ok(Vec::new())
}
fn get_pane_cwd(&self, _name: &str) -> Option<PathBuf> {
self.pane_cwd.lock().unwrap().clone()
}
}
#[test]
fn verify_pane_cwd_ok_on_match() {
let dir = TempDir::new().unwrap();
let driver = StubDriver {
pane_cwd: Mutex::new(Some(dir.path().to_path_buf())),
};
assert!(verify_pane_cwd(&driver, "s", dir.path()).is_ok());
}
#[test]
fn verify_pane_cwd_ok_when_unknown() {
let dir = TempDir::new().unwrap();
let driver = StubDriver {
pane_cwd: Mutex::new(None),
};
assert!(verify_pane_cwd(&driver, "s", dir.path()).is_ok());
}
#[test]
fn verify_pane_cwd_errors_on_mismatch() {
let expected = TempDir::new().unwrap();
let actual = TempDir::new().unwrap();
let driver = StubDriver {
pane_cwd: Mutex::new(Some(actual.path().to_path_buf())),
};
let err = verify_pane_cwd(&driver, "s", expected.path())
.expect_err("pane cwd disagrees with the requested workdir — must fail loudly");
assert!(matches!(err, ManagedError::TmuxUnavailable(_)));
}
}