use std::path::{Path, PathBuf};
use std::sync::Once;
use std::time::{Duration, SystemTime};
use tempfile::TempDir;
#[path = "test_tmux_session.rs"]
pub(crate) mod tmux_session;
pub(crate) use crate::core::spawn_disclaim::disclaimed_output as tmux_spawn;
pub(crate) use crate::core::trusty_tools_config::env_test_lock as lock_path_env;
#[ctor::ctor]
fn arm_home_write_fence() {
crate::core::home_write_fence::arm_for_this_process();
}
#[ctor::ctor]
fn isolate_tmux_server() {
crate::core::tmux_test_isolation::isolate_for_this_process()
.expect("#6542: create this test binary's private tmux directory");
}
#[ctor::dtor]
fn teardown_tmux_server() {
crate::core::tmux_test_isolation::teardown_for_this_process();
}
const DEAD_LOOPBACK_PORT: u16 = 1;
const _: () = assert!(
DEAD_LOOPBACK_PORT < 1024,
"DEAD_LOOPBACK_PORT must stay below 1024 so binding it requires root and the \
OS never hands it out as an ephemeral port (#4306/#4415)"
);
static DEAD_PORT_VERIFIED: Once = Once::new();
pub(crate) fn dead_loopback_url() -> String {
DEAD_PORT_VERIFIED.call_once(|| {
let addr = std::net::SocketAddr::from((std::net::Ipv4Addr::LOCALHOST, DEAD_LOOPBACK_PORT));
match std::net::TcpStream::connect_timeout(&addr, Duration::from_secs(10)) {
Ok(_) => panic!(
"test_support::dead_loopback_url(): something is LISTENING on {addr}, so the \
dead-address fixture's premise is void (#4306/#4415). Stop that listener, or \
pick another privileged, non-ephemeral loopback port for DEAD_LOOPBACK_PORT."
),
Err(e) if e.kind() == std::io::ErrorKind::ConnectionRefused => {}
Err(e) => panic!(
"test_support::dead_loopback_url(): {addr} neither accepted nor REFUSED the \
connect — it failed with {:?} ({e}) (#4306/#4415). The fixture requires a \
prompt ECONNREFUSED; a dropped SYN (TimedOut) would make every daemon-down \
test slow and would hand the error-classification tests the wrong error kind.",
e.kind()
),
}
});
format!("http://127.0.0.1:{DEAD_LOOPBACK_PORT}")
}
pub(crate) const TEST_DIR_PREFIX: &str = "tm-test-";
pub(crate) const STABLE_HOOK_EXE: &str = "/usr/local/bin/tm";
const STALE_AFTER: Duration = Duration::from_secs(24 * 60 * 60);
static SWEEP_ONCE: Once = Once::new();
fn real_system_tmp() -> PathBuf {
#[cfg(unix)]
{
PathBuf::from("/tmp")
}
#[cfg(not(unix))]
{
std::env::temp_dir()
}
}
fn is_meaningful_home_boundary(home: &Path, candidate: &Path) -> bool {
home.components().count() > 1 && home != candidate
}
fn guard_against_project_tree(root: &Path) {
let Some(home) = std::env::var_os("HOME") else {
return;
};
let home = PathBuf::from(home);
if is_meaningful_home_boundary(&home, root) && root.starts_with(&home) {
panic!(
"hermetic test temp root {root:?} resolves inside $HOME ({home:?}) — \
refusing to risk littering a project tree (see #3382)"
);
}
}
pub(crate) fn hermetic_temp_dir() -> TempDir {
SWEEP_ONCE.call_once(sweep_stale_test_dirs);
let root = real_system_tmp();
guard_against_project_tree(&root);
tempfile::Builder::new()
.prefix(TEST_DIR_PREFIX)
.tempdir_in(&root)
.expect("create hermetic test temp dir")
}
fn sweep_stale_test_dirs() {
let root = real_system_tmp();
let Ok(entries) = std::fs::read_dir(&root) else {
return;
};
let now = SystemTime::now();
for entry in entries.flatten() {
let Some(name) = entry.file_name().to_str().map(str::to_owned) else {
continue;
};
if !name.starts_with(TEST_DIR_PREFIX) {
continue;
}
let is_stale = entry
.metadata()
.and_then(|meta| meta.modified())
.ok()
.and_then(|modified| now.duration_since(modified).ok())
.is_some_and(|age| age > STALE_AFTER);
if is_stale {
let _ = std::fs::remove_dir_all(entry.path());
}
}
}
pub(crate) struct DaemonHomeOverride {
allow_production: bool,
}
impl DaemonHomeOverride {
pub(crate) fn new(data_dir: &Path, allow_production: bool) -> Self {
unsafe {
std::env::set_var(trusty_common::DATA_DIR_OVERRIDE_ENV, data_dir);
if allow_production {
std::env::set_var(trusty_common::test_harness::ALLOW_PRODUCTION_ENV, "1");
}
}
Self { allow_production }
}
}
impl Drop for DaemonHomeOverride {
fn drop(&mut self) {
unsafe {
std::env::remove_var(trusty_common::DATA_DIR_OVERRIDE_ENV);
if self.allow_production {
std::env::remove_var(trusty_common::test_harness::ALLOW_PRODUCTION_ENV);
}
}
}
}
pub(crate) fn isolated_daemon_home(allow_production: bool) -> (TempDir, DaemonHomeOverride) {
let dir = hermetic_temp_dir();
let override_guard = DaemonHomeOverride::new(dir.path(), allow_production);
(dir, override_guard)
}
#[cfg(unix)]
pub(crate) fn fake_claude_on_path() -> FakeClaudeOnPath {
use std::os::unix::fs::PermissionsExt;
let dir = hermetic_temp_dir();
let exe = dir.path().join("claude");
std::fs::write(&exe, b"#!/bin/sh\nexit 0\n").expect("write the fake claude");
std::fs::set_permissions(&exe, std::fs::Permissions::from_mode(0o755))
.expect("chmod the fake claude");
let prev = std::env::var_os("PATH");
let mut entries = vec![dir.path().to_path_buf()];
if let Some(ref p) = prev {
entries.extend(std::env::split_paths(p));
}
let joined = std::env::join_paths(entries).expect("join PATH");
unsafe { std::env::set_var("PATH", joined) };
assert!(
trusty_common::bin_resolve::resolve_binary("claude").is_some_and(|found| found == exe),
"the planted stub must WIN the lookup, else the test it guards still \
depends on the host's own Claude Code install"
);
FakeClaudeOnPath { _dir: dir, prev }
}
#[cfg(unix)]
pub(crate) struct FakeClaudeOnPath {
_dir: TempDir,
prev: Option<std::ffi::OsString>,
}
#[cfg(unix)]
impl Drop for FakeClaudeOnPath {
fn drop(&mut self) {
unsafe {
match self.prev.take() {
Some(p) => std::env::set_var("PATH", p),
None => std::env::remove_var("PATH"),
}
}
}
}
pub(crate) fn enable_event_capture() {
static RAISE_MAX_LEVEL: Once = Once::new();
RAISE_MAX_LEVEL.call_once(|| {
let _ = tracing::subscriber::set_global_default(tracing_subscriber::registry());
});
assert!(
tracing::level_filters::LevelFilter::current() >= tracing::Level::WARN,
"test_support::enable_event_capture(): the process-global tracing level \
is {:?}, which discards WARN events before any subscriber sees them \
(#4931). Some test in this binary installed a FILTERED global default \
subscriber; route it through this function, or give it a subscriber \
whose max_level_hint admits WARN.",
tracing::level_filters::LevelFilter::current()
);
}
pub(crate) const FAKE_PANE_SERVER: &str = "1:1";
pub(crate) fn self_describing_pane(name: &str) -> String {
format!("%{name}")
}
pub(crate) fn self_describing_identity(
pane_id: &str,
) -> crate::session_manager::pane_identity::PaneIdentity {
crate::session_manager::pane_identity::PaneIdentity {
pane_id: pane_id.to_owned(),
session_id: "$0".into(),
server: FAKE_PANE_SERVER.into(),
session_name: pane_id.trim_start_matches('%').to_owned(),
}
}
#[cfg(test)]
mod tests {
#[test]
fn a_self_describing_pane_names_its_own_session() {
let pane = super::self_describing_pane("tmpm-test-9101");
let identity = super::self_describing_identity(&pane);
assert_eq!(identity.pane_id, "%tmpm-test-9101");
assert_eq!(identity.session_name, "tmpm-test-9101");
assert_eq!(identity.server, super::FAKE_PANE_SERVER);
}
use super::*;
use std::time::UNIX_EPOCH;
#[test]
fn the_home_write_fence_is_armed_for_the_lib_binary() {
use crate::core::home_write_fence::{armed_roots, fenced_root};
let home = crate::core::host_state_gate::passwd_home_dir().expect("a passwd home");
for dest in [
home.join(".trusty-mpm").join("usage"),
home.join(".claude").join("settings.json"),
home.join(".claude.json"),
] {
assert!(
fenced_root(&dest, armed_roots()).is_some(),
"{} is not fenced; armed roots: {:?}",
dest.display(),
armed_roots()
);
}
}
#[test]
fn real_system_tmp_ignores_tmpdir_env() {
let root = real_system_tmp();
#[cfg(unix)]
assert_eq!(root, PathBuf::from("/tmp"));
assert!(root.exists(), "hermetic root must exist: {root:?}");
}
#[serial_test::serial]
#[test]
fn hermetic_temp_dir_is_prefixed_and_outside_home() {
let dir = hermetic_temp_dir();
let name = dir
.path()
.file_name()
.and_then(|n| n.to_str())
.unwrap_or_default();
assert!(
name.starts_with(TEST_DIR_PREFIX),
"expected {name:?} to start with {TEST_DIR_PREFIX:?}"
);
if let Some(home) = std::env::var_os("HOME") {
let home = PathBuf::from(home);
if is_meaningful_home_boundary(&home, &real_system_tmp()) {
assert!(
!dir.path().starts_with(&home),
"hermetic dir must never resolve inside $HOME: {:?}",
dir.path()
);
}
}
}
struct HomeOverride {
prev: Option<std::ffi::OsString>,
}
impl Drop for HomeOverride {
fn drop(&mut self) {
match self.prev.take() {
Some(v) => unsafe { std::env::set_var("HOME", v) },
None => unsafe { std::env::remove_var("HOME") },
}
}
}
fn override_home(value: &str) -> HomeOverride {
let prev = std::env::var_os("HOME");
unsafe { std::env::set_var("HOME", value) };
HomeOverride { prev }
}
#[serial_test::serial]
#[test]
#[should_panic(expected = "resolves inside $HOME")]
fn guard_panics_on_genuine_home_containment() {
let _home = override_home("/Users/test-user");
guard_against_project_tree(&PathBuf::from("/Users/test-user/trusty-mpm-projects"));
}
#[serial_test::serial]
#[test]
fn guard_does_not_panic_when_home_is_tmp() {
let _home = override_home("/tmp");
guard_against_project_tree(&PathBuf::from("/tmp"));
}
#[serial_test::serial]
#[test]
fn guard_does_not_panic_when_home_is_root() {
let _home = override_home("/");
guard_against_project_tree(&PathBuf::from("/tmp"));
}
#[test]
fn sweep_removes_only_stale_prefixed_dirs() {
let root = real_system_tmp();
let stale = root.join(format!(
"{TEST_DIR_PREFIX}sweep-stale-{}",
std::process::id()
));
let fresh = root.join(format!(
"{TEST_DIR_PREFIX}sweep-fresh-{}",
std::process::id()
));
let unrelated = root.join(format!("not-tm-prefixed-{}", std::process::id()));
std::fs::create_dir_all(&stale).unwrap();
std::fs::create_dir_all(&fresh).unwrap();
std::fs::create_dir_all(&unrelated).unwrap();
let two_days_ago = SystemTime::now() - Duration::from_secs(2 * 24 * 60 * 60);
let times = std::fs::FileTimes::new().set_modified(two_days_ago);
std::fs::File::open(&stale)
.unwrap()
.set_times(times)
.unwrap();
sweep_stale_test_dirs();
assert!(!stale.exists(), "stale tm-test- dir should be swept");
assert!(fresh.exists(), "fresh tm-test- dir should survive");
assert!(unrelated.exists(), "non-prefixed dir must never be touched");
let _ = std::fs::remove_dir_all(&fresh);
let _ = std::fs::remove_dir_all(&unrelated);
let _ = std::fs::remove_dir_all(&stale);
}
#[test]
fn dead_loopback_url_refuses_every_connect() {
let addr: std::net::SocketAddr = dead_loopback_url()
.trim_start_matches("http://")
.parse()
.expect("the fixture's url must be host:port after the scheme");
for attempt in 0..50 {
let err = std::net::TcpStream::connect_timeout(&addr, Duration::from_secs(10))
.expect_err("nothing may be listening on the dead-address port");
assert_eq!(
err.kind(),
std::io::ErrorKind::ConnectionRefused,
"attempt {attempt} to {addr} must be REFUSED (RST), not dropped: {err:?}"
);
}
}
#[test]
fn dead_loopback_port_is_not_bindable_or_ephemeral() {
let addr = std::net::SocketAddr::from((std::net::Ipv4Addr::LOCALHOST, DEAD_LOOPBACK_PORT));
assert!(
std::net::TcpListener::bind(addr).is_err(),
"an unprivileged bind of {addr} must fail — a bindable dead-address port is \
exactly the invalidated premise #4306 describes"
);
for _ in 0..2_000 {
if let Ok(other) = std::net::TcpListener::bind("127.0.0.1:0") {
assert_ne!(
other.local_addr().expect("local addr").port(),
DEAD_LOOPBACK_PORT,
"the OS handed a competing binder the dead-address port"
);
}
}
}
#[test]
fn stale_after_is_positive_duration() {
assert!(STALE_AFTER > Duration::from_secs(0));
assert!(SystemTime::now().duration_since(UNIX_EPOCH).is_ok());
}
#[test]
fn event_capture_admits_warn() {
use tracing_subscriber::layer::SubscriberExt;
super::enable_event_capture();
assert!(
tracing::level_filters::LevelFilter::current() >= tracing::Level::WARN,
"the global level must admit WARN after enable_event_capture()"
);
let buffer = trusty_common::log_buffer::LogBuffer::new(8);
let subscriber = tracing_subscriber::registry().with(
trusty_common::log_buffer::LogBufferLayer::new(buffer.clone()),
);
tracing::subscriber::with_default(subscriber, || {
tracing::warn!("enable-event-capture-probe");
});
let lines = buffer.tail(8);
assert!(
lines
.iter()
.any(|l| l.contains("enable-event-capture-probe")),
"a WARN emitted under with_default must reach the capture buffer, else every capture test in this binary is silently vacuous: {lines:#?}"
);
}
}