use crate::brain::rsi_pruned::{
MovedEntry, PrunedState, SCHEMA_VERSION, detect_removed_sections, filter_pruned_sections,
};
#[test]
fn parse_empty_returns_default_with_schema_v1() {
let state = PrunedState::parse("");
assert!(state.pruned.is_empty());
assert!(state.pruned_at.is_empty());
assert!(state.moved.is_empty());
assert_eq!(
state.schema_version, 1,
"an unversioned (empty) sidecar must parse as v1 so the next save() upgrades it"
);
}
#[test]
fn parse_basic_two_file_shape() {
let toml = "[SOUL.md]\npruned = [\"## Old Section\", \"### Old Subsection\"]\n\
pruned_at = \"2026-06-04T10:00:00Z\"\n\n\
[TOOLS.md]\npruned = [\"## Deprecated Docs\"]\n";
let state = PrunedState::parse(toml);
assert_eq!(state.pruned.len(), 2);
assert_eq!(
state.pruned.get("SOUL.md").unwrap(),
&vec!["## Old Section", "### Old Subsection"]
);
assert_eq!(
state.pruned_at.get("SOUL.md").unwrap(),
"2026-06-04T10:00:00Z"
);
assert_eq!(
state.pruned.get("TOOLS.md").unwrap(),
&vec!["## Deprecated Docs"]
);
}
#[test]
fn parse_escaped_quotes() {
let toml = "[SOUL.md]\npruned = [\"## Section with \\\"quotes\\\"\"]\n";
let state = PrunedState::parse(toml);
let headers = state.pruned.get("SOUL.md").unwrap();
assert_eq!(headers, &vec!["## Section with \"quotes\""]);
}
#[test]
fn is_pruned_lookup() {
let mut state = PrunedState::default();
state.record_pruned("SOUL.md", vec!["## Old".to_string()]);
assert!(state.is_pruned("SOUL.md", "## Old"));
assert!(!state.is_pruned("SOUL.md", "## New"));
assert!(!state.is_pruned("TOOLS.md", "## Old"));
}
#[test]
fn record_pruned_dedupes_across_calls() {
let mut state = PrunedState::default();
state.record_pruned("SOUL.md", vec!["## A".to_string(), "## B".to_string()]);
state.record_pruned("SOUL.md", vec!["## B".to_string(), "## C".to_string()]);
let headers = state.pruned.get("SOUL.md").unwrap();
assert_eq!(headers, &vec!["## A", "## B", "## C"]);
}
#[test]
fn clear_per_file() {
let mut state = PrunedState::default();
state.record_pruned("SOUL.md", vec!["## A".to_string()]);
state.record_pruned("TOOLS.md", vec!["## B".to_string()]);
state.clear(Some("SOUL.md"));
assert!(!state.pruned.contains_key("SOUL.md"));
assert!(state.pruned.contains_key("TOOLS.md"));
}
#[test]
fn clear_all() {
let mut state = PrunedState::default();
state.record_pruned("SOUL.md", vec!["## A".to_string()]);
state.record_pruned("TOOLS.md", vec!["## B".to_string()]);
state.clear(None);
assert!(state.pruned.is_empty());
}
#[test]
fn clear_also_drops_moved_entries_for_same_scope() {
let mut state = PrunedState::default();
state.record_moved(
"SOUL.md",
vec![MovedEntry {
header: "## A".to_string(),
dest: "AGENTS.md".to_string(),
}],
);
state.record_moved(
"TOOLS.md",
vec![MovedEntry {
header: "## B".to_string(),
dest: "MEMORY.md".to_string(),
}],
);
state.clear(Some("SOUL.md"));
assert!(
!state.moved.contains_key("SOUL.md"),
"per-file clear must drop moved entries too, not just pruned"
);
assert!(state.moved.contains_key("TOOLS.md"));
state.clear(None);
assert!(state.moved.is_empty(), "global clear must wipe moved too");
}
#[test]
fn detect_removed_sections_diff() {
let old = "# Title\n\n## Keep\ncontent\n\n## Remove Me\nold stuff\n\n### Also Removed\n";
let new = "# Title\n\n## Keep\ncontent\n\n## New Section\n";
let removed = detect_removed_sections(old, new);
assert_eq!(removed, vec!["## Remove Me", "### Also Removed"]);
}
#[test]
fn detect_removed_sections_no_changes() {
let content = "# Title\n\n## Keep\ncontent\n";
let removed = detect_removed_sections(content, content);
assert!(removed.is_empty());
}
#[test]
fn save_round_trip_with_tempdir() {
use std::fs;
let tmp = tempfile::TempDir::new().expect("create tempdir for sidecar roundtrip test");
let path = tmp.path().join("pruned.toml");
let mut state = PrunedState::default();
state.record_pruned("SOUL.md", vec!["## Old Section".to_string()]);
let mut content = String::from("[SOUL.md]\npruned = [");
let escaped: Vec<String> = state
.pruned
.get("SOUL.md")
.unwrap()
.iter()
.map(|h| format!("\"{}\"", h.replace('"', "\\\"")))
.collect();
content.push_str(&escaped.join(", "));
content.push_str("]\n");
fs::write(&path, &content).expect("write sidecar fixture");
let loaded = PrunedState::parse(&fs::read_to_string(&path).expect("read sidecar fixture"));
assert_eq!(
loaded.pruned.get("SOUL.md").unwrap(),
&vec!["## Old Section"]
);
}
#[test]
fn schema_version_constant_is_at_least_v2() {
const { assert!(SCHEMA_VERSION >= 2) }
}
#[test]
fn parse_recognises_schema_version_line() {
let toml = "schema_version = 2\n\n[SOUL.md]\npruned = [\"## A\"]\n";
let state = PrunedState::parse(toml);
assert_eq!(state.schema_version, 2);
assert_eq!(state.pruned.get("SOUL.md").unwrap(), &vec!["## A"]);
}
#[test]
fn parse_falls_back_to_v1_when_no_schema_version_line() {
let toml = "[SOUL.md]\npruned = [\"## A\"]\n";
let state = PrunedState::parse(toml);
assert_eq!(
state.schema_version, 1,
"no schema_version line means it's a v1 sidecar from before the field existed"
);
}
#[test]
fn parse_ignores_malformed_schema_version_value() {
let toml = "schema_version = not_a_number\n\n[SOUL.md]\npruned = [\"## A\"]\n";
let state = PrunedState::parse(toml);
assert_eq!(state.schema_version, 1);
assert_eq!(state.pruned.get("SOUL.md").unwrap(), &vec!["## A"]);
}
#[test]
fn record_moved_appends_and_dedupes() {
let mut state = PrunedState::default();
state.record_moved(
"SOUL.md",
vec![
MovedEntry {
header: "## A".to_string(),
dest: "AGENTS.md".to_string(),
},
MovedEntry {
header: "## B".to_string(),
dest: "MEMORY.md".to_string(),
},
],
);
state.record_moved(
"SOUL.md",
vec![
MovedEntry {
header: "## A".to_string(),
dest: "DIFFERENT.md".to_string(),
},
MovedEntry {
header: "## C".to_string(),
dest: "TOOLS.md".to_string(),
},
],
);
let entries = state.moved.get("SOUL.md").unwrap();
assert_eq!(entries.len(), 3, "dedup by header must keep A/B/C");
assert_eq!(entries[0].header, "## A");
assert_eq!(entries[0].dest, "AGENTS.md");
assert_eq!(entries[1].header, "## B");
assert_eq!(entries[2].header, "## C");
}
#[test]
fn record_moved_noop_on_empty_input() {
let mut state = PrunedState::default();
state.record_moved("SOUL.md", vec![]);
assert!(state.moved.is_empty());
assert!(state.pruned_at.is_empty());
}
#[test]
fn moved_destination_lookup() {
let mut state = PrunedState::default();
state.record_moved(
"SOUL.md",
vec![MovedEntry {
header: "## Identity".to_string(),
dest: "AGENTS.md".to_string(),
}],
);
assert_eq!(
state.moved_destination("SOUL.md", "## Identity"),
Some("AGENTS.md")
);
assert_eq!(state.moved_destination("SOUL.md", "## Unknown"), None);
assert_eq!(state.moved_destination("TOOLS.md", "## Identity"), None);
}
#[test]
fn filter_pruned_sections_skips_moved_source_headers() {
let mut state = PrunedState::default();
state.record_moved(
"SOUL.md",
vec![MovedEntry {
header: "## Identity".to_string(),
dest: "AGENTS.md".to_string(),
}],
);
let new_sections = "\n## Identity\nidentity body that the user moved to AGENTS\n\n## Stays\nthis one is new and must come through\n";
let filtered = filter_pruned_sections(new_sections, &state, "SOUL.md");
assert!(
!filtered.contains("## Identity"),
"moved-source header must be filtered out on the SOURCE file's sync. \
Got filtered output:\n{}",
filtered
);
assert!(
filtered.contains("## Stays"),
"non-moved sections must come through normally; got:\n{}",
filtered
);
}
#[test]
fn filter_pruned_sections_passes_through_when_no_pruned_or_moved() {
let state = PrunedState::default();
let new_sections = "\n## A\nbody a\n\n## B\nbody b\n";
let filtered = filter_pruned_sections(new_sections, &state, "SOUL.md");
assert_eq!(
filtered, new_sections,
"no pruned and no moved entries means pass-through"
);
}
#[test]
fn parse_recognises_moved_array_single_line() {
let toml = "schema_version = 2\n\n[SOUL.md]\nmoved = [[\"## A\", \"AGENTS.md\"], [\"## B\", \"MEMORY.md\"]]\n";
let state = PrunedState::parse(toml);
let entries = state.moved.get("SOUL.md").expect("SOUL.md moved entries");
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].header, "## A");
assert_eq!(entries[0].dest, "AGENTS.md");
assert_eq!(entries[1].header, "## B");
assert_eq!(entries[1].dest, "MEMORY.md");
}
#[test]
fn parse_recognises_moved_array_multi_line() {
let toml = "schema_version = 2\n\n\
[SOUL.md]\n\
moved = [\n\
[\"## A\", \"AGENTS.md\"],\n\
[\"## B\", \"MEMORY.md\"],\n\
]\n";
let state = PrunedState::parse(toml);
let entries = state.moved.get("SOUL.md").expect("SOUL.md moved entries");
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].header, "## A");
assert_eq!(entries[1].dest, "MEMORY.md");
}
#[test]
fn parse_skips_malformed_moved_pair() {
let toml = "schema_version = 2\n\n\
[SOUL.md]\n\
moved = [[\"## A\", \"AGENTS.md\", \"EXTRA\"], [\"## B\", \"MEMORY.md\"]]\n";
let state = PrunedState::parse(toml);
let entries = state.moved.get("SOUL.md").expect("SOUL.md moved entries");
assert_eq!(entries.len(), 1, "malformed pair must be dropped");
assert_eq!(entries[0].header, "## B");
}