#![cfg(test)]
use crate::commands::promote::{
PROMOTION_THRESHOLD_DEFAULT, PROMOTION_THRESHOLD_ENV, fetch_candidates, handle_promote,
resolve_promotion_threshold, run_promotion, should_promote,
};
use crate::memory::crud::test_fake_embedder;
use crate::sqlite::Database;
static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn create_test_db() -> (tempfile::TempDir, std::path::PathBuf) {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("promote.db");
Database::open(&path).unwrap();
(dir, path)
}
fn seed_candidate(db: &Database, content: &str, retrieval_count: i64) -> String {
let emb = test_fake_embedder(content).unwrap();
let id = db
.insert("proj", content, &emb, None, "fact", "candidate")
.unwrap();
db.conn()
.execute(
"UPDATE memories SET retrieval_count = ? WHERE id = ?",
rusqlite::params![retrieval_count, id],
)
.unwrap();
id
}
fn status_of(db: &Database, id: &str) -> String {
db.conn()
.query_row("SELECT status FROM memories WHERE id = ?", [id], |r| {
r.get::<_, String>(0)
})
.unwrap()
}
fn total_rows(db: &Database) -> i64 {
db.conn()
.query_row("SELECT COUNT(*) FROM memories", [], |r| r.get(0))
.unwrap()
}
#[test]
fn test_should_promote_boundary_strict_greater_than_or_equal() {
let threshold = PROMOTION_THRESHOLD_DEFAULT;
assert!(!should_promote("candidate", 4, threshold));
assert!(should_promote("candidate", 5, threshold));
assert!(should_promote("candidate", 6, threshold));
assert!(!should_promote("candidate", 0, threshold));
}
#[test]
fn test_should_promote_rejects_non_candidate_status() {
assert!(!should_promote("active", 100, 5));
assert!(!should_promote("superseded", 100, 5));
assert!(!should_promote("deprecated", 100, 5));
}
#[test]
fn test_should_promote_respects_configured_threshold() {
assert!(!should_promote("candidate", 1, 2));
assert!(should_promote("candidate", 2, 2));
assert!(should_promote("candidate", 5, 2));
}
#[test]
fn test_resolve_threshold_defaults_when_unset() {
let _guard = ENV_MUTEX.lock().unwrap();
unsafe { std::env::remove_var(PROMOTION_THRESHOLD_ENV) };
assert_eq!(
resolve_promotion_threshold().unwrap(),
PROMOTION_THRESHOLD_DEFAULT
);
}
#[test]
fn test_resolve_threshold_env_override() {
let _guard = ENV_MUTEX.lock().unwrap();
unsafe { std::env::set_var(PROMOTION_THRESHOLD_ENV, "12") };
assert_eq!(resolve_promotion_threshold().unwrap(), 12);
unsafe { std::env::remove_var(PROMOTION_THRESHOLD_ENV) };
}
#[test]
fn test_resolve_threshold_rejects_non_integer() {
let _guard = ENV_MUTEX.lock().unwrap();
unsafe { std::env::set_var(PROMOTION_THRESHOLD_ENV, "abc") };
let err = resolve_promotion_threshold().unwrap_err();
let msg = err.to_string();
assert!(msg.contains(PROMOTION_THRESHOLD_ENV));
assert!(msg.contains("abc"));
unsafe { std::env::remove_var(PROMOTION_THRESHOLD_ENV) };
}
#[test]
fn test_resolve_threshold_rejects_negative() {
let _guard = ENV_MUTEX.lock().unwrap();
unsafe { std::env::set_var(PROMOTION_THRESHOLD_ENV, "-1") };
let err = resolve_promotion_threshold().unwrap_err();
assert!(err.to_string().contains(">= 0"));
unsafe { std::env::remove_var(PROMOTION_THRESHOLD_ENV) };
}
#[test]
fn test_fetch_candidates_returns_only_candidate_rows() {
let (_dir, _path) = create_test_db();
let db = Database::open(_path.as_path()).unwrap();
let _cand = seed_candidate(&db, "the candidate", 7);
let emb_a = test_fake_embedder("an active").unwrap();
let _active = db
.insert("proj", "an active", &emb_a, None, "fact", "active")
.unwrap();
let emb_s = test_fake_embedder("a superseded").unwrap();
let _sup = db
.insert("proj", "a superseded", &emb_s, None, "fact", "superseded")
.unwrap();
let emb_d = test_fake_embedder("a deprecated").unwrap();
let _dep = db
.insert("proj", "a deprecated", &emb_d, None, "fact", "deprecated")
.unwrap();
let candidates = fetch_candidates(&db, "proj").unwrap();
assert_eq!(candidates.len(), 1, "only the candidate row is fetched");
assert_eq!(candidates[0].retrieval_count, 7);
assert_eq!(candidates[0].status, "candidate");
}
#[test]
fn test_run_promotion_promotes_eligible_only() {
let (_dir, path) = create_test_db();
let db = Database::open(path.as_path()).unwrap();
let eligible = seed_candidate(&db, "eligible five", 5);
let ineligible = seed_candidate(&db, "ineligible four", 4);
let sup = {
let emb = test_fake_embedder("sup high").unwrap();
let id = db
.insert("proj", "sup high", &emb, None, "fact", "superseded")
.unwrap();
db.conn()
.execute(
"UPDATE memories SET retrieval_count = 10 WHERE id = ?",
[&id],
)
.unwrap();
id
};
let rows_before = total_rows(&db);
let promoted = run_promotion(&db, "proj").unwrap();
assert_eq!(promoted, 1, "exactly one row should promote");
assert_eq!(
status_of(&db, &eligible),
"active",
"eligible candidate promoted"
);
assert_eq!(
status_of(&db, &ineligible),
"candidate",
"candidate just under threshold stays candidate"
);
assert_eq!(
status_of(&db, &sup),
"superseded",
"superseded row is never promoted"
);
assert_eq!(total_rows(&db), rows_before, "no rows added or removed");
}
#[test]
fn test_run_promotion_zero_when_none_eligible() {
let _guard = ENV_MUTEX.lock().unwrap();
unsafe { std::env::remove_var(PROMOTION_THRESHOLD_ENV) };
let (_dir, path) = create_test_db();
let db = Database::open(path.as_path()).unwrap();
seed_candidate(&db, "low four", 4);
let emb = test_fake_embedder("already active").unwrap();
let _ = db
.insert("proj", "already active", &emb, None, "fact", "active")
.unwrap();
let promoted = run_promotion(&db, "proj").unwrap();
assert_eq!(promoted, 0);
}
#[test]
fn test_run_promotion_respects_env_threshold() {
let (_dir, path) = create_test_db();
let db = Database::open(path.as_path()).unwrap();
let promoted_one = seed_candidate(&db, "at two", 2);
let not_promoted = seed_candidate(&db, "at one", 1);
let _guard = ENV_MUTEX.lock().unwrap();
unsafe { std::env::set_var(PROMOTION_THRESHOLD_ENV, "2") };
let promoted = run_promotion(&db, "proj").unwrap();
unsafe { std::env::remove_var(PROMOTION_THRESHOLD_ENV) };
assert_eq!(promoted, 1);
assert_eq!(status_of(&db, &promoted_one), "active");
assert_eq!(status_of(&db, ¬_promoted), "candidate");
}
#[test]
fn test_run_promotion_is_idempotent() {
let (_dir, path) = create_test_db();
let db = Database::open(path.as_path()).unwrap();
let eligible = seed_candidate(&db, "eligible", 6);
let first = run_promotion(&db, "proj").unwrap();
assert_eq!(first, 1);
let second = run_promotion(&db, "proj").unwrap();
assert_eq!(second, 0);
assert_eq!(status_of(&db, &eligible), "active");
}
#[test]
fn test_handle_promote_json_and_success() {
let (_dir, path) = create_test_db();
{
let db = Database::open(path.as_path()).unwrap();
seed_candidate(&db, "handler eligible", 5);
}
let exit = handle_promote(path.as_path(), "proj", true).expect("handle_promote should succeed");
assert_eq!(exit, std::process::ExitCode::SUCCESS);
}