use rag_rat_core::index::AnchorHealth;
use rag_rat_core::query::orientation::Orientation;
use rag_rat_core::query::tree::{DirTree, TreeNode};
use super::*;
fn status(latest: Option<&str>, update: bool) -> rag_rat_core::version_check::VersionStatus {
rag_rat_core::version_check::VersionStatus {
current_version: "0.5.0".to_string(),
latest_version: latest.map(str::to_string),
update_available: update,
update_command: "cargo install rag-rat --force".to_string(),
checked_at_ms: latest.map(|_| 1),
}
}
#[test]
fn version_line_nags_when_behind() {
let line = version_line(&status(Some("0.6.0"), true)).expect("a behind status renders");
assert!(line.contains("update available"), "got: {line}");
assert!(line.contains("0.5.0 → 0.6.0"), "states current → latest: {line}");
assert!(line.contains("cargo install rag-rat --force"), "states the update command: {line}");
}
#[test]
fn version_line_is_quiet_when_current() {
let line = version_line(&status(Some("0.5.0"), false)).expect("an up-to-date status renders");
assert!(line.contains("0.5.0 (latest on crates.io)"), "got: {line}");
assert!(!line.contains("update available"), "no nag when current: {line}");
}
#[test]
fn version_line_is_none_when_latest_unknown() {
assert_eq!(version_line(&status(None, false)), None, "no cached check → no line");
}
#[test]
fn version_line_marks_a_local_build_ahead_of_crates_io() {
let line = version_line(&status(Some("0.4.0"), false)).expect("ahead status renders");
assert!(line.contains("ahead of crates.io latest 0.4.0"), "got: {line}");
assert!(!line.contains("(latest on crates.io)"), "must not claim it's the latest: {line}");
}
#[test]
fn session_start_json_without_tool_fields_deserializes() {
let json =
r#"{"hook_event_name":"SessionStart","source":"startup","cwd":"/x","session_id":"s"}"#;
let input: HookInput = serde_json::from_str(json).unwrap();
assert_eq!(input.hook_event_name.as_deref(), Some("SessionStart"));
assert_eq!(input.source.as_deref(), Some("startup"));
assert_eq!(input.cwd, "/x");
assert!(input.tool_name.is_empty());
assert!(input.tool_input.is_null());
}
#[test]
fn parses_grep_tool_input() {
let json = r#"{"session_id":"s1","cwd":"/repo","hook_event_name":"PreToolUse",
"tool_name":"Grep","tool_input":{"pattern":"watcher_main","path":"crates"}}"#;
let input: HookInput = serde_json::from_str(json).unwrap();
let search = extract_search(&input).unwrap();
assert_eq!(search.pattern, "watcher_main");
assert_eq!(search.search_path.as_deref(), Some("crates"));
assert_eq!(search.source, "grep_tool");
}
#[test]
fn bash_parser_table() {
let positives = [
("rg watcher_main", "watcher_main", None),
("rg -n 'election retry' crates/", "election retry", Some("crates/")),
("grep -rn foo src", "foo", Some("src")),
("ag --rust frobnicate", "frobnicate", None),
("rg -e 'fn main' --type rust", "fn main", None),
("cd crates && rg spawn_listener", "spawn_listener", None),
("FOO=1 rg spawn_listener", "spawn_listener", None),
("rg -A 3 -B 2 needle haystack/", "needle", Some("haystack/")),
("rg foo | head", "foo", None),
(r#"rg "quoted pattern" src"#, "quoted pattern", Some("src")),
];
for (cmd, pattern, path) in positives {
let got = parse_bash_search(cmd).unwrap_or_else(|| panic!("no match for {cmd}"));
assert_eq!(got.0, pattern, "pattern for {cmd}");
assert_eq!(got.1.as_deref(), path, "path for {cmd}");
}
let negatives = [
"ls -la",
"cargo test",
"rg", "find . -name '*.rs' -exec grep foo {} \\;", "echo `rg foo`", "xargs grep foo", "groups", "git log | rg fix",
"cargo test | grep result",
"gh run view | grep -i error",
"cargo clippy | grep -E warning",
"cargo test |& grep result", "cargo clippy |& rg warning",
];
for cmd in negatives {
assert!(parse_bash_search(cmd).is_none(), "false positive for {cmd}");
}
}
#[test]
fn extract_search_routes_bash_commands() {
let json = r#"{"session_id":"s1","cwd":"/repo","hook_event_name":"PreToolUse",
"tool_name":"Bash","tool_input":{"command":"rg -n watcher_main crates/"}}"#;
let input: HookInput = serde_json::from_str(json).unwrap();
let search = extract_search(&input).unwrap();
assert_eq!(search.pattern, "watcher_main");
assert_eq!(search.source, "bash");
}
#[test]
fn extract_search_ignores_other_tools() {
let json = r#"{"session_id":"s1","cwd":"/repo","hook_event_name":"PreToolUse",
"tool_name":"Read","tool_input":{"path":"/x"}}"#;
let input: HookInput = serde_json::from_str(json).unwrap();
assert!(extract_search(&input).is_none());
}
#[allow(clippy::too_many_arguments)]
fn make_orientation(
root_title: Option<&str>,
nodes: Vec<TreeNode>,
truncated: u32,
load_bearing: Vec<(&str, u64)>,
recent: Vec<&str>,
hot: Vec<&str>,
memory_titles: Vec<&str>,
head: &str,
indexed_head: &str,
anchor: AnchorHealth,
parser_failures: u64,
) -> Orientation {
Orientation {
tree: DirTree { nodes, root_memory_title: root_title.map(str::to_string), truncated },
load_bearing: load_bearing.into_iter().map(|(p, fi)| (p.to_string(), fi)).collect(),
recent_commits: recent.into_iter().map(str::to_string).collect(),
hot_files: hot.into_iter().map(str::to_string).collect(),
active_memory_total: memory_titles.len() as u32,
active_memory_titles: memory_titles.into_iter().map(str::to_string).collect(),
head: head.to_string(),
indexed_head: indexed_head.to_string(),
anchor,
total_files: 42,
parser_failures,
}
}
fn healthy_anchor() -> AnchorHealth {
AnchorHealth { current: 3, relocated: 1, stale: 0, gone: 0 }
}
fn node(depth: u8, label: &str, path: &str, file_count: u32, title: Option<&str>) -> TreeNode {
TreeNode {
depth,
label: label.to_string(),
path: path.to_string(),
file_count,
memory_title: title.map(str::to_string),
}
}
#[test]
fn format_digest_contains_attribution_header() {
let o = make_orientation(
None,
vec![],
0,
vec![],
vec![],
vec![],
vec![],
"abc",
"abc",
healthy_anchor(),
0,
);
let s = format_digest(&o, true, true);
assert!(s.contains("▶ rag-rat repo intelligence"), "missing attribution header");
assert!(s.contains("semantic_search"), "missing tool nudge");
}
#[test]
fn format_digest_purpose_line_when_root_title_present() {
let o = make_orientation(
Some("My project — does amazing things"),
vec![],
0,
vec![],
vec![],
vec![],
vec![],
"abc",
"abc",
healthy_anchor(),
0,
);
let s = format_digest(&o, true, true);
assert!(s.contains("My project — does amazing things"), "missing purpose line");
}
#[test]
fn format_digest_no_purpose_line_when_root_title_absent() {
let o = make_orientation(
None,
vec![],
0,
vec![],
vec![],
vec![],
vec![],
"abc",
"abc",
healthy_anchor(),
0,
);
let s = format_digest(&o, true, true);
assert!(!s.contains("does amazing things"));
}
#[test]
fn format_digest_layout_indents_and_annotates_tree() {
let nodes = vec![
node(0, "src", "src", 5, None),
node(1, "actors", "src/actors", 8, Some("per-domain actors")),
node(1, "data", "src/data", 3, None),
];
let o = make_orientation(
None,
nodes,
0,
vec![],
vec![],
vec![],
vec![],
"abc",
"abc",
healthy_anchor(),
0,
);
let s = format_digest(&o, true, true);
assert!(
s.contains("LAYOUT (42 files · ‹…› = directory memory)"),
"LAYOUT header missing file count; got:\n{s}"
);
assert!(s.contains("\nsrc\n"), "depth-0 node should not be indented");
assert!(
s.contains(" actors ‹per-domain actors›"),
"depth-1 node with title missing or malformed"
);
assert!(s.contains(" data\n"), "depth-1 node without title missing");
}
#[test]
fn format_digest_truncated_note() {
let o = make_orientation(
None,
vec![node(0, "src", "src", 5, None)],
7,
vec![],
vec![],
vec![],
vec![],
"abc",
"abc",
healthy_anchor(),
0,
);
let s = format_digest(&o, true, true);
assert!(s.contains("… (+7 more)"), "missing truncated note");
}
#[test]
fn format_digest_load_bearing_fan_in() {
let o = make_orientation(
None,
vec![],
0,
vec![
("crates/rag-rat-core/src/index/mod.rs", 2286),
("crates/rag-rat-core/src/main.rs", 42),
("src/database.rs", 999),
],
vec![],
vec![],
vec![],
"abc",
"abc",
healthy_anchor(),
0,
);
let s = format_digest(&o, true, true);
assert!(s.contains("load-bearing:"), "missing load-bearing prefix");
assert!(s.contains("index/mod.rs (fan_in 2286)"), "crates path not shortened; got:\n{s}");
assert!(s.contains("main.rs (fan_in 42)"), "crates path not shortened for main.rs; got:\n{s}");
assert!(s.contains("src/database.rs (fan_in 999)"), "non-crates path changed; got:\n{s}");
}
#[test]
fn format_digest_memories_overflow_uses_true_total() {
let titles: Vec<&str> = vec!["alpha", "beta", "gamma"];
let mut o = make_orientation(
None,
vec![],
0,
vec![],
vec![],
vec![],
titles,
"abc",
"abc",
healthy_anchor(),
0,
);
o.active_memory_total = 9;
let s = format_digest(&o, true, true);
assert!(s.contains("alpha"), "first memory title missing");
assert!(s.contains("beta"), "second memory title missing");
assert!(s.contains("gamma"), "third memory title missing");
assert!(s.contains("(+6 more)"), "overflow note must use true total; got:\n{s}");
}
#[test]
fn format_digest_memories_no_overflow_when_three_or_fewer() {
let o = make_orientation(
None,
vec![],
0,
vec![],
vec![],
vec![],
vec!["alpha", "beta", "gamma"],
"abc",
"abc",
healthy_anchor(),
0,
);
let s = format_digest(&o, true, true);
assert!(s.contains("alpha · beta · gamma"), "three titles should be shown");
assert!(!s.contains("more)"), "no overflow note when ≤3 titles");
}
#[test]
fn short_path_strips_crates_prefix() {
assert_eq!(
short_path("crates/rag-rat-core/src/index/mod.rs"),
"index/mod.rs",
"three-segment crates prefix should be stripped"
);
assert_eq!(
short_path("crates/rag-rat-mcp/src/server.rs"),
"server.rs",
"single-file under src should be stripped"
);
}
#[test]
fn short_path_leaves_non_crates_paths_unchanged() {
assert_eq!(short_path("src/database.rs"), "src/database.rs");
assert_eq!(short_path("crates/only-two"), "crates/only-two");
assert_eq!(
short_path("crates/foo/not-src/bar.rs"),
"crates/foo/not-src/bar.rs",
"second segment is 'not-src', must not strip"
);
assert_eq!(short_path(""), "");
}
struct HealthCase {
live: bool,
enabled: bool,
head: &'static str,
indexed: &'static str,
expected: &'static str,
}
#[test]
fn format_digest_health_watcher_combinations() {
let cases = [
HealthCase {
live: true,
enabled: true,
head: "aaa",
indexed: "aaa",
expected: "index fresh (watcher live)",
},
HealthCase {
live: true,
enabled: true,
head: "aaa",
indexed: "bbb",
expected: "index syncing (watcher live)",
},
HealthCase {
live: false,
enabled: true,
head: "aaa",
indexed: "bbb",
expected: "index stale — start the rag-rat MCP server",
},
HealthCase {
live: false,
enabled: false,
head: "aaa",
indexed: "bbb",
expected: "watcher off; index stale — run 'rag-rat index'",
},
HealthCase {
live: false,
enabled: true,
head: "aaa",
indexed: "aaa",
expected: "index fresh",
},
];
for case in &cases {
let o = make_orientation(
None,
vec![],
0,
vec![],
vec![],
vec![],
vec![],
case.head,
case.indexed,
healthy_anchor(),
0,
);
let s = format_digest(&o, case.live, case.enabled);
assert!(
s.contains(case.expected),
"health line mismatch for live={} enabled={} head={}: expected {:?}, got:\n{}",
case.live,
case.enabled,
case.head,
case.expected,
s
);
}
}
#[test]
fn format_digest_gone_adds_doctor_nudge() {
let anchor = AnchorHealth { current: 2, relocated: 0, stale: 1, gone: 3 };
let o =
make_orientation(None, vec![], 0, vec![], vec![], vec![], vec![], "abc", "abc", anchor, 0);
let s = format_digest(&o, true, true);
assert!(s.contains("3 gone → run 'rag-rat memory doctor'"), "missing gone nudge");
}
#[test]
fn format_digest_no_doctor_nudge_when_gone_is_zero() {
let o = make_orientation(
None,
vec![],
0,
vec![],
vec![],
vec![],
vec![],
"abc",
"abc",
healthy_anchor(),
0,
);
let s = format_digest(&o, true, true);
assert!(!s.contains("memory doctor"), "unexpected doctor nudge when gone=0");
}
#[test]
fn format_digest_parser_failures_shown_when_nonzero() {
let o = make_orientation(
None,
vec![],
0,
vec![],
vec![],
vec![],
vec![],
"abc",
"abc",
healthy_anchor(),
5,
);
let s = format_digest(&o, true, true);
assert!(s.contains("parser failures: 5"), "missing parser failures note");
}