use hyperdb_mcp::engine::Engine;
use hyperdb_mcp::error::ErrorCode;
use tempfile::TempDir;
#[test]
fn engine_with_persistent_path_attaches_under_persistent_alias() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("ws.hyper");
let engine = Engine::new(Some(path.to_str().unwrap().into())).unwrap();
assert!(engine.has_persistent());
engine
.execute_command("CREATE TABLE \"persistent\".\"public\".\"smoke\" (x INT)")
.unwrap();
let rows = engine
.execute_query_to_json(
"SELECT tablename FROM \"persistent\".pg_catalog.pg_tables \
WHERE schemaname = 'public' AND tablename = 'smoke'",
)
.unwrap();
assert_eq!(rows.len(), 1);
}
#[test]
fn engine_without_persistent_path_is_ephemeral_only() {
let engine = Engine::new(None).unwrap();
assert!(!engine.has_persistent());
let result =
engine.execute_query_to_json("SELECT 1 FROM \"persistent\".pg_catalog.pg_database");
assert!(
result.is_err(),
"no persistent alias must reject queries naming it"
);
}
#[test]
fn ephemeral_paths_are_unique_per_engine() {
let e1 = Engine::new(None).unwrap();
let e2 = Engine::new(None).unwrap();
assert_ne!(
e1.ephemeral_path(),
e2.ephemeral_path(),
"each engine must own a distinct ephemeral file"
);
}
#[test]
fn persistent_writes_survive_engine_recreate() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("survives.hyper");
let path_str = path.to_str().unwrap().to_string();
{
let engine = Engine::new(Some(path_str.clone())).unwrap();
engine
.execute_command("CREATE TABLE \"persistent\".\"public\".\"keepers\" (n INT)")
.unwrap();
engine
.execute_command(
"INSERT INTO \"persistent\".\"public\".\"keepers\" VALUES (1), (2), (3)",
)
.unwrap();
}
let engine = Engine::new(Some(path_str)).unwrap();
let rows = engine
.execute_query_to_json("SELECT n FROM \"persistent\".\"public\".\"keepers\" ORDER BY n")
.unwrap();
assert_eq!(rows.len(), 3);
assert_eq!(rows[0]["n"], 1);
assert_eq!(rows[2]["n"], 3);
}
#[test]
fn ephemeral_writes_are_discarded_on_drop() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("ws.hyper");
let path_str = path.to_str().unwrap().to_string();
{
let engine = Engine::new(Some(path_str.clone())).unwrap();
engine
.execute_command("CREATE TABLE scratch (id INT)")
.unwrap();
engine
.execute_command("INSERT INTO scratch VALUES (1), (2), (3)")
.unwrap();
}
let engine = Engine::new(Some(path_str)).unwrap();
let rows = engine
.execute_query_to_json(
"SELECT tablename FROM pg_catalog.pg_tables WHERE tablename = 'scratch'",
)
.unwrap_or_default();
assert!(
rows.is_empty(),
"scratch must not survive engine drop (it lived in ephemeral)"
);
}
#[test]
fn resolve_target_db_handles_persistent_presence() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("ws.hyper");
let with_persistent = Engine::new(Some(path.to_str().unwrap().into())).unwrap();
assert_eq!(
with_persistent
.resolve_target_db(Some("persistent"))
.unwrap(),
"persistent"
);
assert_eq!(
with_persistent.resolve_target_db(None).unwrap(),
with_persistent.primary_db_name()
);
let ephemeral_only = Engine::new(None).unwrap();
assert_eq!(
ephemeral_only.resolve_target_db(None).unwrap(),
ephemeral_only.primary_db_name()
);
let err = ephemeral_only
.resolve_target_db(Some("persistent"))
.unwrap_err();
assert_eq!(err.code, ErrorCode::InvalidArgument);
assert!(err.message.contains("--ephemeral-only"));
}
#[test]
fn engine_status_reports_both_paths() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("ws.hyper");
let engine = Engine::new(Some(path.to_str().unwrap().into())).unwrap();
let status = engine.status().unwrap();
assert!(status["has_persistent"].as_bool().unwrap());
assert!(status["ephemeral_path"].is_string());
assert!(status["persistent_path"].is_string());
}
#[test]
fn engine_status_ephemeral_only_reports_no_persistent() {
let engine = Engine::new(None).unwrap();
let status = engine.status().unwrap();
assert!(!status["has_persistent"].as_bool().unwrap());
assert!(status["ephemeral_path"].is_string());
assert!(status["persistent_path"].is_null());
}
#[test]
fn catalog_presence_probe_is_cached_per_db() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("ws.hyper");
let engine = Engine::new(Some(path.to_str().unwrap().into())).unwrap();
let probe_count = std::sync::atomic::AtomicUsize::new(0);
let probe = |_e: &Engine| -> Result<bool, hyperdb_mcp::error::McpError> {
probe_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Ok(false)
};
assert!(!engine.catalog_present_in("persistent", probe).unwrap());
assert_eq!(probe_count.load(std::sync::atomic::Ordering::SeqCst), 1);
assert!(!engine.catalog_present_in("persistent", probe).unwrap());
assert_eq!(probe_count.load(std::sync::atomic::Ordering::SeqCst), 1);
engine.mark_catalog_present_for("persistent");
assert!(engine.catalog_present_in("persistent", probe).unwrap());
assert_eq!(probe_count.load(std::sync::atomic::Ordering::SeqCst), 1);
assert!(!engine.catalog_present_in("other", probe).unwrap());
assert_eq!(probe_count.load(std::sync::atomic::Ordering::SeqCst), 2);
}
#[test]
fn clear_catalog_cache_for_invalidates_alias_entry() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("ws.hyper");
let engine = Engine::new(Some(path.to_str().unwrap().into())).unwrap();
let probe_count = std::sync::atomic::AtomicUsize::new(0);
let probe = |_e: &Engine| -> Result<bool, hyperdb_mcp::error::McpError> {
probe_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Ok(true)
};
assert!(engine.catalog_present_in("foo", probe).unwrap());
assert_eq!(probe_count.load(std::sync::atomic::Ordering::SeqCst), 1);
assert!(engine.catalog_present_in("foo", probe).unwrap());
assert_eq!(probe_count.load(std::sync::atomic::Ordering::SeqCst), 1);
engine.clear_catalog_cache_for("foo");
assert!(engine.catalog_present_in("foo", probe).unwrap());
assert_eq!(probe_count.load(std::sync::atomic::Ordering::SeqCst), 2);
}