From 5a28d02df1bce14fc530f4258f88206daa71b3e8 Mon Sep 17 00:00:00 2001 From: sanil-23 Date: Mon, 25 May 2026 19:21:24 +0200 Subject: [PATCH] test(memory): serialize tests that drive the process-global memory client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `memory::global` is a process-wide singleton (`GLOBAL_CLIENT`). Tests across memory/ops/{learn,documents,tool_memory,kv_graph,sync} and one composio sync test each call `memory::global::init(...)` to re-point it at their own TempDir workspace. Run concurrently by `cargo test`, they race on that single client + its SQLite connection: schema init hits SQLITE_IOERR ("disk I/O error") and rows bleed across tests (assertion failures). Reproduces on main independent of any feature work; passes at `--test-threads=1`. Add a shared async lock (`GLOBAL_MEMORY_TEST_LOCK`, `tokio::sync::Mutex` — no new dependency) and acquire it as the first line of every test that re-points the global, so those tests run serially relative to each other while unrelated tests still parallelize. The lock is acquired before any `WorkspaceEnvGuard` (`TEST_ENV_LOCK`) so lock order is consistent. Verified: 3x full mock suite at default parallelism → 0 failures (was failing nearly every parallel run; e.g. memory::ops at --test-threads=8). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/openhuman/composio/ops_test.rs | 3 +++ src/openhuman/memory/ops/documents.rs | 6 ++++++ src/openhuman/memory/ops/kv_graph.rs | 6 ++++++ src/openhuman/memory/ops/learn.rs | 15 +++++++++++++++ src/openhuman/memory/ops/mod.rs | 9 +++++++++ src/openhuman/memory/ops/sync.rs | 9 +++++++++ src/openhuman/memory/ops/tool_memory.rs | 6 ++++++ 7 files changed, 54 insertions(+) diff --git a/src/openhuman/composio/ops_test.rs b/src/openhuman/composio/ops_test.rs index f148b8857a..f82ba7ffa5 100644 --- a/src/openhuman/composio/ops_test.rs +++ b/src/openhuman/composio/ops_test.rs @@ -728,6 +728,9 @@ async fn composio_execute_via_mock_propagates_backend_error() { #[tokio::test] async fn composio_sync_gmail_via_mock_archives_raw_email_and_updates_outcome() { + let _serial = crate::openhuman::memory::ops::GLOBAL_MEMORY_TEST_LOCK + .lock() + .await; use crate::openhuman::config::TEST_ENV_LOCK; use crate::openhuman::memory_store::content::raw::{raw_rel_path, RawKind}; use crate::openhuman::memory_tree::tree::rpc::{list_chunks_rpc, ListChunksRequest}; diff --git a/src/openhuman/memory/ops/documents.rs b/src/openhuman/memory/ops/documents.rs index 8bd99e2dee..d11f8b82a9 100644 --- a/src/openhuman/memory/ops/documents.rs +++ b/src/openhuman/memory/ops/documents.rs @@ -535,6 +535,9 @@ mod tests { #[tokio::test] async fn direct_document_handlers_roundtrip_through_namespace() { + let _serial = crate::openhuman::memory::ops::GLOBAL_MEMORY_TEST_LOCK + .lock() + .await; ensure_memory_client(); let namespace = unique_namespace("memory-docs-direct"); let key = format!( @@ -613,6 +616,9 @@ mod tests { #[tokio::test] async fn envelope_memory_handlers_report_counts_and_statuses() { + let _serial = crate::openhuman::memory::ops::GLOBAL_MEMORY_TEST_LOCK + .lock() + .await; ensure_memory_client(); let namespace = unique_namespace("memory-docs-envelope"); let key = format!("env{}", &uuid::Uuid::new_v4().as_simple().to_string()[..12]); diff --git a/src/openhuman/memory/ops/kv_graph.rs b/src/openhuman/memory/ops/kv_graph.rs index cc5c9d89db..2405460fb5 100644 --- a/src/openhuman/memory/ops/kv_graph.rs +++ b/src/openhuman/memory/ops/kv_graph.rs @@ -163,6 +163,9 @@ mod tests { #[tokio::test] async fn kv_handlers_roundtrip_scoped_values() { + let _serial = crate::openhuman::memory::ops::GLOBAL_MEMORY_TEST_LOCK + .lock() + .await; ensure_memory_client(); let namespace = unique_namespace("kv-graph-kv"); let key = format!( @@ -216,6 +219,9 @@ mod tests { #[tokio::test] async fn graph_handlers_roundtrip_relation_rows() { + let _serial = crate::openhuman::memory::ops::GLOBAL_MEMORY_TEST_LOCK + .lock() + .await; ensure_memory_client(); let namespace = unique_namespace("kv-graph-rel"); let subject = format!( diff --git a/src/openhuman/memory/ops/learn.rs b/src/openhuman/memory/ops/learn.rs index b4bcc83077..d3f3ca6fdf 100644 --- a/src/openhuman/memory/ops/learn.rs +++ b/src/openhuman/memory/ops/learn.rs @@ -245,6 +245,9 @@ mod tests { #[tokio::test] async fn memory_learn_all_is_noop_for_explicit_empty_namespace_list() { + let _serial = crate::openhuman::memory::ops::GLOBAL_MEMORY_TEST_LOCK + .lock() + .await; ensure_memory_client(); let outcome = memory_learn_all(LearnAllParams { namespaces: Some(vec![]), @@ -258,6 +261,9 @@ mod tests { #[tokio::test] async fn memory_learn_all_is_noop_when_requested_namespaces_do_not_exist() { + let _serial = crate::openhuman::memory::ops::GLOBAL_MEMORY_TEST_LOCK + .lock() + .await; ensure_memory_client(); let missing = format!( "missing{}", @@ -274,6 +280,9 @@ mod tests { #[tokio::test] async fn memory_learn_all_filters_missing_namespaces_and_dedupes_requested_order() { + let _serial = crate::openhuman::memory::ops::GLOBAL_MEMORY_TEST_LOCK + .lock() + .await; let namespace_a = seed_namespace("memory-learn-a").await; let namespace_b = seed_namespace("memory-learn-b").await; let missing = format!( @@ -304,6 +313,9 @@ mod tests { #[tokio::test] async fn memory_learn_all_requires_local_ai_once_existing_namespace_is_selected() { + let _serial = crate::openhuman::memory::ops::GLOBAL_MEMORY_TEST_LOCK + .lock() + .await; let namespace = seed_namespace("memory-learn-runtime").await; let tmp = TempDir::new().expect("tempdir"); let _workspace = write_config_with_runtime_enabled(tmp.path(), false).await; @@ -319,6 +331,9 @@ mod tests { #[tokio::test] async fn memory_learn_all_uses_all_namespaces_when_none_is_requested() { + let _serial = crate::openhuman::memory::ops::GLOBAL_MEMORY_TEST_LOCK + .lock() + .await; let namespace_a = seed_namespace("memory-learn-all-a").await; let namespace_b = seed_namespace("memory-learn-all-b").await; let tmp = TempDir::new().expect("tempdir"); diff --git a/src/openhuman/memory/ops/mod.rs b/src/openhuman/memory/ops/mod.rs index 3676e41070..d7b2c737e6 100644 --- a/src/openhuman/memory/ops/mod.rs +++ b/src/openhuman/memory/ops/mod.rs @@ -70,6 +70,15 @@ pub(crate) use helpers::{ relation_metadata, timestamp_to_rfc3339, validate_memory_relative_path, }; +/// Serializes the tests that drive the process-global memory client +/// (`memory::global`). Each re-points that singleton at its own workspace, so +/// running them concurrently races on one client + SQLite connection +/// (`SQLITE_IOERR` during schema init + cross-test data bleed). Async tests +/// hold this for their whole body; serial execution is their proven-safe mode. +#[cfg(test)] +pub(crate) static GLOBAL_MEMORY_TEST_LOCK: tokio::sync::Mutex<()> = + tokio::sync::Mutex::const_new(()); + #[cfg(test)] #[path = "../ops_tests.rs"] mod tests; diff --git a/src/openhuman/memory/ops/sync.rs b/src/openhuman/memory/ops/sync.rs index 2fa1320ef9..b0a5c77df8 100644 --- a/src/openhuman/memory/ops/sync.rs +++ b/src/openhuman/memory/ops/sync.rs @@ -313,6 +313,9 @@ mod tests { #[tokio::test] async fn memory_sync_channel_publishes_targeted_event() { + let _serial = crate::openhuman::memory::ops::GLOBAL_MEMORY_TEST_LOCK + .lock() + .await; let _guard = test_mutex() .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()); @@ -338,6 +341,9 @@ mod tests { #[tokio::test] async fn memory_sync_all_publishes_broadcast_event() { + let _serial = crate::openhuman::memory::ops::GLOBAL_MEMORY_TEST_LOCK + .lock() + .await; let _guard = test_mutex() .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()); @@ -361,6 +367,9 @@ mod tests { #[tokio::test] async fn memory_ingestion_status_reflects_initialized_client_snapshot() { + let _serial = crate::openhuman::memory::ops::GLOBAL_MEMORY_TEST_LOCK + .lock() + .await; let _guard = test_mutex() .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()); diff --git a/src/openhuman/memory/ops/tool_memory.rs b/src/openhuman/memory/ops/tool_memory.rs index ea60613fbf..ccc310b782 100644 --- a/src/openhuman/memory/ops/tool_memory.rs +++ b/src/openhuman/memory/ops/tool_memory.rs @@ -202,6 +202,9 @@ mod tests { #[tokio::test] async fn tool_rule_put_get_list_and_delete_roundtrip() { + let _serial = crate::openhuman::memory::ops::GLOBAL_MEMORY_TEST_LOCK + .lock() + .await; ensure_memory_client(); let tool_name = unique_tool_name(); @@ -268,6 +271,9 @@ mod tests { #[tokio::test] async fn tool_rules_for_prompt_sorts_by_priority_and_tool_name() { + let _serial = crate::openhuman::memory::ops::GLOBAL_MEMORY_TEST_LOCK + .lock() + .await; ensure_memory_client(); let primary_tool = unique_tool_name(); let secondary_tool = unique_tool_name();