use crate::db::Database;
async fn db_at_version_32() -> Database {
let db = Database::connect_in_memory().await.unwrap();
db.pool
.get()
.await
.unwrap()
.interact(|conn| -> Result<(), String> {
crate::db::database::build_migrations()
.to_version(conn, 32)
.map_err(|e| e.to_string())
})
.await
.unwrap()
.unwrap();
db
}
fn assert_analytics_columns(conn: &rusqlite::Connection) {
let cols: Vec<String> = conn
.prepare("PRAGMA table_info(tool_executions)")
.unwrap()
.query_map([], |r| r.get::<_, String>(1))
.unwrap()
.filter_map(Result::ok)
.collect();
for col in ["provider", "model", "duration_ms"] {
assert!(
cols.iter().any(|c| c == col),
"tool_executions column {col} present, got: {cols:?}"
);
}
}
fn assert_analytics_tables(conn: &rusqlite::Connection) {
for table in [
"phantom_events",
"streaming_recoveries",
"brain_verify_events",
] {
let exists: i64 = conn
.query_row(
"SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = ?1",
[table],
|r| r.get(0),
)
.unwrap();
assert_eq!(exists, 1, "analytics table {table} exists");
}
}
fn assert_tool_execution_insert(conn: &rusqlite::Connection) -> rusqlite::Result<()> {
conn.execute(
"INSERT INTO tool_executions
(id, message_id, tool_name, provider, model, duration_ms)
VALUES
('heal-test-1', 'm1', 'bash', 'test-provider', 'test-model', 42)",
[],
)?;
Ok(())
}
#[tokio::test]
async fn heals_pre_existing_provider_column_at_v32() {
let db = db_at_version_32().await;
db.pool
.get()
.await
.unwrap()
.interact(|conn| {
conn.execute_batch("ALTER TABLE tool_executions ADD COLUMN provider TEXT;")?;
Ok::<(), rusqlite::Error>(())
})
.await
.unwrap()
.unwrap();
db.run_migrations().await.unwrap();
db.pool
.get()
.await
.unwrap()
.interact(|conn| {
let version: i64 = conn.pragma_query_value(None, "user_version", |r| r.get(0))?;
assert_eq!(version, Database::MIGRATION_COUNT as i64);
assert_analytics_columns(conn);
assert_analytics_tables(conn);
assert_tool_execution_insert(conn)?;
Ok::<(), rusqlite::Error>(())
})
.await
.unwrap()
.unwrap();
}
#[tokio::test]
async fn heals_partial_columns_adding_missing_duration_ms() {
let db = db_at_version_32().await;
db.pool
.get()
.await
.unwrap()
.interact(|conn| {
conn.execute_batch(
"ALTER TABLE tool_executions ADD COLUMN provider TEXT;
ALTER TABLE tool_executions ADD COLUMN model TEXT;",
)?;
Ok::<(), rusqlite::Error>(())
})
.await
.unwrap()
.unwrap();
db.run_migrations().await.unwrap();
db.pool
.get()
.await
.unwrap()
.interact(|conn| {
let version: i64 = conn.pragma_query_value(None, "user_version", |r| r.get(0))?;
assert_eq!(version, Database::MIGRATION_COUNT as i64);
assert_analytics_columns(conn);
assert_analytics_tables(conn);
assert_tool_execution_insert(conn)?;
Ok::<(), rusqlite::Error>(())
})
.await
.unwrap()
.unwrap();
}
#[tokio::test]
async fn fresh_migration_adds_analytics_schema_normally() {
let db = Database::connect_in_memory().await.unwrap();
db.run_migrations().await.unwrap();
db.pool
.get()
.await
.unwrap()
.interact(|conn| {
let version: i64 = conn.pragma_query_value(None, "user_version", |r| r.get(0))?;
assert_eq!(version, Database::MIGRATION_COUNT as i64);
assert_analytics_columns(conn);
assert_analytics_tables(conn);
assert_tool_execution_insert(conn)?;
Ok::<(), rusqlite::Error>(())
})
.await
.unwrap()
.unwrap();
}
#[tokio::test]
async fn heal_is_idempotent_across_double_run() {
let db = db_at_version_32().await;
db.pool
.get()
.await
.unwrap()
.interact(|conn| {
conn.execute_batch("ALTER TABLE tool_executions ADD COLUMN provider TEXT;")?;
Ok::<(), rusqlite::Error>(())
})
.await
.unwrap()
.unwrap();
db.run_migrations().await.unwrap();
db.run_migrations().await.unwrap();
db.pool
.get()
.await
.unwrap()
.interact(|conn| {
let version: i64 = conn.pragma_query_value(None, "user_version", |r| r.get(0))?;
assert_eq!(version, Database::MIGRATION_COUNT as i64);
assert_analytics_columns(conn);
assert_analytics_tables(conn);
Ok::<(), rusqlite::Error>(())
})
.await
.unwrap()
.unwrap();
}