use std::path::{Path, PathBuf};
use super::ManagedTmuxDriver;
use super::manager::ManagedError;
use super::record::{ManagedSessionId, ManagedSessionState, SessionRecord};
fn workdir_candidates(record: &SessionRecord) -> [Option<&Path>; 3] {
[
record.last_cwd.as_deref(),
record.workspace_path.as_deref(),
Some(record.cwd.as_path()),
]
}
pub(super) async fn resolve_existing_workdir(
id: &ManagedSessionId,
record: &SessionRecord,
) -> Result<PathBuf, ManagedError> {
for candidate in workdir_candidates(record).into_iter().flatten() {
if tokio::fs::try_exists(candidate).await.unwrap_or(false) {
return Ok(candidate.to_path_buf());
}
}
let missing = record.workspace_path.as_deref().unwrap_or(&record.cwd);
Err(ManagedError::WorkspaceMissing(
id.to_string(),
missing.display().to_string(),
))
}
pub(crate) async fn is_unresumable(record: &SessionRecord) -> bool {
if !matches!(
record.state,
ManagedSessionState::Stopped | ManagedSessionState::Errored
) {
return false;
}
for candidate in workdir_candidates(record).into_iter().flatten() {
match tokio::fs::try_exists(candidate).await {
Ok(true) | Err(_) => return false,
Ok(false) => continue,
}
}
true
}
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,
deliverable_id: None,
pane_id: None,
injection_status: Default::default(),
worktree_owner: None,
}
}
#[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:?}"),
}
}
#[tokio::test]
async fn is_unresumable_true_when_all_candidates_missing_and_stopped() {
let mut record = make_record(
PathBuf::from("/nonexistent/cwd-2595"),
Some(PathBuf::from("/nonexistent/workspace-2595")),
Some(PathBuf::from("/nonexistent/last-cwd-2595")),
);
record.state = ManagedSessionState::Stopped;
assert!(
is_unresumable(&record).await,
"stopped session with no existing candidate must be unresumable"
);
record.state = ManagedSessionState::Errored;
assert!(
is_unresumable(&record).await,
"errored session with no existing candidate must be unresumable"
);
}
#[tokio::test]
async fn is_unresumable_false_when_any_candidate_exists() {
let cwd = TempDir::new().unwrap();
let mut record = make_record(
cwd.path().to_owned(),
Some(PathBuf::from("/nonexistent/workspace-2595")),
Some(PathBuf::from("/nonexistent/last-cwd-2595")),
);
record.state = ManagedSessionState::Stopped;
assert!(
!is_unresumable(&record).await,
"a session with ANY existing candidate must not be flagged unresumable"
);
}
#[tokio::test]
async fn is_unresumable_false_for_non_stopped_states_even_when_all_missing() {
let mut record = make_record(
PathBuf::from("/nonexistent/cwd-2595"),
Some(PathBuf::from("/nonexistent/workspace-2595")),
Some(PathBuf::from("/nonexistent/last-cwd-2595")),
);
for state in [
ManagedSessionState::Active,
ManagedSessionState::Provisioning,
] {
record.state = state;
assert!(
!is_unresumable(&record).await,
"state {:?} must never be flagged unresumable regardless of workdir",
record.state
);
}
}
#[tokio::test]
async fn is_unresumable_fails_open_on_probe_error() {
let dir = TempDir::new().unwrap();
let not_a_dir = dir.path().join("this-is-a-file-not-a-dir");
std::fs::write(¬_a_dir, b"x").expect("write stand-in file");
let unprobable = not_a_dir.join("child-cannot-be-stat-ed");
let mut record = make_record(
PathBuf::from("/nonexistent/cwd-2595-failopen"),
Some(PathBuf::from("/nonexistent/workspace-2595-failopen")),
Some(unprobable),
);
record.state = ManagedSessionState::Stopped;
assert!(
!is_unresumable(&record).await,
"a probe error on any candidate must fail OPEN (never flag dead) — a \
transient I/O/permission/network-volume error is not proof the \
workspace is gone, and the picker's only override would otherwise \
be a destructive delete"
);
}
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(_)));
}
}