use std::fs;
use std::path::{Path, PathBuf};
use crate::address::validate_path_segment;
use crate::error::{AtmError, AtmErrorKind};
use crate::persistence;
use crate::types::IsoTimestamp;
pub fn load_seen_watermark(
home_dir: &Path,
team: &str,
agent: &str,
) -> Result<Option<IsoTimestamp>, AtmError> {
let path = seen_state_path(home_dir, team, agent)?;
if !path.exists() {
return Ok(None);
}
let raw = fs::read_to_string(&path).map_err(|error| {
AtmError::new(
AtmErrorKind::MailboxRead,
format!("failed to read seen-state watermark: {error}"),
)
.with_recovery("Check seen-state file permissions or remove the malformed watermark file before rerunning the read command.")
.with_source(error)
})?;
let trimmed = raw.trim();
if trimmed.is_empty() {
return Ok(None);
}
let parsed = chrono::DateTime::parse_from_rfc3339(trimmed).map_err(|error| {
AtmError::new(
AtmErrorKind::Serialization,
format!("invalid seen-state watermark: {error}"),
)
.with_recovery("Remove the malformed seen-state watermark file so ATM can rebuild it on the next successful read.")
.with_source(error)
})?;
Ok(Some(parsed.with_timezone(&chrono::Utc).into()))
}
pub fn save_seen_watermark(
home_dir: &Path,
team: &str,
agent: &str,
timestamp: IsoTimestamp,
) -> Result<(), AtmError> {
let path = seen_state_path(home_dir, team, agent)?;
persistence::atomic_write_string(
&path,
×tamp.into_inner().to_rfc3339(),
AtmErrorKind::MailboxWrite,
"seen-state watermark",
"Check seen-state directory permissions and rerun the read command.",
)
}
fn seen_state_path(home_dir: &Path, team: &str, agent: &str) -> Result<PathBuf, AtmError> {
validate_path_segment(team, "team")?;
validate_path_segment(agent, "agent")?;
Ok(home_dir
.join(".claude")
.join("teams")
.join(team)
.join(".seen")
.join(agent))
}
#[cfg(test)]
mod tests {
use chrono::TimeZone;
use tempfile::TempDir;
use super::{load_seen_watermark, save_seen_watermark};
use crate::types::IsoTimestamp;
#[test]
fn load_missing_seen_state_returns_none() {
let tempdir = TempDir::new().expect("tempdir");
let loaded = load_seen_watermark(tempdir.path(), "atm-dev", "arch-ctm").expect("load");
assert!(loaded.is_none());
}
#[test]
fn save_and_load_seen_state_round_trips() {
let tempdir = TempDir::new().expect("tempdir");
let timestamp = IsoTimestamp::from_datetime(
chrono::Utc
.with_ymd_and_hms(2026, 3, 30, 0, 0, 0)
.single()
.expect("timestamp"),
);
save_seen_watermark(tempdir.path(), "atm-dev", "arch-ctm", timestamp).expect("save");
let loaded = load_seen_watermark(tempdir.path(), "atm-dev", "arch-ctm").expect("load");
assert_eq!(loaded, Some(timestamp));
}
#[test]
fn load_seen_state_rejects_invalid_team_segment() {
let tempdir = TempDir::new().expect("tempdir");
let error =
load_seen_watermark(tempdir.path(), "../evil", "arch-ctm").expect_err("invalid team");
assert!(error.is_address());
}
#[test]
fn save_seen_state_rejects_invalid_agent_segment() {
let tempdir = TempDir::new().expect("tempdir");
let timestamp = IsoTimestamp::from_datetime(
chrono::Utc
.with_ymd_and_hms(2026, 3, 30, 0, 0, 0)
.single()
.expect("timestamp"),
);
let error = save_seen_watermark(tempdir.path(), "atm-dev", "../evil", timestamp)
.expect_err("invalid agent");
assert!(error.is_address());
}
}