#![allow(
clippy::missing_docs_in_private_items,
reason = "test helpers and fixtures do not need doc comments"
)]
use super::*;
use crate::routines::{slugify, Routine};
fn make_routine(id: &str, title: &str) -> Routine {
Routine {
model: None,
id: id.to_string(),
schedule: "@daily".to_string(),
title: title.to_string(),
agent: "claude".to_string(),
prompt: "do the thing".to_string(),
goal: None,
repositories: vec![],
machines: vec![],
enabled: true,
source: "managed".to_string(),
created_at: 0,
updated_at: 0,
last_manual_trigger_at: None,
last_scheduled_trigger_at: None,
snoozed_until: None,
skip_runs: None,
power_saving: false,
tags: vec![],
ttl_secs: None,
max_runtime_secs: None,
}
}
fn scratch_dir(tag: &str) -> std::path::PathBuf {
std::env::temp_dir().join(format!("moadim-rs-{tag}-{}", uuid::Uuid::new_v4()))
}
fn with_override_home(body: impl FnOnce(&std::path::Path)) {
let home = scratch_dir("override-home");
std::fs::create_dir_all(&home).unwrap();
let previous = std::env::var_os("MOADIM_HOME_OVERRIDE");
unsafe {
std::env::set_var("MOADIM_HOME_OVERRIDE", &home);
}
body(&home);
unsafe {
match previous {
Some(value) => std::env::set_var("MOADIM_HOME_OVERRIDE", value),
None => std::env::remove_var("MOADIM_HOME_OVERRIDE"),
}
}
let _ = std::fs::remove_dir_all(&home);
}
#[test]
fn migrate_prompts_to_subfolder_from_dir_missing_dir_returns() {
let missing = scratch_dir("prompts-subfolder-missing");
migrate_prompts_to_subfolder_from_dir(&missing);
assert!(!missing.exists());
}
#[test]
fn migrate_prompts_to_subfolder_from_dir_migrates_legacy_layout() {
let dir = scratch_dir("prompts-subfolder-migrate");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("loose.txt"), "ignore me").unwrap();
let legacy = dir.join("legacy-routine");
std::fs::create_dir_all(&legacy).unwrap();
std::fs::write(legacy.join("prompt.md"), "old composed body").unwrap();
std::fs::write(
legacy.join("routine.toml"),
"title = \"Legacy\"\nschedule = \"@daily\"\nagent = \"claude\"\nprompt = \"raw prompt\"\n",
)
.unwrap();
migrate_prompts_to_subfolder_from_dir(&dir);
assert!(
!legacy.join("prompt.md").exists(),
"top-level prompt.md should be moved"
);
assert_eq!(
std::fs::read_to_string(legacy.join("prompts").join("prompt.compiled.md")).unwrap(),
"old composed body"
);
assert_eq!(
std::fs::read_to_string(legacy.join("prompts").join("prompt.pure.md")).unwrap(),
"raw prompt"
);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn migrate_prompts_to_subfolder_from_dir_skips_already_migrated() {
let dir = scratch_dir("prompts-subfolder-skip");
std::fs::create_dir_all(&dir).unwrap();
let routine = dir.join("already-migrated");
let prompts = routine.join("prompts");
std::fs::create_dir_all(&prompts).unwrap();
std::fs::write(prompts.join("prompt.compiled.md"), "compiled").unwrap();
std::fs::write(prompts.join("prompt.pure.md"), "pure").unwrap();
std::fs::write(
routine.join("routine.toml"),
"title = \"Already\"\nschedule = \"@daily\"\nagent = \"claude\"\n",
)
.unwrap();
migrate_prompts_to_subfolder_from_dir(&dir);
assert_eq!(
std::fs::read_to_string(prompts.join("prompt.compiled.md")).unwrap(),
"compiled"
);
assert_eq!(
std::fs::read_to_string(prompts.join("prompt.pure.md")).unwrap(),
"pure"
);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn migrate_prompts_to_subfolder_from_dir_defaults_missing_legacy_prompt_to_empty() {
let dir = scratch_dir("prompts-subfolder-no-legacy");
std::fs::create_dir_all(&dir).unwrap();
let routine = dir.join("no-legacy-prompt");
std::fs::create_dir_all(&routine).unwrap();
std::fs::write(
routine.join("routine.toml"),
"title = \"No Legacy\"\nschedule = \"@daily\"\nagent = \"claude\"\n",
)
.unwrap();
migrate_prompts_to_subfolder_from_dir(&dir);
assert_eq!(
std::fs::read_to_string(routine.join("prompts").join("prompt.pure.md")).unwrap(),
""
);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn migrate_prompts_to_subfolder_from_dir_skips_dir_without_routine_toml() {
let dir = scratch_dir("prompts-subfolder-no-toml");
std::fs::create_dir_all(&dir).unwrap();
let orphan = dir.join("orphan-dir");
std::fs::create_dir_all(&orphan).unwrap();
migrate_prompts_to_subfolder_from_dir(&dir);
assert!(!orphan.join("prompts").exists());
std::fs::remove_dir_all(&dir).unwrap();
}
#[cfg(unix)]
#[test]
fn migrate_prompts_to_subfolder_from_dir_logs_on_create_dir_failure() {
let dir = scratch_dir("prompts-subfolder-create-fail");
std::fs::create_dir_all(&dir).unwrap();
let routine = dir.join("blocked-routine");
std::fs::create_dir_all(&routine).unwrap();
std::fs::write(
routine.join("routine.toml"),
"title = \"Blocked\"\nschedule = \"@daily\"\nagent = \"claude\"\n",
)
.unwrap();
std::fs::write(routine.join("prompts"), "i block the prompts dir").unwrap();
migrate_prompts_to_subfolder_from_dir(&dir);
assert!(
routine.join("prompts").is_file(),
"the blocking file is left in place"
);
std::fs::remove_dir_all(&dir).unwrap();
}
#[cfg(unix)]
#[test]
fn migrate_prompts_to_subfolder_from_dir_logs_on_rename_failure() {
use std::os::unix::fs::PermissionsExt;
let dir = scratch_dir("prompts-subfolder-rename-fail");
std::fs::create_dir_all(&dir).unwrap();
let routine = dir.join("rename-fail-routine");
std::fs::create_dir_all(routine.join("prompts")).unwrap();
std::fs::write(
routine.join("routine.toml"),
"title = \"Rename Fail\"\nschedule = \"@daily\"\nagent = \"claude\"\n",
)
.unwrap();
std::fs::write(routine.join("prompt.md"), "old composed body").unwrap();
std::fs::write(routine.join("prompts").join("prompt.pure.md"), "pure").unwrap();
std::fs::set_permissions(&routine, std::fs::Permissions::from_mode(0o555)).unwrap();
migrate_prompts_to_subfolder_from_dir(&dir);
std::fs::set_permissions(&routine, std::fs::Permissions::from_mode(0o755)).unwrap();
assert!(
routine.join("prompt.md").exists(),
"the rename could not happen, so the old file remains"
);
assert!(!routine.join("prompts").join("prompt.compiled.md").exists());
std::fs::remove_dir_all(&dir).unwrap();
}
#[cfg(unix)]
#[test]
fn migrate_prompts_to_subfolder_from_dir_logs_on_pure_write_failure() {
use std::os::unix::fs::PermissionsExt;
let dir = scratch_dir("prompts-subfolder-write-fail");
std::fs::create_dir_all(&dir).unwrap();
let routine = dir.join("write-fail-routine");
let prompts = routine.join("prompts");
std::fs::create_dir_all(&prompts).unwrap();
std::fs::write(
routine.join("routine.toml"),
"title = \"Write Fail\"\nschedule = \"@daily\"\nagent = \"claude\"\nprompt = \"raw\"\n",
)
.unwrap();
std::fs::set_permissions(&prompts, std::fs::Permissions::from_mode(0o555)).unwrap();
migrate_prompts_to_subfolder_from_dir(&dir);
std::fs::set_permissions(&prompts, std::fs::Permissions::from_mode(0o755)).unwrap();
assert!(
!prompts.join("prompt.pure.md").exists(),
"the write could not happen"
);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn migrate_prompts_to_subfolder_public_wrapper_runs() {
with_override_home(|_home| {
migrate_prompts_to_subfolder();
});
}
#[test]
fn migrate_routine_dirs_from_dir_missing_dir_returns() {
let missing = scratch_dir("migrate-missing");
migrate_routine_dirs_from_dir(&missing);
assert!(!missing.exists());
}
#[test]
fn migrate_routine_dirs_from_dir_skips_non_dir_and_unparsable() {
with_override_home(|_home| {
let routines = crate::paths::routines_dir();
std::fs::create_dir_all(&routines).unwrap();
std::fs::write(routines.join("stray.txt"), "ignore me").unwrap();
let garbage = routines.join("garbage-dir");
std::fs::create_dir_all(&garbage).unwrap();
std::fs::write(garbage.join("routine.toml"), "id = \"x\"\nschedu").unwrap();
migrate_routine_dirs_from_dir(&routines);
assert!(routines.join("stray.txt").exists());
assert!(garbage.join("routine.toml").exists());
});
}
#[test]
fn migrate_routine_dirs_from_dir_skips_already_canonical_dir() {
with_override_home(|_home| {
let routines = crate::paths::routines_dir();
std::fs::create_dir_all(&routines).unwrap();
let title = "Rs Canonical Routine";
let slug = slugify(title);
write_routine(&make_routine("rs-canonical-id", title)).unwrap();
assert!(routines.join(&slug).is_dir());
migrate_routine_dirs_from_dir(&routines);
assert!(crate::paths::routine_toml_path(&slug).exists());
assert_eq!(load_routine_from_dir(&slug).unwrap().id, "rs-canonical-id");
});
}
#[test]
fn migrate_routine_dirs_from_dir_migrates_legacy_dir() {
with_override_home(|_home| {
let routines = crate::paths::routines_dir();
std::fs::create_dir_all(&routines).unwrap();
let id = "rs-inner-legacy-uuid";
let title = "Rs Inner Legacy Routine";
let slug = slugify(title);
let legacy_dir = crate::paths::routine_dir(id);
std::fs::create_dir_all(&legacy_dir).unwrap();
let toml = format!(
"id = \"{id}\"\nschedule = \"@daily\"\ntitle = \"{title}\"\nagent = \"claude\"\nprompt = \"task\"\nenabled = true\n"
);
std::fs::write(legacy_dir.join("routine.toml"), toml).unwrap();
std::fs::write(legacy_dir.join("prompt.md"), "legacy prompt").unwrap();
migrate_routine_dirs_from_dir(&routines);
assert!(!legacy_dir.exists(), "legacy UUID dir should be removed");
assert!(crate::paths::routine_toml_path(&slug).exists());
let loaded = load_routine_from_dir(&slug).unwrap();
assert_eq!(loaded.id, id, "UUID id preserved across the dir migration");
});
}
#[cfg(unix)]
#[test]
fn migrate_routine_dirs_from_dir_logs_on_remove_failure() {
use std::os::unix::fs::PermissionsExt;
with_override_home(|_home| {
let routines = crate::paths::routines_dir();
std::fs::create_dir_all(&routines).unwrap();
let id = "rs-remove-fail-uuid";
let title = "Rs Remove Fail Routine";
let slug = slugify(title);
let legacy_dir = crate::paths::routine_dir(id);
std::fs::create_dir_all(&legacy_dir).unwrap();
let toml = format!(
"id = \"{id}\"\nschedule = \"@daily\"\ntitle = \"{title}\"\nagent = \"claude\"\nprompt = \"task\"\nenabled = true\n"
);
std::fs::write(legacy_dir.join("routine.toml"), &toml).unwrap();
std::fs::write(legacy_dir.join("prompt.md"), "legacy").unwrap();
std::fs::set_permissions(&legacy_dir, std::fs::Permissions::from_mode(0o555)).unwrap();
migrate_routine_dirs_from_dir(&routines);
assert!(crate::paths::routine_toml_path(&slug).exists());
std::fs::set_permissions(&legacy_dir, std::fs::Permissions::from_mode(0o755)).unwrap();
assert!(legacy_dir.exists(), "legacy dir survives a failed removal");
});
}
#[test]
fn migrate_routine_dirs_from_dir_logs_on_write_failure() {
with_override_home(|_home| {
let routines = crate::paths::routines_dir();
std::fs::create_dir_all(&routines).unwrap();
let id = "rs-write-fail-uuid";
let title = "Rs Write Fail Routine";
let slug = slugify(title);
let legacy_dir = crate::paths::routine_dir(id);
std::fs::create_dir_all(&legacy_dir).unwrap();
let toml = format!(
"id = \"{id}\"\nschedule = \"@daily\"\ntitle = \"{title}\"\nagent = \"claude\"\nprompt = \"task\"\nenabled = true\n"
);
std::fs::write(legacy_dir.join("routine.toml"), toml).unwrap();
std::fs::write(legacy_dir.join("prompt.md"), "legacy").unwrap();
std::fs::write(routines.join(&slug), "i block the slug dir").unwrap();
migrate_routine_dirs_from_dir(&routines);
assert!(legacy_dir.exists(), "legacy dir is left when write fails");
assert!(routines.join(&slug).is_file());
});
}
#[test]
fn migrate_routine_dirs_public_wrapper_runs() {
with_override_home(|_home| {
migrate_routine_dirs();
});
}