use crate::brain::agent::BrainRebuild;
use crate::brain::prompt_builder::{BrainLoader, RuntimeInfo};
use crate::brain::tools::error::collapse_home;
use std::sync::{Arc, RwLock};
use std::time::{Duration, SystemTime};
use tempfile::TempDir;
fn t0() -> SystemTime {
SystemTime::UNIX_EPOCH + Duration::from_secs(1_000_000_000)
}
fn write_with_mtime(path: &std::path::Path, content: &str, mtime: SystemTime) {
std::fs::write(path, content).expect("write brain file");
std::fs::File::options()
.write(true)
.open(path)
.expect("open for mtime")
.set_modified(mtime)
.expect("set mtime");
}
#[test]
fn render_returns_seed_until_a_brain_file_changes() {
let dir = TempDir::new().unwrap();
let soul = dir.path().join("SOUL.md");
write_with_mtime(&soul, "--- SOUL.md ---\noriginal personality\n", t0());
let loader = BrainLoader::new(dir.path().to_path_buf());
let rebuild = BrainRebuild::new(
loader,
None,
true, false, "SEED-VERBATIM".to_string(),
None, );
assert_eq!(rebuild.render(), "SEED-VERBATIM");
assert_eq!(rebuild.render(), "SEED-VERBATIM", "still warm on repeat");
write_with_mtime(
&soul,
"--- SOUL.md ---\nEDITED_PERSONALITY_MARKER\n",
t0() + Duration::from_secs(60),
);
let rebuilt = rebuild.render();
assert_ne!(rebuilt, "SEED-VERBATIM", "stale seed must be dropped");
assert!(
rebuilt.contains("EDITED_PERSONALITY_MARKER"),
"rebuilt brain must reflect the on-disk edit, got:\n{rebuilt}"
);
}
#[test]
fn rebuild_is_cached_after_a_change_until_the_next_change() {
let dir = TempDir::new().unwrap();
let soul = dir.path().join("SOUL.md");
write_with_mtime(&soul, "--- SOUL.md ---\nv1\n", t0());
let loader = BrainLoader::new(dir.path().to_path_buf());
let rebuild = BrainRebuild::new(loader, None, true, false, "SEED".to_string(), None);
write_with_mtime(
&soul,
"--- SOUL.md ---\nMARKER_V2\n",
t0() + Duration::from_secs(60),
);
let first = rebuild.render();
assert!(first.contains("MARKER_V2"));
assert_eq!(rebuild.render(), first, "no change → cached render reused");
}
#[test]
fn no_rebuild_handle_falls_back_to_static_brain() {
let dir = TempDir::new().unwrap();
let loader = BrainLoader::new(dir.path().to_path_buf());
let rebuild = BrainRebuild::new(loader, None, true, false, "ONLY-SEED".to_string(), None);
assert_eq!(rebuild.render(), "ONLY-SEED");
}
#[test]
fn render_follows_working_directory_change() {
let proj_a = TempDir::new().unwrap();
std::fs::write(proj_a.path().join("AGENTS.md"), "project a rules").unwrap();
let proj_b = TempDir::new().unwrap();
std::fs::write(proj_b.path().join("CLAUDE.md"), "project b rules").unwrap();
let brain_dir = TempDir::new().unwrap();
let loader = BrainLoader::new(brain_dir.path().to_path_buf());
let cwd = Arc::new(RwLock::new(proj_a.path().to_path_buf()));
let rebuild = BrainRebuild::new(
loader,
Some(RuntimeInfo::default()),
true,
false,
"SEED".to_string(),
Some(Arc::clone(&cwd)),
);
*cwd.write().unwrap() = proj_b.path().to_path_buf();
let out = rebuild.render();
let b_header = format!(
"Project Directive Files (in {}/)",
collapse_home(proj_b.path())
);
assert!(
out.contains(&b_header),
"render must scan the new cwd (project B), got:\n{out}"
);
assert!(
!out.contains(&collapse_home(proj_a.path())),
"the old startup directory (project A) must not leak into the index"
);
}
#[test]
fn render_rebuilds_when_a_directive_file_is_added() {
let proj = TempDir::new().unwrap(); let brain_dir = TempDir::new().unwrap();
let loader = BrainLoader::new(brain_dir.path().to_path_buf());
let cwd = Arc::new(RwLock::new(proj.path().to_path_buf()));
let rebuild = BrainRebuild::new(
loader,
Some(RuntimeInfo::default()),
true,
false,
"SEED".to_string(),
Some(Arc::clone(&cwd)),
);
assert_eq!(rebuild.render(), "SEED");
std::fs::write(proj.path().join("AGENTS.md"), "freshly added rules").unwrap();
let out = rebuild.render();
assert_ne!(
out, "SEED",
"a new directive file must invalidate the cache"
);
assert!(
out.contains("Project Directive Files"),
"rebuilt brain must include the directive index, got:\n{out}"
);
}