#![cfg(unix)]
use std::path::Path;
use std::process::{Command as StdCommand, Stdio};
use macot::expert::add::{ExpertAddRequest, ExpertAddService};
use macot::expert::role::{BuiltinRole, RoleSpec};
use macot::experts::persist::ManifestPersistor;
use macot::session::TmuxManager;
use regex::Regex;
use tempfile::TempDir;
fn tmux_available() -> bool {
StdCommand::new("tmux")
.arg("-V")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
struct SessionGuard {
name: String,
}
impl Drop for SessionGuard {
fn drop(&mut self) {
let _ = StdCommand::new("tmux")
.args(["kill-session", "-t", &self.name])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
}
}
fn create_detached_session(name: &str, cwd: &Path) {
let status = StdCommand::new("tmux")
.args([
"new-session",
"-d",
"-s",
name,
"-c",
cwd.to_str().expect("cwd UTF-8"),
"-x",
"200",
"-y",
"50",
])
.status()
.expect("failed to spawn tmux");
assert!(
status.success(),
"tmux new-session -d -s {name} failed (is another session with that name running?)"
);
}
fn list_window_names(session: &str) -> Vec<String> {
let output = StdCommand::new("tmux")
.args(["list-windows", "-t", session, "-F", "#{window_name}"])
.output()
.expect("tmux list-windows");
if !output.status.success() {
return Vec::new();
}
String::from_utf8_lossy(&output.stdout)
.lines()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
}
fn make_service(
project_root: &Path,
session: &str,
session_hash: &str,
) -> ExpertAddService<TmuxManager> {
ExpertAddService::new(
project_root.to_path_buf(),
session.to_string(),
session_hash.to_string(),
TmuxManager::new(session.to_string()),
)
}
#[tokio::test]
#[ignore]
async fn dynamic_add_creates_real_tmux_window() {
if !tmux_available() {
eprintln!("tmux not available, skipping");
return;
}
let tmp = TempDir::new().expect("tempdir");
let session = format!("macot-e2e-{}-add", std::process::id());
create_detached_session(&session, tmp.path());
let _guard = SessionGuard {
name: session.clone(),
};
let svc = make_service(tmp.path(), &session, "e2e-add");
let added = svc
.add_expert(ExpertAddRequest {
role: RoleSpec::Builtin(BuiltinRole::General),
name: Some("Smerdyakov".to_string()),
})
.await
.expect("add_expert succeeds against real tmux");
assert_eq!(added.expert_id, 0);
assert_eq!(added.name, "Smerdyakov");
assert_eq!(added.role, "general");
let names = list_window_names(&session);
assert!(
names.iter().any(|n| n == "expert0"),
"Property 4: tmux should expose window 'expert0', got {names:?}"
);
assert!(svc.prompt_path(0).exists(), "prompt file");
assert!(svc.settings_path(0).exists(), "settings file");
assert!(svc.status_path(0).exists(), "status file");
assert!(svc.context_path(0).exists(), "context.yaml");
let entries = ManifestPersistor::new(tmp.path().to_path_buf())
.load_entries()
.expect("manifest readable");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].expert_id, 0);
assert_eq!(entries[0].name, "Smerdyakov");
}
#[tokio::test]
#[ignore]
async fn dynamic_add_creates_distinct_windows_for_sequential_adds() {
if !tmux_available() {
eprintln!("tmux not available, skipping");
return;
}
let tmp = TempDir::new().expect("tempdir");
let session = format!("macot-e2e-{}-seq", std::process::id());
create_detached_session(&session, tmp.path());
let _guard = SessionGuard {
name: session.clone(),
};
let svc = make_service(tmp.path(), &session, "e2e-seq");
for _ in 0..3 {
svc.add_expert(ExpertAddRequest {
role: RoleSpec::Builtin(BuiltinRole::General),
name: None,
})
.await
.expect("sequential add");
}
let names = list_window_names(&session);
for n in 0..3u32 {
let target = format!("expert{n}");
assert!(
names.contains(&target),
"tmux should expose '{target}', got {names:?}"
);
}
}
#[tokio::test]
#[ignore]
async fn name_pool_drains_to_fallback_format() {
if !tmux_available() {
eprintln!("tmux not available, skipping");
return;
}
let tmp = TempDir::new().expect("tempdir");
let session = format!("macot-e2e-{}-pool", std::process::id());
create_detached_session(&session, tmp.path());
let _guard = SessionGuard {
name: session.clone(),
};
let svc = make_service(tmp.path(), &session, "e2e-pool");
let total: u32 = 13;
let mut names: Vec<String> = Vec::with_capacity(total as usize);
for _ in 0..total {
let added = svc
.add_expert(ExpertAddRequest {
role: RoleSpec::Builtin(BuiltinRole::General),
name: None,
})
.await
.expect("add_expert with no name");
names.push(added.name);
}
let fallback_re = Regex::new(r"^Expert\d{2}$").unwrap();
let fallback_count = names.iter().filter(|n| fallback_re.is_match(n)).count();
assert!(
fallback_count >= 1,
"after {total} adds at least one name should match `Expert\\d{{2}}`, got {names:?}"
);
let mut sorted = names.clone();
sorted.sort();
sorted.dedup();
assert_eq!(
sorted.len(),
names.len(),
"auto-pick must not produce duplicates: {names:?}"
);
}
#[tokio::test]
#[ignore]
async fn down_cleanup_removes_dynamic_state() {
if !tmux_available() {
eprintln!("tmux not available, skipping");
return;
}
let tmp = TempDir::new().expect("tempdir");
let session = format!("macot-e2e-{}-cleanup", std::process::id());
create_detached_session(&session, tmp.path());
let guard = SessionGuard {
name: session.clone(),
};
let svc = make_service(tmp.path(), &session, "e2e-cleanup");
svc.add_expert(ExpertAddRequest {
role: RoleSpec::Builtin(BuiltinRole::General),
name: Some("Cleanup".to_string()),
})
.await
.expect("add_expert");
assert!(svc.manifest_path().exists());
assert!(svc.prompt_path(0).exists());
assert!(svc.context_path(0).exists());
drop(guard);
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let alive = StdCommand::new("tmux")
.args(["has-session", "-t", &session])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
assert!(!alive, "tmux session should be gone after kill-session");
std::fs::remove_dir_all(tmp.path().join(".macot")).expect("rm -rf .macot");
assert!(!svc.manifest_path().exists());
assert!(!svc.prompt_path(0).exists());
assert!(!svc.context_path(0).exists());
assert!(!svc.expert_roles_path().exists());
}