#![cfg(feature = "documents")]
use std::fs;
use std::path::Path;
use std::process::Command;
use basemind::config::ConfigV1;
use basemind::scanner::{EmbedMode, ScanReport, ScanSource, scan, scan_paths};
use basemind::store::Store;
use tempfile::TempDir;
const DOC_NAMES: &[&str] = &[
"alpha.svg",
"beta.svg",
"gamma.svg",
"delta.svg",
"epsilon.svg",
"zeta.svg",
"eta.svg",
];
fn doc_body(name: &str, salt: &str) -> String {
let mut lines = String::with_capacity(4096);
for line in 0..64 {
lines.push_str(&format!(
"<text>{name} line {line}: the quick brown fox jumps over the lazy dog while {salt} watches</text>\n"
));
}
format!("<svg xmlns=\"http://www.w3.org/2000/svg\">\n{lines}</svg>\n")
}
fn fixture() -> (TempDir, ConfigV1) {
basemind::store::init_isolated_cache();
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
let _ = Command::new("git").args(["init", "-q"]).current_dir(root).status();
let salt = root.display().to_string();
for name in DOC_NAMES {
fs::write(root.join(name), doc_body(name, &salt)).expect("write fixture doc");
}
let mut cfg = ConfigV1::with_defaults();
cfg.documents.enabled = true;
cfg.documents.embed = false;
(dir, cfg)
}
fn full_scan(root: &Path, store: &mut Store, cfg: &ConfigV1) -> ScanReport {
scan(root, store, cfg, ScanSource::WorkingTree, EmbedMode::Inline).expect("scan")
}
#[test]
fn fresh_scan_extracts_every_doc_once() {
let (dir, cfg) = fixture();
let root = dir.path();
let mut store = Store::open(root, basemind::store::VIEW_WORKING).unwrap();
let report = full_scan(root, &mut store, &cfg);
assert_eq!(report.stats.docs_indexed, 7, "all 7 docs extracted on first scan");
assert_eq!(
report.stats.reused_doc_extraction, 0,
"nothing to reuse on a fresh corpus"
);
for name in DOC_NAMES {
assert!(store.lookup_doc(name).is_some(), "{name} tracked in doc_files");
}
}
#[test]
fn idempotent_rescan_refires_zero_extractions() {
let (dir, cfg) = fixture();
let root = dir.path();
let mut store = Store::open(root, basemind::store::VIEW_WORKING).unwrap();
full_scan(root, &mut store, &cfg);
let second = full_scan(root, &mut store, &cfg);
assert_eq!(
second.stats.docs_indexed, 0,
"unchanged docs must short-circuit as Unchanged, not re-extract"
);
assert_eq!(second.stats.reused_doc_extraction, 0);
}
#[test]
fn rename_churn_reuses_cached_extraction() {
let (dir, cfg) = fixture();
let root = dir.path();
let mut store = Store::open(root, basemind::store::VIEW_WORKING).unwrap();
full_scan(root, &mut store, &cfg);
fs::rename(root.join("alpha.svg"), root.join("alpha-renamed.svg")).unwrap();
let report = scan_paths(
root,
&mut store,
&cfg,
&[root.join("alpha.svg"), root.join("alpha-renamed.svg")],
EmbedMode::Inline,
)
.unwrap();
assert_eq!(report.stats.removed, 1, "old doc path purged");
assert_eq!(report.stats.docs_indexed, 1, "new doc path indexed");
assert_eq!(
report.stats.reused_doc_extraction, 1,
"rename must be served from the cached blob — zero fresh extraction (issue #44)"
);
assert!(store.lookup_doc("alpha.svg").is_none(), "old rel gone from doc_files");
assert!(
store.lookup_doc("alpha-renamed.svg").is_some(),
"new rel tracked in doc_files"
);
}
#[test]
fn mtime_only_subtree_churn_refires_nothing() {
let (dir, cfg) = fixture();
let root = dir.path();
let salt = root.display().to_string();
let mut store = Store::open(root, basemind::store::VIEW_WORKING).unwrap();
full_scan(root, &mut store, &cfg);
for name in DOC_NAMES {
let abs = root.join(name);
fs::remove_file(&abs).unwrap();
fs::write(&abs, doc_body(name, &salt)).unwrap();
}
let paths: Vec<_> = DOC_NAMES.iter().map(|n| root.join(n)).collect();
let report = scan_paths(root, &mut store, &cfg, &paths, EmbedMode::Inline).unwrap();
assert_eq!(
report.stats.docs_indexed, 0,
"identical content must short-circuit via the hash fast path"
);
assert_eq!(report.stats.removed, 0);
}
#[test]
fn repeated_churn_cycles_accumulate_zero_fresh_extractions() {
let (dir, cfg) = fixture();
let root = dir.path();
let salt = root.display().to_string();
let mut store = Store::open(root, basemind::store::VIEW_WORKING).unwrap();
full_scan(root, &mut store, &cfg);
let mut total_indexed = 0usize;
let mut total_reused = 0usize;
let mut alpha_current = "alpha.svg".to_string();
for cycle in 0..3 {
let alpha_next = if cycle % 2 == 0 {
"alpha-renamed.svg"
} else {
"alpha.svg"
};
fs::rename(root.join(&alpha_current), root.join(alpha_next)).unwrap();
let report = scan_paths(
root,
&mut store,
&cfg,
&[root.join(&alpha_current), root.join(alpha_next)],
EmbedMode::Inline,
)
.unwrap();
total_indexed += report.stats.docs_indexed;
total_reused += report.stats.reused_doc_extraction;
alpha_current = alpha_next.to_string();
for name in DOC_NAMES.iter().filter(|n| **n != "alpha.svg") {
let abs = root.join(name);
fs::remove_file(&abs).unwrap();
fs::write(&abs, doc_body(name, &salt)).unwrap();
}
let paths: Vec<_> = DOC_NAMES
.iter()
.filter(|n| **n != "alpha.svg")
.map(|n| root.join(n))
.collect();
let report = scan_paths(root, &mut store, &cfg, &paths, EmbedMode::Inline).unwrap();
total_indexed += report.stats.docs_indexed;
total_reused += report.stats.reused_doc_extraction;
}
assert_eq!(
total_indexed - total_reused,
0,
"churn cycles must accumulate ZERO fresh doc extractions (issue #44 loop)"
);
assert!(total_indexed >= 3, "each rename cycle re-indexes the bounced doc");
}