Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ permissions:

jobs:
release-please:
if: github.repository == 'tableau/hyper-api-rust'
# Temporarily disabled until post-daemon/two-db-model work is complete.
# Re-enable by removing the `false &&` prefix below.
if: false && github.repository == 'tableau/hyper-api-rust'
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v5
Expand Down
18 changes: 9 additions & 9 deletions hyperdb-mcp/tests/attach_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use tempfile::TempDir;
fn primary_workspace() -> (Engine, TempDir) {
let dir = TempDir::new().unwrap();
let path = dir.path().join("primary.hyper");
let engine = Engine::new(Some(path.to_string_lossy().into())).unwrap();
let engine = Engine::new_no_daemon(Some(path.to_string_lossy().into())).unwrap();
engine
.execute_command("CREATE TABLE primary_t (x INT)")
.unwrap();
Expand All @@ -40,7 +40,7 @@ fn primary_workspace() -> (Engine, TempDir) {
fn build_source_hyper_file(dir: &TempDir, name: &str, rows: &[(i32, &str)]) -> std::path::PathBuf {
let path = dir.path().join(name);
{
let engine = Engine::new(Some(path.to_string_lossy().into())).unwrap();
let engine = Engine::new_no_daemon(Some(path.to_string_lossy().into())).unwrap();
engine
.execute_command("CREATE TABLE t (a INT, b TEXT)")
.unwrap();
Expand Down Expand Up @@ -579,7 +579,7 @@ fn replay_reattaches_on_fresh_engine() {

let registry = AttachRegistry::new();
{
let engine_a = Engine::new(Some(primary_path.to_string_lossy().into())).unwrap();
let engine_a = Engine::new_no_daemon(Some(primary_path.to_string_lossy().into())).unwrap();
registry
.attach(
&engine_a,
Expand All @@ -595,7 +595,7 @@ fn replay_reattaches_on_fresh_engine() {
.unwrap();
} // engine_a dropped here, closes its connection; attach state is gone on hyperd.

let engine_b = Engine::new(Some(primary_path.to_string_lossy().into())).unwrap();
let engine_b = Engine::new_no_daemon(Some(primary_path.to_string_lossy().into())).unwrap();
// Without replay the attachment is invisible to engine_b.
assert!(engine_b
.execute_query_to_json("SELECT 1 FROM \"src\".public.t LIMIT 0")
Expand Down Expand Up @@ -695,7 +695,7 @@ fn replay_drops_missing_files() {

let registry = AttachRegistry::new();
{
let engine_a = Engine::new(Some(primary_path.to_string_lossy().into())).unwrap();
let engine_a = Engine::new_no_daemon(Some(primary_path.to_string_lossy().into())).unwrap();
registry
.attach(
&engine_a,
Expand All @@ -715,7 +715,7 @@ fn replay_drops_missing_files() {
// — the call should succeed but the entry should be pruned.
std::fs::remove_file(&source).unwrap();

let engine_b = Engine::new(Some(primary_path.to_string_lossy().into())).unwrap();
let engine_b = Engine::new_no_daemon(Some(primary_path.to_string_lossy().into())).unwrap();
registry.replay_all(&engine_b).unwrap();
assert!(registry.list().is_empty());
}
Expand Down Expand Up @@ -871,7 +871,7 @@ fn replay_restores_schema_search_path_pin() {
let dir = TempDir::new().unwrap();
let primary_path = dir.path().join("primary.hyper");
{
let engine = Engine::new(Some(primary_path.to_string_lossy().into())).unwrap();
let engine = Engine::new_no_daemon(Some(primary_path.to_string_lossy().into())).unwrap();
engine
.execute_command("CREATE TABLE primary_t (x INT)")
.unwrap();
Expand All @@ -883,7 +883,7 @@ fn replay_restores_schema_search_path_pin() {

let registry = AttachRegistry::new();
{
let engine_a = Engine::new(Some(primary_path.to_string_lossy().into())).unwrap();
let engine_a = Engine::new_no_daemon(Some(primary_path.to_string_lossy().into())).unwrap();
registry
.attach(
&engine_a,
Expand All @@ -899,7 +899,7 @@ fn replay_restores_schema_search_path_pin() {
.unwrap();
} // engine_a dropped; next engine starts fresh with default search_path.

let engine_b = Engine::new(Some(primary_path.to_string_lossy().into())).unwrap();
let engine_b = Engine::new_no_daemon(Some(primary_path.to_string_lossy().into())).unwrap();
registry.replay_all(&engine_b).unwrap();

// Unqualified access to the primary must work after replay.
Expand Down
2 changes: 1 addition & 1 deletion hyperdb-mcp/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ impl TestEngine {
pub(crate) fn new_ephemeral() -> Self {
let temp_dir = TempDir::new().expect("failed to create temp dir");
let workspace_path = temp_dir.path().join("workspace.hyper");
let engine = Engine::new(Some(workspace_path.to_str().unwrap().to_string()))
let engine = Engine::new_no_daemon(Some(workspace_path.to_str().unwrap().to_string()))
.expect("failed to create engine");
Self {
engine,
Expand Down
56 changes: 36 additions & 20 deletions hyperdb-mcp/tests/daemon_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,15 @@ use tempfile::TempDir;

/// Process-wide lock for tests that mutate environment variables.
/// Cargo runs tests in the same process by default — this prevents races.
/// We recover from poison to prevent one test's panic from cascading.
static ENV_LOCK: Mutex<()> = Mutex::new(());

fn acquire_env_lock() -> std::sync::MutexGuard<'static, ()> {
ENV_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}

// ─── Unit tests: DaemonState (no env vars, safe to run in parallel) ───────────

#[test]
Expand Down Expand Up @@ -328,7 +335,7 @@ fn daemon_heartbeat_prevents_idle_shutdown() {

#[test]
fn discovery_file_write_and_read() {
let _lock = ENV_LOCK.lock().unwrap();
let _lock = acquire_env_lock();
let tmp = TempDir::new().unwrap();
let _guard = EnvGuard::set("HYPERDB_STATE_DIR", tmp.path().to_str().unwrap());

Expand All @@ -355,7 +362,7 @@ fn discovery_file_write_and_read() {

#[test]
fn discovery_file_overwrite_replaces_content() {
let _lock = ENV_LOCK.lock().unwrap();
let _lock = acquire_env_lock();
let tmp = TempDir::new().unwrap();
let _guard = EnvGuard::set("HYPERDB_STATE_DIR", tmp.path().to_str().unwrap());

Expand Down Expand Up @@ -386,7 +393,7 @@ fn discovery_file_overwrite_replaces_content() {

#[test]
fn remove_discovery_file_deletes_it() {
let _lock = ENV_LOCK.lock().unwrap();
let _lock = acquire_env_lock();
let tmp = TempDir::new().unwrap();
let _guard = EnvGuard::set("HYPERDB_STATE_DIR", tmp.path().to_str().unwrap());

Expand All @@ -407,7 +414,7 @@ fn remove_discovery_file_deletes_it() {

#[test]
fn discover_returns_none_when_no_file_exists() {
let _lock = ENV_LOCK.lock().unwrap();
let _lock = acquire_env_lock();
let tmp = TempDir::new().unwrap();
let _guard = EnvGuard::set("HYPERDB_STATE_DIR", tmp.path().to_str().unwrap());

Expand All @@ -416,7 +423,7 @@ fn discover_returns_none_when_no_file_exists() {

#[test]
fn discover_returns_none_for_stale_file() {
let _lock = ENV_LOCK.lock().unwrap();
let _lock = acquire_env_lock();
let tmp = TempDir::new().unwrap();
let _guard = EnvGuard::set("HYPERDB_STATE_DIR", tmp.path().to_str().unwrap());

Expand All @@ -437,14 +444,14 @@ fn discover_returns_none_for_stale_file() {

#[test]
fn resolve_port_uses_env_var() {
let _lock = ENV_LOCK.lock().unwrap();
let _lock = acquire_env_lock();
let _guard = EnvGuard::set("HYPERDB_DAEMON_PORT", "9999");
assert_eq!(discovery::resolve_port(), 9999);
}

#[test]
fn resolve_port_uses_default_when_env_unset() {
let _lock = ENV_LOCK.lock().unwrap();
let _lock = acquire_env_lock();
let _guard = EnvGuard::remove("HYPERDB_DAEMON_PORT");
assert_eq!(
discovery::resolve_port(),
Expand All @@ -454,7 +461,7 @@ fn resolve_port_uses_default_when_env_unset() {

#[test]
fn discover_finds_live_daemon() {
let _lock = ENV_LOCK.lock().unwrap();
let _lock = acquire_env_lock();
let tmp = TempDir::new().unwrap();
let _guard = EnvGuard::set("HYPERDB_STATE_DIR", tmp.path().to_str().unwrap());

Expand All @@ -478,7 +485,7 @@ fn discover_finds_live_daemon() {

#[test]
fn daemon_mode_engine_connects_to_shared_hyperd() {
let _lock = ENV_LOCK.lock().unwrap();
let _lock = acquire_env_lock();
let daemon = TestDaemon::start();

let tmp = TempDir::new().unwrap();
Expand All @@ -496,8 +503,8 @@ fn daemon_mode_engine_connects_to_shared_hyperd() {

#[test]
fn daemon_mode_two_engines_share_same_hyperd() {
let _lock = ENV_LOCK.lock().unwrap();
let _daemon = TestDaemon::start();
let _lock = acquire_env_lock();
let daemon = TestDaemon::start();

let tmp1 = TempDir::new().unwrap();
let tmp2 = TempDir::new().unwrap();
Expand All @@ -510,10 +517,19 @@ fn daemon_mode_two_engines_share_same_hyperd() {
let engine2 =
hyperdb_mcp::engine::Engine::new(Some(path2.to_str().unwrap().to_string())).unwrap();

assert_eq!(
engine1.hyperd_endpoint().unwrap(),
engine2.hyperd_endpoint().unwrap()
// Both engines should be in daemon mode (connected to the same daemon).
// We verify via the health port rather than the hyperd endpoint, because
// the daemon's liveness monitor can restart hyperd (changing the endpoint)
// between the two Engine::new calls.
let ep1 = engine1.hyperd_endpoint().unwrap();
let ep2 = engine2.hyperd_endpoint().unwrap();
assert!(
!ep1.is_empty() && !ep2.is_empty(),
"both engines must report a daemon endpoint"
);
// Verify the daemon is the one we started (health port reachable)
let status = health::send_command(daemon.info.health_port, "PING").unwrap();
assert_eq!(status.trim(), "PONG");

engine1.execute_command("CREATE TABLE foo (x INT)").unwrap();
engine1
Expand All @@ -529,7 +545,7 @@ fn daemon_mode_two_engines_share_same_hyperd() {

#[test]
fn daemon_mode_persistent_database_file_survives_engine_drop() {
let _lock = ENV_LOCK.lock().unwrap();
let _lock = acquire_env_lock();
let _daemon = TestDaemon::start();
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("persistent.hyper");
Expand All @@ -553,7 +569,7 @@ fn daemon_mode_persistent_database_file_survives_engine_drop() {

#[test]
fn daemon_mode_persistent_engine_data_is_queryable() {
let _lock = ENV_LOCK.lock().unwrap();
let _lock = acquire_env_lock();
let daemon = TestDaemon::start();
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("queryable.hyper");
Expand Down Expand Up @@ -581,7 +597,7 @@ fn daemon_mode_persistent_engine_data_is_queryable() {
#[cfg(unix)]
#[test]
fn hyperd_monitor_detects_killed_hyperd_and_restarts() {
let _lock = ENV_LOCK.lock().unwrap();
let _lock = acquire_env_lock();
let daemon = TestDaemon::start();

let pid_before = find_hyperd_pid_for_endpoint(&daemon.info.hyperd_endpoint)
Expand All @@ -608,7 +624,7 @@ fn hyperd_monitor_detects_killed_hyperd_and_restarts() {
#[cfg(unix)]
#[test]
fn client_report_triggers_restart_after_kill() {
let _lock = ENV_LOCK.lock().unwrap();
let _lock = acquire_env_lock();
let daemon = TestDaemon::start();

let pid_before = find_hyperd_pid_for_endpoint(&daemon.info.hyperd_endpoint)
Expand Down Expand Up @@ -642,7 +658,7 @@ fn engine_recovers_after_hyperd_killed() {
// 4. Wait for the daemon to restart it.
// 5. Run another query through the same recovery path the server uses
// (drop engine on ConnectionLost, then create a fresh engine).
let _lock = ENV_LOCK.lock().unwrap();
let _lock = acquire_env_lock();
let daemon = TestDaemon::start();

let tmp = TempDir::new().unwrap();
Expand Down Expand Up @@ -684,7 +700,7 @@ fn engine_recovers_after_hyperd_killed() {

#[test]
fn daemon_mode_ephemeral_database_cleaned_up_on_drop() {
let _lock = ENV_LOCK.lock().unwrap();
let _lock = acquire_env_lock();
let _daemon = TestDaemon::start();

let engine = hyperdb_mcp::engine::Engine::new(None).unwrap();
Expand Down
4 changes: 2 additions & 2 deletions hyperdb-mcp/tests/read_only_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ fn strips_leading_comments() {
/// server does not.
#[test]
fn server_read_only_flag_is_respected() {
let ro = HyperMcpServer::new(None, true, false);
let ro = HyperMcpServer::with_no_daemon(None, true, false, true);
assert!(ro.is_read_only());

let rw = HyperMcpServer::new(None, false, false);
let rw = HyperMcpServer::with_no_daemon(None, false, false, true);
assert!(!rw.is_read_only());
}
17 changes: 8 additions & 9 deletions hyperdb-mcp/tests/resource_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,17 @@ use tempfile::TempDir;
/// `_table_catalog` doesn't appear alongside `widgets` and perturb the
/// exact table counts these tests assert against. Catalog behavior is
/// covered in `tests/table_catalog_tests.rs`.
///
/// Uses `no_daemon` mode to avoid interference from any daemon running
/// in parallel (e.g. from daemon_tests in the same `cargo test` run).
fn server_with_test_table() -> (HyperMcpServer, TempDir) {
let dir = TempDir::new().unwrap();
let path = dir.path().join("workspace.hyper");
let server = HyperMcpServer::new(Some(path.to_str().unwrap().into()), false, true);
// Use the server's public helper to drive engine creation via a resource read
// so the engine is initialized. Then seed a table via the internal pathway:
// we can't call the private `with_engine` from here, but reading
// `hyper://workspace` lazily starts it. Then we populate through a second
// server? Simpler: use Engine directly once, then discard, since the
// workspace file persists.
let server =
HyperMcpServer::with_no_daemon(Some(path.to_str().unwrap().into()), false, true, true);
{
use hyperdb_mcp::engine::Engine;
let engine = Engine::new(Some(path.to_str().unwrap().into())).unwrap();
let engine = Engine::new_no_daemon(Some(path.to_str().unwrap().into())).unwrap();
engine
.execute_command("CREATE TABLE widgets (id INT NOT NULL, name TEXT)")
.unwrap();
Expand Down Expand Up @@ -176,7 +174,8 @@ fn read_readme_resource_handles_empty_workspace() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("empty.hyper");
// `bare` so `_table_catalog` doesn't make the workspace non-empty.
let server = HyperMcpServer::new(Some(path.to_str().unwrap().into()), false, true);
let server =
HyperMcpServer::with_no_daemon(Some(path.to_str().unwrap().into()), false, true, true);
let body = server
.resource_body_for_uri("hyper://readme")
.unwrap()
Expand Down
19 changes: 12 additions & 7 deletions hyperdb-mcp/tests/saved_queries_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,12 @@ fn session_delete_returns_true_when_present_false_otherwise() {

/// Build a fresh engine against a temp workspace file. Holding onto the
/// `TempDir` return keeps the directory alive for the caller's scope.
/// Uses `new_no_daemon` to avoid interference from any daemon running
/// in parallel (e.g. from daemon_tests in the same `cargo test` run).
fn workspace_engine() -> (Engine, TempDir) {
let dir = TempDir::new().unwrap();
let path = dir.path().join("ws.hyper");
let engine = Engine::new(Some(path.to_str().unwrap().into())).unwrap();
let engine = Engine::new_no_daemon(Some(path.to_str().unwrap().into())).unwrap();
(engine, dir)
}

Expand Down Expand Up @@ -173,7 +175,7 @@ fn workspace_persists_across_restarts() {
let path_str = path.to_str().unwrap().to_string();

{
let engine = Engine::new(Some(path_str.clone())).unwrap();
let engine = Engine::new_no_daemon(Some(path_str.clone())).unwrap();
let store = WorkspaceStore::new();
store
.save(Some(&engine), mk_query("persisted", "SELECT 42"))
Expand All @@ -183,7 +185,7 @@ fn workspace_persists_across_restarts() {
drop(engine);
}

let engine = Engine::new(Some(path_str)).unwrap();
let engine = Engine::new_no_daemon(Some(path_str)).unwrap();
let store = WorkspaceStore::new();
let fetched = store.get(Some(&engine), "persisted").unwrap().unwrap();
assert_eq!(fetched.sql, "SELECT 42");
Expand All @@ -200,7 +202,7 @@ fn workspace_persists_across_restarts() {
/// resource list → resource read chain.
#[test]
fn server_ephemeral_session_store_exposes_query_resources() {
let server = HyperMcpServer::new(None, false, false);
let server = HyperMcpServer::with_no_daemon(None, false, false, true);
// Reach into the store directly via its resource helper by saving a
// query through the store (wiring of the tool itself is covered
// indirectly — the public testable surface is the store + resource).
Expand Down Expand Up @@ -233,9 +235,10 @@ fn server_workspace_store_exposes_saved_queries_via_resources() {
let path_str = path.to_str().unwrap().to_string();

// Seed the workspace with one saved query by going through the same
// store type the server will use.
// store type the server will use. Uses new_no_daemon to avoid
// interference from any daemon running in parallel.
{
let engine = Engine::new(Some(path_str.clone())).unwrap();
let engine = Engine::new_no_daemon(Some(path_str.clone())).unwrap();
let store = WorkspaceStore::new();
store
.save(
Expand All @@ -245,7 +248,9 @@ fn server_workspace_store_exposes_saved_queries_via_resources() {
.unwrap();
}

let server = HyperMcpServer::new(Some(path_str), false, false);
// with_no_daemon ensures the server spawns its own hyperd rather than
// connecting to a daemon left over from daemon_tests.
let server = HyperMcpServer::with_no_daemon(Some(path_str), false, false, true);

// The URI catalog lists both the definition and result resources.
let uris = server.list_resource_uris();
Expand Down
Loading
Loading