use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use std::time::Duration;
use crate::Workspace;
use crate::util::UnwrapPoison;
use anyhow::Result;
use chrono::{DateTime, Duration as ChronoDuration, Utc};
use futures_util::FutureExt;
static TEMP_ROOT: OnceLock<PathBuf> = OnceLock::new();
static LEGACY_TEMP_DIR: OnceLock<PathBuf> = OnceLock::new();
#[cfg(windows)]
const TEMP_ENV_VARS: &[&str] = &["TMP", "TEMP"];
#[cfg(not(windows))]
const TEMP_ENV_VARS: &[&str] = &["TMPDIR"];
#[must_use]
fn temp_root() -> Option<&'static Path> {
TEMP_ROOT.get().map(PathBuf::as_path)
}
#[must_use]
pub(crate) fn legacy_temp_dir() -> Option<&'static Path> {
LEGACY_TEMP_DIR.get().map(PathBuf::as_path)
}
#[must_use]
pub(crate) fn shell_tmpdir() -> String {
temp_root().map_or_else(temp_baseline, |p| p.to_string_lossy().into_owned())
}
#[cfg(unix)]
fn temp_baseline() -> String {
"/tmp".to_string()
}
#[cfg(not(unix))]
fn temp_baseline() -> String {
std::env::temp_dir().to_string_lossy().into_owned()
}
#[must_use]
pub(crate) fn shell_temp_vars() -> Vec<(String, String)> {
let value = shell_tmpdir();
TEMP_ENV_VARS
.iter()
.map(|name| ((*name).to_string(), value.clone()))
.collect()
}
pub fn init_temp_root() -> anyhow::Result<()> {
let legacy = std::env::temp_dir();
#[cfg(unix)]
let root = PathBuf::from("/tmp/mahbot");
#[cfg(not(unix))]
let root = private_temp_root(&legacy)?;
match std::fs::create_dir(&root) {
Ok(()) => {
#[cfg(unix)]
restrict_private_root(&root)?;
}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => verify_private_root(&root)?,
Err(e) => anyhow::bail!("temp root {}: create failed: {e}", root.display()),
}
let _ = TEMP_ROOT.set(root.clone());
let _ = LEGACY_TEMP_DIR.set(legacy);
unsafe {
for (name, value) in shell_temp_vars() {
std::env::set_var(name, value);
}
}
tracing::info!(root = %root.display(), "Pinned daemon temp root");
Ok(())
}
#[cfg(not(unix))]
fn private_temp_root(legacy: &Path) -> Result<PathBuf> {
anyhow::ensure!(
legacy.is_absolute(),
"OS temp dir {} is not an absolute path — refusing to derive the private temp root from it",
legacy.display()
);
Ok(legacy.join("mahbot"))
}
#[cfg(unix)]
fn restrict_private_root(root: &Path) -> Result<()> {
std::fs::set_permissions(root, std::os::unix::fs::PermissionsExt::from_mode(0o700))
.map_err(|e| anyhow::anyhow!("temp root {}: chmod 0700 failed: {e}", root.display()))
}
fn verify_private_root(root: &Path) -> Result<()> {
let meta = std::fs::symlink_metadata(root)
.map_err(|e| anyhow::anyhow!("temp root {}: stat failed: {e}", root.display()))?;
if is_link_like(&meta) {
anyhow::bail!(
"temp root {} is a link or reparse point — refusing to use it",
root.display()
);
}
if !meta.is_dir() {
anyhow::bail!(
"temp root {} exists and is not a directory — refusing to use it",
root.display()
);
}
anyhow::ensure!(
is_owned_by_current_user(root)?,
"temp root {} is not owned by the current user — refusing a squatted path",
root.display()
);
#[cfg(unix)]
self_heal_loose_mode(root, &meta)?;
Ok(())
}
#[cfg(windows)]
#[must_use]
fn is_link_like(meta: &std::fs::Metadata) -> bool {
use std::os::windows::fs::MetadataExt as _;
let reparse_point = windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT;
meta.file_attributes() & reparse_point != 0
}
#[cfg(not(windows))]
#[must_use]
fn is_link_like(meta: &std::fs::Metadata) -> bool {
meta.file_type().is_symlink()
}
#[cfg(unix)]
fn is_owned_by_current_user(root: &Path) -> Result<bool> {
use std::os::unix::fs::MetadataExt as _;
let meta = std::fs::symlink_metadata(root)
.map_err(|e| anyhow::anyhow!("temp root {}: stat failed: {e}", root.display()))?;
Ok(meta.uid() == unsafe { libc::geteuid() })
}
#[cfg(windows)]
fn is_owned_by_current_user(root: &Path) -> Result<bool> {
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::Security::{TokenOwner, TokenUser};
let owner = file_owner_sid(root)?;
let token = open_process_token()?;
let user = token_sid(token, TokenUser);
let default_owner = token_sid(token, TokenOwner);
unsafe { CloseHandle(token) };
if owner == user? {
return Ok(true);
}
Ok(default_owner.is_ok_and(|sid| sid == owner))
}
#[cfg(windows)]
fn open_process_token() -> Result<windows_sys::Win32::Foundation::HANDLE> {
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Security::TOKEN_QUERY;
use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
let mut token: HANDLE = 0;
let opened = unsafe {
OpenProcessToken(
GetCurrentProcess(),
TOKEN_QUERY,
std::ptr::addr_of_mut!(token),
)
};
if opened == 0 {
return Err(anyhow::anyhow!(
"OpenProcessToken failed: {}",
std::io::Error::last_os_error()
));
}
Ok(token)
}
#[cfg(windows)]
fn token_sid(token: windows_sys::Win32::Foundation::HANDLE, class: i32) -> Result<Vec<u8>> {
use windows_sys::Win32::Foundation::PSID;
use windows_sys::Win32::Security::GetTokenInformation;
let mut len = 0_u32;
unsafe {
GetTokenInformation(
token,
class,
std::ptr::null_mut(),
0,
std::ptr::addr_of_mut!(len),
);
}
if len == 0 {
return Err(anyhow::anyhow!(
"GetTokenInformation({class}) size query failed: {}",
std::io::Error::last_os_error()
));
}
let mut buf = vec![0_u8; len as usize];
let ok = unsafe {
GetTokenInformation(
token,
class,
buf.as_mut_ptr().cast(),
len,
std::ptr::addr_of_mut!(len),
)
};
if ok == 0 {
return Err(anyhow::anyhow!(
"GetTokenInformation({class}) failed: {}",
std::io::Error::last_os_error()
));
}
let sid = unsafe { buf.as_ptr().cast::<PSID>().read_unaligned() };
copy_sid(sid)
}
#[cfg(windows)]
fn file_owner_sid(root: &Path) -> Result<Vec<u8>> {
use windows_sys::Win32::Security::{
GetFileSecurityW, GetSecurityDescriptorOwner, OWNER_SECURITY_INFORMATION,
};
let Some(mut wide) = crate::util::wide_path(root) else {
anyhow::bail!(
"temp root {}: path contains an interior NUL",
root.display()
);
};
wide.push(0);
let mut needed = 0_u32;
unsafe {
GetFileSecurityW(
wide.as_ptr(),
OWNER_SECURITY_INFORMATION,
std::ptr::null_mut(),
0,
std::ptr::addr_of_mut!(needed),
);
}
if needed == 0 {
return Err(anyhow::anyhow!(
"GetFileSecurityW({}) size query failed: {}",
root.display(),
std::io::Error::last_os_error()
));
}
let mut descriptor = vec![0_u64; needed.div_ceil(8) as usize];
let ok = unsafe {
GetFileSecurityW(
wide.as_ptr(),
OWNER_SECURITY_INFORMATION,
descriptor.as_mut_ptr().cast(),
needed,
std::ptr::addr_of_mut!(needed),
)
};
if ok == 0 {
return Err(anyhow::anyhow!(
"GetFileSecurityW({}) failed: {}",
root.display(),
std::io::Error::last_os_error()
));
}
let mut owner = std::ptr::null_mut();
let mut defaulted = 0;
let ok = unsafe {
GetSecurityDescriptorOwner(
descriptor.as_mut_ptr().cast(),
std::ptr::addr_of_mut!(owner),
std::ptr::addr_of_mut!(defaulted),
)
};
if ok == 0 {
return Err(anyhow::anyhow!(
"GetSecurityDescriptorOwner({}) failed: {}",
root.display(),
std::io::Error::last_os_error()
));
}
copy_sid(owner)
}
#[cfg(windows)]
fn copy_sid(sid: windows_sys::Win32::Foundation::PSID) -> Result<Vec<u8>> {
use windows_sys::Win32::Security::GetLengthSid;
if sid.is_null() {
anyhow::bail!("security descriptor reports no owner SID");
}
let len = unsafe { GetLengthSid(sid) };
if len == 0 {
return Err(anyhow::anyhow!(
"GetLengthSid failed: {}",
std::io::Error::last_os_error()
));
}
Ok(unsafe { std::slice::from_raw_parts(sid.cast::<u8>(), len as usize) }.to_vec())
}
#[cfg(unix)]
fn self_heal_loose_mode(root: &Path, meta: &std::fs::Metadata) -> Result<()> {
use std::os::unix::fs::MetadataExt as _;
let mode = meta.mode() & 0o777;
if mode & 0o077 != 0 {
std::fs::set_permissions(root, std::os::unix::fs::PermissionsExt::from_mode(0o700))
.map_err(|e| {
anyhow::anyhow!(
"temp root {} has group/other permissions (mode {mode:o}) and re-chmod 0700 failed: {e}",
root.display()
)
})?;
tracing::warn!(
root = %root.display(),
mode = format_args!("{mode:o}"),
"Temp root had loose permissions — re-chmod 0700 (self-heal, path owned by self)"
);
}
Ok(())
}
struct CleanupRoot {
path: PathBuf,
private: bool,
}
#[must_use]
fn cleanup_scan_roots() -> Vec<CleanupRoot> {
let mut roots = Vec::new();
if let Some(root) = temp_root() {
roots.push(CleanupRoot {
path: root.to_path_buf(),
private: true,
});
}
if let Some(legacy) = legacy_temp_dir() {
roots.push(CleanupRoot {
path: legacy.to_path_buf(),
private: false,
});
}
#[cfg(unix)]
roots.extend([
CleanupRoot {
path: PathBuf::from("/tmp"),
private: false,
},
CleanupRoot {
path: PathBuf::from("/private/tmp"),
private: false,
},
]);
cleanup_roots_from(roots)
}
#[must_use]
fn cleanup_roots_from(candidates: Vec<CleanupRoot>) -> Vec<CleanupRoot> {
let mut roots: Vec<CleanupRoot> = Vec::new();
for candidate in candidates {
let Ok(resolved) = std::fs::canonicalize(&candidate.path) else {
continue;
};
let path = crate::util::strip_verbatim_prefix(&resolved);
if !path.is_dir() || roots.iter().any(|root| root.path == path) {
continue;
}
roots.push(CleanupRoot {
path,
private: candidate.private,
});
}
roots
}
#[must_use]
pub(crate) fn bare_mktemp_landing_root() -> PathBuf {
#[cfg(target_os = "macos")]
{
if let Some(legacy) = legacy_temp_dir() {
return legacy.to_path_buf();
}
}
std::env::temp_dir()
}
const TEMP_CLEANUP_WAKE: Duration = Duration::from_mins(30);
const GIB: u64 = 1 << 30;
const TEMP_CLEANUP_LOW_FREE_FLOOR: u64 = 10 * GIB;
const TEMP_CLEANUP_RECOVERED_FREE_FLOOR: u64 = 15 * GIB;
const TEMP_CLEANUP_LOW_FREE_PCT: u64 = 10;
const TEMP_CLEANUP_RECOVERED_FREE_PCT: u64 = 15;
pub(crate) const TEMP_CLEANUP_LAST_RUN_KV_KEY: &str = "temp_cleanup_last_run_at";
pub(crate) const TEMP_CLEANUP_MODE_KV_KEY: &str = "temp_cleanup_mode";
static LAST_DISPATCH: std::sync::Mutex<Option<DateTime<Utc>>> = std::sync::Mutex::new(None);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CleanupMode {
Daily,
Weekly,
}
impl CleanupMode {
const fn as_str(self) -> &'static str {
match self {
Self::Daily => "daily",
Self::Weekly => "weekly",
}
}
fn parse(raw: &str) -> Option<Self> {
match raw {
"daily" => Some(Self::Daily),
"weekly" => Some(Self::Weekly),
_ => None,
}
}
fn interval(self) -> ChronoDuration {
match self {
Self::Daily => ChronoDuration::days(1),
Self::Weekly => ChronoDuration::days(7),
}
}
}
fn daily_entry_threshold(capacity: u64) -> u64 {
(capacity.saturating_mul(TEMP_CLEANUP_LOW_FREE_PCT) / 100).max(TEMP_CLEANUP_LOW_FREE_FLOOR)
}
fn weekly_return_threshold(capacity: u64) -> u64 {
(capacity.saturating_mul(TEMP_CLEANUP_RECOVERED_FREE_PCT) / 100)
.max(TEMP_CLEANUP_RECOVERED_FREE_FLOOR)
}
fn mode_after_free(current: CleanupMode, free: u64, capacity: u64) -> CleanupMode {
if free < daily_entry_threshold(capacity) {
CleanupMode::Daily
} else if free > weekly_return_threshold(capacity) {
CleanupMode::Weekly
} else {
current
}
}
async fn stored_cleanup_mode(store: Option<&crate::config_db::ConfigStore>) -> CleanupMode {
let Some(store) = store else {
return CleanupMode::Weekly;
};
match store.get_kv(TEMP_CLEANUP_MODE_KV_KEY).await {
Ok(Some(raw)) => CleanupMode::parse(&raw).unwrap_or_else(|| {
tracing::warn!(value = %raw, "Unrecognized temp cleaner mode — using weekly");
CleanupMode::Weekly
}),
Ok(None) => CleanupMode::Weekly,
Err(e) => {
tracing::warn!(error = %e, "Failed to read temp cleaner mode — using weekly");
CleanupMode::Weekly
}
}
}
fn parse_last_run(raw: &str) -> Option<DateTime<Utc>> {
match crate::db::parse_utc_timestamp(raw) {
Ok(last) => Some(last),
Err(e) => {
tracing::warn!(value = %raw, error = %e, "Unparseable temp cleaner last run — treating as due");
None
}
}
}
fn temp_cleanup_due(
last_run_at: Option<DateTime<Utc>>,
mode: CleanupMode,
now: DateTime<Utc>,
) -> bool {
match last_run_at {
None => true,
Some(last) => now.signed_duration_since(last) >= mode.interval(),
}
}
pub async fn run_temp_cleanup_loop() {
loop {
if !crate::shutdown::sleep_or_shutdown_or_drain(TEMP_CLEANUP_WAKE).await {
break;
}
temp_cleanup_tick().await;
}
}
async fn temp_cleanup_tick() {
let store = crate::config_db::CONFIG_STORE.get();
let current = stored_cleanup_mode(store).await;
let mode = match crate::util::with_block_in_place(|| {
crate::util::disk::free_and_capacity(&std::env::temp_dir())
}) {
Some((free, capacity)) => mode_after_free(current, free, capacity),
None => current,
};
if mode != current
&& let Some(store) = store
{
if let Err(e) = store.set_kv(TEMP_CLEANUP_MODE_KV_KEY, mode.as_str()).await {
tracing::warn!(error = %e, "Failed to persist temp cleaner mode");
}
tracing::info!(mode = mode.as_str(), "Temp cleaner cadence mode changed");
}
let persisted = match store {
Some(store) => match store.get_kv(TEMP_CLEANUP_LAST_RUN_KV_KEY).await {
Ok(v) => v,
Err(e) => {
tracing::warn!(error = %e, "Failed to read temp cleaner last run — treating as due");
None
}
},
None => None,
};
let last_run = [
persisted.as_deref().and_then(parse_last_run),
*LAST_DISPATCH.lock().unwrap_poison(),
]
.into_iter()
.flatten()
.max();
if !temp_cleanup_due(last_run, mode, Utc::now()) {
return;
}
let roots = crate::util::with_block_in_place(cleanup_scan_roots);
if roots.is_empty() {
tracing::debug!("Temp cleaner has no existing scan root — skipping this pass");
return;
}
if let Err(e) = dispatch_temp_cleanup(&roots).await {
tracing::warn!(error = %e, "Temp cleaner dispatch failed");
}
}
const TEMP_CLEANUP_PROMPT_KEY: &str = "sanitation/temp_cleanup.md";
#[must_use]
fn render_scan_roots(roots: &[CleanupRoot]) -> String {
roots
.iter()
.enumerate()
.map(|(idx, root)| {
let role = if root.private {
"the daemon's own private temp root"
} else {
"an OS temp area the daemon and its shells use"
};
format!("{}. `{}` — {role}", idx + 1, root.path.display())
})
.collect::<Vec<_>>()
.join("\n")
}
#[must_use]
fn render_temp_cleanup_prompt(roots: &[CleanupRoot]) -> String {
let tools = crate::prompt::load_prompt(if cfg!(windows) {
"sanitation/temp_cleanup_tools_windows.md"
} else {
"sanitation/temp_cleanup_tools_unix.md"
});
crate::prompt::substitute(
&crate::prompt::load_prompt(TEMP_CLEANUP_PROMPT_KEY),
&[
("{{scan_roots}}", &render_scan_roots(roots)),
("{{platform_tools}}", tools.trim_end()),
],
)
}
const TEMP_CLEANUP_WORKSPACE_NAME: &str = "tmp";
async fn dispatch_temp_cleanup(roots: &[CleanupRoot]) -> Result<()> {
let conn = &crate::session::store().conn;
let job_id = crate::generate_id();
let ws = Workspace::ephemeral_run(TEMP_CLEANUP_WORKSPACE_NAME, &roots[0].path);
let prompt = render_temp_cleanup_prompt(roots);
crate::jobs::spawn_job(
conn,
&job_id,
&prompt,
&ws.name,
"",
"",
crate::Role::Sanitation,
&[crate::jobs::NewAgent {
agent_id: crate::research_cleanup::cleanup_agent_id(&job_id),
kind: crate::jobs::AgentKind::Sanitation,
idx: None,
task: prompt.clone(),
}],
&crate::jobs::SpawnChild::TempCleanup,
None,
)
.await
.map_err(|e| {
tracing::error!(job = %job_id, error = %e, "Failed to spawn temp cleaner job");
e
})?;
let now = Utc::now();
*LAST_DISPATCH.lock().unwrap_poison() = Some(now);
if let Some(store) = crate::config_db::CONFIG_STORE.get()
&& let Err(e) = store
.set_kv(TEMP_CLEANUP_LAST_RUN_KV_KEY, &now.to_rfc3339())
.await
{
tracing::warn!(error = %e, "Failed to record temp cleaner run start");
}
tracing::info!(job = %job_id, "Temp cleaner dispatched");
run_temp_cleanup_and_finish(&job_id, &ws, &prompt).await;
Ok(())
}
async fn run_temp_cleanup_and_finish(job_id: &str, ws: &Workspace, prompt: &str) {
let agent_id = crate::research_cleanup::cleanup_agent_id(job_id);
let run = std::panic::AssertUnwindSafe(crate::agent::run_default_agent(
&agent_id,
crate::Role::Sanitation,
ws,
prompt,
false,
None,
None,
None,
))
.catch_unwind()
.await;
match run {
Ok((agent, response)) => {
let report = response.unwrap_or_else(|| {
format!(
"Temp cleaner FAILED (job {job_id}): {}",
agent
.failure
.clone()
.unwrap_or_else(|| "no failure detail".to_string())
)
});
tracing::info!(
job = %job_id,
agent = %agent_id,
"Temp cleaner finished: {}",
crate::util::scrub_credentials(&report)
);
}
Err(payload) => tracing::error!(
job = %job_id,
agent = %agent_id,
"Temp cleaner panicked: {}",
crate::util::panic_message(&*payload)
),
}
let _ = crate::jobs::terminalize_job(&crate::session::store().conn, job_id).await;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn legacy_capture_and_pin_are_consistent() {
assert!(temp_root().is_none());
assert!(legacy_temp_dir().is_none());
let vars = shell_temp_vars();
assert_eq!(vars.len(), TEMP_ENV_VARS.len());
assert!(vars.iter().all(|(_, value)| *value == shell_tmpdir()));
#[cfg(unix)]
assert_eq!(shell_tmpdir(), "/tmp");
}
#[test]
fn verify_private_root_refuses_a_non_directory() {
let dir = tempfile::tempdir().expect("temp dir");
let file = dir.path().join("mahbot");
std::fs::write(&file, b"not a directory").expect("write file");
let err = verify_private_root(&file).expect_err("a regular file must be refused");
assert!(err.to_string().contains("not a directory"), "{err}");
}
#[cfg(unix)]
#[test]
fn verify_private_root_accepts_our_0700_directory() {
let dir = tempfile::tempdir().expect("temp dir");
let root = dir.path().join("mahbot");
std::fs::create_dir(&root).expect("create root");
std::fs::set_permissions(&root, std::os::unix::fs::PermissionsExt::from_mode(0o700))
.expect("chmod");
verify_private_root(&root).expect("our own 0700 directory is reusable");
}
#[cfg(unix)]
#[test]
fn verify_private_root_refuses_a_symlink() {
let dir = tempfile::tempdir().expect("temp dir");
let target = dir.path().join("target");
std::fs::create_dir(&target).expect("create target");
let link = dir.path().join("mahbot");
std::os::unix::fs::symlink(&target, &link).expect("symlink");
let err = verify_private_root(&link).expect_err("a symlink must be refused");
assert!(err.to_string().contains("link or reparse point"), "{err}");
}
#[cfg(unix)]
#[test]
fn verify_private_root_self_heals_a_loose_mode() {
use std::os::unix::fs::MetadataExt as _;
let dir = tempfile::tempdir().expect("temp dir");
let root = dir.path().join("mahbot");
std::fs::create_dir(&root).expect("create root");
std::fs::set_permissions(&root, std::os::unix::fs::PermissionsExt::from_mode(0o755))
.expect("chmod");
verify_private_root(&root).expect("a loose mode on our own path is self-healed");
let mode = std::fs::metadata(&root).expect("stat root").mode() & 0o777;
assert_eq!(mode, 0o700);
}
#[test]
fn prompt_renders_every_scan_root() {
let roots = vec![
CleanupRoot {
path: PathBuf::from("/tmp/mahbot"),
private: true,
},
CleanupRoot {
path: PathBuf::from("/tmp"),
private: false,
},
];
let prompt = render_temp_cleanup_prompt(&roots);
for root in &roots {
assert!(prompt.contains(&root.path.display().to_string()));
}
assert!(!prompt.contains("{{"), "unsubstituted placeholder remains");
}
#[test]
fn cleanup_roots_from_dedupes_and_drops_absent_candidates() {
let dir = tempfile::tempdir().expect("temp dir");
let root = dir.path().to_path_buf();
let candidates = |private_first: bool| {
let direct = CleanupRoot {
path: root.clone(),
private: private_first,
};
let via_dot = CleanupRoot {
path: root.join("."),
private: !private_first,
};
let absent = CleanupRoot {
path: root.join("never-created"),
private: private_first,
};
cleanup_roots_from(vec![direct, via_dot, absent])
};
let roots = candidates(true);
assert_eq!(roots.len(), 1);
assert_eq!(
roots[0].path,
crate::util::strip_verbatim_prefix(&std::fs::canonicalize(&root).expect("canonical"))
);
assert!(roots[0].private);
let roots = candidates(false);
assert_eq!(roots.len(), 1);
assert!(!roots[0].private);
let absent = std::env::temp_dir().join(format!("mahbot-absent-{}", crate::generate_id()));
let roots = cleanup_roots_from(vec![CleanupRoot {
path: absent,
private: true,
}]);
assert!(roots.is_empty());
}
#[test]
fn mode_after_free_applies_hysteresis() {
let small = 50 * GIB;
let big = 460 * GIB;
assert_eq!(daily_entry_threshold(small), 10 * GIB);
assert_eq!(weekly_return_threshold(small), 15 * GIB);
assert_eq!(daily_entry_threshold(big), 46 * GIB);
assert_eq!(weekly_return_threshold(big), 69 * GIB);
assert_eq!(
mode_after_free(CleanupMode::Weekly, 5 * GIB, small),
CleanupMode::Daily
);
assert_eq!(
mode_after_free(CleanupMode::Daily, 20 * GIB, small),
CleanupMode::Weekly
);
assert_eq!(
mode_after_free(CleanupMode::Daily, 12 * GIB, small),
CleanupMode::Daily
);
assert_eq!(
mode_after_free(CleanupMode::Weekly, 12 * GIB, small),
CleanupMode::Weekly
);
}
#[test]
fn due_uses_wall_clock_and_mode_interval() {
let now = Utc::now();
assert!(temp_cleanup_due(None, CleanupMode::Weekly, now));
let two_days_ago = now - ChronoDuration::days(2);
assert!(temp_cleanup_due(
Some(two_days_ago),
CleanupMode::Daily,
now
));
assert!(!temp_cleanup_due(
Some(two_days_ago),
CleanupMode::Weekly,
now
));
let eight_days_ago = now - ChronoDuration::days(8);
assert!(temp_cleanup_due(
Some(eight_days_ago),
CleanupMode::Weekly,
now
));
let recent = now - ChronoDuration::hours(1);
assert!(!temp_cleanup_due(Some(recent), CleanupMode::Daily, now));
}
}