use std::path::PathBuf;
use crate::core::hook::HookEvent;
use crate::core::hook::HookEventRecord;
use crate::core::memory::MemoryPressure;
use crate::core::memory::MemoryUsage;
use crate::core::overseer_config::OverseerConfig;
use crate::core::paths::FrameworkPaths;
use crate::core::session::{ControlModel, SessionStatus};
use super::core::{DaemonState, HOOK_HISTORY_LIMIT, ReapResult};
use super::overseer::build_overseer;
use crate::core::session::{Session, SessionId};
fn hermetic_state() -> (DaemonState, tempfile::TempDir) {
let dir = tempfile::tempdir().expect("temp dir for hermetic DaemonState");
let paths = FrameworkPaths::under(dir.path());
let state = DaemonState::with_paths(&paths);
(state, dir)
}
fn sample_session() -> Session {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let mut s = Session::new(SessionId::new(), "/tmp/p", ControlModel::Tmux, None);
s.tmux_name = format!("tmpm-test-{n}");
s.status = SessionStatus::Active;
s
}
#[test]
fn register_and_list_sessions() {
let state = DaemonState::new();
let s = sample_session();
let id = s.id;
state.register_session(s);
assert_eq!(state.list_sessions().len(), 1);
assert!(state.session(id).is_some());
assert!(state.remove_session(id).is_some());
assert!(state.list_sessions().is_empty());
}
#[test]
fn update_session_mutates_existing() {
let state = DaemonState::new();
let s = sample_session();
let id = s.id;
state.register_session(s);
let ran = state.update_session(&id, |session| {
session.status = SessionStatus::Paused;
session.pause_summary = Some("note".to_string());
});
assert!(ran);
let updated = state.session(id).expect("session exists");
assert_eq!(updated.status, SessionStatus::Paused);
assert_eq!(updated.pause_summary.as_deref(), Some("note"));
}
#[test]
fn update_session_missing_is_false() {
let state = DaemonState::new();
let ran = state.update_session(&SessionId::new(), |_| {});
assert!(!ran);
}
#[test]
fn register_and_list_projects() {
let state = DaemonState::new();
assert!(state.list_projects().is_empty());
let info = state.register_project(PathBuf::from("/work/demo"));
assert_eq!(info.name, "demo");
assert_eq!(state.list_projects().len(), 1);
state.register_project(PathBuf::from("/work/demo"));
assert_eq!(state.list_projects().len(), 1);
state.register_project(PathBuf::from("/work/other"));
assert_eq!(state.list_projects().len(), 2);
}
#[test]
fn project_lookup_by_path() {
let state = DaemonState::new();
state.register_project(PathBuf::from("/work/demo"));
assert!(state.project(std::path::Path::new("/work/demo")).is_some());
assert!(
state
.project(std::path::Path::new("/work/missing"))
.is_none()
);
}
#[test]
fn list_sessions_for_project_filters() {
let state = DaemonState::new();
let mut in_proj = sample_session();
in_proj.project_path = Some(PathBuf::from("/work/demo"));
let mut other_proj = sample_session();
other_proj.project_path = Some(PathBuf::from("/work/other"));
let no_proj = sample_session();
state.register_session(in_proj.clone());
state.register_session(other_proj);
state.register_session(no_proj);
let listed = state.list_sessions_for_project(std::path::Path::new("/work/demo"));
assert_eq!(listed.len(), 1);
assert_eq!(listed[0].id, in_proj.id);
}
#[test]
fn find_session_by_id_or_name() {
let state = DaemonState::new();
let s = sample_session();
let id = s.id;
let name = s.tmux_name.clone();
state.register_session(s);
assert!(state.find_session(&id.0.to_string()).is_some());
assert!(state.find_session(&name).is_some());
assert!(state.find_session("tmpm-no-such-name").is_none());
assert!(
state
.find_session(&SessionId::new().0.to_string())
.is_none()
);
}
#[test]
fn breaker_tracks_outcomes() {
let state = DaemonState::new();
for _ in 0..3 {
state.record_outcome("research", false);
}
let cb = state.breaker("research");
assert!(!cb.allows_delegation());
state.record_outcome("research", true);
assert_eq!(state.breaker("research").consecutive_failures, 0);
}
#[test]
fn memory_pressure_is_classified() {
let state = DaemonState::new();
let id = SessionId::new();
let pressure = state.record_memory(
id,
MemoryUsage {
used_tokens: 900,
window_tokens: 1000,
},
);
assert_eq!(pressure, MemoryPressure::Compact);
assert!(state.memory_for(id).is_some());
}
#[test]
fn trusty_addrs_round_trip() {
let state = DaemonState::new();
assert!(state.trusty_addrs().is_none());
let addrs = crate::daemon::discover::TrustyAddrs {
memory: "127.0.0.1:3038".parse().unwrap(),
search: "127.0.0.1:7878".parse().unwrap(),
};
state.set_trusty_addrs(addrs);
let got = state.trusty_addrs().expect("addrs stored");
assert_eq!(got.memory, "127.0.0.1:3038".parse().unwrap());
assert_eq!(got.search, "127.0.0.1:7878".parse().unwrap());
}
#[test]
fn reap_dead_sessions() {
let state = DaemonState::new();
let alive_a = sample_session();
let alive_b = sample_session();
let dead = sample_session();
let (id_a, id_b, id_dead) = (alive_a.id, alive_b.id, dead.id);
state.register_session(alive_a.clone());
state.register_session(alive_b.clone());
state.register_session(dead);
assert_eq!(state.list_sessions().len(), 3);
let live: std::collections::HashSet<String> =
[alive_a.tmux_name.clone(), alive_b.tmux_name.clone()]
.into_iter()
.collect();
let result = state.reap_against(&live);
assert_eq!(result.reaped, 1);
assert_eq!(result.stopped, 0);
assert!(state.session(id_a).is_some());
assert!(state.session(id_b).is_some());
assert!(state.session(id_dead).is_none());
assert_eq!(state.reap_against(&live), ReapResult::default());
}
#[test]
fn reap_against_empty_live_removes_all_tmux_sessions() {
let state = DaemonState::new();
state.register_session(sample_session());
state.register_session(sample_session());
let result = state.reap_against(&std::collections::HashSet::new());
assert_eq!(result.reaped, 2);
assert!(state.list_sessions().is_empty());
}
#[test]
fn reap_keeps_native_sessions() {
let state = DaemonState::new();
let mut native = sample_session();
native.origin = crate::core::session::SessionHost::Native;
native.pid = Some(9999);
let native_id = native.id;
let tmux = sample_session();
let tmux_id = tmux.id;
state.register_session(native);
state.register_session(tmux);
let result = state.reap_against(&std::collections::HashSet::new());
assert_eq!(result.reaped, 1);
assert!(state.session(native_id).is_some());
assert!(state.session(tmux_id).is_none());
}
#[test]
fn set_session_pid_updates_field() {
let (state, _tmp) = hermetic_state();
let s = sample_session();
let id = s.id;
state.register_session(s);
assert_eq!(state.session(id).unwrap().pid, None);
assert!(state.set_session_pid(id, 4242));
assert_eq!(state.session(id).unwrap().pid, Some(4242));
assert!(!state.set_session_pid(SessionId::new(), 1));
}
#[test]
fn pid_registry_is_under_framework_root() {
let (state, _tmp) = hermetic_state();
let expected = state.framework_root().join("pids");
assert_eq!(state.pid_registry().dir(), expected.as_path());
}
#[test]
fn set_session_pid_writes_pidfile() {
let (state, _tmp) = hermetic_state();
let s = sample_session();
let id = s.id;
state.register_session(s);
assert!(state.set_session_pid(id, 4242));
let entries = state.pid_registry().entries().unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].session_id, id.0.to_string());
assert_eq!(entries[0].pid, 4242);
state.remove_session(id);
assert!(state.pid_registry().entries().unwrap().is_empty());
}
#[tokio::test]
async fn gather_live_session_ids_unions_both() {
let (state, _tmp) = hermetic_state();
let a = sample_session();
let b = sample_session();
let (id_a, id_b) = (a.id, b.id);
state.register_session(a);
state.register_session(b);
let ids = state.gather_live_session_ids().await;
assert!(ids.contains(&id_a.0.to_string()));
assert!(ids.contains(&id_b.0.to_string()));
}
#[test]
fn reap_marks_stopped_when_pid_dead() {
let state = DaemonState::new();
let mut session = sample_session();
session.pid = Some(u32::MAX);
let id = session.id;
let tmux_name = session.tmux_name.clone();
state.register_session(session);
let live: std::collections::HashSet<String> = [tmux_name].into_iter().collect();
let result = state.reap_against(&live);
assert_eq!(result.reaped, 0);
assert_eq!(result.stopped, 1);
let after = state.session(id).expect("session is kept, not removed");
assert_eq!(after.status, SessionStatus::Stopped);
}
#[test]
fn new_reads_default_when_optimizer_file_missing() {
let state = DaemonState::new();
assert_eq!(
state.optimizer_config().default_level,
crate::core::compress::CompressionLevel::Trim
);
}
#[test]
fn reload_optimizer_config_picks_up_file_changes() {
use std::io::Write;
let state = DaemonState::new();
let dir = tempfile::tempdir().expect("temp dir");
let path = dir.path().join("optimizer.toml");
let mut file = std::fs::File::create(&path).expect("create file");
writeln!(file, "[default]\nlevel = \"caveman\"").expect("write file");
state
.reload_optimizer_config_from(&path)
.expect("reload succeeds");
assert_eq!(
state.optimizer_config().default_level,
crate::core::compress::CompressionLevel::Caveman
);
state
.reload_optimizer_config_from(&dir.path().join("absent.toml"))
.expect("missing file is not an error");
assert_eq!(
state.optimizer_config().default_level,
crate::core::compress::CompressionLevel::Trim
);
}
#[test]
fn new_overseer_is_disabled_when_file_missing() {
let (state, _dir) = hermetic_state();
assert!(!state.overseer().is_enabled());
}
#[test]
fn overseer_is_deterministic_without_llm() {
let cfg = OverseerConfig::default();
let build = build_overseer(cfg);
assert!(!build.overseer.is_enabled());
assert_eq!(build.handler, "deterministic");
assert!(build.llm.is_none());
}
#[test]
fn overseer_falls_back_when_llm_key_missing() {
let mut cfg = OverseerConfig::default();
cfg.llm.enabled = true;
cfg.llm.api_key_env = "TRUSTY_MPM_DEFINITELY_NOT_SET".to_string(); let build = build_overseer(cfg);
assert!(!build.overseer.is_enabled());
assert_eq!(build.handler, "deterministic");
assert!(build.llm.is_none());
}
#[test]
fn llm_overseer_is_none_without_key() {
let (state, _dir) = hermetic_state();
assert!(state.llm_overseer().is_none());
}
#[test]
fn overseer_handler_reports_strategy() {
let (state, _dir) = hermetic_state();
assert_eq!(state.overseer_handler(), "deterministic");
}
#[test]
fn overseer_is_accessible() {
let (state, _dir) = hermetic_state();
let overseer = state.overseer();
assert!(!overseer.is_enabled());
}
#[test]
fn audit_logger_is_accessible() {
let state = DaemonState::new();
let audit = state.audit();
assert_eq!(
audit.path().extension().and_then(|e| e.to_str()),
Some("jsonl")
);
}
#[test]
fn hook_history_is_bounded() {
let state = DaemonState::new();
let id = SessionId::new();
for _ in 0..(HOOK_HISTORY_LIMIT + 50) {
state.push_hook_event(HookEventRecord::now(
id,
HookEvent::PreToolUse,
serde_json::Value::Null,
));
}
assert_eq!(state.recent_hook_events().len(), HOOK_HISTORY_LIMIT);
assert_eq!(state.hook_events_for(id).len(), HOOK_HISTORY_LIMIT);
}
#[test]
fn pairing_round_trip() {
let dir = tempfile::tempdir().expect("temp dir");
let state = DaemonState::with_root(dir.path().to_path_buf());
assert_eq!(state.paired_chat_id(), None);
let code = state.generate_pair_code();
assert_eq!(code.len(), 6);
assert!(code.chars().all(|c| c.is_ascii_alphanumeric()));
assert!(state.confirm_pair_code(&code, 12345678));
assert_eq!(state.paired_chat_id(), Some(12345678));
assert!(!state.confirm_pair_code(&code, 999));
}
#[test]
fn wrong_pair_code_is_rejected() {
let dir = tempfile::tempdir().expect("temp dir");
let state = DaemonState::with_root(dir.path().to_path_buf());
let _code = state.generate_pair_code();
assert!(!state.confirm_pair_code("ZZZZZZ", 12345678));
assert_eq!(state.paired_chat_id(), None);
}
#[test]
fn pairing_persists_to_disk() {
let dir = tempfile::tempdir().expect("temp dir");
let root = dir.path().to_path_buf();
let state = DaemonState::with_root(root.clone());
let code = state.generate_pair_code();
assert!(state.confirm_pair_code(&code, 555));
assert_eq!(
crate::daemon::pairing_store::load(&root).map(|r| r.chat_id),
Some(555)
);
let restored = DaemonState::with_root(root);
assert_eq!(restored.paired_chat_id(), Some(555));
}
#[test]
fn pairing_reset_clears_disk() {
let dir = tempfile::tempdir().expect("temp dir");
let root = dir.path().to_path_buf();
let state = DaemonState::with_root(root.clone());
let code = state.generate_pair_code();
assert!(state.confirm_pair_code(&code, 777));
state.clear_pairing();
assert_eq!(state.paired_chat_id(), None);
assert!(crate::daemon::pairing_store::load(&root).is_none());
}
#[test]
fn pairing_code_persists_to_disk() {
let dir = tempfile::tempdir().expect("temp dir");
let root = dir.path().to_path_buf();
let state = DaemonState::with_root(root.clone());
let code = state.generate_pair_code();
let pending = crate::daemon::pairing_store::load_pending(&root).expect("pending code on disk");
assert_eq!(pending.code, code);
}
#[test]
fn pairing_confirms_shared_disk_code() {
let dir = tempfile::tempdir().expect("temp dir");
let root = dir.path().to_path_buf();
let minting = DaemonState::with_root(root.clone());
let code = minting.generate_pair_code();
let confirming = DaemonState::with_root(root.clone());
assert!(
confirming.confirm_pair_code(&code, 31415),
"code minted on a sibling instance must validate via the shared store"
);
assert_eq!(confirming.paired_chat_id(), Some(31415));
let replay = DaemonState::with_root(root);
assert!(!replay.confirm_pair_code(&code, 999));
}
#[test]
fn concurrent_pairing_confirms() {
let dir = tempfile::tempdir().expect("temp dir");
let root = dir.path().to_path_buf();
let minting = DaemonState::with_root(root.clone());
let code = minting.generate_pair_code();
let confirming1 = std::sync::Arc::new(DaemonState::with_root(root.clone()));
let confirming2 = std::sync::Arc::new(DaemonState::with_root(root.clone()));
let c1_ref = confirming1.clone();
let c2_ref = confirming2.clone();
let code1 = code.clone();
let code2 = code.clone();
let c1 = std::thread::spawn(move || c1_ref.confirm_pair_code(&code1, 111));
let c2 = std::thread::spawn(move || c2_ref.confirm_pair_code(&code2, 222));
let res1 = c1.join().expect("thread 1 must not panic");
let res2 = c2.join().expect("thread 2 must not panic");
assert!(
res1 ^ res2,
"exactly one concurrent confirm must win the atomic claim (got res1={res1} res2={res2})"
);
if res1 {
assert_eq!(
confirming1.paired_chat_id(),
Some(111),
"winner (thread 1) must have chat_id 111 registered"
);
assert_eq!(
confirming2.paired_chat_id(),
None,
"loser (thread 2) must not have registered any chat_id"
);
} else {
assert_eq!(
confirming2.paired_chat_id(),
Some(222),
"winner (thread 2) must have chat_id 222 registered"
);
assert_eq!(
confirming1.paired_chat_id(),
None,
"loser (thread 1) must not have registered any chat_id"
);
}
assert!(
crate::daemon::pairing_store::load_pending(&root).is_none(),
"pending code file must be absent after a winning confirm"
);
}
struct MinFakeDriver {
sessions: std::sync::Mutex<Vec<String>>,
}
impl MinFakeDriver {
fn new() -> std::sync::Arc<Self> {
std::sync::Arc::new(Self {
sessions: std::sync::Mutex::new(Vec::new()),
})
}
}
impl crate::session_manager::ManagedTmuxDriver for MinFakeDriver {
fn create_session(
&self,
name: &str,
_: &str,
) -> Result<(), crate::session_manager::ManagedError> {
self.sessions.lock().unwrap().push(name.to_owned());
Ok(())
}
fn kill_session(&self, name: &str) -> Result<(), crate::session_manager::ManagedError> {
self.sessions.lock().unwrap().retain(|n| n != name);
Ok(())
}
fn send_line(&self, _: &str, _: &str) -> Result<(), crate::session_manager::ManagedError> {
Ok(())
}
fn capture(&self, _: &str, _: usize) -> Result<String, crate::session_manager::ManagedError> {
Ok(String::new())
}
fn list_sessions(&self) -> Result<Vec<String>, crate::session_manager::ManagedError> {
Ok(self.sessions.lock().unwrap().clone())
}
}
#[tokio::test]
async fn reap_dead_managed_sessions_marks_stopped() {
let tmp = tempfile::TempDir::new().unwrap();
let fake = MinFakeDriver::new();
let mgr = crate::session_manager::SessionManager::new(tmp.path(), fake)
.await
.expect("session manager");
let mgr = std::sync::Arc::new(mgr);
let record = mgr
.create(
"task".into(),
Some(PathBuf::from("/tmp/test-reap")),
None,
None,
None,
None,
)
.await
.expect("create");
let id = record.id;
mgr.set_workspace(
&id,
PathBuf::from("/tmp/test-reap"),
crate::session_manager::ManagedSessionState::Active,
)
.await
.expect("set Active");
let state = DaemonState::with_session_manager(std::sync::Arc::clone(&mgr));
let live = std::collections::HashSet::new();
state.reap_managed_against(&live).await;
let after = mgr.get(&id).await.expect("get after reap");
assert_eq!(
after.state,
crate::session_manager::ManagedSessionState::Stopped,
"reap_managed_against must mark Active session Stopped when tmux_name absent (#1744)"
);
}
#[tokio::test]
async fn project_registry_seeds_session_history_without_prewarmed_managed_sessions() {
let dir = tempfile::tempdir().expect("temp dir");
let paths = FrameworkPaths::under(dir.path());
let data_dir = paths.root.join("session-manager");
tokio::fs::create_dir_all(&data_dir)
.await
.expect("mkdir session-manager data dir");
let fake_tmux: std::sync::Arc<dyn crate::session_manager::ManagedTmuxDriver> =
std::sync::Arc::new(crate::session_manager::FakeNoopTmuxDriver);
let seed_mgr = crate::session_manager::SessionManager::new(&data_dir, fake_tmux)
.await
.expect("seed session manager");
seed_mgr
.create(
"task".into(),
Some(PathBuf::from("/tmp/wt-3822-hardening")),
None,
None,
Some("https://github.com/octocat/Hello-World.git".into()),
Some("main".into()),
)
.await
.expect("seed session record");
drop(seed_mgr);
let fresh = DaemonState::with_paths(&paths);
let registry = fresh.project_registry().await;
let all = registry.list().await.expect("list");
assert!(
all.iter()
.any(|p| p.repo_url == "https://github.com/octocat/Hello-World.git"),
"a session persisted by an earlier process must be registered at the FIRST \
project_registry() touch, even though nothing warmed managed_sessions first \
(#3822 hardening — project_registry() must warm session_manager() itself, \
not merely peek at whether something else already did): {all:?}"
);
}
#[test]
fn manager_state_is_provisioned() {
let (state, _dir) = hermetic_state();
let manager = state.manager_state();
assert_eq!(
manager.palace().id(),
crate::daemon::manager::PORTFOLIO_PALACE_ID,
"daemon startup must auto-provision the portfolio manager palace"
);
}