#![allow(clippy::field_reassign_with_default)]
use std::collections::HashMap;
use super::*;
#[test]
fn test_build_empty() {
let all = HashMap::new();
let context = build_directive_context(&all, &[]);
assert!(context.is_empty());
}
#[test]
fn test_build_with_active() {
let mut all = HashMap::new();
all.insert(
"focus".into(),
Directive { name:"focus".into(), content:"stay concise\nuse 1-2 tools".into() },
);
let context = build_directive_context(&all, &["focus".into()]);
assert!(context.contains("[directives: focus]"));
assert!(context.contains("focus:\n"));
assert!(context.contains("stay concise"));
assert!(context.contains("use 1-2 tools"));
}
#[test]
fn test_build_with_active_injects_full_body_not_just_first_line() {
let mut all = HashMap::new();
all.insert(
"focus".into(),
Directive {
name:"focus".into(),
content:"# focus - stay targeted, minimal tool usage\n\n# Each turn: use at most 1-2 tools.\n\n- One \
primary action per turn\n- Prefer aphrodite_retrieve over re-reading"
.into(),
},
);
let context = build_directive_context(&all, &["focus".into()]);
assert!(
context.contains("Each turn: use at most 1-2 tools."),
"body line missing: {context}"
);
assert!(
context.contains("One primary action per turn"),
"bullet line missing: {context}"
);
assert!(!context.contains('#'), "leading # markers must be stripped: {context}");
}
#[test]
fn test_handle_action_all_actions_and_unknown() {
let mut state = crate::state::AphroditeState::default();
state
.directives
.insert("focus".into(), Directive { name:"focus".into(), content:"stay focused".into() });
state
.directives
.insert("lazy".into(), Directive { name:"lazy".into(), content:"defer work".into() });
let r = handle_action(&mut state, "list", "");
assert_eq!(r["available"], serde_json::json!(["focus", "lazy"]));
assert_eq!(r["active"], serde_json::json!([]));
let r = handle_action(&mut state, "swap", "focus");
assert_eq!(r["swapped"], "focus");
assert_eq!(state.active_directives, vec!["focus".to_string()]);
let r = handle_action(&mut state, "swap", "nonexistent");
assert!(r["error"].as_str().unwrap().contains("unknown directive"));
let r = handle_action(&mut state, "remove", "focus");
assert_eq!(r["active"], serde_json::json!([]));
let r = handle_action(&mut state, "load", "lazy");
assert_eq!(r["loaded"], "lazy");
assert_eq!(r["active"], serde_json::json!(["lazy"]));
let r = handle_action(&mut state, "load", "lazy");
assert_eq!(r["loaded"], "lazy", "re-loading must be idempotent");
let r = handle_action(&mut state, "load", "ghost");
assert!(r["error"].as_str().unwrap().contains("unknown directive"));
let r = handle_action(&mut state, "add", "focus");
assert_eq!(r["active"], serde_json::json!(["lazy", "focus"]));
let r = handle_action(&mut state, "reset", "");
assert_eq!(r["active"], serde_json::json!([]));
assert!(state.active_directives.is_empty());
let r = handle_action(&mut state, "bogus", "");
assert!(r["error"].as_str().unwrap().contains("unknown action"));
}
#[test]
fn test_one_shot_nudge_renders_once_then_purged() {
let mut s = crate::state::AphroditeState::default();
s.turn_counter = 5;
crate::flow::push_nudge(&mut s, "step back and restate the goal", 1);
s.turn_counter = 6;
let ctx6 = crate::flow::build_turn_context(&mut s, None);
assert!(ctx6.contains("[nudge:"), "one-shot nudge must render at turn 6: {ctx6}");
assert!(ctx6.contains("step back and restate the goal"));
crate::hooks::post_llm_call(&mut s);
assert_eq!(s.turn_counter, 7);
let ctx7 = crate::flow::build_turn_context(&mut s, None);
assert!(!ctx7.contains("[nudge:"), "expired nudge must be gone at turn 7: {ctx7}");
}
#[test]
fn test_nudge_cap_drops_oldest() {
let mut s = crate::state::AphroditeState::default();
for i in 0..5 {
crate::flow::push_nudge(&mut s, &format!("nudge number {i}"), 10);
}
assert_eq!(s.ephemeral_directives.len(), 4, "cap must hold at 4 stored entries");
assert!(
!s.ephemeral_directives
.iter()
.any(|e| e.inline.as_deref() == Some("nudge number 0")),
"the oldest nudge must have been dropped"
);
assert!(
s.ephemeral_directives
.iter()
.any(|e| e.inline.as_deref() == Some("nudge number 4")),
"the newest nudge must survive"
);
}
#[test]
fn test_list_shows_ephemeral_and_reset_clears_them() {
let mut s = crate::state::AphroditeState::default();
s.turn_counter = 3;
crate::flow::push_nudge(&mut s, "watch out", 2);
s.manual_directive_turn = Some(3);
let listed = handle_action(&mut s, "list", "");
let eph = listed["ephemeral"].as_array().expect("ephemeral array");
assert_eq!(eph.len(), 1);
assert_eq!(eph[0]["inline"], "watch out");
assert_eq!(eph[0]["expires_after_turn"], 5);
handle_action(&mut s, "reset", "");
assert!(s.ephemeral_directives.is_empty(), "reset must clear ephemeral nudges");
assert!(s.manual_directive_turn.is_none(), "reset must clear the manual latch");
}
#[test]
fn test_manual_mutation_sets_manual_directive_turn() {
let mut s = crate::state::AphroditeState::default();
s.turn_counter = 12;
s.directives
.insert("focus".into(), Directive { name:"focus".into(), content:"stay focused".into() });
handle_action(&mut s, "swap", "focus");
assert_eq!(s.manual_directive_turn, Some(12), "a manual swap must latch the turn");
s.manual_directive_turn = None;
handle_action(&mut s, "swap", "nope");
assert!(s.manual_directive_turn.is_none(), "a failed swap must not latch");
}
#[test]
fn test_build_with_active_caps_combined_output() {
let mut all = HashMap::new();
all.insert(
"big".into(),
Directive { name:"big".into(), content:"x".repeat(MAX_DIRECTIVE_CHARS) },
);
all.insert(
"also-big".into(),
Directive { name:"also-big".into(), content:"y".repeat(MAX_DIRECTIVE_CHARS) },
);
let context = build_directive_context(&all, &["big".into(), "also-big".into()]);
assert!(
context.len() <= MAX_COMBINED_CHARS + 10,
"combined output must respect the cap: {} chars",
context.len()
);
}
struct TempDir(std::path::PathBuf);
impl TempDir {
fn new(tag:&str) -> Self {
let path = std::env::temp_dir().join(format!(
"aphrodite-directives-test-{tag}-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&path).unwrap();
Self(path)
}
fn path(&self) -> std::path::PathBuf { self.0.clone() }
}
impl Drop for TempDir {
fn drop(&mut self) { let _ = std::fs::remove_dir_all(&self.0); }
}
#[test]
fn test_load_directives_reads_md_files_from_dir() {
let dir = TempDir::new("basic");
std::fs::write(dir.path().join("focus.md"), "# focus\nstay concise").unwrap();
std::fs::write(dir.path().join("explore.md"), "# explore\nlook around").unwrap();
let loaded = load_directives(&dir.path());
assert_eq!(loaded.len(), 2);
assert_eq!(loaded["focus"].content, "# focus\nstay concise");
assert_eq!(loaded["explore"].content, "# explore\nlook around");
}
#[test]
fn test_load_directives_skips_non_md_files() {
let dir = TempDir::new("skip-non-md");
std::fs::write(dir.path().join("focus.md"), "keep me").unwrap();
std::fs::write(dir.path().join("README.txt"), "not a directive").unwrap();
std::fs::write(dir.path().join("notes"), "no extension at all").unwrap();
let loaded = load_directives(&dir.path());
assert_eq!(loaded.len(), 1);
assert!(loaded.contains_key("focus"));
}
#[test]
fn test_load_directives_existing_empty_dir_returns_empty_map() {
let dir = TempDir::new("empty-dir");
std::fs::write(dir.path().join("README.txt"), "not a directive").unwrap();
let loaded = load_directives(&dir.path());
assert!(
loaded.is_empty(),
"an existing empty directives dir must yield an empty map, not builtins"
);
}
#[test]
fn test_load_directives_empty_md_file_loads_empty_content() {
let dir = TempDir::new("empty-md");
std::fs::write(dir.path().join("empty.md"), "").unwrap();
let loaded = load_directives(&dir.path());
assert_eq!(loaded.len(), 1, "the empty .md file must load, not builtins");
assert!(loaded.contains_key("empty"));
assert_eq!(loaded["empty"].content, "");
}
#[test]
fn test_load_directives_whitespace_only_loads_verbatim() {
let dir = TempDir::new("whitespace-md");
let ws = " \n\t\n \n";
std::fs::write(dir.path().join("ws.md"), ws).unwrap();
let loaded = load_directives(&dir.path());
assert_eq!(loaded.len(), 1);
assert_eq!(loaded["ws"].content, ws, "whitespace-only content must load verbatim");
}
#[test]
fn test_load_directives_missing_dir_returns_builtins() {
let missing = std::env::temp_dir().join("aphrodite-directives-test-does-not-exist");
let loaded = load_directives(&missing);
assert!(
!loaded.is_empty(),
"missing dir should fall back to baked-in built-in directives"
);
assert!(loaded.contains_key("focus"), "built-in directives must include 'focus'");
}
#[test]
fn test_loaded_builtins_contains_all_seven() {
let builtins = loaded_builtins();
assert_eq!(builtins.len(), 7, "should have 7 baked-in directives");
assert!(builtins.contains_key("focus"));
assert!(builtins.contains_key("foresight"));
assert!(builtins.contains_key("ccr-handling"));
assert!(builtins.contains_key("cleanup"));
assert!(builtins.contains_key("explore"));
assert!(builtins.contains_key("lazy"));
assert!(builtins.contains_key("lazy-eval"));
}
#[test]
fn test_load_directives_truncates_at_max_chars_with_ellipsis() {
let dir = TempDir::new("truncate");
let oversized = "x".repeat(MAX_DIRECTIVE_CHARS + 500);
std::fs::write(dir.path().join("huge.md"), &oversized).unwrap();
let loaded = load_directives(&dir.path());
let content = &loaded["huge"].content;
assert_eq!(content.chars().count(), MAX_DIRECTIVE_CHARS + 1);
assert!(content.ends_with('…'));
}
#[test]
fn test_load_directives_under_cap_is_not_truncated() {
let dir = TempDir::new("under-cap");
let small = "short directive body";
std::fs::write(dir.path().join("small.md"), small).unwrap();
let loaded = load_directives(&dir.path());
assert_eq!(loaded["small"].content, small);
assert!(!loaded["small"].content.ends_with('…'));
}