use engram_core::migrations::{
self, FEEDBACK_QUERY_ID_KEY, FEEDBACK_QUERY_ID_TARGET, feedback_query_id_v1,
};
use engram_storage::{Database, Memory};
fn column_exists(database: &Database, table: &str, column: &str) -> bool {
let mut statement = database
.connection()
.prepare(&format!("PRAGMA table_info({table})"))
.expect("prepare table_info");
statement
.query_map([], |row| row.get::<_, String>(1))
.expect("query table_info")
.filter_map(Result::ok)
.any(|name| name == column)
}
fn read_meta(database: &Database, key: &str) -> Option<String> {
use rusqlite::OptionalExtension;
database
.connection()
.query_row(
"SELECT value FROM schema_meta WHERE key = ?1",
[key],
|row| row.get::<_, String>(0),
)
.optional()
.expect("query meta")
}
fn strip_query_id_column(database: &Database) {
database
.connection()
.execute_batch(
"DROP TABLE feedback_tracking; \
CREATE TABLE feedback_tracking ( \
memory_id TEXT NOT NULL, \
searched_at TEXT NOT NULL, \
judged BOOLEAN DEFAULT FALSE, \
judged_at TEXT, \
FOREIGN KEY (memory_id) REFERENCES memories(id) \
);",
)
.expect("rebuild legacy feedback_tracking");
}
#[test]
fn legacy_database_gains_query_id_column() {
let database = Database::in_memory().expect("in-memory db");
strip_query_id_column(&database);
assert!(
!column_exists(&database, "feedback_tracking", "query_id"),
"precondition: legacy table lacks query_id"
);
let applied = feedback_query_id_v1::run(&database).expect("migration runs");
assert!(applied, "migration must report it ran on a legacy db");
assert!(
column_exists(&database, "feedback_tracking", "query_id"),
"migration must add query_id column"
);
assert_eq!(
read_meta(&database, FEEDBACK_QUERY_ID_KEY).as_deref(),
Some(FEEDBACK_QUERY_ID_TARGET)
);
}
#[test]
fn second_run_is_idempotent_noop() {
let database = Database::in_memory().expect("in-memory db");
strip_query_id_column(&database);
let first = feedback_query_id_v1::run(&database).expect("first run");
assert!(first, "first run applies");
let second = feedback_query_id_v1::run(&database).expect("second run");
assert!(
!second,
"second run must be a no-op (meta already at target)"
);
assert_eq!(
read_meta(&database, FEEDBACK_QUERY_ID_KEY).as_deref(),
Some(FEEDBACK_QUERY_ID_TARGET)
);
}
#[test]
fn fresh_database_skips_alter_and_records_meta() {
let database = Database::in_memory().expect("in-memory db");
assert!(
column_exists(&database, "feedback_tracking", "query_id"),
"precondition: fresh schema already has query_id"
);
assert!(
read_meta(&database, FEEDBACK_QUERY_ID_KEY).is_none(),
"precondition: meta not yet written"
);
let applied = feedback_query_id_v1::run(&database).expect("migration runs on fresh db");
assert!(
applied,
"first run on a fresh db still writes meta and returns true"
);
assert!(column_exists(&database, "feedback_tracking", "query_id"));
assert_eq!(
read_meta(&database, FEEDBACK_QUERY_ID_KEY).as_deref(),
Some(FEEDBACK_QUERY_ID_TARGET)
);
}
fn seed_memory(database: &Database, id: &str) {
let memory = Memory {
id: id.to_string(),
memory_type: "decision".to_string(),
context: "context".to_string(),
action: "action".to_string(),
result: "result".to_string(),
score: 0.0,
embedding_context: None,
embedding_action: None,
embedding_result: None,
indexed: false,
tags: None,
project: None,
parent_id: None,
source_ids: None,
insight_type: None,
created_at: "2026-05-01T00:00:00Z".to_string(),
updated_at: "2026-05-01T00:00:00Z".to_string(),
used_count: 0,
last_used_at: None,
superseded_by: None,
};
database.insert_memory(&memory).expect("seed memory");
}
#[test]
fn track_search_round_trips_query_id_after_migration() {
let database = Database::in_memory().expect("in-memory db");
strip_query_id_column(&database);
seed_memory(&database, "memory-1");
let applied = feedback_query_id_v1::run(&database).expect("migration runs");
assert!(applied, "migration must report it ran on a legacy db");
database
.track_search("memory-1", "query-42", "2026-05-01T00:00:01Z")
.expect("track_search must succeed against the migrated column");
let stored: Option<String> = database
.connection()
.query_row(
"SELECT query_id FROM feedback_tracking WHERE memory_id = ?1",
["memory-1"],
|row| row.get(0),
)
.expect("read back tracked row");
assert_eq!(
stored.as_deref(),
Some("query-42"),
"the migrated query_id column must round-trip a real track_search insert"
);
}
#[test]
fn run_pending_reports_feedback_query_id_applied() {
let database = Database::in_memory().expect("in-memory db");
strip_query_id_column(&database);
let report = migrations::run_pending(&database).expect("run_pending");
assert!(
report.feedback_query_id_v1_applied,
"run_pending must apply the feedback query_id migration on a legacy db"
);
assert!(column_exists(&database, "feedback_tracking", "query_id"));
}