use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tsafe_core::agent::{AgentRequest, AgentResponse, AgentSession, AgentSessionState};
fn run_once(
req: AgentRequest,
peer_pid: Option<u32>,
token: &str,
idle_secs: u64,
absolute_secs: i64, ) -> (AgentResponse, AgentSessionState) {
let now = Instant::now();
let absolute_deadline = if absolute_secs >= 0 {
now + Duration::from_secs(absolute_secs as u64)
} else {
now - Duration::from_secs((-absolute_secs) as u64)
};
let mut session = AgentSession::new(token, idle_secs, absolute_deadline);
let outcome = session.handle_request(&req, peer_pid, "vault-password", now);
(outcome.response, outcome.state)
}
#[test]
fn agent_pid_reuse_attack_rejected_without_valid_session_token() {
let now = Instant::now();
let absolute = now + Duration::from_secs(300);
let mut session = AgentSession::new("original-token-abc", 300, absolute);
let pid_reuse_req = AgentRequest::OpenVault {
profile: "default".into(),
session_token: "guessed-wrong-token".into(),
requesting_pid: 1000,
};
let outcome = session.handle_request(&pid_reuse_req, Some(1000), "vault-password", now);
assert!(
!outcome.stop,
"a rejected PID-reuse attempt must not stop the agent (still valid for other callers)"
);
assert_eq!(
outcome.state,
AgentSessionState::Active,
"session remains active after a failed PID-reuse attempt"
);
match outcome.response {
AgentResponse::Err { reason } => {
assert!(
reason.contains("invalid session token"),
"rejection reason must mention the token: {reason}"
);
}
other => panic!("expected Err, got {other:?}"),
}
}
#[test]
fn agent_pid_reuse_attack_rejected_when_transport_pid_mismatches_claim() {
let now = Instant::now();
let absolute = now + Duration::from_secs(300);
let mut session = AgentSession::new("real-token-xyz", 300, absolute);
let req = AgentRequest::OpenVault {
profile: "default".into(),
session_token: "real-token-xyz".into(),
requesting_pid: 1000, };
let outcome = session.handle_request(&req, Some(9999), "vault-password", now);
assert!(!outcome.stop);
assert_eq!(outcome.state, AgentSessionState::Active);
match outcome.response {
AgentResponse::Err { reason } => {
assert!(
reason.contains("does not match the connecting process"),
"rejection must mention PID mismatch: {reason}"
);
}
other => panic!("expected PID-mismatch Err, got {other:?}"),
}
}
#[test]
fn agent_legitimate_process_with_correct_token_and_pid_is_accepted() {
let now = Instant::now();
let absolute = now + Duration::from_secs(300);
let mut session = AgentSession::new("legit-token", 300, absolute);
let req = AgentRequest::OpenVault {
profile: "default".into(),
session_token: "legit-token".into(),
requesting_pid: 42,
};
let outcome = session.handle_request(&req, Some(42), "vault-password", now);
assert!(!outcome.stop);
assert_eq!(outcome.state, AgentSessionState::Active);
match outcome.response {
AgentResponse::Password { password } => assert_eq!(password, "vault-password"),
other => panic!("expected Password, got {other:?}"),
}
}
#[test]
fn agent_expired_absolute_ttl_rejects_open_vault_and_returns_error_not_password() {
let now = Instant::now();
let absolute = now - Duration::from_secs(1);
let mut session = AgentSession::new("ttl-token", 300, absolute);
let req = AgentRequest::OpenVault {
profile: "default".into(),
session_token: "ttl-token".into(),
requesting_pid: 7,
};
let outcome = session.handle_request(&req, Some(7), "vault-password", now);
assert!(outcome.stop, "expired session must set stop=true");
assert_eq!(outcome.state, AgentSessionState::Expired);
match &outcome.response {
AgentResponse::Err { reason } => {
assert!(
reason.contains("expired"),
"error must say 'expired', got: {reason}"
);
assert!(!reason.is_empty(), "error reason must not be empty");
}
AgentResponse::Password { .. } => {
panic!("expired session must NEVER return a password")
}
other => panic!("expected Err, got {other:?}"),
}
}
#[test]
fn agent_idle_timeout_staleness_produces_distinct_idle_reason() {
let base = Instant::now();
let absolute = base + Duration::from_secs(3600); let mut session = AgentSession::new("idle-token", 1, absolute);
let later = base + Duration::from_secs(2);
let req = AgentRequest::OpenVault {
profile: "default".into(),
session_token: "idle-token".into(),
requesting_pid: 8,
};
let outcome = session.handle_request(&req, Some(8), "vault-password", later);
assert!(outcome.stop);
assert_eq!(outcome.state, AgentSessionState::Expired);
match &outcome.response {
AgentResponse::Err { reason } => {
assert!(
reason.contains("idle"),
"idle-TTL expiry must mention 'idle': {reason}"
);
}
other => panic!("expected Err, got {other:?}"),
}
}
#[test]
fn agent_live_session_within_ttl_window_is_never_falsely_expired() {
let now = Instant::now();
let absolute = now + Duration::from_secs(60);
let mut session = AgentSession::new("live-token", 60, absolute);
let req = AgentRequest::OpenVault {
profile: "default".into(),
session_token: "live-token".into(),
requesting_pid: 5,
};
let outcome = session.handle_request(&req, Some(5), "vault-password", now);
assert!(!outcome.stop);
assert_eq!(outcome.state, AgentSessionState::Active);
match outcome.response {
AgentResponse::Password { password } => assert_eq!(password, "vault-password"),
other => panic!("expected Password, got {other:?}"),
}
}
#[test]
fn agent_request_at_exact_absolute_deadline_boundary_is_expired() {
let now = Instant::now();
let absolute = now;
let mut session = AgentSession::new("boundary-token", 300, absolute);
let req = AgentRequest::OpenVault {
profile: "default".into(),
session_token: "boundary-token".into(),
requesting_pid: 9,
};
let outcome = session.handle_request(&req, Some(9), "vault-password", now);
assert!(
outcome.stop,
"request at boundary must be treated as expired"
);
assert_eq!(outcome.state, AgentSessionState::Expired);
match outcome.response {
AgentResponse::Err { reason } => assert!(reason.contains("expired"), "{reason}"),
other => panic!("expected Err at boundary, got {other:?}"),
}
}
#[test]
fn agent_idle_refresh_cannot_extend_past_absolute_deadline() {
let base = Instant::now();
let absolute = base + Duration::from_secs(5);
let mut session = AgentSession::new("cap-token", 10_000, absolute);
let open = session.handle_request(
&AgentRequest::OpenVault {
profile: "default".into(),
session_token: "cap-token".into(),
requesting_pid: 3,
},
Some(3),
"vault-password",
base,
);
assert_eq!(open.state, AgentSessionState::Active);
let later = base + Duration::from_secs(6);
let outcome = session.handle_request(&AgentRequest::Ping, Some(3), "vault-password", later);
assert!(
outcome.stop,
"session past absolute deadline must be expired even after an idle refresh"
);
assert_eq!(
outcome.state,
AgentSessionState::Expired,
"uncapped idle refresh would incorrectly keep the session alive past absolute_deadline"
);
}
#[test]
fn agent_stale_session_token_after_restart_is_rejected() {
let now = Instant::now();
let absolute = now + Duration::from_secs(300);
let old_token = "pre-restart-token";
let mut new_session = AgentSession::new("post-restart-token", 300, absolute);
let req = AgentRequest::OpenVault {
profile: "default".into(),
session_token: old_token.into(),
requesting_pid: 100,
};
let outcome = new_session.handle_request(&req, Some(100), "vault-password", now);
assert!(!outcome.stop);
assert_eq!(outcome.state, AgentSessionState::Active);
match outcome.response {
AgentResponse::Err { reason } => {
assert!(
reason.contains("invalid session token"),
"stale token replay must mention 'invalid session token': {reason}"
);
}
other => panic!("expected Err for stale token replay, got {other:?}"),
}
}
#[test]
fn agent_new_session_token_after_restart_is_accepted() {
let now = Instant::now();
let absolute = now + Duration::from_secs(300);
let mut new_session = AgentSession::new("post-restart-token", 300, absolute);
let req = AgentRequest::OpenVault {
profile: "default".into(),
session_token: "post-restart-token".into(),
requesting_pid: 200,
};
let outcome = new_session.handle_request(&req, Some(200), "vault-password", now);
assert!(!outcome.stop);
assert_eq!(outcome.state, AgentSessionState::Active);
match outcome.response {
AgentResponse::Password { password } => assert_eq!(password, "vault-password"),
other => panic!("expected Password for fresh post-restart token, got {other:?}"),
}
}
#[test]
fn agent_locked_session_is_irrevocable_and_does_not_leak_password() {
let now = Instant::now();
let absolute = now + Duration::from_secs(300);
let mut session = AgentSession::new("lock-token", 300, absolute);
let lock_outcome = session.handle_request(
&AgentRequest::Lock {
session_token: "lock-token".into(),
},
Some(1),
"vault-password",
now,
);
assert!(lock_outcome.stop);
assert_eq!(lock_outcome.state, AgentSessionState::Locked);
for req in [
AgentRequest::Ping,
AgentRequest::OpenVault {
profile: "default".into(),
session_token: "lock-token".into(),
requesting_pid: 1,
},
] {
let outcome = session.handle_request(&req, Some(1), "vault-password", now);
assert!(outcome.stop, "locked session must always stop");
assert_eq!(outcome.state, AgentSessionState::Locked);
match &outcome.response {
AgentResponse::Err { reason } => {
assert!(
reason.contains("locked"),
"post-lock error must mention 'locked': {reason}"
);
}
AgentResponse::Password { .. } => panic!("locked session must never return password"),
other => panic!("expected Err after lock, got {other:?}"),
}
}
}
#[test]
fn agent_concurrent_requests_maintain_session_integrity() {
use std::thread;
let now = Instant::now();
let absolute = now + Duration::from_secs(3600);
let session = Arc::new(Mutex::new(AgentSession::new(
"shared-token",
3600,
absolute,
)));
let password = "shared-secret";
let thread_count = 32;
let requests_per_thread = 50;
let handles: Vec<_> = (0..thread_count)
.map(|i| {
let session = Arc::clone(&session);
thread::spawn(move || {
let pid = 1000 + i;
let mut successes = 0u32;
let mut rejections = 0u32;
for _ in 0..requests_per_thread {
let req = AgentRequest::OpenVault {
profile: "default".into(),
session_token: "shared-token".into(),
requesting_pid: pid,
};
let mut s = session.lock().expect("session mutex must not be poisoned");
let outcome = s.handle_request(&req, Some(pid), password, Instant::now());
match &outcome.response {
AgentResponse::Password { password: pw } => {
assert_eq!(pw, password, "password must be exactly the vault password");
successes += 1;
}
AgentResponse::Err { .. } => {
rejections += 1;
}
other => {
panic!("unexpected response variant from OpenVault: {other:?}");
}
}
}
(successes, rejections)
})
})
.collect();
let mut total_success = 0u32;
let mut total_rejection = 0u32;
for h in handles {
let (s, r) = h.join().expect("thread must not panic");
total_success += s;
total_rejection += r;
}
let total = total_success + total_rejection;
assert_eq!(
total,
thread_count * requests_per_thread,
"all requests must be accounted for"
);
let final_state = session
.lock()
.expect("mutex poisoned")
.state(Instant::now());
assert_eq!(
final_state,
AgentSessionState::Active,
"session must remain Active after concurrent load"
);
}
#[test]
fn agent_expired_session_stays_expired_under_concurrent_pressure() {
use std::thread;
let now = Instant::now();
let absolute = now - Duration::from_secs(1);
let session = Arc::new(Mutex::new(AgentSession::new(
"expired-shared",
300,
absolute,
)));
let handles: Vec<_> = (0..8)
.map(|i| {
let session = Arc::clone(&session);
thread::spawn(move || {
let pid = 2000 + i as u32;
let req = AgentRequest::OpenVault {
profile: "default".into(),
session_token: "expired-shared".into(),
requesting_pid: pid,
};
let mut s = session.lock().expect("mutex poisoned");
let outcome = s.handle_request(&req, Some(pid), "secret", Instant::now());
assert!(
!matches!(outcome.response, AgentResponse::Password { .. }),
"expired session must not yield password"
);
outcome.state
})
})
.collect();
for h in handles {
let state = h.join().expect("thread must not panic");
assert_eq!(
state,
AgentSessionState::Expired,
"all concurrent callers must see Expired state"
);
}
}
#[test]
fn agent_empty_session_token_is_rejected_without_panic() {
let (resp, state) = run_once(
AgentRequest::OpenVault {
profile: "default".into(),
session_token: String::new(),
requesting_pid: 1,
},
Some(1),
"real-token",
60,
60,
);
assert_eq!(state, AgentSessionState::Active);
match resp {
AgentResponse::Err { reason } => {
assert!(
reason.contains("invalid session token"),
"empty token must yield 'invalid session token': {reason}"
);
}
other => panic!("expected Err for empty token, got {other:?}"),
}
}
#[test]
fn agent_whitespace_only_session_token_is_rejected() {
let (resp, state) = run_once(
AgentRequest::OpenVault {
profile: "default".into(),
session_token: " \t\n ".into(),
requesting_pid: 2,
},
Some(2),
"real-token",
60,
60,
);
assert_eq!(state, AgentSessionState::Active);
match resp {
AgentResponse::Err { reason } => {
assert!(reason.contains("invalid session token"), "{reason}")
}
other => panic!("expected Err for whitespace token, got {other:?}"),
}
}
#[test]
fn agent_very_long_session_token_is_rejected_without_panic() {
let long_token: String = "a".repeat(65536);
let (resp, state) = run_once(
AgentRequest::OpenVault {
profile: "default".into(),
session_token: long_token,
requesting_pid: 3,
},
Some(3),
"real-token",
60,
60,
);
assert_eq!(state, AgentSessionState::Active);
match resp {
AgentResponse::Err { reason } => {
assert!(reason.contains("invalid session token"), "{reason}")
}
other => panic!("expected Err for overlong token, got {other:?}"),
}
}
#[test]
fn agent_null_byte_in_session_token_is_rejected_without_panic() {
let (resp, state) = run_once(
AgentRequest::OpenVault {
profile: "default".into(),
session_token: "tok\x00en".into(),
requesting_pid: 4,
},
Some(4),
"real-token",
60,
60,
);
assert_eq!(state, AgentSessionState::Active);
match resp {
AgentResponse::Err { reason } => {
assert!(reason.contains("invalid session token"), "{reason}")
}
other => panic!("expected Err for null-byte token, got {other:?}"),
}
}
#[test]
fn agent_nearly_matching_session_token_is_rejected() {
let real = "abcdef0123456789";
let almost = "abcdef0123456788";
let (resp, state) = run_once(
AgentRequest::OpenVault {
profile: "default".into(),
session_token: almost.into(),
requesting_pid: 5,
},
Some(5),
real,
60,
60,
);
assert_eq!(state, AgentSessionState::Active);
match resp {
AgentResponse::Err { reason } => {
assert!(reason.contains("invalid session token"), "{reason}")
}
other => panic!("expected Err for near-match token, got {other:?}"),
}
}
#[test]
fn agent_unicode_session_token_mismatch_is_rejected_without_panic() {
let (resp, state) = run_once(
AgentRequest::OpenVault {
profile: "default".into(),
session_token: "héllo-wörld-🔐".into(),
requesting_pid: 6,
},
Some(6),
"real-token",
60,
60,
);
assert_eq!(state, AgentSessionState::Active);
match resp {
AgentResponse::Err { reason } => {
assert!(reason.contains("invalid session token"), "{reason}")
}
other => panic!("expected Err for unicode token mismatch, got {other:?}"),
}
}
#[test]
fn agent_malformed_lock_token_does_not_lock_session() {
let now = Instant::now();
let absolute = now + Duration::from_secs(300);
let mut session = AgentSession::new("real-lock-token", 300, absolute);
let bad_lock = session.handle_request(
&AgentRequest::Lock {
session_token: "wrong-lock-token".into(),
},
Some(1),
"vault-password",
now,
);
assert!(!bad_lock.stop, "bad lock token must not stop the agent");
assert_eq!(bad_lock.state, AgentSessionState::Active);
match bad_lock.response {
AgentResponse::Err { reason } => {
assert!(reason.contains("invalid session token"), "{reason}");
}
other => panic!("expected Err, got {other:?}"),
}
let follow = session.handle_request(
&AgentRequest::OpenVault {
profile: "default".into(),
session_token: "real-lock-token".into(),
requesting_pid: 1,
},
Some(1),
"vault-password",
now,
);
match follow.response {
AgentResponse::Password { password } => assert_eq!(password, "vault-password"),
other => panic!("expected Password after failed lock, got {other:?}"),
}
}