use std::fs;
use lantern::feedback::{Feedback, get_feedback_score, record_feedback};
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)
}
#[test]
fn new_chunks_default_to_neutral_feedback_score() {
let (_root, store) = setup_store_with(&[("a.md", "Lanterns glow in the dark forest.")]);
let hits = search(&store, "lantern", SearchOptions::default()).unwrap();
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].feedback_score, 0);
}
#[test]
fn record_feedback_updates_score_and_is_visible_on_next_search() {
let (_root, store) = setup_store_with(&[("a.md", "Lanterns glow in the dark forest.")]);
let chunk_id = {
let hits = search(&store, "lantern", SearchOptions::default()).unwrap();
hits[0].chunk_id.clone()
};
assert_eq!(record_feedback(&store, &chunk_id, Feedback::Up).unwrap(), 1);
assert_eq!(record_feedback(&store, &chunk_id, Feedback::Up).unwrap(), 2);
assert_eq!(
record_feedback(&store, &chunk_id, Feedback::Down).unwrap(),
1
);
assert_eq!(
record_feedback(&store, &chunk_id, Feedback::Custom(5)).unwrap(),
6
);
assert_eq!(get_feedback_score(&store, &chunk_id).unwrap(), Some(6));
let hits = search(&store, "lantern", SearchOptions::default()).unwrap();
let hit = hits.iter().find(|h| h.chunk_id == chunk_id).unwrap();
assert_eq!(hit.feedback_score, 6);
}
#[test]
fn positive_feedback_raises_confidence_relative_to_neutral() {
let (_root, store) = setup_store_with(&[
("a.md", "Lanterns glow in the dark forest."),
("b.md", "Lanterns are useful in old mines as well."),
]);
store
.conn()
.execute(
"UPDATE chunks SET timestamp_unix = 1, last_accessed_at = NULL, access_count = 0",
[],
)
.unwrap();
let first_pass = search(&store, "lantern", SearchOptions::default()).unwrap();
let rated_id = first_pass[0].chunk_id.clone();
let other_id = first_pass[1].chunk_id.clone();
for _ in 0..2 {
record_feedback(&store, &rated_id, Feedback::Up).unwrap();
}
assert_eq!(get_feedback_score(&store, &rated_id).unwrap(), Some(2));
let after = search(&store, "lantern", SearchOptions::default()).unwrap();
let rated = after.iter().find(|h| h.chunk_id == rated_id).unwrap();
let other = after.iter().find(|h| h.chunk_id == other_id).unwrap();
assert_eq!(rated.feedback_score, 2);
assert_eq!(other.feedback_score, 0);
assert!(
rated.confidence >= other.confidence,
"feedback should not reduce a chunk's confidence"
);
}
#[test]
fn negative_feedback_lowers_confidence_relative_to_neutral() {
let (_root, store) = setup_store_with(&[
("a.md", "Lanterns glow in the dark forest."),
("b.md", "Lanterns are useful in old mines as well."),
]);
store
.conn()
.execute(
"UPDATE chunks SET timestamp_unix = 1, last_accessed_at = NULL, access_count = 0",
[],
)
.unwrap();
let first_pass = search(&store, "lantern", SearchOptions::default()).unwrap();
let rated_id = first_pass[0].chunk_id.clone();
let other_id = first_pass[1].chunk_id.clone();
for _ in 0..2 {
record_feedback(&store, &rated_id, Feedback::Down).unwrap();
}
assert_eq!(get_feedback_score(&store, &rated_id).unwrap(), Some(-2));
let after = search(&store, "lantern", SearchOptions::default()).unwrap();
let rated = after.iter().find(|h| h.chunk_id == rated_id).unwrap();
let other = after.iter().find(|h| h.chunk_id == other_id).unwrap();
assert_eq!(rated.feedback_score, -2);
assert_eq!(other.feedback_score, 0);
assert!(
rated.confidence < other.confidence,
"negative feedback should strictly lower confidence below the neutral peer \
(rated={}, other={})",
rated.confidence,
other.confidence,
);
}
#[test]
fn record_feedback_errors_for_missing_chunk() {
let (_root, store) = setup_store_with(&[("a.md", "just a seed chunk")]);
let err = record_feedback(&store, "does-not-exist", Feedback::Up).unwrap_err();
assert!(
err.to_string().contains("no chunk"),
"unexpected error: {err}"
);
assert_eq!(get_feedback_score(&store, "does-not-exist").unwrap(), None);
}