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 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);
}
fn unisolated_running_delegation(
session: crate::core::session::SessionId,
cwd: &std::path::Path,
) -> crate::core::agent::Delegation {
let mut d = crate::core::agent::Delegation::new(
session,
None,
"rust-engineer",
crate::core::agent::ModelTier::Sonnet,
"finish the work",
);
d.status = crate::core::agent::DelegationStatus::Running;
d.cwd = Some(cwd.to_path_buf());
d.isolation = None;
d
}
#[test]
fn reap_stales_a_dead_sessions_delegations() {
let state = DaemonState::new();
let session = sample_session();
let id = session.id;
let cwd = std::path::PathBuf::from("/repo/main");
state.register_session(session);
state.upsert_delegation(unisolated_running_delegation(id, &cwd));
assert_eq!(
state.live_shared_tree_writers(&cwd, None).len(),
1,
"the record blocks a dispatch while its session is alive"
);
let result = state.reap_against(&std::collections::HashSet::new());
assert_eq!(result.reaped, 1);
assert!(
state.live_shared_tree_writers(&cwd, None).is_empty(),
"a dead session's records must stop blocking dispatch"
);
let after = state.all_delegations();
assert_eq!(after.len(), 1, "the record is staled, never dropped");
assert_eq!(
after[0].status,
crate::core::agent::DelegationStatus::Stale,
"Stale records that tracking gave up, and stays resolvable by a late stop"
);
assert!(
after[0].ended_at.is_none(),
"staling must not stamp ended_at — that field means `reached a terminal status`"
);
}
#[test]
fn reap_leaves_a_live_sessions_delegations_alone() {
let state = DaemonState::new();
let session = sample_session();
let id = session.id;
let tmux_name = session.tmux_name.clone();
let cwd = std::path::PathBuf::from("/repo/main");
state.register_session(session);
state.upsert_delegation(unisolated_running_delegation(id, &cwd));
let live: std::collections::HashSet<String> = [tmux_name].into_iter().collect();
let result = state.reap_against(&live);
assert_eq!(result.reaped, 0);
assert_eq!(
state.live_shared_tree_writers(&cwd, None).len(),
1,
"a live session's agent must still block a second writer in its checkout"
);
}
#[test]
fn a_recorded_worktree_outranks_a_missing_isolation_declaration() {
let state = DaemonState::new();
let session = sample_session();
let id = session.id;
let cwd = std::path::PathBuf::from("/repo/main");
state.register_session(session);
let mut d = unisolated_running_delegation(id, &cwd);
let tree = std::path::PathBuf::from("/repo/main/.claude/worktrees/agent-aa049179cd3e4e1bb");
d.worktree_path = Some(tree.clone());
d.last_agent_cwd = Some(tree);
state.upsert_delegation(d);
assert!(
state.live_shared_tree_writers(&cwd, None).is_empty(),
"an agent that reported a tree of its own is not writing in the shared checkout"
);
}
#[test]
fn an_agent_that_leaves_its_worktree_blocks_the_shared_tree_again() {
let state = DaemonState::new();
let session = sample_session();
let id = session.id;
let cwd = std::path::PathBuf::from("/repo/main");
let tree = std::path::PathBuf::from("/repo/main/.claude/worktrees/agent-enterexit");
state.register_session(session);
let mut d = unisolated_running_delegation(id, &cwd);
d.worktree_path = Some(tree.clone());
d.last_agent_cwd = Some(tree.clone());
let delegation_id = d.id;
state.upsert_delegation(d);
assert!(
state.live_shared_tree_writers(&cwd, None).is_empty(),
"while it is in its own tree it is not writing in the shared checkout"
);
state.mutate_delegation(delegation_id, |d| d.last_agent_cwd = Some(cwd.clone()));
assert_eq!(
state.live_shared_tree_writers(&cwd, None).len(),
1,
"once it walks back into the shared checkout it must block again"
);
assert_eq!(
state
.all_delegations()
.into_iter()
.next()
.expect("one")
.worktree_path,
Some(tree),
"and the ownership latch must survive, or the reap loses the tree's owner"
);
}
#[test]
fn an_unrecorded_worktree_leaves_the_isolation_test_deciding() {
let state = DaemonState::new();
let session = sample_session();
let id = session.id;
let cwd = std::path::PathBuf::from("/repo/main");
state.register_session(session);
state.upsert_delegation(unisolated_running_delegation(id, &cwd));
assert_eq!(
state.live_shared_tree_writers(&cwd, None).len(),
1,
"no recorded worktree means undetermined, and the guard still blocks"
);
let state = DaemonState::new();
let session = sample_session();
let id = session.id;
state.register_session(session);
let mut d = unisolated_running_delegation(id, &cwd);
d.worktree_path = Some(cwd.clone());
d.last_agent_cwd = Some(cwd.clone());
state.upsert_delegation(d);
assert_eq!(
state.live_shared_tree_writers(&cwd, None).len(),
1,
"a recorded path equal to the dispatch cwd names the shared tree, not a separate one"
);
}
#[test]
fn a_type_reconciled_record_still_occupies_the_tree_for_a_dispatch() {
let state = DaemonState::new();
let session = sample_session();
let id = session.id;
let cwd = std::path::PathBuf::from("/repo/main");
state.register_session(session);
let mut d = unisolated_running_delegation(id, &cwd);
d.started_at = Some(chrono::Utc::now());
d.status = crate::core::agent::DelegationStatus::Stale;
d.stale_by_agent_type = true;
state.upsert_delegation(d);
assert!(
state.live_shared_tree_writers(&cwd, None).is_empty(),
"the ADR-0049 commit guard honours the reconciliation — that is the point of #6556"
);
assert_eq!(
state.shared_tree_occupants(&cwd, None).len(),
1,
"but the tree is still occupied, because the identification was by type"
);
let mut recorded = false;
let (occupants, claimed) = state.claim_shared_tree_dispatch(
&cwd,
None,
true,
crate::daemon::state::sessions::SharedTreeQuestion::Dispatch,
|_| recorded = true,
);
assert_eq!(occupants.len(), 1, "the deny must name the occupant");
assert!(
!claimed,
"a second file-mutating dispatch must not be admitted"
);
assert!(
!recorded,
"and nothing may be recorded when the claim is refused"
);
}
#[test]
fn a_type_reconciled_record_stops_occupying_at_its_original_budget() {
let state = DaemonState::new();
let session = sample_session();
let id = session.id;
let cwd = std::path::PathBuf::from("/repo/main");
state.register_session(session);
let mut d = unisolated_running_delegation(id, &cwd);
d.started_at = Some(
chrono::Utc::now()
- chrono::Duration::seconds(
crate::daemon::state::sessions::RUNNING_STALE_AFTER_SECS + 60,
),
);
d.status = crate::core::agent::DelegationStatus::Stale;
d.stale_by_agent_type = true;
state.upsert_delegation(d);
assert!(
state.shared_tree_occupants(&cwd, None).is_empty(),
"past its own liveness budget the record occupies nothing — the sweep would \
have staled it by now regardless"
);
}
#[test]
fn an_ordinarily_staled_record_occupies_nothing() {
let state = DaemonState::new();
let session = sample_session();
let id = session.id;
let cwd = std::path::PathBuf::from("/repo/main");
state.register_session(session);
let mut d = unisolated_running_delegation(id, &cwd);
d.started_at = Some(chrono::Utc::now());
d.status = crate::core::agent::DelegationStatus::Stale;
state.upsert_delegation(d);
assert!(
state.shared_tree_occupants(&cwd, None).is_empty(),
"only a TYPE-reconciled stale keeps occupancy; #6497's reaper must still release"
);
}
#[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 new_never_writes_under_the_real_home() {
use crate::core::home_write_fence::{armed_roots, fenced_root};
use crate::daemon::audit::AuditEntry;
assert!(!armed_roots().is_empty(), "the lib binary arms the fence");
let state = DaemonState::new();
let audit = state.audit();
for path in [state.framework_root(), audit.path()] {
assert!(
fenced_root(path, armed_roots()).is_none(),
"{} sits under a fenced home root {:?}",
path.display(),
armed_roots()
);
}
audit.log(AuditEntry {
ts: "2026-09-26T00:00:00Z".into(),
session: "tmpm-8545".into(),
event: "PreToolUse".into(),
tool: None,
decision: "allow".into(),
reason: "#8545 regression".into(),
handler: "deterministic".into(),
});
let written = std::fs::read_to_string(audit.path()).expect("the audit line landed");
assert!(written.contains("#8545 regression"), "{written}");
}
#[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)"
);
assert_eq!(
after.stop_cause,
Some(crate::session_manager::StopCause::Unexpected),
"an empty live set is a whole-server loss, not a decision about this session"
);
assert!(after.is_auto_resumable());
}
#[tokio::test]
async fn reap_managed_against_skips_a_session_whose_resume_is_in_flight() {
let tmp = tempfile::TempDir::new().unwrap();
let mgr = crate::session_manager::SessionManager::new(tmp.path(), MinFakeDriver::new())
.await
.expect("session manager");
let mgr = std::sync::Arc::new(mgr);
let id = active_session_in_state(&mgr, "resuming", "/tmp/test-reap-in-flight").await;
let state = DaemonState::with_session_manager(std::sync::Arc::clone(&mgr));
let claim = mgr.begin_resume(&id).expect("claim granted");
let mut live = std::collections::HashSet::new();
live.insert("some-other-live-session".to_string());
state.reap_managed_against(&live).await;
let after = mgr.get(&id).await.expect("get after reap");
assert_eq!(
after.state,
crate::session_manager::ManagedSessionState::Active,
"the reaper must not stop a session whose resume is in flight (#8233)"
);
assert_eq!(
after.stop_cause, None,
"and must write no stop cause for it either — a `Deliberate` stamp here \
is what made such a session permanently un-auto-resumable"
);
drop(claim);
}
async fn active_session_in_state(
mgr: &std::sync::Arc<crate::session_manager::SessionManager>,
task: &str,
workspace: &str,
) -> crate::session_manager::ManagedSessionId {
let record = mgr
.create(
task.into(),
Some(PathBuf::from(workspace)),
None,
None,
None,
None,
)
.await
.expect("create");
mgr.set_workspace(
&record.id,
PathBuf::from(workspace),
crate::session_manager::ManagedSessionState::Active,
)
.await
.expect("set Active");
record.id
}
#[tokio::test]
async fn reap_marks_a_targeted_kill_deliberate() {
let tmp = tempfile::TempDir::new().unwrap();
let mgr = crate::session_manager::SessionManager::new(tmp.path(), MinFakeDriver::new())
.await
.expect("session manager");
let mgr = std::sync::Arc::new(mgr);
let id = active_session_in_state(&mgr, "killed", "/tmp/test-reap-killed").await;
let state = DaemonState::with_session_manager(std::sync::Arc::clone(&mgr));
let live: std::collections::HashSet<String> =
["someone-elses-shell".to_string()].into_iter().collect();
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
);
assert_eq!(
after.stop_cause,
Some(crate::session_manager::StopCause::Deliberate)
);
assert!(
!after.is_auto_resumable(),
"a session killed out-of-band while the server is alive must not be auto-resumed"
);
}
#[tokio::test]
async fn reap_leaves_a_whole_server_loss_auto_resumable() {
let tmp = tempfile::TempDir::new().unwrap();
let mgr = crate::session_manager::SessionManager::new(tmp.path(), MinFakeDriver::new())
.await
.expect("session manager");
let mgr = std::sync::Arc::new(mgr);
let first = active_session_in_state(&mgr, "fleet-a", "/tmp/test-reap-fleet-a").await;
let second = active_session_in_state(&mgr, "fleet-b", "/tmp/test-reap-fleet-b").await;
let state = DaemonState::with_session_manager(std::sync::Arc::clone(&mgr));
let live = std::collections::HashSet::new();
state.reap_managed_against(&live).await;
for id in [first, second] {
let after = mgr.get(&id).await.expect("get after reap");
assert_eq!(
after.state,
crate::session_manager::ManagedSessionState::Stopped,
"the safety net still converges the record (#1744)"
);
assert_eq!(
after.stop_cause,
Some(crate::session_manager::StopCause::Unexpected)
);
assert!(
after.is_auto_resumable(),
"losing the tmux server is not a decision about any session — the supervisor \
must still be able to restore this fleet"
);
}
}
#[tokio::test]
async fn the_tmux_gone_reaper_marks_a_supervisor_record_stopped_without_teardown() {
let tmp = tempfile::TempDir::new().unwrap();
let driver = MinFakeDriver::new();
let mgr = crate::session_manager::SessionManager::new(tmp.path(), driver.clone())
.await
.expect("session manager");
let mgr = std::sync::Arc::new(mgr);
let id = active_session_in_state(&mgr, "architect", "/tmp/test-reap-architect").await;
let mut record = mgr.get(&id).await.expect("record");
record.kind = crate::session_manager::SessionKind::Supervisor;
let name = record.tmux_name.clone();
mgr.store.write().await.upsert(record).await.expect("seed");
let state = DaemonState::with_session_manager(std::sync::Arc::clone(&mgr));
let live: std::collections::HashSet<String> =
["someone-elses-shell".to_string()].into_iter().collect();
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
);
assert_eq!(
after.stop_cause,
Some(crate::session_manager::StopCause::Deliberate)
);
assert!(
driver.sessions.lock().unwrap().contains(&name),
"the reaper killed a pane carrying the Architect's name"
);
}
#[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"
);
}
#[tokio::test]
async fn session_manager_refuses_tmux_on_a_scratch_framework_root() {
let tmp = tempfile::tempdir().unwrap();
let state = DaemonState::with_root(tmp.path().to_path_buf());
let mgr = state.session_manager().await;
let live = mgr.tmux_driver().list_sessions();
assert!(
live.is_err(),
"a scratch framework root must leave tmux unobservable, got {live:?}"
);
}
#[test]
fn head_write_without_a_caller_excludes_nothing() {
let (state, _dir) = hermetic_state();
let session = crate::core::session::SessionId(uuid::Uuid::new_v4());
let cwd = std::path::PathBuf::from("/repo/main");
state.upsert_delegation(unisolated_running_delegation(session, &cwd));
assert_eq!(
state.live_shared_tree_writers_excluding(&cwd, None, None),
vec!["rust-engineer".to_string()],
"an unidentified caller must still be told about the writer"
);
assert!(
state
.live_shared_tree_writers_excluding(&cwd, None, Some(session))
.is_empty(),
"and naming that session must exclude its own record"
);
}
#[test]
fn a_live_agent_is_counted_in_the_worktree_it_stands_in() {
let state = DaemonState::new();
let session = sample_session();
let id = session.id;
let checkout = std::path::PathBuf::from("/repo/main");
let tree = checkout.join(".claude/worktrees/agent-parked");
state.register_session(session);
let mut d = unisolated_running_delegation(id, &checkout);
d.isolation = Some("worktree".to_string());
d.worktree_path = Some(tree.clone());
d.last_agent_cwd = Some(tree.join("crates/foo"));
state.upsert_delegation(d);
assert_eq!(
state.live_shared_tree_writers_excluding(&tree, None, None),
vec!["rust-engineer".to_string()],
"a tree-holders query must name the agent standing in the tree"
);
assert!(
state
.live_shared_tree_writers_excluding(&tree, None, Some(id))
.is_empty(),
"a session-scoped HEAD write still excludes its own session (#6797)"
);
assert!(
state.live_shared_tree_writers(&checkout, None).is_empty(),
"and the agent is still not a writer in the checkout it left"
);
}
#[test]
fn a_record_stamped_in_a_relocated_pm_cwd_is_not_a_writer_there() {
let state = DaemonState::new();
let session = sample_session();
let id = session.id;
let relocated = std::path::PathBuf::from("/repo/main/.claude/worktrees/agent-engineer");
state.register_session(session);
let mut d = unisolated_running_delegation(id, &relocated);
d.agent = "qa".to_string();
d.last_agent_cwd = Some(std::path::PathBuf::from(
"/repo/main/.claude/worktrees/agent-qa/tests",
));
state.upsert_delegation(d);
assert!(
state.shared_tree_occupants(&relocated, None).is_empty(),
"an agent standing in another harness tree is not a writer in the PM's cwd"
);
}
#[test]
fn a_live_agent_occupies_its_worktree_for_a_dispatch_from_inside_it() {
let state = DaemonState::new();
let session = sample_session();
let id = session.id;
let checkout = std::path::PathBuf::from("/repo/main");
let tree = checkout.join(".claude/worktrees/agent-live");
state.register_session(session);
let mut d = unisolated_running_delegation(id, &checkout);
d.isolation = Some("worktree".to_string());
d.worktree_path = Some(tree.clone());
d.last_agent_cwd = Some(tree.clone());
state.upsert_delegation(d);
assert_eq!(
state.shared_tree_occupants(&tree, None),
vec!["rust-engineer".to_string()]
);
assert!(state.shared_tree_occupants(&checkout, None).is_empty());
}
#[test]
fn a_read_only_agent_in_a_linked_worktree_is_not_a_writer() {
let state = DaemonState::new();
let session = sample_session();
let id = session.id;
let checkout = std::path::PathBuf::from("/repo/main");
let tree = checkout.join(".claude/worktrees/agent-parked");
state.register_session(session);
let mut d = unisolated_running_delegation(id, &checkout);
d.agent = "research".to_string();
d.last_agent_cwd = Some(tree.clone());
state.upsert_delegation(d);
assert!(
state
.live_shared_tree_writers_excluding(&tree, None, None)
.is_empty()
);
}