use std::path::{Path, PathBuf};
pub const MAX_MEMORY_CHARS: usize = 800;
#[must_use]
pub fn default_memory_path() -> PathBuf {
if let Ok(custom) = std::env::var("CLAUDETTE_MEMORY") {
if !custom.is_empty() {
return PathBuf::from(custom);
}
}
let home = std::env::var("USERPROFILE")
.or_else(|_| std::env::var("HOME"))
.unwrap_or_else(|_| ".".to_string());
PathBuf::from(home).join(".claudette").join("CLAUDETTE.MD")
}
#[must_use]
pub fn try_load_memory() -> Option<String> {
try_load_memory_at(&default_memory_path())
}
#[must_use]
pub fn try_load_memory_at(path: &Path) -> Option<String> {
let raw = std::fs::read_to_string(path).ok()?;
let trimmed = raw.trim();
if trimmed.is_empty() {
return None;
}
Some(cap_memory(trimmed))
}
fn cap_memory(content: &str) -> String {
let count = content.chars().count();
if count <= MAX_MEMORY_CHARS {
return content.to_string();
}
let truncated: String = content.chars().take(MAX_MEMORY_CHARS).collect();
format!("{truncated}\n…[truncated to {MAX_MEMORY_CHARS}/{count} chars]")
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn temp_memory_file(label: &str, contents: &str) -> PathBuf {
let dir = std::env::temp_dir().join("claudette-test-memory");
let _ = std::fs::create_dir_all(&dir);
let path = dir.join(format!(
"{label}-{}-{}.md",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_nanos())
));
let mut f = std::fs::File::create(&path).unwrap();
f.write_all(contents.as_bytes()).unwrap();
path
}
#[test]
fn missing_file_returns_none() {
let path = std::env::temp_dir().join("claudette-no-such-memory-xyz.md");
let _ = std::fs::remove_file(&path);
assert!(try_load_memory_at(&path).is_none());
}
#[test]
fn empty_file_returns_none() {
let path = temp_memory_file("empty", " \n \t \n");
assert!(try_load_memory_at(&path).is_none());
let _ = std::fs::remove_file(&path);
}
#[test]
fn small_file_loads_verbatim_after_trim() {
let path = temp_memory_file("small", " hello world \n");
let loaded = try_load_memory_at(&path).expect("expected Some");
assert_eq!(loaded, "hello world");
let _ = std::fs::remove_file(&path);
}
#[test]
fn oversize_file_is_truncated_with_marker() {
let big = "a".repeat(MAX_MEMORY_CHARS + 200);
let path = temp_memory_file("big", &big);
let loaded = try_load_memory_at(&path).expect("expected Some");
assert!(loaded.starts_with(&"a".repeat(MAX_MEMORY_CHARS)));
assert!(loaded.contains("[truncated"));
let _ = std::fs::remove_file(&path);
}
#[test]
fn cap_memory_under_budget_is_identity() {
let s = "small note";
assert_eq!(cap_memory(s), s);
}
#[test]
fn cap_memory_truncates_on_char_boundary_for_multibyte() {
let multi: String = "🤖".repeat(MAX_MEMORY_CHARS + 50);
let capped = cap_memory(&multi);
let robot_count = capped.chars().filter(|c| *c == '🤖').count();
assert!(
robot_count <= MAX_MEMORY_CHARS,
"kept {robot_count} robots, expected ≤{MAX_MEMORY_CHARS}"
);
assert!(capped.contains("[truncated"));
}
#[test]
fn cap_memory_exactly_at_budget_passes_through() {
let s = "x".repeat(MAX_MEMORY_CHARS);
let capped = cap_memory(&s);
assert_eq!(capped.chars().count(), MAX_MEMORY_CHARS);
assert!(!capped.contains("[truncated"));
}
}