use crate::paths::AgentsHome;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Marker {
pub uuid: String,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StopAction {
Stop(String),
RemoveStale,
}
pub fn should_mark(agent_self: bool, driver_lib: bool, uuid: Option<&str>) -> Option<&str> {
if !agent_self || driver_lib {
return None;
}
let uuid = uuid?;
if is_valid_uuid(uuid) {
Some(uuid)
} else {
None
}
}
pub fn stop_decision(short: Option<String>) -> StopAction {
match short {
Some(s) => StopAction::Stop(s),
None => StopAction::RemoveStale,
}
}
fn is_valid_uuid(uuid: &str) -> bool {
!uuid.is_empty() && uuid.len() <= 64 && uuid.bytes().all(|b| b.is_ascii_hexdigit() || b == b'-')
}
pub fn write_marker(home: &AgentsHome, uuid: &str, reason: &str) -> std::io::Result<PathBuf> {
if !is_valid_uuid(uuid) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("refusing to write terminal-stop marker for invalid uuid: {uuid:?}"),
));
}
let dir = home.terminal_stop_dir();
std::fs::create_dir_all(&dir)?;
let path = dir.join(uuid);
std::fs::write(&path, reason.as_bytes())?;
Ok(path)
}
pub fn read_markers(home: &AgentsHome) -> Vec<Marker> {
read_markers_in(&home.terminal_stop_dir())
}
fn read_markers_in(dir: &Path) -> Vec<Marker> {
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return Vec::new(),
};
let mut out = Vec::new();
for entry in entries.flatten() {
let name = entry.file_name();
let uuid = match name.to_str() {
Some(s) if is_valid_uuid(s) => s.to_string(),
_ => continue,
};
let reason = std::fs::read_to_string(entry.path())
.unwrap_or_default()
.trim()
.to_string();
out.push(Marker { uuid, reason });
}
out.sort_by(|a, b| a.uuid.cmp(&b.uuid));
out
}
pub fn remove_marker(home: &AgentsHome, uuid: &str) {
if !is_valid_uuid(uuid) {
return;
}
if let Err(e) = std::fs::remove_file(home.terminal_stop_dir().join(uuid)) {
if e.kind() != std::io::ErrorKind::NotFound {
eprintln!("terminal_stop: failed to remove marker {uuid}: {e}");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const UUID: &str = "c79bce5b-1682-4a8c-97f4-d6254cf18112";
#[test]
fn marks_spawned_non_driven_session() {
assert_eq!(should_mark(true, false, Some(UUID)), Some(UUID));
}
#[test]
fn operator_terminal_not_marked() {
assert_eq!(should_mark(false, false, Some(UUID)), None);
}
#[test]
fn loop_run_driven_not_marked() {
assert_eq!(should_mark(true, true, Some(UUID)), None);
}
#[test]
fn missing_uuid_not_marked() {
assert_eq!(should_mark(true, false, None), None);
assert_eq!(should_mark(true, false, Some("")), None);
}
#[test]
fn separator_bearing_uuid_rejected() {
assert_eq!(should_mark(true, false, Some("../../etc/passwd")), None);
assert_eq!(should_mark(true, false, Some("a/b")), None);
}
#[test]
fn roster_present_stops_absent_removes() {
assert_eq!(
stop_decision(Some("c79bce5b".to_string())),
StopAction::Stop("c79bce5b".to_string())
);
assert_eq!(stop_decision(None), StopAction::RemoveStale);
}
#[test]
fn write_marker_rejects_invalid_uuid() {
let home = AgentsHome::at(std::env::temp_dir().join("fno-ts-invalid-xyz"));
assert!(write_marker(&home, "../../etc/passwd", "DonePRGreen").is_err());
}
#[test]
fn write_read_remove_roundtrip() {
let tmp = std::env::temp_dir().join(format!("fno-ts-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&tmp);
let home = AgentsHome::at(&tmp);
let p = write_marker(&home, UUID, "DonePRGreen").unwrap();
assert!(p.exists());
let markers = read_markers(&home);
assert_eq!(
markers,
vec![Marker {
uuid: UUID.to_string(),
reason: "DonePRGreen".to_string(),
}]
);
remove_marker(&home, UUID);
assert!(read_markers(&home).is_empty());
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn read_skips_non_uuid_files() {
let tmp = std::env::temp_dir().join(format!("fno-ts-skip-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&tmp);
let dir = tmp.join("terminal-stop");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("README.txt"), b"not a uuid").unwrap();
std::fs::write(dir.join(UUID), b"NoWork").unwrap();
let markers = read_markers(&AgentsHome::at(&tmp));
assert_eq!(markers.len(), 1);
assert_eq!(markers[0].uuid, UUID);
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn read_missing_dir_is_empty() {
let home = AgentsHome::at(std::env::temp_dir().join("fno-ts-nonexistent-xyz"));
assert!(read_markers(&home).is_empty());
}
}