use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Mutex, OnceLock};
use serde::{Deserialize, Serialize};
pub const MARKER_FILENAME: &str = ".claudette-mission.json";
pub const ACTIVE_POINTER_FILENAME: &str = "active_mission.json";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Mission {
pub slug: String,
pub path: PathBuf,
pub repo: Option<String>,
pub created_at: i64,
#[serde(default)]
pub ephemeral: bool,
}
fn active_slot() -> &'static Mutex<Option<Mission>> {
static SLOT: OnceLock<Mutex<Option<Mission>>> = OnceLock::new();
SLOT.get_or_init(|| Mutex::new(None))
}
#[must_use]
pub fn missions_root() -> PathBuf {
let home = std::env::var("USERPROFILE")
.or_else(|_| std::env::var("HOME"))
.unwrap_or_else(|_| ".".to_string());
PathBuf::from(home).join(".claudette").join("missions")
}
pub fn validate_slug(slug: &str) -> Result<String, String> {
let trimmed = slug.trim();
if trimmed.is_empty() {
return Err("mission: empty slug".to_string());
}
if trimmed.contains("..") {
return Err(format!("mission: slug may not contain '..' ({trimmed})"));
}
if trimmed.contains('/') || trimmed.contains('\\') {
return Err(format!(
"mission: slug must be a single directory name, not a path ({trimmed})"
));
}
if trimmed.contains(':') {
return Err(format!("mission: slug may not contain ':' ({trimmed})"));
}
Ok(trimmed.to_string())
}
#[must_use]
pub fn active_mission() -> Option<Mission> {
active_slot().lock().ok().and_then(|guard| guard.clone())
}
#[must_use]
pub fn active_cwd() -> PathBuf {
if let Some(m) = active_mission() {
return m.path;
}
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
}
pub fn set_active(mission: Mission) -> Result<(), String> {
let mut guard = active_slot()
.lock()
.map_err(|_| "mission: active slot poisoned".to_string())?;
if let Some(existing) = guard.as_ref() {
return Err(format!(
"mission: '{}' is already active — exit it first with mission_exit",
existing.slug
));
}
if !mission.ephemeral {
let _ = write_active_pointer(&mission);
}
*guard = Some(mission);
Ok(())
}
pub fn clear_active() -> Option<String> {
let mut guard = active_slot().lock().ok()?;
let taken = guard.take();
remove_active_pointer();
taken.map(|m| m.slug)
}
static BROWNFIELD_FAILED: AtomicBool = AtomicBool::new(false);
pub fn mark_brownfield_failed() {
BROWNFIELD_FAILED.store(true, Ordering::SeqCst);
}
#[must_use]
pub fn brownfield_failed_this_session() -> bool {
BROWNFIELD_FAILED.load(Ordering::SeqCst)
}
pub fn clear_brownfield_failed() {
BROWNFIELD_FAILED.store(false, Ordering::SeqCst);
}
pub fn try_bootstrap_local_mission() -> Result<Mission, String> {
let toplevel = match std::process::Command::new("git")
.args(["rev-parse", "--show-toplevel"])
.output()
{
Ok(out) if out.status.success() => {
let raw = String::from_utf8_lossy(&out.stdout).trim().to_string();
if raw.is_empty() {
return Err("not inside a git working tree (empty toplevel)".to_string());
}
PathBuf::from(raw)
}
Ok(out) => {
let stderr = String::from_utf8_lossy(&out.stderr);
return Err(format!(
"git rev-parse --show-toplevel failed: {}",
stderr.trim().chars().take(160).collect::<String>()
));
}
Err(e) => return Err(format!("git not on PATH: {e}")),
};
if !path_under_permitted_roots(&toplevel) {
return Err(format!(
"git repo at {} is outside $HOME and CLAUDETTE_WORKSPACE — \
set CLAUDETTE_WORKSPACE=\"$(pwd)\" first if you intend forge \
to operate on this tree",
toplevel.display()
));
}
let slug = toplevel
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("local")
.to_string();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.ok()
.and_then(|d| i64::try_from(d.as_secs()).ok())
.unwrap_or(0);
Ok(Mission {
slug,
path: toplevel,
repo: None,
created_at: now,
ephemeral: true,
})
}
#[must_use]
pub fn path_under_permitted_roots(path: &Path) -> bool {
let home = std::env::var("USERPROFILE")
.or_else(|_| std::env::var("HOME"))
.ok()
.map(PathBuf::from);
if let Some(home) = home {
if path.starts_with(&home) {
return true;
}
}
if let Ok(ws) = std::env::var("CLAUDETTE_WORKSPACE") {
#[cfg(unix)]
let sep = ':';
#[cfg(not(unix))]
let sep = ';';
for root in ws.split(sep).map(str::trim).filter(|s| !s.is_empty()) {
if path.starts_with(root) {
return true;
}
}
}
false
}
#[must_use]
pub fn marker_path_for(mission_path: &Path) -> PathBuf {
mission_path.join(MARKER_FILENAME)
}
#[must_use]
pub fn active_pointer_path() -> PathBuf {
let claudette_dir = missions_root()
.parent()
.map_or_else(|| PathBuf::from("."), Path::to_path_buf);
claudette_dir.join(ACTIVE_POINTER_FILENAME)
}
pub fn write_active_pointer(mission: &Mission) -> Result<(), String> {
let path = active_pointer_path();
if let Some(parent) = path.parent() {
if !parent.exists() {
std::fs::create_dir_all(parent)
.map_err(|e| format!("mission: create {} failed: {e}", parent.display()))?;
}
}
let json = serde_json::to_string_pretty(mission)
.map_err(|e| format!("mission: serialize active pointer: {e}"))?;
std::fs::write(&path, json)
.map_err(|e| format!("mission: write {} failed: {e}", path.display()))?;
Ok(())
}
pub fn remove_active_pointer() {
let _ = std::fs::remove_file(active_pointer_path());
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RehydrateOutcome {
None,
Rehydrated(Mission),
Cleared { reason: String, path: PathBuf },
}
pub fn try_rehydrate_active_mission() -> RehydrateOutcome {
let path = active_pointer_path();
if !path.exists() {
return RehydrateOutcome::None;
}
let text = match std::fs::read_to_string(&path) {
Ok(t) => t,
Err(e) => {
let _ = std::fs::remove_file(&path);
return RehydrateOutcome::Cleared {
reason: format!("read failed: {e}"),
path,
};
}
};
let mission: Mission = match serde_json::from_str(&text) {
Ok(m) => m,
Err(e) => {
let _ = std::fs::remove_file(&path);
return RehydrateOutcome::Cleared {
reason: format!("parse failed: {e}"),
path,
};
}
};
if !mission.path.is_dir() {
let _ = std::fs::remove_file(&path);
return RehydrateOutcome::Cleared {
reason: format!("mission tree {} no longer exists", mission.path.display()),
path,
};
}
if !mission.path.join(".git").exists() {
let _ = std::fs::remove_file(&path);
return RehydrateOutcome::Cleared {
reason: format!("{} is not a git working tree", mission.path.display()),
path,
};
}
match set_active(mission.clone()) {
Ok(()) => RehydrateOutcome::Rehydrated(mission),
Err(e) => {
let _ = std::fs::remove_file(&path);
RehydrateOutcome::Cleared {
reason: format!("install failed: {e}"),
path,
}
}
}
}
pub fn save_marker(mission: &Mission) -> Result<(), String> {
let path = marker_path_for(&mission.path);
let json = serde_json::to_string_pretty(mission)
.map_err(|e| format!("mission: serialize marker: {e}"))?;
std::fs::write(&path, json)
.map_err(|e| format!("mission: write {} failed: {e}", path.display()))?;
Ok(())
}
pub fn add_marker_to_git_exclude(mission_path: &Path) -> Result<(), String> {
let info_dir = mission_path.join(".git").join("info");
if !info_dir.is_dir() {
if std::fs::create_dir_all(&info_dir).is_err() {
return Ok(());
}
}
let exclude_path = info_dir.join("exclude");
let existing = std::fs::read_to_string(&exclude_path).unwrap_or_default();
if existing
.lines()
.any(|l| l.trim() == MARKER_FILENAME || l.trim() == format!("/{MARKER_FILENAME}"))
{
return Ok(());
}
let mut updated = existing;
if !updated.is_empty() && !updated.ends_with('\n') {
updated.push('\n');
}
updated.push_str(MARKER_FILENAME);
updated.push('\n');
std::fs::write(&exclude_path, updated)
.map_err(|e| format!("mission: write {} failed: {e}", exclude_path.display()))?;
Ok(())
}
pub fn load_marker(mission_path: &Path) -> Result<Mission, String> {
let path = marker_path_for(mission_path);
let text = std::fs::read_to_string(&path)
.map_err(|e| format!("mission: read {} failed: {e}", path.display()))?;
serde_json::from_str(&text).map_err(|e| format!("mission: parse {}: {e}", path.display()))
}
pub fn list_missions() -> Result<Vec<Mission>, String> {
let root = missions_root();
if !root.exists() {
return Ok(Vec::new());
}
let mut out = Vec::new();
let read = std::fs::read_dir(&root)
.map_err(|e| format!("mission: read {} failed: {e}", root.display()))?;
for entry in read.flatten() {
let p = entry.path();
if !p.is_dir() {
continue;
}
if let Ok(m) = load_marker(&p) {
out.push(m);
}
}
out.sort_by(|a, b| a.slug.cmp(&b.slug));
Ok(out)
}
pub fn list_orphan_slugs() -> Result<Vec<String>, String> {
let root = missions_root();
if !root.exists() {
return Ok(Vec::new());
}
let mut out = Vec::new();
let read = std::fs::read_dir(&root)
.map_err(|e| format!("mission: read {} failed: {e}", root.display()))?;
for entry in read.flatten() {
let p = entry.path();
if !p.is_dir() {
continue;
}
if load_marker(&p).is_err() {
if let Some(name) = p.file_name().and_then(|s| s.to_str()) {
out.push(name.to_string());
}
}
}
out.sort();
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn path_under_permitted_roots_accepts_home_subpath() {
let home = std::env::var("USERPROFILE")
.or_else(|_| std::env::var("HOME"))
.expect("HOME or USERPROFILE must be set for this test");
let candidate = PathBuf::from(&home).join("subdir").join("repo");
assert!(
path_under_permitted_roots(&candidate),
"{} should be permitted under {}",
candidate.display(),
home
);
}
#[test]
fn path_under_permitted_roots_rejects_system_dir() {
let _guard = crate::test_env_lock();
let prev_ws = std::env::var("CLAUDETTE_WORKSPACE").ok();
std::env::remove_var("CLAUDETTE_WORKSPACE");
#[cfg(unix)]
let bad = PathBuf::from("/etc/something");
#[cfg(not(unix))]
let bad = PathBuf::from("C:\\Windows\\System32");
assert!(
!path_under_permitted_roots(&bad),
"{} must NOT be permitted",
bad.display()
);
if let Some(v) = prev_ws {
std::env::set_var("CLAUDETTE_WORKSPACE", v);
}
}
#[test]
fn path_under_permitted_roots_accepts_workspace_entry() {
let _guard = crate::test_env_lock();
let prev_ws = std::env::var("CLAUDETTE_WORKSPACE").ok();
let root = std::env::temp_dir().join("claudette-workspace-root-test");
std::fs::create_dir_all(&root).unwrap();
std::env::set_var("CLAUDETTE_WORKSPACE", &root);
let candidate = root.join("nested-repo");
assert!(
path_under_permitted_roots(&candidate),
"{} should be permitted via CLAUDETTE_WORKSPACE",
candidate.display()
);
match prev_ws {
Some(v) => std::env::set_var("CLAUDETTE_WORKSPACE", v),
None => std::env::remove_var("CLAUDETTE_WORKSPACE"),
}
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn try_bootstrap_local_mission_in_this_repo_succeeds() {
match try_bootstrap_local_mission() {
Ok(m) => {
assert!(m.ephemeral, "auto-bootstrapped mission must be ephemeral");
assert!(m.path.is_dir(), "bootstrap path must be a directory");
assert!(m.path.join(".git").exists(), "must be a git repo");
assert!(
m.repo.is_none(),
"ephemeral mission has no GH repo metadata"
);
}
Err(why) => {
assert!(
!why.is_empty(),
"bootstrap error must have a non-empty reason"
);
}
}
}
#[test]
fn validate_slug_accepts_simple_name() {
assert_eq!(
validate_slug("django__issue-12345").unwrap(),
"django__issue-12345"
);
}
#[test]
fn validate_slug_trims_whitespace() {
assert_eq!(validate_slug(" hello ").unwrap(), "hello");
}
#[test]
fn validate_slug_rejects_traversal_and_separators() {
for bad in ["..", "foo/../bar", "a/b", "a\\b", "C:\\evil", "", " "] {
assert!(validate_slug(bad).is_err(), "expected reject for `{bad}`");
}
}
#[test]
fn missions_root_under_home_or_userprofile() {
let root = missions_root();
let display = root.display().to_string();
assert!(
display.contains(".claudette") && display.ends_with("missions"),
"unexpected missions_root: {display}"
);
}
#[test]
fn marker_round_trip() {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_nanos());
let tmp = std::env::temp_dir().join(format!("claudette-mission-test-{nanos}"));
std::fs::create_dir_all(&tmp).unwrap();
let mission = Mission {
slug: "abcc-cleanup".to_string(),
path: tmp.clone(),
repo: Some("mrdushidush/agent-battle-command-center".to_string()),
created_at: 1_700_000_000,
ephemeral: false,
};
save_marker(&mission).expect("save_marker should succeed");
let loaded = load_marker(&tmp).expect("load_marker should succeed");
assert_eq!(loaded, mission);
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn load_marker_errors_on_missing_file() {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_nanos());
let tmp = std::env::temp_dir().join(format!("claudette-mission-missing-{nanos}"));
std::fs::create_dir_all(&tmp).unwrap();
let err = load_marker(&tmp).expect_err("expected error on missing marker");
assert!(err.contains("read") || err.contains("failed"), "got: {err}");
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn list_missions_skips_dirs_without_markers() {
let out = list_missions().expect("list_missions should not fail");
for m in &out {
assert!(!m.slug.is_empty(), "listed mission has empty slug");
}
}
#[test]
fn list_orphan_slugs_runs_cleanly() {
let orphans = list_orphan_slugs().expect("list_orphan_slugs should not fail");
for s in &orphans {
assert!(!s.is_empty(), "orphan with empty name");
}
let missions = list_missions().expect("list_missions should not fail");
for m in &missions {
assert!(
!orphans.contains(&m.slug),
"{} listed as both mission and orphan",
m.slug
);
}
}
#[test]
fn active_cwd_falls_back_to_process_cwd_when_no_mission() {
if active_mission().is_none() {
let cwd = active_cwd();
assert!(
cwd.is_dir(),
"active_cwd fallback {} not a directory",
cwd.display()
);
}
}
#[test]
fn marker_path_for_appends_filename() {
let p = Path::new("/tmp/foo");
assert_eq!(marker_path_for(p), p.join(MARKER_FILENAME));
}
#[test]
fn marker_filename_is_dotfile() {
assert!(MARKER_FILENAME.starts_with('.'));
}
fn unique_tmp(prefix: &str) -> PathBuf {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_nanos());
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!("{prefix}-{nanos}-{seq}"))
}
#[test]
fn add_marker_to_git_exclude_writes_into_fresh_repo() {
let tmp = unique_tmp("claudette-exclude-fresh");
std::fs::create_dir_all(tmp.join(".git").join("info")).unwrap();
add_marker_to_git_exclude(&tmp).expect("should succeed");
let body = std::fs::read_to_string(tmp.join(".git").join("info").join("exclude")).unwrap();
assert!(
body.lines().any(|l| l.trim() == MARKER_FILENAME),
"marker not written into exclude file:\n{body}"
);
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn add_marker_to_git_exclude_preserves_existing_rules() {
let tmp = unique_tmp("claudette-exclude-preserve");
let info = tmp.join(".git").join("info");
std::fs::create_dir_all(&info).unwrap();
std::fs::write(info.join("exclude"), "# user rules\n*.log\nnotes/\n").unwrap();
add_marker_to_git_exclude(&tmp).expect("should succeed");
let body = std::fs::read_to_string(info.join("exclude")).unwrap();
assert!(body.contains("*.log"), "preexisting rules dropped: {body}");
assert!(body.contains("notes/"), "preexisting rules dropped: {body}");
assert!(
body.lines().any(|l| l.trim() == MARKER_FILENAME),
"marker not appended: {body}"
);
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn add_marker_to_git_exclude_is_idempotent() {
let tmp = unique_tmp("claudette-exclude-idem");
std::fs::create_dir_all(tmp.join(".git").join("info")).unwrap();
add_marker_to_git_exclude(&tmp).unwrap();
add_marker_to_git_exclude(&tmp).unwrap();
let body = std::fs::read_to_string(tmp.join(".git").join("info").join("exclude")).unwrap();
let count = body.lines().filter(|l| l.trim() == MARKER_FILENAME).count();
assert_eq!(count, 1, "marker line written more than once:\n{body}");
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn add_marker_to_git_exclude_creates_info_dir_if_missing() {
let tmp = unique_tmp("claudette-exclude-noinfo");
std::fs::create_dir_all(tmp.join(".git")).unwrap();
add_marker_to_git_exclude(&tmp).expect("should succeed");
let body = std::fs::read_to_string(tmp.join(".git").join("info").join("exclude")).unwrap();
assert!(body.lines().any(|l| l.trim() == MARKER_FILENAME));
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn brownfield_failed_flag_round_trips() {
let _guard = crate::test_env_lock();
let prior = brownfield_failed_this_session();
clear_brownfield_failed();
assert!(!brownfield_failed_this_session());
mark_brownfield_failed();
assert!(brownfield_failed_this_session());
clear_brownfield_failed();
assert!(!brownfield_failed_this_session());
if prior {
mark_brownfield_failed();
}
}
#[test]
fn active_pointer_path_sits_under_claudette_home() {
let p = active_pointer_path();
let s = p.display().to_string();
assert!(
s.contains(".claudette") && s.ends_with(ACTIVE_POINTER_FILENAME),
"unexpected active pointer path: {s}"
);
}
fn make_fake_git_tree(prefix: &str) -> PathBuf {
let dir = unique_tmp(prefix);
std::fs::create_dir_all(dir.join(".git")).unwrap();
dir
}
fn with_temp_home<F, T>(f: F) -> T
where
F: FnOnce(&Path) -> T,
{
let _guard = crate::test_env_lock();
#[cfg(windows)]
let key = "USERPROFILE";
#[cfg(not(windows))]
let key = "HOME";
let prev = std::env::var(key).ok();
let tmp = unique_tmp("claudette-fakehome");
std::fs::create_dir_all(&tmp).unwrap();
std::env::set_var(key, &tmp);
let out = f(&tmp);
match prev {
Some(v) => std::env::set_var(key, v),
None => std::env::remove_var(key),
}
let _ = std::fs::remove_dir_all(&tmp);
out
}
#[test]
fn rehydrate_returns_none_when_pointer_missing() {
with_temp_home(|_| {
let r = try_rehydrate_active_mission();
assert_eq!(r, RehydrateOutcome::None);
});
}
#[test]
fn rehydrate_clears_pointer_when_mission_tree_missing() {
with_temp_home(|home| {
let _ = clear_active();
let phantom = home.join("missions").join("ghost-repo");
let mission = Mission {
slug: "ghost-repo".to_string(),
path: phantom,
repo: Some("octocat/ghost".to_string()),
created_at: 1,
ephemeral: false,
};
write_active_pointer(&mission).expect("write should succeed");
assert!(active_pointer_path().exists());
let r = try_rehydrate_active_mission();
match r {
RehydrateOutcome::Cleared { reason, .. } => {
assert!(
reason.contains("no longer exists"),
"unexpected reason: {reason}"
);
}
other => panic!("expected Cleared, got {other:?}"),
}
assert!(
!active_pointer_path().exists(),
"stale pointer must be removed"
);
assert!(
active_mission().is_none(),
"no mission should have been installed"
);
});
}
#[test]
fn rehydrate_clears_pointer_when_not_a_git_tree() {
with_temp_home(|home| {
let _ = clear_active();
let bare = home.join("bare-dir");
std::fs::create_dir_all(&bare).unwrap();
let mission = Mission {
slug: "bare".to_string(),
path: bare,
repo: None,
created_at: 1,
ephemeral: false,
};
write_active_pointer(&mission).expect("write should succeed");
let r = try_rehydrate_active_mission();
match r {
RehydrateOutcome::Cleared { reason, .. } => {
assert!(
reason.contains("not a git working tree"),
"unexpected reason: {reason}"
);
}
other => panic!("expected Cleared, got {other:?}"),
}
assert!(active_mission().is_none());
});
}
#[test]
fn rehydrate_installs_valid_mission_and_round_trips() {
with_temp_home(|_home| {
let _ = clear_active();
let tree = make_fake_git_tree("claudette-rehydrate-ok");
let mission = Mission {
slug: "rehydrate-ok".to_string(),
path: tree.clone(),
repo: Some("octocat/Hello-World".to_string()),
created_at: 1_700_000_000,
ephemeral: false,
};
write_active_pointer(&mission).expect("write should succeed");
let r = try_rehydrate_active_mission();
match r {
RehydrateOutcome::Rehydrated(m) => assert_eq!(m.slug, "rehydrate-ok"),
other => panic!("expected Rehydrated, got {other:?}"),
}
let active = active_mission().expect("active mission must be installed");
assert_eq!(active.slug, "rehydrate-ok");
assert_eq!(active.repo.as_deref(), Some("octocat/Hello-World"));
let _ = clear_active();
let _ = std::fs::remove_dir_all(&tree);
assert!(
!active_pointer_path().exists(),
"clear_active must remove the pointer"
);
});
}
#[test]
fn rehydrate_clears_pointer_on_malformed_json() {
with_temp_home(|_home| {
let _ = clear_active();
let path = active_pointer_path();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(&path, "{not valid json").unwrap();
let r = try_rehydrate_active_mission();
match r {
RehydrateOutcome::Cleared { reason, .. } => {
assert!(reason.contains("parse failed"), "got: {reason}");
}
other => panic!("expected Cleared, got {other:?}"),
}
assert!(!path.exists(), "malformed pointer must be removed");
});
}
#[test]
fn set_active_persists_non_ephemeral_only() {
with_temp_home(|_home| {
let _ = clear_active();
assert!(!active_pointer_path().exists());
let ephemeral = Mission {
slug: "eph".to_string(),
path: make_fake_git_tree("claudette-eph"),
repo: None,
created_at: 1,
ephemeral: true,
};
set_active(ephemeral.clone()).unwrap();
assert!(
!active_pointer_path().exists(),
"ephemeral mission must NOT write the pointer file"
);
let _ = clear_active();
let _ = std::fs::remove_dir_all(&ephemeral.path);
let persistent = Mission {
slug: "stay".to_string(),
path: make_fake_git_tree("claudette-stay"),
repo: None,
created_at: 1,
ephemeral: false,
};
set_active(persistent.clone()).unwrap();
assert!(
active_pointer_path().exists(),
"non-ephemeral mission must write the pointer file"
);
let _ = clear_active();
let _ = std::fs::remove_dir_all(&persistent.path);
assert!(
!active_pointer_path().exists(),
"clear_active must remove the pointer"
);
});
}
}