use std::fs;
use lantern::forget::forget;
use lantern::ingest::ingest_path;
use lantern::search::{SearchOptions, search};
use lantern::store::Store;
use tempfile::tempdir;
fn setup_store_with(files: &[(&str, &str)]) -> (tempfile::TempDir, Store) {
let root = tempdir().unwrap();
let mut store = Store::initialize(&root.path().join("store")).unwrap();
let data = root.path().join("data");
fs::create_dir_all(&data).unwrap();
for (name, body) in files {
fs::write(data.join(name), body).unwrap();
}
ingest_path(&mut store, &data).unwrap();
(root, store)
}
fn counts(store: &Store) -> (i64, i64, i64) {
let conn = store.conn();
let sources: i64 = conn
.query_row("SELECT COUNT(*) FROM sources", [], |r| r.get(0))
.unwrap();
let chunks: i64 = conn
.query_row("SELECT COUNT(*) FROM chunks", [], |r| r.get(0))
.unwrap();
let fts: i64 = conn
.query_row("SELECT COUNT(*) FROM chunks_fts", [], |r| r.get(0))
.unwrap();
(sources, chunks, fts)
}
#[test]
fn empty_pattern_is_rejected() {
let (_root, mut store) = setup_store_with(&[("a.md", "content")]);
let err = forget(&mut store, "", true).unwrap_err();
assert!(err.to_string().contains("must not be empty"));
let err = forget(&mut store, " ", true).unwrap_err();
assert!(err.to_string().contains("must not be empty"));
}
#[test]
fn no_matches_reports_empty_and_changes_nothing() {
let (_root, mut store) = setup_store_with(&[("a.md", "content")]);
let before = counts(&store);
let report = forget(&mut store, "does-not-match", true).unwrap();
assert!(report.removed.is_empty());
assert!(report.applied);
assert_eq!(counts(&store), before);
}
#[test]
fn removes_matching_source_chunks_and_fts_entries() {
let (_root, mut store) = setup_store_with(&[
("apples.md", "apple notes with needle keyword"),
("bananas.md", "unrelated banana words"),
]);
let (src_before, chunk_before, fts_before) = counts(&store);
assert_eq!(src_before, 2);
assert!(chunk_before >= 2);
assert_eq!(chunk_before, fts_before);
let report = forget(&mut store, "apples", true).unwrap();
assert_eq!(report.removed.len(), 1);
assert!(report.removed[0].uri.ends_with("/apples.md"));
assert!(report.removed[0].chunks > 0);
let (src_after, chunk_after, fts_after) = counts(&store);
assert_eq!(src_after, 1);
assert_eq!(chunk_after, chunk_before - report.removed[0].chunks);
assert_eq!(
chunk_after, fts_after,
"fts index must stay aligned with chunks"
);
let hits = search(&store, "needle", SearchOptions::default()).unwrap();
assert!(hits.is_empty());
let hits = search(&store, "banana", SearchOptions::default()).unwrap();
assert_eq!(hits.len(), 1);
}
#[test]
fn dry_run_preserves_state_but_still_lists_candidates() {
let (_root, mut store) = setup_store_with(&[("apples.md", "one"), ("bananas.md", "two")]);
let before = counts(&store);
let report = forget(&mut store, "apples", false).unwrap();
assert!(!report.applied);
assert_eq!(report.removed.len(), 1);
assert!(report.removed[0].uri.ends_with("/apples.md"));
assert_eq!(
counts(&store),
before,
"dry-run must not modify the database"
);
}
#[test]
fn substring_pattern_matches_any_source_containing_it() {
let (_root, mut store) = setup_store_with(&[
("alpha-1.md", "first"),
("alpha-2.md", "second"),
("beta.md", "third"),
]);
let report = forget(&mut store, "alpha", true).unwrap();
assert_eq!(report.removed.len(), 2);
let (src_after, _, _) = counts(&store);
assert_eq!(src_after, 1);
}