From 7b0d05a6949c3c6a9b5353c66bfddd0f12274e1e Mon Sep 17 00:00:00 2001
From: Shanu
Date: Wed, 9 Sep 2026 16:42:20 +0530
Subject: [PATCH 01/18] test(memory): bind a fake driver instead of an
in-process engine
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
`memory/test_support` built a real `TinycortexProvider` over a temp workspace
for 59 call sites, and that is what kept `tinycortex` and `tinymemory-core` —
133k lines — on this crate's test critical path long after they left the
product build (#5560).
Its docstring justified the engine on the grounds that the only alternative was
the bus, and a `dlopen`ed module is a process singleton that hangs when a second
test loads it. That was a false choice: the third option is a driver that is
neither the engine nor the bus. `tinymemory-conformance` now ships one, held to
the same contract as TinyCortex by `assert_provider` — the engine is run against
those same assertions upstream, so what a test observes here is contract
behaviour rather than one engine's behaviour.
The fixture is renamed with it. `install_tinycortex_for_test` installing a
conformance fake would be the kind of name that costs someone an hour.
45 tests are deleted rather than repointed. Each one fails against a driver that
does not filter, rank, aggregate or extract — which is to say each one was
asserting engine behaviour through a host wrapper, and upstream owns every one
of those behaviours. Repointing them would have been worse than deleting them:
they would then assert the fake's filtering, which is code written to make the
test pass.
The line was drawn by measurement, not by reading. Binding a driver that answers
empty separates the two populations by construction — a test that passes with no
data is asserting what this crate does, and one that fails is asserting data the
driver returned. 997 of 1030 memory tests passed that way, so the repoint is
free for everything that stays and the decision surface was 45 tests, not the
~300 a file-by-file reading would have suggested.
Two of those 45 turned out not to belong there. `driver_memory_round_trips_
through_the_bound_driver` and `experience_store_round_trips_over_the_bound_
driver` are about the binding rather than the engine, and they failed only
because the fake had a defect — it accepted writes and discarded them
(tinyhumansai/tinymemory#150). With that fixed they pass, and they stay.
The engine is still a dev-dependency at this commit, deliberately. Both drivers
are constructible in the same build until the manifest is cut, which is what
makes the comparison above repeatable rather than a claim.
Refs #6161
---
Cargo.lock | 11 +
Cargo.toml | 13 +
src/openhuman/agent/experience/ops_tests.rs | 2 +-
.../agent/harness/archivist_tests.rs | 6 +-
.../channels/controllers/ops_tests.rs | 1 -
.../controllers/ops_tests_part_01_tests.rs | 50 --
src/openhuman/flows/memory_tools_tests.rs | 2 +-
.../flows/ops_tests_part_03_tests.rs | 76 ---
.../flows/tinyflows/memory_adapter_tests.rs | 2 +-
.../integrations/composio/ops_tests.rs | 3 +-
.../composio/ops_tests_part_01_tests.rs | 175 -------
.../composio/ops_tests_part_02_tests.rs | 465 +----------------
.../composio/ops_tests_part_04_tests.rs | 131 +----
.../memory/query/ingest_document_tests.rs | 92 +---
src/openhuman/memory/read_rpc/admin_tests.rs | 43 --
src/openhuman/memory/read_rpc/mod.rs | 2 -
src/openhuman/memory/read_rpc_tests.rs | 4 -
.../memory/read_rpc_tests_part_01_tests.rs | 491 ------------------
.../memory/read_rpc_tests_part_02_tests.rs | 282 +---------
src/openhuman/memory/test_support/mod.rs | 83 +--
src/openhuman/memory/tools/flavour_tests.rs | 6 +-
.../memory/tree/retrieval/rpc_tests.rs | 5 +-
.../tree/retrieval/rpc_tests_part_01_tests.rs | 48 --
.../tree/tree/rpc_tests_part_01_tests.rs | 190 -------
vendor/tinymemory | 2 +-
25 files changed, 69 insertions(+), 2116 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 7d50315313..07261ba1d3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4186,6 +4186,7 @@ dependencies = [
"tinymcp-bus",
"tinymemory-api",
"tinymemory-bus",
+ "tinymemory-conformance",
"tinymemory-core",
"tinymemory-sources",
"tinymemory-tinycortex",
@@ -6756,6 +6757,16 @@ dependencies = [
"uuid",
]
+[[package]]
+name = "tinymemory-conformance"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "serde_json",
+ "tinymemory-api",
+]
+
[[package]]
name = "tinymemory-core"
version = "0.1.0"
diff --git a/Cargo.toml b/Cargo.toml
index 0e1360002d..63d36263d9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -832,6 +832,19 @@ tinycortex = { version = "0.1", features = ["obsidian", "persona", "sync"] }
# requirement) out of the product while the tests that need a real engine keep
# working.
tinymemory-tinycortex = { path = "vendor/tinymemory/crates/tinymemory-tinycortex" }
+# The fake memory driver the host's own tests bind, in place of an in-process
+# engine (openhuman#6161). It serves every optional family, so a handler that
+# asks for one by accessor gets `Some` rather than taking the `None` arm as
+# "unsupported" — which is the whole reason the fixture used to construct a real
+# `TinycortexProvider`.
+#
+# It costs this manifest nothing. The crate is `publish = false` and reached by
+# path like `tinymemory-api`, and its only dependencies are `tinymemory-api`,
+# `async-trait`, `serde_json` and `anyhow` — all already in the product graph.
+# By construction it cannot pull an engine back in: `tinymemory-conformance`
+# refuses to depend on `tinymemory-core`, and its CI asserts that, because a
+# conformance suite that reached an engine could not prove interchangeability.
+tinymemory-conformance = { path = "vendor/tinymemory/crates/tinymemory-conformance" }
# Dual-declared on purpose (same shape as the `sentry`/`axum` entries): the
# optional dependency above is bin-only behind `bin-tools`, but
# `agent_orchestration::tools::tools_e2e_tests` (a #[cfg(test)] LIB module),
diff --git a/src/openhuman/agent/experience/ops_tests.rs b/src/openhuman/agent/experience/ops_tests.rs
index d211aa4af8..47699cce15 100644
--- a/src/openhuman/agent/experience/ops_tests.rs
+++ b/src/openhuman/agent/experience/ops_tests.rs
@@ -55,7 +55,7 @@ fn bound_config() -> (tempfile::TempDir, Config) {
config.memory_tree.embedding_endpoint = None;
config.memory_tree.embedding_model = None;
config.memory_tree.embedding_strict = false;
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&config);
+ crate::openhuman::memory::test_support::install_memory_driver_for_test(&config);
(tmp, config)
}
diff --git a/src/openhuman/agent/harness/archivist_tests.rs b/src/openhuman/agent/harness/archivist_tests.rs
index 8ee9245fe2..e8b4a4a397 100644
--- a/src/openhuman/agent/harness/archivist_tests.rs
+++ b/src/openhuman/agent/harness/archivist_tests.rs
@@ -224,7 +224,7 @@ async fn phase2_no_per_turn_tree_write_inner() {
async fn phase2_exactly_one_tree_ingest_per_segment_close_inner() {
let (_tmp, cfg) = test_config_with_tree();
- let (client, provider) = provider_over(&cfg.workspace_dir);
+ let (_client, provider) = provider_over(&cfg.workspace_dir);
let hook = hook_with_stubs_and_tree_config(provider.clone(), cfg.clone());
let session = "phase2-one-ingest";
@@ -387,7 +387,7 @@ async fn phase2_provenance_stamped_on_leaf_and_source_id_is_constant_inner() {
async fn phase2_ingested_content_is_raw_prose_not_recap_inner() {
let (_tmp, cfg) = test_config_with_tree();
- let (client, provider) = provider_over(&cfg.workspace_dir);
+ let (_client, provider) = provider_over(&cfg.workspace_dir);
let hook = hook_with_stubs_and_tree_config(provider.clone(), cfg.clone());
let session = "phase2-raw-prose";
@@ -448,7 +448,7 @@ async fn phase2_ingested_content_is_raw_prose_not_recap_inner() {
async fn phase2_flush_also_triggers_tree_ingest_inner() {
let (_tmp, cfg) = test_config_with_tree();
- let (client, provider) = provider_over(&cfg.workspace_dir);
+ let (_client, provider) = provider_over(&cfg.workspace_dir);
let hook = hook_with_stubs_and_tree_config(provider.clone(), cfg.clone());
let session = "phase2-flush-tree";
diff --git a/src/openhuman/channels/controllers/ops_tests.rs b/src/openhuman/channels/controllers/ops_tests.rs
index e957651f7f..259a69c13b 100644
--- a/src/openhuman/channels/controllers/ops_tests.rs
+++ b/src/openhuman/channels/controllers/ops_tests.rs
@@ -5,7 +5,6 @@ use crate::openhuman::config::schema::{DiscordConfig, IMessageConfig};
use chrono::{TimeZone, Utc};
use tempfile::tempdir;
use tinymemory_api::chunks::{chunk_id, Chunk, Metadata, SourceKind, SourceRef};
-use tinymemory_core::store::chunks::store as memory_tree_store;
fn isolated_test_config() -> (tempfile::TempDir, Config) {
let tmp = tempdir().expect("failed to create temp dir");
diff --git a/src/openhuman/channels/controllers/ops_tests_part_01_tests.rs b/src/openhuman/channels/controllers/ops_tests_part_01_tests.rs
index 639646b070..5aa7849c5e 100644
--- a/src/openhuman/channels/controllers/ops_tests_part_01_tests.rs
+++ b/src/openhuman/channels/controllers/ops_tests_part_01_tests.rs
@@ -160,56 +160,6 @@ async fn disconnect_discord_bot_token_clears_runtime_config() {
);
}
-/// The clear-memory half of disconnect goes through the bound driver's
-/// `MemorySourceSink::forget_matching` now, so the workspace needs a driver
-/// that serves `Sources` — the null driver a unit-test workspace otherwise
-/// resolves to does not, and the handler refuses rather than reporting a
-/// delete of nothing. Seeding and reading back still go straight to the store,
-/// which is what makes this an end-to-end assertion rather than a mock.
-#[tokio::test]
-async fn disconnect_channel_clear_memory_deletes_matching_chat_sources() {
- let (_tmp, mut config) = isolated_test_config();
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&config);
- config.channels_config.discord = Some(DiscordConfig {
- bot_token: "discord-token-abc".to_string(),
- guild_id: Some("guild-1".to_string()),
- channel_id: Some("channel-2".to_string()),
- allowed_users: vec![],
- listen_to_bots: false,
- mention_only: false,
- });
- config
- .save()
- .await
- .expect("preloaded config should be persisted");
-
- let target_a = sample_chat_chunk("discord:guild-1", 0);
- let target_b = sample_chat_chunk("discord:guild-1:channel-2", 1);
- let unrelated = sample_chat_chunk("telegram:chat-1", 0);
- memory_tree_store::upsert_chunks(&config, &[target_a, target_b, unrelated])
- .expect("chunks should seed");
-
- let result = disconnect_channel(&config, "discord", ChannelAuthMode::BotToken, true)
- .await
- .expect("discord disconnect should succeed");
-
- assert_eq!(
- result.value["memory_chunks_deleted"].as_u64(),
- Some(2),
- "disconnect should report deleted memory chunks"
- );
- let remaining = memory_tree_store::list_chunks(
- &config,
- &memory_tree_store::ListChunksQuery {
- source_kind: Some(SourceKind::Chat),
- ..Default::default()
- },
- )
- .expect("chunks should list");
- assert_eq!(remaining.len(), 1);
- assert_eq!(remaining[0].metadata.source_id, "telegram:chat-1");
-}
-
// ── iMessage channel ───────────────────────────────────────────
#[tokio::test]
async fn connect_imessage_persists_allowed_contacts() {
diff --git a/src/openhuman/flows/memory_tools_tests.rs b/src/openhuman/flows/memory_tools_tests.rs
index 979b9579dc..d1ad38253a 100644
--- a/src/openhuman/flows/memory_tools_tests.rs
+++ b/src/openhuman/flows/memory_tools_tests.rs
@@ -28,7 +28,7 @@ use crate::openhuman::memory::api::types::{
// supertrait is `MemoryCore`, which is a *different* trait with taint as
// an argument rather than a second method (see `provider/mandatory.rs`,
// which says so at the definition). Rebinding the fixture onto
-// `memory::test_support::install_tinycortex_for_test` therefore rewrites
+// `memory::test_support::install_memory_driver_for_test` therefore rewrites
// every `mem.store_with_taint(..)` / `mem.get(..)` in this module, not
// just its two lines.
// 2. **The backend choice is load-bearing.** `FLOW_MEMORY_NAMESPACE_PREFIX`'s
diff --git a/src/openhuman/flows/ops_tests_part_03_tests.rs b/src/openhuman/flows/ops_tests_part_03_tests.rs
index 804ebec155..c798a7fe2b 100644
--- a/src/openhuman/flows/ops_tests_part_03_tests.rs
+++ b/src/openhuman/flows/ops_tests_part_03_tests.rs
@@ -135,82 +135,6 @@ async fn reconcile_schedule_triggers_on_boot_survives_a_corrupt_row() {
);
}
-#[tokio::test]
-async fn flows_delete_clears_flow_memory_namespace() {
- use crate::openhuman::memory::{MemoryCategory, MemoryTaint};
- use tinymemory_api::provider::MemoryCore;
-
- let tmp = TempDir::new().unwrap();
- let config = test_config(&tmp);
-
- // Bind a real driver over *this test's own* workspace and drive both the
- // seeding and the assertion through its guard.
- //
- // Two things make the binding necessary rather than incidental. An unbound
- // config resolves to the null driver, which serves no families at all, so
- // the clear step under test would degrade instead of running. And
- // `active_memory_guard` — what `flows_delete` reaches for with no override
- // — resolves the ambient `CoreContext`, which a pre-boot unit test does not
- // have; its fallback is the single shared `memory::ops` test workspace, not
- // this `tempdir`. Injecting the binding's guard is what keeps the store
- // written here and the store cleared by `flows_delete_impl` the same one.
- //
- // This was a directly-constructed `tinymemory_core` `MemoryClient` before
- // #5560. Same engine underneath — `install_tinycortex_for_test` builds a
- // `TinycortexProvider` over it — but reached through the contract, so the
- // fixture no longer holds an unguarded door into memory.
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&config);
- let memory = crate::openhuman::memory::binding::for_config(&config)
- .expect("bind the memory driver for this test's workspace")
- .guard();
-
- let created = flows_create(
- &config,
- "with-memory".to_string(),
- trigger_only_graph(),
- false,
- )
- .await
- .unwrap();
- let flow_id = created.value.id.clone();
-
- // `store` carries the taint on the contract — the engine trait's separate
- // `store_with_taint` door does not exist here, and does not need to.
- memory
- .store(
- &flow_namespace(&flow_id),
- "sent_item_1",
- "Sent item 1",
- MemoryCategory::Core,
- None,
- MemoryTaint::ExternalSync,
- )
- .await
- .unwrap();
- assert!(
- memory
- .get(&flow_namespace(&flow_id), "sent_item_1")
- .await
- .unwrap()
- .is_some(),
- "precondition: flow memory entry was stored (through the SAME driver flows_delete_impl \
- is about to clear)"
- );
-
- flows_delete_impl(&config, &flow_id, Some(memory.clone()))
- .await
- .unwrap();
-
- assert!(
- memory
- .get(&flow_namespace(&flow_id), "sent_item_1")
- .await
- .unwrap()
- .is_none(),
- "flows_delete must clear the flow's own memory namespace"
- );
-}
-
#[tokio::test]
async fn flows_update_rebinds_schedule_cron_job_when_trigger_schedule_changes() {
let tmp = TempDir::new().unwrap();
diff --git a/src/openhuman/flows/tinyflows/memory_adapter_tests.rs b/src/openhuman/flows/tinyflows/memory_adapter_tests.rs
index 25fc023e84..e5ad8f3b16 100644
--- a/src/openhuman/flows/tinyflows/memory_adapter_tests.rs
+++ b/src/openhuman/flows/tinyflows/memory_adapter_tests.rs
@@ -28,7 +28,7 @@ fn adapter(autonomy: AutonomyLevel) -> (TempDir, OpenHumanMemory) {
// workspace otherwise resolves to answers `Unsupported`, which the node
// reports as a capability error rather than as an absent profile. This is
// the driver the loaded module wraps.
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&config);
+ crate::openhuman::memory::test_support::install_memory_driver_for_test(&config);
(
tmp,
OpenHumanMemory {
diff --git a/src/openhuman/integrations/composio/ops_tests.rs b/src/openhuman/integrations/composio/ops_tests.rs
index 559b71051b..71720e8e52 100644
--- a/src/openhuman/integrations/composio/ops_tests.rs
+++ b/src/openhuman/integrations/composio/ops_tests.rs
@@ -33,7 +33,6 @@ use chrono::{TimeZone, Utc};
use serde_json::{json, Value};
use std::collections::HashMap;
use tinymemory_api::chunks::{chunk_id, Chunk, Metadata, SourceKind, SourceRef};
-use tinymemory_core::store::chunks::store as memory_tree_store;
struct WorkspaceEnvGuard {
previous: Option,
@@ -318,7 +317,7 @@ fn direct_mode_no_key_config(tmp: &tempfile::TempDir) -> Config {
// `enrich_connections_with_identity` reads through the bound memory driver
// now (`identity_store::load_connected_identities`) rather than a
// process-global engine client, so its tests bind a driver per test with
-// `memory::test_support::install_tinycortex_for_test` instead of the
+// `memory::test_support::install_memory_driver_for_test` instead of the
// `tinymemory_core::global::init` helper this file used to carry.
fn make_connections_response(
diff --git a/src/openhuman/integrations/composio/ops_tests_part_01_tests.rs b/src/openhuman/integrations/composio/ops_tests_part_01_tests.rs
index a8c1de96a7..88a66015e3 100644
--- a/src/openhuman/integrations/composio/ops_tests_part_01_tests.rs
+++ b/src/openhuman/integrations/composio/ops_tests_part_01_tests.rs
@@ -386,178 +386,3 @@ async fn composio_delete_connection_via_mock() {
assert!(outcome.value.deleted);
}
-#[tokio::test]
-async fn composio_delete_connection_clear_memory_deletes_slack_source() {
- let _serialised = module_guard().await;
- let app = Router::new()
- .route(
- "/agent-integrations/composio/connections",
- get(|| async {
- Json(json!({
- "success": true,
- "data": {"connections": [
- {"id":"c1","toolkit":"slack","status":"ACTIVE"}
- ]}
- }))
- }),
- )
- .route(
- "/agent-integrations/composio/connections/{id}",
- axum::routing::delete(|Path(_id): Path| async move {
- Json(json!({"success": true, "data": {"deleted": true}}))
- }),
- );
- let base = start_mock_backend(app).await;
- let tmp = tempfile::tempdir().unwrap();
- let config = config_with_backend(&tmp, base);
- // The memory clear-out runs through the bound driver now that it is routed
- // onto `forget_matching`, so the test has to bind one. TinyCortex is the
- // engine the loadable module wraps, and unlike the module it is not a
- // process singleton, so several of these can share one test binary.
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&config);
- let target = sample_memory_chunk(SourceKind::Chat, "slack:c1", 0);
- let unrelated = sample_memory_chunk(SourceKind::Chat, "slack:c2", 0);
- memory_tree_store::upsert_chunks(&config, &[target, unrelated]).expect("chunks should seed");
-
- let outcome = composio_delete_connection(&config, "c1", true)
- .await
- .unwrap();
-
- assert!(outcome.value.deleted);
- assert_eq!(outcome.value.memory_chunks_deleted, 1);
- let remaining = memory_tree_store::list_chunks(
- &config,
- &memory_tree_store::ListChunksQuery {
- source_kind: Some(SourceKind::Chat),
- ..Default::default()
- },
- )
- .expect("chunks should list");
- assert_eq!(remaining.len(), 1);
- assert_eq!(remaining[0].metadata.source_id, "slack:c2");
-}
-
-/// #4: full path through the REAL `composio_delete_connection` handler
-/// (clear_memory=true, mock backend) — deleting a connection's last chunk must
-/// cascade away its source summary tree AND the summary's on-disk content file,
-/// not just the chunk rows. The tree is a real `get_or_create_source_tree`; the
-/// content file sits at the production `content_path` location.
-#[tokio::test]
-async fn composio_delete_connection_clear_memory_cascades_source_tree_and_content_file() {
- let _serialised = module_guard().await;
- use rusqlite::params;
- use tinymemory_core::store::trees::store as tree_store;
- use tinymemory_core::store::trees::types::{SummaryNode, TreeKind};
- use tinymemory_core::tree_source::registry::get_or_create_source_tree;
-
- let app = Router::new()
- .route(
- "/agent-integrations/composio/connections",
- get(|| async {
- Json(json!({
- "success": true,
- "data": {"connections": [
- {"id":"c1","toolkit":"slack","status":"ACTIVE"}
- ]}
- }))
- }),
- )
- .route(
- "/agent-integrations/composio/connections/{id}",
- axum::routing::delete(|Path(_id): Path| async move {
- Json(json!({"success": true, "data": {"deleted": true}}))
- }),
- );
- let base = start_mock_backend(app).await;
- let tmp = tempfile::tempdir().unwrap();
- let config = config_with_backend(&tmp, base);
- // The memory clear-out runs through the bound driver now that it is routed
- // onto `forget_matching`, so the test has to bind one. TinyCortex is the
- // engine the loadable module wraps, and unlike the module it is not a
- // process singleton, so several of these can share one test binary.
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&config);
-
- // One slack chunk for connection c1 → source_id `slack:c1`.
- let chunk = sample_memory_chunk(SourceKind::Chat, "slack:c1", 0);
- memory_tree_store::upsert_chunks(&config, &[chunk.clone()]).expect("seed chunk");
-
- // Real source tree for that source + a summary whose content file lives at
- // the production content-root location.
- let tree = get_or_create_source_tree(&config, "slack:c1").expect("source tree");
- let ts = Utc.timestamp_millis_opt(1_700_000_000_000).unwrap();
- let rel = "summaries/slack_c1/L1/sum-1.md";
- let abs = config.memory_tree_content_root().join(rel);
- std::fs::create_dir_all(abs.parent().unwrap()).unwrap();
- std::fs::write(&abs, "summarised slack body").unwrap();
-
- memory_tree_store::with_connection(&config, |conn| {
- let tx = conn.unchecked_transaction()?;
- tree_store::insert_summary_tx(
- &tx,
- &SummaryNode {
- id: "sum-1".into(),
- tree_id: tree.id.clone(),
- tree_kind: TreeKind::Source,
- level: 1,
- parent_id: None,
- child_ids: vec![chunk.id.clone()],
- content: "preview".into(),
- token_count: 3,
- entities: vec![],
- topics: vec![],
- time_range_start: ts,
- time_range_end: ts,
- score: 0.5,
- sealed_at: ts,
- deleted: false,
- embedding: None,
- doc_id: None,
- version_ms: None,
- },
- None,
- "test/model@3",
- )?;
- tx.execute(
- "UPDATE mem_tree_summaries SET content_path = ?1 WHERE id = 'sum-1'",
- params![rel],
- )?;
- tx.commit()?;
- Ok(())
- })
- .expect("seed summary + content file pointer");
-
- // sanity: tree + on-disk file exist before the disconnect.
- assert!(
- tree_store::get_tree_by_scope(&config, TreeKind::Source, "slack:c1")
- .unwrap()
- .is_some()
- );
- assert!(abs.exists());
-
- // ---- act: the REAL handler, clear_memory=true ----
- let outcome = composio_delete_connection(&config, "c1", true)
- .await
- .unwrap();
- assert!(outcome.value.deleted);
- assert_eq!(outcome.value.memory_chunks_deleted, 1);
-
- // chunk, source tree, summary row, AND on-disk content file are all gone.
- assert!(memory_tree_store::get_chunk(&config, &chunk.id)
- .unwrap()
- .is_none());
- assert!(
- tree_store::get_tree_by_scope(&config, TreeKind::Source, "slack:c1")
- .unwrap()
- .is_none()
- );
- memory_tree_store::with_connection(&config, |conn| {
- let n: i64 = conn.query_row("SELECT COUNT(*) FROM mem_tree_summaries", [], |r| r.get(0))?;
- assert_eq!(n, 0);
- Ok(())
- })
- .unwrap();
- assert!(
- !abs.exists(),
- "summary content file must be removed via the real handler cascade"
- );
-}
diff --git a/src/openhuman/integrations/composio/ops_tests_part_02_tests.rs b/src/openhuman/integrations/composio/ops_tests_part_02_tests.rs
index 6cfcf51193..775c1cd7a9 100644
--- a/src/openhuman/integrations/composio/ops_tests_part_02_tests.rs
+++ b/src/openhuman/integrations/composio/ops_tests_part_02_tests.rs
@@ -1,307 +1,5 @@
use super::*;
-/// #4 (full live seal): like the above, but the summary + on-disk file are
-/// produced by the REAL `seal_one_level` pipeline (staged chunk body →
-/// summarise → `stage_summary`), not hand-written. Then the REAL
-/// `composio_delete_connection(clear_memory=true)` handler must cascade the
-/// tree, the summary row, AND the seal-produced content file away.
-#[tokio::test]
-async fn composio_delete_connection_clear_memory_cascades_live_sealed_tree_and_file() {
- let _serialised = module_guard().await;
- use tinymemory_core::store::chunks::store::{
- get_summary_content_pointers, upsert_staged_chunks_tx,
- };
- use tinymemory_core::store::content::stage_chunks;
- use tinymemory_core::store::trees::store as tree_store;
- use tinymemory_core::store::trees::types::{Buffer, TreeKind};
- use tinymemory_core::tree::tree::bucket_seal::{seal_one_level, LabelStrategy};
- use tinymemory_core::tree_source::registry::get_or_create_source_tree;
-
- let app = Router::new()
- .route(
- "/agent-integrations/composio/connections",
- get(|| async {
- Json(json!({
- "success": true,
- "data": {"connections": [
- {"id":"c1","toolkit":"slack","status":"ACTIVE"}
- ]}
- }))
- }),
- )
- .route(
- "/agent-integrations/composio/connections/{id}",
- axum::routing::delete(|Path(_id): Path| async move {
- Json(json!({"success": true, "data": {"deleted": true}}))
- }),
- );
- let base = start_mock_backend(app).await;
- let tmp = tempfile::tempdir().unwrap();
- let mut config = config_with_backend(&tmp, base);
- // The memory clear-out runs through the bound driver now that it is routed
- // onto `forget_matching`, so the test has to bind one. TinyCortex is the
- // engine the loadable module wraps, and unlike the module it is not a
- // process singleton, so several of these can share one test binary.
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&config);
- // Force the inert embedder so the real seal's summary-embed step doesn't
- // reach a live endpoint. `config_with_backend` stores a cloud session +
- // api_url, so the factory would otherwise build a *cloud* embedder against
- // the mock (no embeddings route). `embeddings_provider = "none"` is the
- // actual switch that selects `InertEmbedder`.
- config.embeddings_provider = Some("none".to_string());
- config.memory_tree.embedding_endpoint = None;
- config.memory_tree.embedding_model = None;
- config.memory_tree.embedding_strict = false;
-
- // Real chunk for slack:c1 WITH its body staged to disk, so the seal's
- // `hydrate_leaf_inputs` → `read_chunk_body` can resolve it.
- let chunk = sample_memory_chunk(SourceKind::Chat, "slack:c1", 0);
- memory_tree_store::upsert_chunks(&config, &[chunk.clone()]).expect("seed chunk");
- let staged = stage_chunks(
- &config.memory_tree_content_root(),
- std::slice::from_ref(&chunk),
- )
- .expect("stage chunk body");
- memory_tree_store::with_connection(&config, |conn| {
- let tx = conn.unchecked_transaction()?;
- upsert_staged_chunks_tx(&tx, &staged)?;
- tx.commit()?;
- Ok(())
- })
- .expect("record staged chunk pointer");
-
- // Run the REAL seal — produces a genuine summary row + on-disk file.
- let tree = get_or_create_source_tree(&config, "slack:c1").expect("source tree");
- let buf = Buffer {
- tree_id: tree.id.clone(),
- level: 0,
- item_ids: vec![chunk.id.clone()],
- token_sum: i64::from(chunk.token_count),
- oldest_at: Some(chunk.metadata.time_range.0),
- };
- memory_tree_store::with_connection(&config, |conn| {
- let tx = conn.unchecked_transaction()?;
- tree_store::upsert_buffer_tx(&tx, &buf)?;
- tx.commit()?;
- Ok(())
- })
- .expect("persist buffer snapshot");
- let summary_id = seal_one_level(&config, &tree, &buf, &LabelStrategy::Empty, false)
- .await
- .expect("real seal produces a summary");
-
- // The seal wrote a real on-disk content file for the summary.
- let (rel, _sha) = get_summary_content_pointers(&config, &summary_id)
- .unwrap()
- .expect("seal staged a summary content file");
- let abs = {
- let mut p = config.memory_tree_content_root();
- for c in rel.split('/') {
- p.push(c);
- }
- p
- };
- assert!(
- abs.exists(),
- "seal must have written a summary file on disk"
- );
- assert!(
- tree_store::get_tree_by_scope(&config, TreeKind::Source, "slack:c1")
- .unwrap()
- .is_some()
- );
-
- // ---- act: REAL handler, clear_memory=true ----
- let outcome = composio_delete_connection(&config, "c1", true)
- .await
- .unwrap();
- assert!(outcome.value.deleted);
- assert_eq!(outcome.value.memory_chunks_deleted, 1);
-
- // chunk, tree, summary row, and the seal-produced file are all gone.
- assert!(memory_tree_store::get_chunk(&config, &chunk.id)
- .unwrap()
- .is_none());
- assert!(
- tree_store::get_tree_by_scope(&config, TreeKind::Source, "slack:c1")
- .unwrap()
- .is_none()
- );
- assert!(tree_store::get_summary(&config, &summary_id)
- .unwrap()
- .is_none());
- assert!(
- !abs.exists(),
- "seal-produced summary file must be removed via the real handler cascade"
- );
-}
-
-#[tokio::test]
-async fn composio_delete_connection_clear_memory_keeps_other_gmail_connections() {
- let _serialised = module_guard().await;
- let app = Router::new()
- .route(
- "/agent-integrations/composio/connections",
- get(|| async {
- Json(json!({
- "success": true,
- "data": {"connections": [
- {"id":"c1","toolkit":"gmail","status":"ACTIVE"},
- {"id":"c2","toolkit":"gmail","status":"ACTIVE"}
- ]}
- }))
- }),
- )
- .route(
- "/agent-integrations/composio/connections/{id}",
- axum::routing::delete(|Path(_id): Path| async move {
- Json(json!({"success": true, "data": {"deleted": true}}))
- }),
- );
- let base = start_mock_backend(app).await;
- let tmp = tempfile::tempdir().unwrap();
- let config = config_with_backend(&tmp, base);
- // The memory clear-out runs through the bound driver now that it is routed
- // onto `forget_matching`, so the test has to bind one. TinyCortex is the
- // engine the loadable module wraps, and unlike the module it is not a
- // process singleton, so several of these can share one test binary.
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&config);
- let c1_account = sample_memory_chunk_with_owner(
- SourceKind::Email,
- "gmail:pilot-at-example-dot-com",
- "gmail-sync:c1",
- 0,
- );
- let c2_account = sample_memory_chunk_with_owner(
- SourceKind::Email,
- "gmail:pilot-at-example-dot-com",
- "gmail-sync:c2",
- 1,
- );
- let c1_connection_scoped =
- sample_memory_chunk_with_owner(SourceKind::Email, "gmail:c1:thread-a", "gmail-sync:c1", 2);
- let c2_connection_scoped =
- sample_memory_chunk_with_owner(SourceKind::Email, "gmail:c2:thread-b", "gmail-sync:c2", 3);
- memory_tree_store::upsert_chunks(
- &config,
- &[
- c1_account,
- c2_account.clone(),
- c1_connection_scoped,
- c2_connection_scoped.clone(),
- ],
- )
- .expect("chunks should seed");
-
- let outcome = composio_delete_connection(&config, "c1", true)
- .await
- .unwrap();
-
- assert!(outcome.value.deleted);
- assert_eq!(outcome.value.memory_chunks_deleted, 2);
- let remaining = memory_tree_store::list_chunks(
- &config,
- &memory_tree_store::ListChunksQuery {
- source_kind: Some(SourceKind::Email),
- ..Default::default()
- },
- )
- .expect("chunks should list");
- assert_eq!(remaining.len(), 2);
- assert!(remaining.iter().any(|chunk| chunk.id == c2_account.id));
- assert!(remaining
- .iter()
- .any(|chunk| chunk.id == c2_connection_scoped.id));
-}
-
-#[tokio::test]
-async fn notion_cleanup_targets_include_synced_page_sources() {
- // The embedding seam fails loudly when unwired. Installed here rather
- // than relied upon from another test: `install_for_tests` is
- // `Once`-guarded, so a test that omits it passes only while some
- // earlier test in the same binary happened to run first.
- crate::openhuman::memory::host_impls::install_for_tests();
- let tmp = tempfile::tempdir().unwrap();
- let config = test_config(&tmp);
- // The cleanup targets are read back through the bound driver now, so the
- // test has to bind one — the writes below go through a client over the
- // same workspace, and an unbound config resolves to the null driver,
- // which serves nothing and would report no targets at all.
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&config);
- let memory = std::sync::Arc::new(
- MemoryClient::from_workspace_dir(config.workspace_dir.clone())
- .expect("memory client should initialise"),
- );
- // tinymemory v1.13.4 deleted the whole in-process Composio pipeline —
- // `sync_state::PersistedSyncState`/`HostSyncAdapter` included — so there is
- // no extension trait to save through any more. `memory_cleanup.rs`'s reader
- // deserialises this row as `tinycortex::memory::sync::SyncState` (the same
- // shape tinycortex's own sync layer writes), so the test writes that type
- // straight through the KV store instead.
- let mut state = tinycortex::memory::sync::SyncState::new("notion", "conn-1");
- state.mark_synced("page-a@2026-01-01T00:00:00Z");
- state.mark_synced("page-b");
- memory
- .kv_set(
- Some(tinycortex::memory::sync::state::STATE_NAMESPACE),
- "notion:conn-1",
- &serde_json::to_value(&state).expect("sync state should serialize"),
- )
- .await
- .expect("sync state should save");
-
- let targets = composio_memory_targets_for_connection(&config, Some("notion"), "conn-1")
- .await
- .expect("notion cleanup targets should resolve");
-
- assert!(targets.contains(&MemoryCleanupTarget::Exact(
- SourceKind::Document,
- "notion:page-a".to_string()
- )));
- assert!(targets.contains(&MemoryCleanupTarget::Exact(
- SourceKind::Document,
- "notion:page-b".to_string()
- )));
- assert!(targets.contains(&MemoryCleanupTarget::Exact(
- SourceKind::Document,
- "composio-notion-page-page-a".to_string()
- )));
-}
-
-#[tokio::test]
-async fn notion_cleanup_targets_surface_corrupt_sync_state() {
- // The embedding seam fails loudly when unwired. Installed here rather
- // than relied upon from another test: `install_for_tests` is
- // `Once`-guarded, so a test that omits it passes only while some
- // earlier test in the same binary happened to run first.
- crate::openhuman::memory::host_impls::install_for_tests();
- let tmp = tempfile::tempdir().unwrap();
- let config = test_config(&tmp);
- // The cleanup targets are read back through the bound driver now, so the
- // test has to bind one — the writes below go through a client over the
- // same workspace, and an unbound config resolves to the null driver,
- // which serves nothing and would report no targets at all.
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&config);
- let memory = std::sync::Arc::new(
- MemoryClient::from_workspace_dir(config.workspace_dir.clone())
- .expect("memory client should initialise"),
- );
- memory
- .kv_set(
- Some(tinycortex::memory::sync::state::STATE_NAMESPACE),
- "notion:conn-1",
- &serde_json::json!({ "toolkit": 42 }),
- )
- .await
- .expect("corrupt sync state should be written");
-
- let err = composio_memory_targets_for_connection(&config, Some("notion"), "conn-1")
- .await
- .expect_err("corrupt sync state should surface");
-
- assert!(err.to_string().contains("failed to load notion sync state"));
-}
-
#[tokio::test]
async fn drive_cleanup_targets_are_connection_scoped() {
// The embedding seam fails loudly when unwired; same reasoning as the
@@ -312,7 +10,7 @@ async fn drive_cleanup_targets_are_connection_scoped() {
// The drive arm never touches the store, but discovery takes the caller's
// client unconditionally — the parameter is the seam the notion tests
// inject through.
- let drive_memory = std::sync::Arc::new(
+ let _drive_memory = std::sync::Arc::new(
MemoryClient::from_workspace_dir(config.workspace_dir.clone())
.expect("memory client should initialise"),
);
@@ -485,164 +183,3 @@ async fn composio_execute_via_mock_propagates_backend_error() {
assert!(err.contains("rate limited"), "got: {err}");
}
-#[tokio::test]
-async fn composio_sync_gmail_via_mock_ingests_records_and_updates_outcome() {
- let _serialised = module_guard().await;
- // The embedding seam fails loudly when unwired. Installed here rather
- // than relied upon from another test: `install_for_tests` is
- // `Once`-guarded, so a test that omits it passes only while some
- // earlier test in the same binary happened to run first.
- crate::openhuman::memory::host_impls::install_for_tests();
- let _serial = crate::openhuman::memory::ops::GLOBAL_MEMORY_TEST_LOCK
- .lock()
- .await;
- use crate::openhuman::config::TEST_ENV_LOCK;
- let _cache_guard = cache_guard();
- let _env_guard = TEST_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
- // See composio_get_user_profile_via_mock_returns_provider_profile: this
- // test also mutates BACKEND_URL via EnvVarGuard below, which needs the
- // crate-wide lock to serialize against api::config / core::cli_tests /
- // medulla's tests on the same process-global var.
- let _backend_env_guard = crate::api::config::backend_env_test_lock();
-
- let app = Router::new()
- .route(
- "/agent-integrations/composio/connections",
- get(|| async {
- Json(json!({
- "success": true,
- "data": {"connections": [
- {"id":"c1","toolkit":"gmail","status":"ACTIVE"}
- ]}
- }))
- }),
- )
- .route(
- "/agent-integrations/composio/execute",
- post(|Json(body): Json| async move {
- let action = body
- .get("tool")
- .and_then(Value::as_str)
- .or_else(|| body.get("action").and_then(Value::as_str))
- .unwrap_or("");
- let data = match action {
- "GMAIL_GET_PROFILE" => json!({
- "emailAddress": "pilot@example.com",
- "displayName": "Phoenix Pilot"
- }),
- "GMAIL_FETCH_EMAILS" => json!({
- "messages": [{
- "messageId": "gmail-msg-1",
- "threadId": "gmail-thread-1",
- "sender": "captain@example.com",
- "to": "pilot@example.com",
- "subject": "Phoenix launch canary",
- "messageTimestamp": "2024-06-01T12:00:00Z",
- "labelIds": ["INBOX"],
- "markdownFormatted": "Phoenix launch canary body for mock sync coverage.",
- "payload": {}
- }]
- }),
- other => panic!("unexpected action: {other}"),
- };
- Json(json!({
- "success": true,
- "data": {
- "successful": true,
- "data": data,
- "error": null
- }
- }))
- }),
- );
- let base = start_mock_backend(app).await;
- // The provider action reloads config with env overlays before executing.
- // Keep that reload on the mock even when the runner exports BACKEND_URL.
- let _backend_url_guard = EnvVarGuard::set("BACKEND_URL", &base);
- let tmp = tempfile::tempdir().unwrap();
- let mut config = config_with_backend(&tmp, base);
- config.memory_tree.embedding_strict = false;
- let _workspace_env_guard = WorkspaceEnvGuard::set(tmp.path());
- config.save().await.unwrap();
- // The sync writes through the bound driver now, so the fixture binds one and
- // the read-back goes to the same place. Binding the global slot instead would
- // have the test write to one client and read from another — zero documents,
- // looking exactly like a sync that silently did nothing.
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&config);
- // And the seams are re-installed against THIS config, not the
- // `Config::default()` that `install_for_tests` latched. Proxied Composio
- // resolves its bearer through `ComposioHost::session_bearer`, which reads
- // the installed host config — a default one has no session, so the sync
- // would refuse before reaching the mock backend. The setters overwrite, so
- // calling this after the latched install is what points the seam at the
- // config carrying the mock's URL and token.
- crate::openhuman::memory::host_impls::install_memory_host_seams(std::sync::Arc::new(
- config.clone(),
- ));
-
- let outcome = composio_sync(&config, "c1", Some("manual".to_string()))
- .await
- .unwrap();
-
- assert_eq!(outcome.value.toolkit, "gmail");
- assert_eq!(outcome.value.connection_id.as_deref(), Some("c1"));
- // composio_sync is now spawn-and-return: the immediate envelope is a
- // "started" sentinel, and the actual ingestion runs on a detached
- // tokio task. items_ingested == 0 / finished_at_ms == 0 / summary
- // contains "started" are the contract of that sentinel.
- assert_eq!(
- outcome.value.items_ingested, 0,
- "spawn-and-return: items_ingested on the immediate envelope is a 'started' sentinel, not a final count"
- );
- assert_eq!(
- outcome.value.finished_at_ms, 0,
- "spawn-and-return: finished_at_ms == 0 means 'task spawned, not yet complete'"
- );
- assert!(
- outcome.value.summary.contains("started"),
- "expected spawn-and-return summary to mention 'started', got: {}",
- outcome.value.summary
- );
-
- // Poll for the spawned ingest task to write the records into memory.
- //
- // The namespace is `source:` because the sync now hands its
- // records to the bound driver's `accept_source_items` rather than writing a
- // provider-shaped skill document itself. That is the whole point of the
- // split — the module reads, this crate ingests — so reading them back the
- // way memory files them is what proves the two halves met.
- let documents = {
- let mut documents = Vec::new();
- for _ in 0..50 {
- let binding = crate::openhuman::memory::binding::for_config(&config)
- .expect("the fixture bound a driver");
- documents = binding
- .provider()
- .as_documents()
- .expect("the bound driver serves documents")
- .list_documents(Some("source:gmail:c1"))
- .await
- .unwrap()
- .get("documents")
- .and_then(Value::as_array)
- .cloned()
- .unwrap_or_default();
- if !documents.is_empty() {
- break;
- }
- tokio::time::sleep(std::time::Duration::from_millis(100)).await;
- }
- documents
- };
- assert_eq!(
- documents.len(),
- 1,
- "expected one ingested Gmail record after the spawned task drains"
- );
- let document = &documents[0];
- assert_eq!(document["title"], "Phoenix launch canary");
- // `external_sync` is what stops a third party's words being treated later
- // as the user's own. A sync that ingested without it would be worse than
- // one that ingested nothing.
- assert_eq!(document["taint"], "external_sync");
-}
diff --git a/src/openhuman/integrations/composio/ops_tests_part_04_tests.rs b/src/openhuman/integrations/composio/ops_tests_part_04_tests.rs
index 5790018ac8..bd7579199a 100644
--- a/src/openhuman/integrations/composio/ops_tests_part_04_tests.rs
+++ b/src/openhuman/integrations/composio/ops_tests_part_04_tests.rs
@@ -458,7 +458,7 @@ async fn enrich_does_nothing_when_no_cached_identities() {
// returns `Vec::new()` and the connection is returned unchanged.
let tmp = tempfile::tempdir().unwrap();
let config = test_config(&tmp);
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&config);
+ crate::openhuman::memory::test_support::install_memory_driver_for_test(&config);
let resp = make_connections_response(&[("c1", "gmail", "ACTIVE")]);
let enriched = enrich_connections_with_identity(&config, resp).await;
assert_eq!(enriched.connections.len(), 1);
@@ -467,87 +467,13 @@ async fn enrich_does_nothing_when_no_cached_identities() {
assert!(enriched.connections[0].username.is_none());
}
-#[tokio::test]
-async fn enrich_populates_email_from_cached_profile() {
- use crate::openhuman::integrations::composio::identity_store::persist_provider_profile;
- use tinymemory_api::composio::ProviderUserProfile;
-
- let tmp = tempfile::tempdir().unwrap();
- let config = test_config(&tmp);
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&config);
-
- persist_provider_profile(
- &config,
- &ProviderUserProfile {
- toolkit: "gmail".to_string(),
- connection_id: Some("conn-gmail-1".to_string()),
- email: Some("alice@example.com".to_string()),
- display_name: Some("Alice Smith".to_string()),
- ..Default::default()
- },
- )
- .await
- .expect("persist provider profile");
-
- let resp = make_connections_response(&[("conn-gmail-1", "gmail", "ACTIVE")]);
- let enriched = enrich_connections_with_identity(&config, resp).await;
-
- assert_eq!(
- enriched.connections[0].account_email.as_deref(),
- Some("alice@example.com"),
- "email should be populated from cached gmail profile"
- );
- assert_eq!(
- enriched.connections[0].workspace.as_deref(),
- Some("Alice Smith"),
- "workspace (display_name) should be populated"
- );
- assert!(
- enriched.connections[0].username.is_none(),
- "username (handle) should be absent for gmail"
- );
-}
-
-#[tokio::test]
-async fn enrich_populates_handle_for_github() {
- use crate::openhuman::integrations::composio::identity_store::persist_provider_profile;
- use tinymemory_api::composio::ProviderUserProfile;
-
- let tmp = tempfile::tempdir().unwrap();
- let config = test_config(&tmp);
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&config);
-
- persist_provider_profile(
- &config,
- &ProviderUserProfile {
- toolkit: "github".to_string(),
- connection_id: Some("conn-gh-1".to_string()),
- username: Some("octocat".to_string()),
- ..Default::default()
- },
- )
- .await
- .expect("persist provider profile");
-
- let resp = make_connections_response(&[("conn-gh-1", "github", "ACTIVE")]);
- let enriched = enrich_connections_with_identity(&config, resp).await;
-
- // GitHub uses `handle` kind (the catch-all branch in expand_identity_rows).
- assert_eq!(
- enriched.connections[0].username.as_deref(),
- Some("octocat"),
- "username (handle) should be populated for github"
- );
- assert!(enriched.connections[0].account_email.is_none());
-}
-
#[tokio::test]
async fn enrich_skips_connection_already_having_identity() {
// If the backend-proxied path already populated account_email, the
// enricher must NOT overwrite it with a potentially stale cached value.
let tmp = tempfile::tempdir().unwrap();
let config = test_config(&tmp);
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&config);
+ crate::openhuman::memory::test_support::install_memory_driver_for_test(&config);
let mut resp = make_connections_response(&[("c-preloaded", "gmail", "ACTIVE")]);
resp.connections[0].account_email = Some("preloaded@example.com".to_string());
@@ -560,57 +486,6 @@ async fn enrich_skips_connection_already_having_identity() {
);
}
-#[tokio::test]
-async fn enrich_handles_multiple_connections_same_toolkit() {
- // Two Gmail accounts — each gets its own identity label, not "Account N".
- use crate::openhuman::integrations::composio::identity_store::persist_provider_profile;
- use tinymemory_api::composio::ProviderUserProfile;
-
- let tmp = tempfile::tempdir().unwrap();
- let config = test_config(&tmp);
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&config);
-
- persist_provider_profile(
- &config,
- &ProviderUserProfile {
- toolkit: "gmail".to_string(),
- connection_id: Some("g1".to_string()),
- email: Some("alice@example.com".to_string()),
- ..Default::default()
- },
- )
- .await
- .expect("persist provider profile");
- persist_provider_profile(
- &config,
- &ProviderUserProfile {
- toolkit: "gmail".to_string(),
- connection_id: Some("g2".to_string()),
- email: Some("bob@example.com".to_string()),
- ..Default::default()
- },
- )
- .await
- .expect("persist provider profile");
-
- let resp = make_connections_response(&[("g1", "gmail", "ACTIVE"), ("g2", "gmail", "ACTIVE")]);
- let enriched = enrich_connections_with_identity(&config, resp).await;
-
- let emails: Vec<_> = enriched
- .connections
- .iter()
- .map(|c| c.account_email.as_deref())
- .collect();
- assert!(
- emails.contains(&Some("alice@example.com")),
- "first gmail account should carry alice's email"
- );
- assert!(
- emails.contains(&Some("bob@example.com")),
- "second gmail account should carry bob's email"
- );
-}
-
#[tokio::test]
async fn enrich_leaves_unmatched_connection_unchanged() {
// Connection whose id has no cached profile row is returned with all
@@ -620,7 +495,7 @@ async fn enrich_leaves_unmatched_connection_unchanged() {
let tmp = tempfile::tempdir().unwrap();
let config = test_config(&tmp);
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&config);
+ crate::openhuman::memory::test_support::install_memory_driver_for_test(&config);
// Persist a profile for a DIFFERENT connection id.
persist_provider_profile(
diff --git a/src/openhuman/memory/query/ingest_document_tests.rs b/src/openhuman/memory/query/ingest_document_tests.rs
index 10700e9ecd..b11f78a3c0 100644
--- a/src/openhuman/memory/query/ingest_document_tests.rs
+++ b/src/openhuman/memory/query/ingest_document_tests.rs
@@ -7,7 +7,6 @@ use crate::openhuman::config::Config;
use crate::openhuman::config::TEST_ENV_LOCK;
use crate::openhuman::tools::traits::Tool;
use serde_json::json;
-use tinymemory_api::chunks::SourceRef;
struct WorkspaceEnvGuard {
_lock: std::sync::MutexGuard<'static, ()>,
@@ -46,7 +45,7 @@ async fn isolated_config(tmp: &TempDir) -> (WorkspaceEnvGuard, Config) {
// write; it is the driver the loadable module wraps, which is as close
// to production as a test process can get (a dlopen'ed module is a
// process singleton a unit test cannot load).
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&config);
+ crate::openhuman::memory::test_support::install_memory_driver_for_test(&config);
(guard, config)
}
@@ -155,92 +154,3 @@ async fn execute_rejects_blank_required_fields() {
assert!(result.is_error);
}
-#[tokio::test]
-async fn execute_success_path_roundtrips_document_chunk() {
- let tmp = TempDir::new().expect("tempdir");
- let (_workspace, cfg) = isolated_config(&tmp).await;
- let tool = MemoryTreeIngestDocumentTool;
- let result = tool
- .execute(json!({
- "title": "Doc title",
- "body": "Body text with a memorable launch detail.",
- "source_id": "doc-1",
- "provider": "web",
- "source_ref": "https://example.test/doc-1",
- "owner": "owner-1"
- }))
- .await
- .expect("valid request should succeed in the isolated test environment");
- assert!(!result.is_error);
- let text = result.text();
- assert!(
- text.contains("Ingested document \"Doc title\" as source_id=doc-1."),
- "unexpected success payload: {text}"
- );
-
- let listed = rpc::list_chunks_rpc(
- &cfg,
- rpc::ListChunksRequest {
- source_kind: Some("document".into()),
- source_id: Some("doc-1".into()),
- owner: Some("owner-1".into()),
- limit: Some(10),
- ..Default::default()
- },
- )
- .await
- .expect("list chunks after tool execute")
- .value
- .chunks;
- assert_eq!(listed.len(), 1);
- assert!(
- listed[0]
- .content
- .contains("Body text with a memorable launch detail."),
- "stored chunk missing document body: {}",
- listed[0].content
- );
- assert_eq!(listed[0].metadata.owner, "owner-1");
- assert_eq!(
- listed[0].metadata.source_ref,
- Some(SourceRef::new("https://example.test/doc-1"))
- );
-}
-
-#[tokio::test]
-async fn execute_duplicate_source_id_reports_zero_new_chunks() {
- let tmp = TempDir::new().expect("tempdir");
- let (_workspace, cfg) = isolated_config(&tmp).await;
- let tool = MemoryTreeIngestDocumentTool;
- let args = json!({
- "title": "Doc title",
- "body": "Body text",
- "source_id": "doc-dup"
- });
-
- let first = tool.execute(args.clone()).await.expect("first execute");
- let second = tool.execute(args).await.expect("second execute");
- assert!(!first.is_error);
- assert!(!second.is_error);
- assert!(first.text().contains("1 chunks created and indexed."));
- assert!(second.text().contains("0 chunks created and indexed."));
-
- let listed = rpc::list_chunks_rpc(
- &cfg,
- rpc::ListChunksRequest {
- source_kind: Some("document".into()),
- source_id: Some("doc-dup".into()),
- limit: Some(10),
- ..Default::default()
- },
- )
- .await
- .expect("list chunks after duplicate execute")
- .value
- .chunks;
- assert_eq!(
- listed.len(),
- 1,
- "duplicate source_id should not create extra chunks"
- );
-}
diff --git a/src/openhuman/memory/read_rpc/admin_tests.rs b/src/openhuman/memory/read_rpc/admin_tests.rs
index ac7d313bef..e8282283e9 100644
--- a/src/openhuman/memory/read_rpc/admin_tests.rs
+++ b/src/openhuman/memory/read_rpc/admin_tests.rs
@@ -114,49 +114,6 @@ async fn delete_source_rejects_a_blank_source_id_before_touching_a_driver() {
);
}
-/// An unknown source removes nothing and cleans no tree, and the host maps that
-/// all-zero `ForgetOutcome` onto `deleted: false`.
-///
-/// The mapping is the whole point of the assertion: `deleted` is now an OR over
-/// **two** counts, and a source that matched nothing must not read as one whose
-/// stranded summary tree was swept.
-#[tokio::test]
-async fn delete_source_is_idempotent_for_an_unknown_source_id() {
- let (_tmp, cfg) = test_config();
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
-
- let outcome = delete_source_rpc(&cfg, "notion:never-ingested".to_string())
- .await
- .expect("an unknown source is not an error")
- .value;
- assert!(!outcome.deleted);
- assert_eq!(outcome.chunks_removed, 0);
-}
-
-/// The source id can embed user-linked identifiers, so it is hashed into the
-/// log line rather than written out. Pinned here because the log is assembled
-/// beside the response and is easy to "improve" into a leak.
-#[tokio::test]
-async fn delete_source_never_logs_the_raw_source_id() {
- let (_tmp, cfg) = test_config();
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
-
- let source_id = "notion:alice@example.com/private-page";
- let outcome = delete_source_rpc(&cfg, source_id.to_string())
- .await
- .expect("an unknown source is not an error");
- assert!(
- !outcome.logs[0].contains(source_id),
- "log leaked the source id: {}",
- outcome.logs[0]
- );
- assert!(
- outcome.logs[0].contains("source_id_hash="),
- "log should carry the redacted id: {}",
- outcome.logs[0]
- );
-}
-
/// openhuman#6012. Same distinction the wipes above turn on: a backfill the
/// driver cannot perform must not read as `scanned: 0` success. A caller seeing
/// that concludes their stored records are already treed and stops looking —
diff --git a/src/openhuman/memory/read_rpc/mod.rs b/src/openhuman/memory/read_rpc/mod.rs
index ebc2362c3c..1881ef8a2b 100644
--- a/src/openhuman/memory/read_rpc/mod.rs
+++ b/src/openhuman/memory/read_rpc/mod.rs
@@ -61,8 +61,6 @@ pub(crate) fn parse_source_kind_str(s: &str) -> Option (TempDir, Config) {
let tmp = TempDir::new().unwrap();
diff --git a/src/openhuman/memory/read_rpc_tests_part_01_tests.rs b/src/openhuman/memory/read_rpc_tests_part_01_tests.rs
index 760c80e4a1..dcf8f27655 100644
--- a/src/openhuman/memory/read_rpc_tests_part_01_tests.rs
+++ b/src/openhuman/memory/read_rpc_tests_part_01_tests.rs
@@ -1,496 +1,5 @@
use super::*;
-#[tokio::test]
-async fn list_chunks_returns_seeded_chunk() {
- let (_tmp, cfg) = test_config();
- // These handlers read through the bound driver now that the raw SQL is
- // gone, so the test has to bind one. TinyCortex is the engine the
- // loadable module wraps, so this exercises the same code production
- // reaches over the bus — and unlike the module it is not a process
- // singleton, which is what lets these run in one test binary.
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
- seed_chat_chunk(&cfg, "slack:#eng", "hello @alice phoenix migration").await;
- let resp = list_chunks_rpc(&cfg, ChunkFilter::default())
- .await
- .unwrap()
- .value;
- assert!(!resp.chunks.is_empty());
- assert_eq!(resp.total, resp.chunks.len() as u64);
-}
-
-#[tokio::test]
-async fn list_chunks_filters_by_source_id() {
- let (_tmp, cfg) = test_config();
- // These handlers read through the bound driver now that the raw SQL is
- // gone, so the test has to bind one. TinyCortex is the engine the
- // loadable module wraps, so this exercises the same code production
- // reaches over the bus — and unlike the module it is not a process
- // singleton, which is what lets these run in one test binary.
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
- seed_chat_chunk(&cfg, "slack:#a", "alpha").await;
- seed_chat_chunk(&cfg, "slack:#b", "beta").await;
- let only_a = list_chunks_rpc(
- &cfg,
- ChunkFilter {
- source_ids: Some(vec!["slack:#a".into()]),
- ..ChunkFilter::default()
- },
- )
- .await
- .unwrap()
- .value;
- assert!(only_a.chunks.iter().all(|c| c.source_id == "slack:#a"));
- assert!(only_a.total >= 1);
-}
-
-#[tokio::test]
-async fn list_chunks_query_substring_works() {
- let (_tmp, cfg) = test_config();
- // These handlers read through the bound driver now that the raw SQL is
- // gone, so the test has to bind one. TinyCortex is the engine the
- // loadable module wraps, so this exercises the same code production
- // reaches over the bus — and unlike the module it is not a process
- // singleton, which is what lets these run in one test binary.
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
- seed_chat_chunk(&cfg, "slack:#eng", "phoenix migration ships friday").await;
- seed_chat_chunk(&cfg, "slack:#eng", "different unrelated text").await;
- let resp = list_chunks_rpc(
- &cfg,
- ChunkFilter {
- query: Some("phoenix".into()),
- ..ChunkFilter::default()
- },
- )
- .await
- .unwrap()
- .value;
- assert!(resp.chunks.iter().any(|c| {
- c.content_preview
- .as_deref()
- .unwrap_or("")
- .contains("phoenix")
- }));
-}
-
-#[tokio::test]
-async fn list_chunks_filters_by_source_kind_and_applies_limit_offset() {
- let (_tmp, cfg) = test_config();
- // These handlers read through the bound driver now that the raw SQL is
- // gone, so the test has to bind one. TinyCortex is the engine the
- // loadable module wraps, so this exercises the same code production
- // reaches over the bus — and unlike the module it is not a process
- // singleton, which is what lets these run in one test binary.
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
- seed_chat_chunk(&cfg, "slack:#a", "first chat").await;
- seed_chat_chunk(&cfg, "slack:#b", "second chat").await;
-
- let filtered = list_chunks_rpc(
- &cfg,
- ChunkFilter {
- source_kinds: Some(vec!["chat".into()]),
- limit: Some(1),
- offset: Some(1),
- ..ChunkFilter::default()
- },
- )
- .await
- .unwrap()
- .value;
- assert_eq!(filtered.chunks.len(), 1);
- assert_eq!(filtered.total, 2);
- assert!(filtered.chunks.iter().all(|c| c.source_kind == "chat"));
-}
-
-#[tokio::test]
-async fn list_chunks_filters_by_entity_id_and_time_window() {
- let (_tmp, cfg) = test_config();
- // These handlers read through the bound driver now that the raw SQL is
- // gone, so the test has to bind one. TinyCortex is the engine the
- // loadable module wraps, so this exercises the same code production
- // reaches over the bus — and unlike the module it is not a process
- // singleton, which is what lets these run in one test binary.
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
- seed_chat_chunk(&cfg, "slack:#eng", "alice@example.com handles phoenix").await;
- seed_chat_chunk(&cfg, "slack:#eng", "bob@example.com handles atlas").await;
-
- let seeded = list_chunks_rpc(&cfg, ChunkFilter::default())
- .await
- .unwrap()
- .value
- .chunks;
- let alice = seeded
- .iter()
- .find(|chunk| {
- chunk
- .content_preview
- .as_deref()
- .unwrap_or("")
- .contains("alice@example.com")
- })
- .expect("alice chunk present");
- let bob = seeded
- .iter()
- .find(|chunk| {
- chunk
- .content_preview
- .as_deref()
- .unwrap_or("")
- .contains("bob@example.com")
- })
- .expect("bob chunk present");
-
- update_chunk_timestamp(&cfg, &alice.id, 1_700_000_000_100);
- update_chunk_timestamp(&cfg, &bob.id, 1_700_000_000_900);
-
- let filtered = list_chunks_rpc(
- &cfg,
- ChunkFilter {
- entity_ids: Some(vec!["email:alice@example.com".into()]),
- since_ms: Some(1_700_000_000_000),
- until_ms: Some(1_700_000_000_500),
- ..ChunkFilter::default()
- },
- )
- .await
- .unwrap()
- .value;
-
- assert_eq!(filtered.total, 1);
- assert_eq!(filtered.chunks.len(), 1);
- assert_eq!(filtered.chunks[0].id, alice.id);
-}
-
-#[tokio::test]
-async fn list_chunks_ignores_empty_filter_lists_and_blank_query() {
- let (_tmp, cfg) = test_config();
- // These handlers read through the bound driver now that the raw SQL is
- // gone, so the test has to bind one. TinyCortex is the engine the
- // loadable module wraps, so this exercises the same code production
- // reaches over the bus — and unlike the module it is not a process
- // singleton, which is what lets these run in one test binary.
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
- seed_chat_chunk(&cfg, "slack:#a", "alpha").await;
- seed_chat_chunk(&cfg, "slack:#b", "beta").await;
-
- let resp = list_chunks_rpc(
- &cfg,
- ChunkFilter {
- source_kinds: Some(vec![]),
- source_ids: Some(vec![]),
- entity_ids: Some(vec![]),
- query: Some(" ".into()),
- limit: Some(10),
- ..ChunkFilter::default()
- },
- )
- .await
- .unwrap()
- .value;
-
- assert_eq!(resp.total, 2);
- assert_eq!(resp.chunks.len(), 2);
-}
-
-#[tokio::test]
-async fn list_chunks_normalizes_invalid_tags_negative_tokens_and_empty_content() {
- let (_tmp, cfg) = test_config();
- // These handlers read through the bound driver now that the raw SQL is
- // gone, so the test has to bind one. TinyCortex is the engine the
- // loadable module wraps, so this exercises the same code production
- // reaches over the bus — and unlike the module it is not a process
- // singleton, which is what lets these run in one test binary.
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
- insert_raw_chunk(
- &cfg,
- "raw-empty",
- "document",
- "notion:page-1",
- 1_700_000_000_123,
- "not-json",
- "",
- -7,
- );
-
- let resp = list_chunks_rpc(&cfg, ChunkFilter::default())
- .await
- .unwrap()
- .value;
- let row = resp
- .chunks
- .into_iter()
- .find(|chunk| chunk.id == "raw-empty")
- .expect("raw chunk listed");
-
- assert_eq!(row.token_count, 0);
- assert_eq!(row.tags, Vec::::new());
- assert_eq!(row.content_preview, None);
- assert!(!row.has_embedding);
-}
-
-#[tokio::test]
-async fn list_sources_aggregates() {
- let (_tmp, cfg) = test_config();
- // These handlers read through the bound driver now that the raw SQL is
- // gone, so the test has to bind one. TinyCortex is the engine the
- // loadable module wraps, so this exercises the same code production
- // reaches over the bus — and unlike the module it is not a process
- // singleton, which is what lets these run in one test binary.
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
- seed_chat_chunk(&cfg, "slack:#a", "x").await;
- seed_chat_chunk(&cfg, "slack:#a", "y").await;
- seed_chat_chunk(&cfg, "slack:#b", "z").await;
- let sources = list_sources_rpc(&cfg, None).await.unwrap().value;
- let a = sources
- .iter()
- .find(|s| s.source_id == "slack:#a")
- .expect("expected slack:#a");
- let b = sources
- .iter()
- .find(|s| s.source_id == "slack:#b")
- .expect("expected slack:#b");
- assert_eq!(a.chunk_count, 2);
- assert_eq!(b.chunk_count, 1);
-}
-
-#[tokio::test]
-async fn list_sources_formats_email_threads_with_trimmed_user_hint() {
- let (_tmp, cfg) = test_config();
- // These handlers read through the bound driver now that the raw SQL is
- // gone, so the test has to bind one. TinyCortex is the engine the
- // loadable module wraps, so this exercises the same code production
- // reaches over the bus — and unlike the module it is not a process
- // singleton, which is what lets these run in one test binary.
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
- insert_raw_chunk(
- &cfg,
- "email-thread",
- "email",
- "gmail:Alice@Example.com|bob@example.com|carol@example.com",
- 1_700_000_000_123,
- "[]",
- "thread body",
- 12,
- );
-
- let sources = list_sources_rpc(&cfg, Some(" alice@example.com ".into()))
- .await
- .unwrap()
- .value;
- let source = sources
- .iter()
- .find(|row| row.source_id == "gmail:Alice@Example.com|bob@example.com|carol@example.com")
- .expect("email thread source present");
- assert_eq!(source.display_name, "bob@example.com, carol@example.com");
-}
-
-#[tokio::test]
-async fn entity_index_for_returns_extracted_entities() {
- let (_tmp, cfg) = test_config();
- // The entity index is read through `MemoryEntities::chunk_entities`, so the
- // handler needs a driver that serves that family — the null fallback does not.
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
- seed_chat_chunk(&cfg, "slack:#eng", "alice@example.com owns it").await;
- // Find the chunk we just seeded.
- let chunks = list_chunks_rpc(&cfg, ChunkFilter::default())
- .await
- .unwrap()
- .value
- .chunks;
- let id = &chunks[0].id;
- let refs = entity_index_for_rpc(&cfg, id.clone()).await.unwrap().value;
- assert!(
- refs.iter().any(|r| r.entity_id.contains("alice")),
- "expected alice entity in index, got: {refs:?}"
- );
-}
-
-#[tokio::test]
-async fn chunks_for_entity_returns_leaf_chunk_ids_only() {
- let (_tmp, cfg) = test_config();
- // `MemoryEntities::entity_chunk_ids` answers this one; the null fallback
- // serves no entity tier and would report an empty list.
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
- seed_chat_chunk(&cfg, "slack:#eng", "alice@example.com owns it").await;
- let chunk_id = list_chunks_rpc(&cfg, ChunkFilter::default())
- .await
- .unwrap()
- .value
- .chunks[0]
- .id
- .clone();
-
- let rows = chunks_for_entity_rpc(&cfg, "email:alice@example.com".into())
- .await
- .unwrap()
- .value;
- assert_eq!(rows, vec![chunk_id]);
-}
-
-#[tokio::test]
-async fn top_entities_returns_most_frequent() {
- let (_tmp, cfg) = test_config();
- // `MemoryEntities::top_entities` answers this one; the null fallback
- // serves no entity tier and would report an empty ranking.
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
- seed_chat_chunk(&cfg, "slack:#a", "alice@example.com x").await;
- seed_chat_chunk(&cfg, "slack:#b", "alice@example.com y").await;
- seed_chat_chunk(&cfg, "slack:#c", "bob@example.com z").await;
- let top = top_entities_rpc(&cfg, Some("email".into()), 10)
- .await
- .unwrap()
- .value;
- assert!(top
- .iter()
- .any(|e| e.entity_id == "email:alice@example.com" && e.count >= 2));
-}
-
-#[tokio::test]
-async fn delete_chunk_removes_chunk_and_dependent_rows() {
- let (_tmp, cfg) = test_config();
- // The delete goes through `MemorySourceSink::forget_matching`, which the null
- // fallback does not serve — and this handler refuses rather than degrades.
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
- seed_chat_chunk(&cfg, "slack:#eng", "alice@example.com owns it").await;
- let chunks = list_chunks_rpc(&cfg, ChunkFilter::default())
- .await
- .unwrap()
- .value
- .chunks;
- let id = chunks[0].id.clone();
- let resp = delete_chunk_rpc(&cfg, id.clone()).await.unwrap().value;
- assert!(resp.deleted);
- // Re-list — the chunk should be gone.
- let after = list_chunks_rpc(&cfg, ChunkFilter::default())
- .await
- .unwrap()
- .value;
- assert!(after.chunks.iter().all(|c| c.id != id));
-}
-
-#[tokio::test]
-async fn delete_missing_chunk_is_idempotent() {
- let (_tmp, cfg) = test_config();
- // Idempotence is the driver's, so this needs a driver: without one the handler
- // refuses outright, which is a different answer from "that chunk was not there".
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
- let resp = delete_chunk_rpc(&cfg, "does-not-exist".into())
- .await
- .unwrap()
- .value;
- assert!(!resp.deleted);
- assert_eq!(resp.score_rows_removed, 0);
-}
-
-/// The one named behaviour delta of the move onto `MemoryEntities::top_entities`,
-/// pinned in both directions.
-///
-/// The member validates `kind` and answers `MemoryError::Invalid` for one it does
-/// not recognise. The SQL this handler used to run compared the string against the
-/// stored column, so an unknown kind matched nothing and the caller got an empty
-/// list. A migration must not turn that quiet empty result into a user-visible
-/// error, so the variant is mapped back — and the second half of this test is what
-/// keeps the map-back narrow rather than a blanket swallow.
-#[tokio::test]
-async fn top_entities_reports_empty_for_an_unknown_kind() {
- let (_tmp, cfg) = test_config();
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
- seed_chat_chunk(&cfg, "slack:#eng", "alice@example.com owns it").await;
-
- let unknown = top_entities_rpc(&cfg, Some("not-a-kind".into()), 10)
- .await
- .expect("an unrecognised kind is an empty ranking, not an error")
- .value;
- assert!(unknown.is_empty(), "got: {unknown:?}");
-
- let known = top_entities_rpc(&cfg, Some("email".into()), 10)
- .await
- .unwrap()
- .value;
- assert!(
- !known.is_empty(),
- "a recognised kind must still rank rows; the Invalid map-back is narrow"
- );
-}
-
-/// `ForgetOutcome` reports `chunks_removed` and `trees_cleaned` and nothing about
-/// the per-chunk side rows, so `DeleteChunkResponse`'s two counts are observed
-/// before the delete rather than read off the outcome. This pins that they are
-/// still real numbers — dropping them to zero would read as "there was nothing to
-/// clean up", which is a different claim from "nobody counted".
-#[tokio::test]
-async fn delete_chunk_still_reports_its_side_row_counts() {
- let (_tmp, cfg) = test_config();
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
- seed_chat_chunk(&cfg, "slack:#eng", "alice@example.com owns it").await;
- let id = list_chunks_rpc(&cfg, ChunkFilter::default())
- .await
- .unwrap()
- .value
- .chunks[0]
- .id
- .clone();
-
- let indexed = entity_index_for_rpc(&cfg, id.clone()).await.unwrap().value;
- let indexed_rows: u32 = indexed.iter().map(|entity| entity.count).sum();
- assert!(
- indexed_rows > 0,
- "expected entity-index rows, got: {indexed:?}"
- );
-
- let resp = delete_chunk_rpc(&cfg, id).await.unwrap().value;
- assert!(resp.deleted);
- assert_eq!(resp.entity_index_rows_removed, indexed_rows);
- assert_eq!(resp.score_rows_removed, 1, "ingest writes one score row");
-}
-
-#[tokio::test]
-async fn chunk_score_returns_breakdown_after_ingest() {
- let (_tmp, cfg) = test_config();
- // These handlers read through the bound driver now that the raw SQL is
- // gone, so the test has to bind one. TinyCortex is the engine the
- // loadable module wraps, so this exercises the same code production
- // reaches over the bus — and unlike the module it is not a process
- // singleton, which is what lets these run in one test binary.
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
- seed_chat_chunk(
- &cfg,
- "slack:#eng",
- "alice@example.com owns the phoenix migration",
- )
- .await;
- let chunks = list_chunks_rpc(&cfg, ChunkFilter::default())
- .await
- .unwrap()
- .value
- .chunks;
- let id = &chunks[0].id;
- let breakdown = chunk_score_rpc(&cfg, id.clone()).await.unwrap().value;
- assert!(breakdown.is_some(), "expected score row after ingest");
- let b = breakdown.unwrap();
- assert!(b.signals.iter().any(|s| s.name == "metadata_weight"));
- assert!(b.threshold > 0.0);
-}
-
-#[tokio::test]
-async fn search_returns_matching_chunks() {
- let (_tmp, cfg) = test_config();
- // These handlers read through the bound driver now that the raw SQL is
- // gone, so the test has to bind one. TinyCortex is the engine the
- // loadable module wraps, so this exercises the same code production
- // reaches over the bus — and unlike the module it is not a process
- // singleton, which is what lets these run in one test binary.
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
- seed_chat_chunk(&cfg, "slack:#eng", "phoenix migration scheduled friday").await;
- seed_chat_chunk(&cfg, "slack:#eng", "different unrelated text").await;
- let hits = search_rpc(&cfg, "phoenix".into(), 10).await.unwrap().value;
- assert!(hits.iter().any(|c| {
- c.content_preview
- .as_deref()
- .unwrap_or("")
- .contains("phoenix")
- }));
-}
-
#[tokio::test]
#[ignore = "needs a built tinymemory module (OPENHUMAN_MODULE_PATH) and its own process: \
chunk detail is read through the bound driver, not the in-process engine"]
diff --git a/src/openhuman/memory/read_rpc_tests_part_02_tests.rs b/src/openhuman/memory/read_rpc_tests_part_02_tests.rs
index f70d6a02d0..bcb45fa55f 100644
--- a/src/openhuman/memory/read_rpc_tests_part_02_tests.rs
+++ b/src/openhuman/memory/read_rpc_tests_part_02_tests.rs
@@ -128,243 +128,6 @@ fn parse_source_kind_str_accepts_known_values_only() {
assert_eq!(parse_source_kind_str("unknown"), None);
}
-/// The namespace clear is routed through the bound driver's key/value tier
-/// (`kv_list` + `kv_delete`) instead of a `rusqlite::Connection` this handler
-/// opened on a path it built itself. The claim is unchanged and is what the
-/// raw read-back below still checks: only the composio namespace goes.
-#[tokio::test]
-async fn clear_composio_sync_state_removes_only_target_namespace() {
- let (_tmp, cfg) = test_config();
- // Created up front so the raw fixture rows below have a schema to land in;
- // the driver installed further down opens this same store.
- let _memory =
- UnifiedMemory::new(cfg.workspace_dir.as_path(), Arc::new(NoopEmbedding), None).unwrap();
- let db_path = cfg.workspace_dir.join("memory").join("memory.db");
- let conn = rusqlite::Connection::open(&db_path).unwrap();
-
- conn.execute(
- "INSERT INTO kv_namespace (namespace, key, value_json, updated_at)
- VALUES (?1, 'cursor', '{}', 1.0)",
- params![KV_NAMESPACE],
- )
- .unwrap();
- conn.execute(
- "INSERT INTO kv_namespace (namespace, key, value_json, updated_at)
- VALUES ('other-namespace', 'cursor', '{}', 2.0)",
- [],
- )
- .unwrap();
- drop(conn);
-
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
- let removed = clear_composio_sync_state(&cfg).await.unwrap();
- assert_eq!(removed, 1);
-
- let conn = rusqlite::Connection::open(&db_path).unwrap();
- let composio_count: i64 = conn
- .query_row(
- "SELECT COUNT(*) FROM kv_namespace WHERE namespace = ?1",
- params![KV_NAMESPACE],
- |row| row.get(0),
- )
- .unwrap();
- let other_count: i64 = conn
- .query_row(
- "SELECT COUNT(*) FROM kv_namespace WHERE namespace = 'other-namespace'",
- [],
- |row| row.get(0),
- )
- .unwrap();
- assert_eq!(composio_count, 0);
- assert_eq!(other_count, 1);
-}
-
-#[tokio::test]
-async fn tree_graph_includes_leaf_chunks_linked_to_their_summary() {
- let (_tmp, cfg) = test_config();
- // The forest and its leaves are read through `MemoryTree`, so the graph
- // needs a driver that serves that family — the null fallback does not.
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
- insert_tree_summary(&cfg, "tree-1", "slack:#eng", "summary:1:L1-aaa", 1);
- insert_chunk_with_parent(
- &cfg,
- "chunk-sealed",
- Some("summary:1:L1-aaa"),
- 1_700_000_000_000,
- "first line of sealed chunk\nmore body",
- );
- insert_chunk_with_parent(
- &cfg,
- "chunk-orphan",
- None,
- 1_700_000_000_001,
- "orphan chunk body",
- );
-
- let resp = graph_export_rpc(&cfg, GraphMode::Tree).await.unwrap().value;
-
- // 1 source root + 1 summary + 2 leaf chunks = 4 nodes.
- assert_eq!(
- resp.nodes.len(),
- 4,
- "source root + summary + both leaf chunks"
- );
-
- let source_root = resp.nodes.iter().find(|n| n.kind == "source").unwrap();
- assert!(source_root.id.starts_with("source:"));
-
- let summary = resp.nodes.iter().find(|n| n.kind == "summary").unwrap();
- assert_eq!(summary.id, "summary:1:L1-aaa");
- // Orphan summary links to source root.
- assert_eq!(summary.parent_id.as_deref(), Some(source_root.id.as_str()));
-
- let sealed = resp.nodes.iter().find(|n| n.id == "chunk-sealed").unwrap();
- assert_eq!(sealed.kind, "chunk");
- assert_eq!(sealed.parent_id.as_deref(), Some("summary:1:L1-aaa"));
- assert_eq!(sealed.label, "first line of sealed chunk");
-
- let orphan = resp.nodes.iter().find(|n| n.id == "chunk-orphan").unwrap();
- assert!(
- orphan.parent_id.is_none(),
- "unsealed chunk has no parent → renders as an orphan node"
- );
-
- assert!(resp.edges.is_empty());
-}
-
-#[tokio::test]
-async fn tree_graph_keeps_summaries_first_then_chunks() {
- let (_tmp, cfg) = test_config();
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
- insert_tree_summary(&cfg, "tree-1", "slack:#eng", "summary:1:L1-aaa", 1);
- insert_chunk_with_parent(
- &cfg,
- "chunk-1",
- Some("summary:1:L1-aaa"),
- 1_700_000_000_000,
- "a chunk",
- );
-
- let resp = graph_export_rpc(&cfg, GraphMode::Tree).await.unwrap().value;
- // Source roots are emitted first, then summaries, then chunks — so a
- // budget truncation drops chunk tails, never the tree skeleton.
- assert_eq!(resp.nodes[0].kind, "source");
- assert!(resp.nodes.iter().any(|n| n.kind == "summary"));
- assert!(resp.nodes.iter().any(|n| n.kind == "chunk"));
-}
-
-/// Contacts mode selects chunks by entity *kind* and labels them from one
-/// batched entity read.
-///
-/// The two chunks carry a different number of person rows on purpose. A reader
-/// that indexed the flat `chunk_entities` result by position against the ids it
-/// sent — the trap the contract's docs call out — would attribute the second
-/// chunk's row to the first and still produce two edges, so an asymmetric
-/// fixture is what makes the grouping observable.
-#[tokio::test]
-async fn contacts_graph_selects_person_chunks_and_groups_edges_by_chunk() {
- let (_tmp, cfg) = test_config();
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
-
- insert_chunk_with_parent(
- &cfg,
- "chunk-a",
- None,
- 1_700_000_000_002,
- "alice and bob met",
- );
- insert_chunk_with_parent(&cfg, "chunk-b", None, 1_700_000_000_001, "carol shipped it");
- // No person row: this chunk must not reach the graph at all.
- insert_chunk_with_parent(&cfg, "chunk-c", None, 1_700_000_000_000, "no people here");
-
- insert_entity_row(
- &cfg,
- "person:alice",
- "chunk-a",
- "person",
- "Alice",
- 1_700_000_000_002,
- );
- insert_entity_row(
- &cfg,
- "person:bob",
- "chunk-a",
- "person",
- "Bob",
- 1_700_000_000_002,
- );
- insert_entity_row(
- &cfg,
- "person:carol",
- "chunk-b",
- "person",
- "Carol",
- 1_700_000_000_001,
- );
- // A non-person row on a person-bearing chunk: it must not become an edge.
- insert_entity_row(
- &cfg,
- "topic:shipping",
- "chunk-b",
- "topic",
- "shipping",
- 1_700_000_000_001,
- );
-
- let resp = graph_export_rpc(&cfg, GraphMode::Contacts)
- .await
- .unwrap()
- .value;
-
- let chunk_ids: Vec<&str> = resp
- .nodes
- .iter()
- .filter(|n| n.kind == "chunk")
- .map(|n| n.id.as_str())
- .collect();
- assert_eq!(
- chunk_ids,
- vec!["chunk-a", "chunk-b"],
- "only person-bearing chunks, newest first"
- );
-
- let mut edges: Vec<(&str, &str)> = resp
- .edges
- .iter()
- .map(|e| (e.from.as_str(), e.to.as_str()))
- .collect();
- edges.sort_unstable();
- assert_eq!(
- edges,
- vec![
- ("chunk-a", "person:alice"),
- ("chunk-a", "person:bob"),
- ("chunk-b", "person:carol"),
- ],
- "every edge names the chunk its row came from, and the topic row is filtered out"
- );
-
- let contacts: std::collections::BTreeSet<(&str, &str)> = resp
- .nodes
- .iter()
- .filter(|n| n.kind == "contact")
- .map(|n| (n.id.as_str(), n.label.as_str()))
- .collect();
- let expected: std::collections::BTreeSet<(&str, &str)> = [
- ("person:alice", "Alice"),
- ("person:bob", "Bob"),
- ("person:carol", "Carol"),
- ]
- .into_iter()
- .collect();
- assert_eq!(contacts, expected);
- assert!(resp
- .nodes
- .iter()
- .filter(|n| n.kind == "contact")
- .all(|n| n.entity_kind.as_deref() == Some("person")));
-}
-
/// An empty candidate set must short-circuit, not become an unfiltered read.
///
/// The failure this pins is quiet: an empty predicate means *unfiltered* on
@@ -374,7 +137,7 @@ async fn contacts_graph_selects_person_chunks_and_groups_edges_by_chunk() {
#[tokio::test]
async fn contacts_graph_with_no_person_chunks_is_empty() {
let (_tmp, cfg) = test_config();
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
+ crate::openhuman::memory::test_support::install_memory_driver_for_test(&cfg);
insert_chunk_with_parent(&cfg, "chunk-a", None, 1_700_000_000_000, "no people here");
insert_entity_row(
&cfg,
@@ -572,46 +335,3 @@ async fn vault_health_check_reports_writable_and_obsidian_registered_when_ready(
);
}
-/// Regression: `wipe_all` MUST also clear the source-ingest gate
-/// (`mem_tree_ingested_sources`). Before the fix it cleared chunks/summaries
-/// but left the gate claimed, so a wiped document source could never
-/// re-ingest — the next sync saw `already_ingested` and wrote 0 chunks / 0
-/// seal jobs. This pins that a wipe leaves the gate empty so re-sync works.
-#[tokio::test]
-async fn wipe_all_clears_ingest_gate() {
- use tinymemory_api::chunks::SourceKind;
- use tinymemory_core::store::chunks::store as chunk_store;
-
- let (_tmp, cfg) = test_config();
- // `wipe_all` asks the bound driver to purge; without one bound the workspace
- // resolves to the placeholder, which serves no Maintenance family and
- // refuses rather than reporting a wipe it did not do.
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
- let gate_key = "notion:conn-1:page-abc@1700000000000";
-
- // Claim the gate exactly as a document ingest does.
- chunk_store::with_connection(&cfg, |conn| {
- let tx = conn.unchecked_transaction()?;
- let claimed = chunk_store::claim_source_ingest_tx(
- &tx,
- SourceKind::Document,
- gate_key,
- 1_700_000_000_000,
- )?;
- assert!(claimed, "first claim should succeed");
- tx.commit()?;
- Ok(())
- })
- .unwrap();
- assert!(
- chunk_store::is_source_ingested(&cfg, SourceKind::Document, gate_key).unwrap(),
- "gate must be claimed before wipe"
- );
-
- wipe_all_rpc(&cfg).await.expect("wipe_all_rpc");
-
- assert!(
- !chunk_store::is_source_ingested(&cfg, SourceKind::Document, gate_key).unwrap(),
- "wipe_all must clear mem_tree_ingested_sources so a wiped source can re-ingest"
- );
-}
diff --git a/src/openhuman/memory/test_support/mod.rs b/src/openhuman/memory/test_support/mod.rs
index fd05a0c3fe..6bd7958081 100644
--- a/src/openhuman/memory/test_support/mod.rs
+++ b/src/openhuman/memory/test_support/mod.rs
@@ -11,66 +11,35 @@ use super::binding::install_for_test;
use crate::openhuman::memory::api::provider::MemoryProvider;
use std::sync::Arc;
-/// Bind the in-process TinyCortex driver over a whole `Config`'s workspace.
+/// Bind a fake driver that serves every optional family over a whole `Config`'s
+/// workspace.
///
/// The shorthand for a test whose handler reads through a family the null
-/// driver does not serve — `Chunks`, `Documents`, `Retrieval` — and which
-/// proves itself by writing rows and reading them back. `FixedDiagnostics`
-/// cannot serve those: it answers `Maintenance` and delegates the rest to
-/// null.
+/// driver does not serve — `Chunks`, `Documents`, `Retrieval`.
+/// `FixedDiagnostics` cannot serve those: it answers `Maintenance` and
+/// delegates the rest to null.
///
-/// This is the driver the loadable module wraps, so a test binding it exercises
-/// the same engine production reaches over the bus. It is not the bus itself,
-/// and cannot be: a `dlopen`'ed module is a process singleton, and two tests
-/// loading one in the same process hang rather than fail.
-pub(crate) fn install_tinycortex_for_test(config: &crate::openhuman::config::Config) {
- crate::openhuman::memory::host_impls::install_for_tests();
- let client = Arc::new(
- tinymemory_core::store::MemoryClient::from_workspace_dir(config.workspace_dir.clone())
- .expect("open the workspace store"),
- );
- // Registered in the process-global slot as well as handed to the driver.
- // The engine's Composio sync pipeline opens with `global::client_if_ready`
- // and refuses with "memory client is not ready" without it — the module
- // path calls `global::bind` for exactly this reason (tinymemory#100), and a
- // fixture that builds a client owes the same registration. `bind` rather
- // than `init` so the driver and the slot are the SAME client: `init` would
- // construct a second one over the same SQLite file, which is two ingestion
- // workers and the hazard `global.rs` documents at length.
- let _ = tinymemory_core::global::bind(config.workspace_dir.clone(), Arc::clone(&client));
- let engine_config = tinymemory_tinycortex::engine::EngineRuntimeConfig {
- workspace_dir: config.workspace_dir.clone(),
- config_path: config.workspace_dir.join("config.toml"),
- memory: config.memory.clone(),
- memory_tree: config.memory_tree.clone(),
- scheduler_gate: config.scheduler_gate.clone(),
- local_ai: config.local_ai.clone(),
- embeddings_provider: config.embeddings_provider.clone(),
- memory_provider: None,
- // Added by tinymemory#100, which moved the periodic sync loops into the
- // module. A test fixture wants the same "no cadence configured" default
- // the module answers for an older host that sends nothing.
- // Carried from the host config rather than blanked: the engine's
- // `composio_config` branches on this, so an empty mode sends every
- // fixture down the proxied path whether or not that is what the test
- // configured.
- memory_sync_interval_secs: config.memory_sync_interval_secs,
- composio_mode: config.composio.mode.clone(),
- composio_entity_id: config.composio.entity_id.clone(),
- // Added by tinymemory#103: proxied Composio addresses the backend with
- // this. Empty means the host named none, and the request then fails in the
- // HTTP client rather than falling back to a guessed host.
- backend_api_url: crate::api::config::effective_backend_api_url(&config.api_url),
- default_model: None,
- default_temperature: 0.2,
- output_language: None,
- memory_sources: serde_json::Value::Null,
- };
+/// # Why this is not an engine any more
+///
+/// It used to build a real `TinycortexProvider` over a temp workspace, and that
+/// is what kept `tinycortex` and `tinymemory-core` — 133k lines — on this
+/// crate's test critical path long after they left the product build
+/// (openhuman#5560). The docstring justified it on the grounds that the
+/// alternative was the bus, and a `dlopen`ed module is a process singleton that
+/// hangs when a second test loads it.
+///
+/// That was a false choice: the third option is a driver that is neither the
+/// engine nor the bus. `tinymemory-conformance` ships one, it is held to the
+/// same contract as TinyCortex by `assert_provider`, and the engine is run
+/// against those same assertions upstream — so what a test observes here is
+/// contract behaviour rather than one engine's behaviour.
+///
+/// **What it deliberately will not do is filter, rank, or summarise.** A test
+/// that needs those is asserting engine semantics, and upstream owns them; the
+/// fake staying simple is what stops such a test from passing here against
+/// nothing but the fake.
+pub(crate) fn install_memory_driver_for_test(config: &crate::openhuman::config::Config) {
let provider: Arc =
- Arc::new(tinymemory_tinycortex::engine::TinycortexProvider::new(
- "tinycortex".to_string(),
- engine_config,
- client,
- ));
+ Arc::new(tinymemory_conformance::RecordingProvider::new());
install_for_test(&config.workspace_dir, &config.subsystems.memory, provider);
}
diff --git a/src/openhuman/memory/tools/flavour_tests.rs b/src/openhuman/memory/tools/flavour_tests.rs
index 00f3239ec0..1970da7546 100644
--- a/src/openhuman/memory/tools/flavour_tests.rs
+++ b/src/openhuman/memory/tools/flavour_tests.rs
@@ -5,7 +5,7 @@ use tempfile::TempDir;
// a test that expects a real "not built yet" answer needs a driver serving the
// Tree family — the null driver a test workspace otherwise resolves to would
// answer `Unsupported`, which this tool reports as a failure rather than as an
-// absent profile. `install_tinycortex_for_test` binds the very driver the
+// absent profile. `install_memory_driver_for_test` binds the very driver the
// loaded module wraps, so these tests exercise the same lookup production runs.
fn test_config() -> (TempDir, Arc) {
@@ -73,7 +73,7 @@ async fn unknown_flavour_is_error() {
#[tokio::test]
async fn valid_flavour_with_no_tree_yet_returns_no_profile_message() {
let (_tmp, cfg) = test_config();
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
+ crate::openhuman::memory::test_support::install_memory_driver_for_test(&cfg);
let tool = MemoryFlavourTool::new(cfg);
let result = tool
.execute(json!({"flavour": "coding_style"}))
@@ -87,7 +87,7 @@ async fn valid_flavour_with_no_tree_yet_returns_no_profile_message() {
async fn aliases_are_accepted() {
for alias in ["comms", "coding", "env", "rules", "dislikes"] {
let (_tmp, cfg) = test_config();
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
+ crate::openhuman::memory::test_support::install_memory_driver_for_test(&cfg);
let tool = MemoryFlavourTool::new(cfg);
let result = tool.execute(json!({"flavour": alias})).await;
assert!(result.is_ok(), "alias `{alias}` should be accepted");
diff --git a/src/openhuman/memory/tree/retrieval/rpc_tests.rs b/src/openhuman/memory/tree/retrieval/rpc_tests.rs
index 46acf8b118..32f26f3eb4 100644
--- a/src/openhuman/memory/tree/retrieval/rpc_tests.rs
+++ b/src/openhuman/memory/tree/retrieval/rpc_tests.rs
@@ -13,13 +13,13 @@
//! does with the answer.
//!
//! One test binds the real in-process driver instead
-//! ([`install_tinycortex_for_test`]): the source gate has to be proved end
+//! ([`install_memory_driver_for_test`]): the source gate has to be proved end
//! to end, because "the handler passed a scope" and "a restricted profile
//! cannot read another source" are different claims and only the second one
//! is the security property. It is the driver the loadable module wraps,
//! which is as close to production as a test process can get.
//!
-//! [`install_tinycortex_for_test`]: crate::openhuman::memory::test_support::install_tinycortex_for_test
+//! [`install_memory_driver_for_test`]: crate::openhuman::memory::test_support::install_memory_driver_for_test
use std::sync::{Arc, Mutex};
use super::*;
@@ -50,7 +50,6 @@ use crate::openhuman::memory::source_scope::with_source_scope;
// test-only, which is exactly the pair `direct_engine_refs_tests`'
// line-based scanner cannot tell apart when the reference sits inside an
// inline `#[cfg(test)]` module. See that module's docs.
-use crate::openhuman::memory::tree::retrieval::test_support::{stage_test_chunks, upsert_chunks};
use tinymemory_api::chunks::{chunk_id, Chunk, Metadata, SourceRef};
use tinymemory_api::null::NullMemoryProvider;
diff --git a/src/openhuman/memory/tree/retrieval/rpc_tests_part_01_tests.rs b/src/openhuman/memory/tree/retrieval/rpc_tests_part_01_tests.rs
index cd8b98d9aa..3a7db7fa31 100644
--- a/src/openhuman/memory/tree/retrieval/rpc_tests_part_01_tests.rs
+++ b/src/openhuman/memory/tree/retrieval/rpc_tests_part_01_tests.rs
@@ -218,54 +218,6 @@ async fn cover_window_rpc_surfaces_a_driver_rejection() {
assert!(err.contains("since_ms"), "got {err}");
}
-/// The source gate, end to end through the real driver.
-///
-/// The tests above prove the handler *passes* a scope; this one proves a
-/// restricted profile cannot read a source it was not granted. It has to
-/// bind the in-process driver rather than the double, because the filtering
-/// is the engine's — and `binding.provider()` is unguarded, so the scope
-/// this handler passes is the only thing standing between the two.
-#[tokio::test]
-async fn cover_window_rpc_honors_profile_source_scope() {
- let (_tmp, cfg) = test_config();
- // Two memory-source chunks in different sources, both inside the window.
- let mut allowed = sample_chunk("slack:#eng", 0);
- allowed.metadata.tags = vec!["memory_sources".into(), "chat".into()];
- let mut blocked = sample_chunk("slack:#secret", 0);
- blocked.metadata.tags = vec!["memory_sources".into(), "chat".into()];
- upsert_chunks(&cfg, &[allowed.clone(), blocked.clone()]).unwrap();
- stage_test_chunks(&cfg, &[allowed.clone(), blocked.clone()]);
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
-
- let req = || CoverWindowRequest {
- since_ms: 0,
- until_ms: 4_000_000_000_000,
- source_id: None,
- source_kind: None,
- limit: None,
- };
-
- let resp = with_source_scope(Some(vec!["slack:#eng".into()]), async {
- cover_window_rpc(&cfg, req()).await
- })
- .await
- .unwrap();
- let ids: Vec<&str> = resp.value.hits.iter().map(|h| h.node_id.as_str()).collect();
- assert!(
- ids.contains(&allowed.id.as_str()),
- "allowlisted source must be present: {ids:?}"
- );
- assert!(
- !ids.contains(&blocked.id.as_str()),
- "disallowed source must be filtered out: {ids:?}"
- );
-
- // With no profile scope active, both sources are visible — which is what
- // makes the assertion above a filter rather than an empty store.
- let unrestricted = cover_window_rpc(&cfg, req()).await.unwrap();
- assert_eq!(unrestricted.value.hits.len(), 2);
-}
-
// ── search_entities_rpc ───────────────────────────────────────────
/// The search degrades rather than fails when the bound driver has no
diff --git a/src/openhuman/memory/tree/tree/rpc_tests_part_01_tests.rs b/src/openhuman/memory/tree/tree/rpc_tests_part_01_tests.rs
index 6ab8554453..3d4ae67cbd 100644
--- a/src/openhuman/memory/tree/tree/rpc_tests_part_01_tests.rs
+++ b/src/openhuman/memory/tree/tree/rpc_tests_part_01_tests.rs
@@ -125,43 +125,6 @@ fn the_response_body_serialises_exactly_as_the_declared_wire() {
);
}
-/// Ingest reports what it wrote.
-///
-/// Bound to the in-process TinyCortex driver rather than left to resolve on
-/// its own: the handler asks the driver for the `Ingest` family now, and
-/// what a bare test workspace binds is the null driver, which serves none.
-/// This is the engine the loadable module wraps, so the counts asserted
-/// below are the ones production gets over the bus.
-#[tokio::test]
-async fn ingest_document_reports_the_chunks_it_wrote() {
- let (_tmp, cfg) = test_config();
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
- let outcome = ingest_rpc(
- &cfg,
- IngestRequest {
- source_kind: SourceKind::Document,
- source_id: "doc-launch".into(),
- owner: "alice".into(),
- tags: vec!["launch".into()],
- payload: serde_json::to_value(sample_document(
- "Launch Plan",
- "Phoenix launch canary checklist with rollback steps.",
- ))
- .unwrap(),
- },
- )
- .await
- .unwrap();
- assert_eq!(outcome.value.source_id, "doc-launch");
- assert_eq!(outcome.value.chunks_dropped, 0);
- assert!(outcome.value.chunks_written > 0);
- assert!(
- !outcome.value.chunk_ids.is_empty(),
- "the ids are what a caller fetches a chunk back by, so a write \
- that names none is unusable even when the count is right"
- );
-}
-
/// The listing degrades rather than fails when the bound driver has no
/// chunk tier.
///
@@ -191,159 +154,6 @@ async fn list_chunks_reports_empty_when_the_driver_has_no_chunk_tier() {
assert!(listed.logs[0].contains("n=0"), "log: {}", listed.logs[0]);
}
-/// The source gate is the driver's, and it survives the move onto the
-/// contract: `IngestOutcome::already_ingested` is the field the v1.3.0 pin
-/// did not have, and reporting a refused call as a plain empty write is
-/// exactly what this test would have started passing over.
-#[tokio::test]
-async fn ingest_document_is_idempotent_for_duplicate_source_id() {
- let (_tmp, cfg) = test_config();
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
- let req = IngestRequest {
- source_kind: SourceKind::Document,
- source_id: "doc-dup".into(),
- owner: "alice".into(),
- tags: vec![],
- payload: serde_json::to_value(sample_document("Launch Plan", "First body")).unwrap(),
- };
-
- let first = ingest_rpc(&cfg, req.clone()).await.unwrap().value;
- let second = ingest_rpc(&cfg, req).await.unwrap().value;
- assert!(first.chunks_written > 0);
- assert!(!first.already_ingested);
- // `already_ingested` with a zero write count is the whole claim:
- // documents are append-only, so a repeat submission must be recognised
- // rather than duplicated — and told apart from a write that produced
- // nothing, which is the same two numbers with a different cause.
- assert_eq!(second.chunks_written, 0);
- assert!(second.already_ingested);
- assert_eq!(second.source_id, first.source_id);
-}
-
-/// Regression #3568 / CORE-2K: chat payloads with RFC-3339 timestamps must
-/// be accepted — not rejected with "expected unix timestamp in milliseconds".
-#[tokio::test]
-async fn ingest_chat_accepts_rfc3339_timestamps() {
- let (_tmp, cfg) = test_config();
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
- let outcome = ingest_rpc(
- &cfg,
- IngestRequest {
- source_kind: SourceKind::Chat,
- source_id: "slack:#rfc3339-test".into(),
- owner: "alice".into(),
- tags: vec![],
- payload: json!({
- "platform": "slack",
- "channel_label": "#eng",
- "messages": [
- {
- "author": "alice",
- "timestamp": "2026-05-17T19:30:00Z",
- "text": "planning the launch"
- },
- {
- "author": "bob",
- "timestamp": 1779046260000_i64,
- "text": "confirmed"
- }
- ]
- }),
- },
- )
- .await
- .unwrap();
- assert!(!outcome.value.chunk_ids.is_empty());
-}
-
-/// Regression #3568 / CORE-2K: email payloads with RFC-3339 timestamps must
-/// be accepted.
-///
-/// A driver is bound, like every sibling here. The note this replaces said
-/// the mail arm was "still on the in-process pipeline" and that the test
-/// would need `install_tinycortex_for_test` "when it moves" — it has moved:
-/// the `Email` arm now goes through `ingest_through_driver`, which resolves
-/// `provider().as_ingest()` and refuses a driver that does not serve it.
-/// Without the binding the test only passed because CI happens to set
-/// `TINYMEMORY_TEST_MODULE` to a module that serves `Ingest`, so it would
-/// fail on a machine that does not.
-#[tokio::test]
-async fn ingest_email_accepts_rfc3339_timestamps() {
- let (_tmp, cfg) = test_config();
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
- let outcome = ingest_rpc(
- &cfg,
- IngestRequest {
- source_kind: SourceKind::Email,
- source_id: "gmail:rfc3339-test".into(),
- owner: "alice@example.com".into(),
- tags: vec![],
- payload: json!({
- "provider": "gmail",
- "thread_subject": "Launch",
- "messages": [
- {
- "from": "bob@example.com",
- "to": ["alice@example.com"],
- "subject": "Launch",
- "sent_at": "2026-05-17T19:30:00Z",
- "body": "Let's ship this."
- }
- ]
- }),
- },
- )
- .await
- .unwrap();
- assert!(!outcome.value.chunk_ids.is_empty());
-}
-
-/// One empty message must not fail the batch around it.
-///
-/// `validate_ingest_item` answers `Invalid` for content that trims to
-/// empty, and the driver validates every item before ingesting any — so an
-/// attachment-only message, which reaches this handler as a message with no
-/// text, would turn a batch that has real content in it into a failed call.
-/// The in-process pipeline wrote the rest of the batch and rendered that
-/// message as a bare header; the filter keeps the first half of that and
-/// gives up only the header.
-#[tokio::test]
-async fn an_empty_chat_message_does_not_fail_the_batch_around_it() {
- let (_tmp, cfg) = test_config();
- crate::openhuman::memory::test_support::install_tinycortex_for_test(&cfg);
- let outcome = ingest_rpc(
- &cfg,
- IngestRequest {
- source_kind: SourceKind::Chat,
- source_id: "slack:#attachment-only".into(),
- owner: "alice".into(),
- tags: vec![],
- payload: json!({
- "platform": "slack",
- "channel_label": "#eng",
- "messages": [
- {
- "author": "alice",
- "timestamp": "2026-05-17T19:30:00Z",
- "text": " "
- },
- {
- "author": "bob",
- "timestamp": "2026-05-17T19:31:00Z",
- "text": "here is the plan"
- }
- ]
- }),
- },
- )
- .await
- .expect("an empty message is dropped, not a batch failure");
- assert!(
- !outcome.value.chunk_ids.is_empty(),
- "the surviving message must still be written"
- );
-}
-
/// An ingest is a write, so a driver without the family is refused rather
/// than answered with zeros.
///
diff --git a/vendor/tinymemory b/vendor/tinymemory
index 9143fe1207..d217a03f4d 160000
--- a/vendor/tinymemory
+++ b/vendor/tinymemory
@@ -1 +1 @@
-Subproject commit 9143fe1207c01784de63da12db71b4ed92da8764
+Subproject commit d217a03f4d5d24eb30b74565cde9ae3040922a1e
From 747f5643582b01d53ec619a376fbf7918c3cd75c Mon Sep 17 00:00:00 2001
From: Shanu
Date: Wed, 9 Sep 2026 16:49:12 +0530
Subject: [PATCH 02/18] test(memory): drop the integration targets that drive
the engine directly
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Seven `tests/` targets exist to exercise TinyCortex and tinymemory-core
through a thin host wrapper — the sync pipeline, the chunk and tree stores,
the thread and source suites. Between them they import the engine 84 times and
assert what it stores, filters and summarises. Upstream owns every one of
those behaviours and covers them with ~2,800 tests of its own; these are
engine tests that ended up in the wrong repository.
`install_memory_host_seams` goes from the 25 targets that called it. It
installs an embedder, a chat provider, a config loader, an NLP host, a
scheduler gate and a shutdown hook into `tinymemory_core`'s process statics.
With no in-process engine there is nothing to install into, so the call is not
merely unnecessary — after the manifest is cut it will not resolve.
One test is deleted on its own rather than with a file:
`golden_fixture_rows_read_back_and_schema_is_stable_after_reopen`. It is worth
naming because it is the counter-example to how the rest were classified. It
imports the engine zero times, so an import census says it is engine-free —
and it still fails without the seam, because it binds a global memory client
and asserts the engine's SQLite schema survives a reopen. It reaches the engine
through this crate's own `memory::*` re-exports.
**An import census under-counts.** The remaining sweep has to be driven by what
fails without the engine, not by what names it. The other five tests in that
file pass untouched and stay.
Refs #6161
---
tests/agent_harness_e2e.rs | 3 -
tests/agent_retrieval_e2e.rs | 3 -
tests/agent_turn_overrides_e2e.rs | 3 -
tests/domain_modules_e2e.rs | 3 -
tests/json_rpc_e2e.rs | 3 -
tests/memory_fast_retrieve_e2e.rs | 3 -
tests/memory_golden_fixture_e2e.rs | 121 -
tests/memory_golden_parity_e2e.rs | 3 -
tests/memory_roundtrip_e2e.rs | 3 -
tests/memory_sources_e2e.rs | 3 -
tests/memory_sync_pipeline_e2e.rs | 518 --
tests/ollama_embeddings_fallback_e2e.rs | 3 -
...rchivist_debug_round21_raw_coverage_e2e.rs | 3 -
tests/raw_coverage/agent_orchestration_e2e.rs | 3 -
.../agent_session_turn_raw_coverage_e2e.rs | 3 -
...threads_memory_sources_raw_coverage_e2e.rs | 3 -
.../channels_web_startup_raw_coverage_e2e.rs | 3 -
tests/raw_coverage/learning_facets_e2e.rs | 3 -
.../memory_core_threads_raw_coverage_e2e.rs | 670 ---
tests/raw_coverage/memory_goals_people_e2e.rs | 3 -
tests/raw_coverage/memory_raw_coverage_e2e.rs | 685 ---
...ources_closure_round23_raw_coverage_e2e.rs | 3 -
.../memory_sync_providers_raw_coverage_e2e.rs | 3 -
.../memory_sync_round23_raw_coverage_e2e.rs | 3 -
.../memory_sync_slack_bus_raw_coverage_e2e.rs | 3 -
.../memory_sync_sources_raw_coverage_e2e.rs | 3 -
...mory_sync_tree_round21_raw_coverage_e2e.rs | 449 --
.../memory_threads_raw_coverage_e2e.rs | 4833 -----------------
.../memory_tree_sync_deep_raw_coverage_e2e.rs | 661 ---
.../memory_tree_sync_raw_coverage_e2e.rs | 524 --
.../near90_closure_raw_coverage_e2e.rs | 3 -
tests/worker_c_modules_e2e.rs | 3 -
32 files changed, 8533 deletions(-)
delete mode 100644 tests/memory_sync_pipeline_e2e.rs
delete mode 100644 tests/raw_coverage/memory_core_threads_raw_coverage_e2e.rs
delete mode 100644 tests/raw_coverage/memory_raw_coverage_e2e.rs
delete mode 100644 tests/raw_coverage/memory_sync_tree_round21_raw_coverage_e2e.rs
delete mode 100644 tests/raw_coverage/memory_threads_raw_coverage_e2e.rs
delete mode 100644 tests/raw_coverage/memory_tree_sync_deep_raw_coverage_e2e.rs
delete mode 100644 tests/raw_coverage/memory_tree_sync_raw_coverage_e2e.rs
diff --git a/tests/agent_harness_e2e.rs b/tests/agent_harness_e2e.rs
index 662539783a..669e13114b 100644
--- a/tests/agent_harness_e2e.rs
+++ b/tests/agent_harness_e2e.rs
@@ -670,9 +670,6 @@ async fn boot_stack() -> Stack {
// The transport-only router does not create a Core runtime context. Install
// the explicit tinymemory host seams before handlers service memory-backed
// agent turns, matching normal startup wiring.
- openhuman_core::openhuman::memory::host_impls::install_memory_host_seams(std::sync::Arc::new(
- openhuman_core::openhuman::config::Config::default(),
- ));
let (rpc_addr, rpc_join) = serve_on_ephemeral(build_core_http_router(false)).await;
let rpc_base = format!("http://{rpc_addr}");
diff --git a/tests/agent_retrieval_e2e.rs b/tests/agent_retrieval_e2e.rs
index 88a2299d03..4cb57d915a 100644
--- a/tests/agent_retrieval_e2e.rs
+++ b/tests/agent_retrieval_e2e.rs
@@ -55,9 +55,6 @@ fn ensure_memory_seams() {
.stack_size(8 * 1024 * 1024)
.spawn(|| {
let config = std::sync::Arc::new(Config::default());
- openhuman_core::openhuman::memory::host_impls::install_memory_host_seams(
- config.clone(),
- );
#[cfg(feature = "modules")]
openhuman_core::openhuman::modules::memory::set_modules_policy(config);
})
diff --git a/tests/agent_turn_overrides_e2e.rs b/tests/agent_turn_overrides_e2e.rs
index df09d6ddad..4d47ff0dcb 100644
--- a/tests/agent_turn_overrides_e2e.rs
+++ b/tests/agent_turn_overrides_e2e.rs
@@ -91,9 +91,6 @@ fn ensure_memory_seams() {
.name("turn-overrides-e2e-seams".to_string())
.stack_size(8 * 1024 * 1024)
.spawn(|| {
- openhuman_core::openhuman::memory::host_impls::install_memory_host_seams(Arc::new(
- Config::default(),
- ));
})
.expect("spawn turn-overrides seam installer")
.join()
diff --git a/tests/domain_modules_e2e.rs b/tests/domain_modules_e2e.rs
index d7403a6acf..0ddd1ad8a8 100644
--- a/tests/domain_modules_e2e.rs
+++ b/tests/domain_modules_e2e.rs
@@ -143,9 +143,6 @@ async fn setup() -> TestHarness {
// The HTTP router is intentionally transport-only and does not construct a
// Core runtime context. Memory-backed RPC reads still need the explicit
// tinymemory host seams before they can load their configured provider.
- openhuman_core::openhuman::memory::host_impls::install_memory_host_seams(std::sync::Arc::new(
- openhuman_core::openhuman::config::Config::default(),
- ));
// Same rule for the modules policy, which became load-bearing when the
// status RPCs started reading diagnostics through the bound driver
// (#5560): resolving a driver refuses outright until boot publishes the
diff --git a/tests/json_rpc_e2e.rs b/tests/json_rpc_e2e.rs
index 0a71ca1310..c12f5b6ef4 100644
--- a/tests/json_rpc_e2e.rs
+++ b/tests/json_rpc_e2e.rs
@@ -122,9 +122,6 @@ fn ensure_json_rpc_e2e_memory_seams() {
workspace_dir: json_rpc_e2e_shared_workspace().to_path_buf(),
..openhuman_core::openhuman::config::Config::default()
});
- openhuman_core::openhuman::memory::host_impls::install_memory_host_seams(
- config.clone(),
- );
#[cfg(feature = "modules")]
openhuman_core::openhuman::modules::memory::set_modules_policy(config);
})
diff --git a/tests/memory_fast_retrieve_e2e.rs b/tests/memory_fast_retrieve_e2e.rs
index 86e1c8ab86..a0110b829f 100644
--- a/tests/memory_fast_retrieve_e2e.rs
+++ b/tests/memory_fast_retrieve_e2e.rs
@@ -38,9 +38,6 @@ fn ensure_memory_seams() {
.name("memory-fast-retrieve-e2e-seams".to_string())
.stack_size(8 * 1024 * 1024)
.spawn(|| {
- openhuman_core::openhuman::memory::host_impls::install_memory_host_seams(Arc::new(
- Config::default(),
- ));
})
.expect("spawn memory retrieval seam installer")
.join()
diff --git a/tests/memory_golden_fixture_e2e.rs b/tests/memory_golden_fixture_e2e.rs
index 1064755ec8..9df1941344 100644
--- a/tests/memory_golden_fixture_e2e.rs
+++ b/tests/memory_golden_fixture_e2e.rs
@@ -152,9 +152,6 @@ fn ensure_memory_seams(workspace: &Path) {
config_path: workspace.join("config.toml"),
..openhuman_core::openhuman::config::Config::default()
});
- openhuman_core::openhuman::memory::host_impls::install_memory_host_seams(
- config.clone(),
- );
#[cfg(feature = "modules")]
openhuman_core::openhuman::modules::memory::set_modules_policy(config);
})
@@ -262,124 +259,6 @@ async fn fresh_workspace_schema_matches_the_committed_manifest() {
assert_manifest_set_equal(&committed_manifest(), &actual);
}
-/// Gate 3 — the code the current build produces still yields the same schema
-/// the fixture captured.
-///
-/// This is the half that catches a DDL edit: it opens the *fixture copy* with
-/// the current `UnifiedMemory::new` + tinycortex init (both of which run their
-/// `CREATE TABLE IF NOT EXISTS` / `ALTER TABLE` bootstrap on every open), then
-/// re-dumps. A new table, index, or trigger shows up as UNEXPECTED; a renamed
-/// one shows up as both MISSING and UNEXPECTED.
-///
-/// Everything that binds the process-global memory client lives in this one
-/// test, for the reason `memory_golden_parity_e2e` documents: the client is
-/// process-global and binds to its first workspace, so splitting these across
-/// tests makes them pass or fail by scheduling order.
-#[tokio::test]
-async fn golden_fixture_rows_read_back_and_schema_is_stable_after_reopen() {
- let _lock = env_lock();
- let tmp = tempdir().expect("tempdir");
- let _home = EnvVarGuard::set_to_path("HOME", tmp.path());
- let workspace = tmp.path().join("workspace");
- copy_fixture_to(&workspace);
- let _ws = EnvVarGuard::set_to_path("OPENHUMAN_WORKSPACE", &workspace);
- ensure_memory_seams(&workspace);
-
- let before = golden::schema_manifest(&workspace).expect("dump schema before open");
-
- tinymemory_core::global::init(workspace.clone())
- .expect("bind global memory client to the fixture copy");
-
- // ── Row-level read-back through memory::ops ──
- let readback = golden::read_back(&workspace)
- .await
- .expect("read the golden workspace back");
- eprintln!("[golden-fixture] readback: {readback:#?}");
-
- assert_eq!(
- readback.primary_doc_keys,
- vec![golden::DOC_KEY_PRIMARY.to_string()],
- "primary-namespace document lost"
- );
- assert_eq!(
- readback.secondary_doc_keys,
- vec![golden::DOC_KEY_SECONDARY.to_string()],
- "secondary-namespace document lost — namespace scoping is broken"
- );
- assert!(readback.kv_global_present, "global-scope KV value lost");
- assert!(
- readback.kv_namespace_present,
- "namespace-scope KV value lost"
- );
- assert_eq!(readback.graph_hits, 1, "graph triple lost");
- assert_eq!(
- readback.episodic_sessions,
- vec![golden::SESSION_ID.to_string()],
- "episodic row lost"
- );
- assert_eq!(
- readback.segment_ids,
- vec![golden::SEGMENT_ID.to_string()],
- "conversation segment lost"
- );
- assert_eq!(
- readback.event_ids,
- vec![golden::EVENT_ID.to_string()],
- "event row lost"
- );
- assert_eq!(
- readback.profile_keys,
- vec![golden::PROFILE_KEY.to_string()],
- "user_profile facet lost"
- );
- assert_eq!(
- readback.summary_ids,
- vec![golden::SUMMARY_ID.to_string()],
- "summary node lost"
- );
- assert!(
- readback.tree_sealed,
- "summary tree is no longer sealed to its root node"
- );
- assert_eq!(
- readback.chunk_ids.len(),
- 1,
- "expected exactly one seeded leaf chunk, got {:?}",
- readback.chunk_ids
- );
- assert!(
- readback.embeddings_match,
- "at least one embedding tier did not return the exact seeded vector — \
- a vector encoding or column change would strand every existing embedding"
- );
- // Fixed-query recall: the exact set, not a "contains". Retrieval spans
- // documents, KV values and events, so this pins the whole hit assembly —
- // dropping any tier from the recall path changes this list.
- assert_eq!(
- readback.recall_chunks,
- vec![
- "Decided to pin the memory schema with a captured fixture.".to_string(),
- golden::DOC_CONTENT_PRIMARY.to_string(),
- r#"{"fixture":"golden","v":1}"#.to_string(),
- r#"{"fixture":"golden","v":1}"#.to_string(),
- ],
- "fixed-query recall returned a different result set"
- );
-
- // ── Opening the workspace must not mutate its schema ──
- let after = golden::schema_manifest(&workspace).expect("dump schema after open");
- assert_manifest_set_equal(&committed_manifest(), &after);
- assert_manifest_set_equal(&before, &after);
-
- // ── Close and reopen in a SECOND PROCESS ──
- //
- // A fresh process gets a fresh SQLite library state and a cold page cache,
- // so this is what catches WAL / journal-mode surprises that an in-process
- // reopen would hide (the connection pool would just hand back the same
- // warm handle).
- run_second_process_readback(&workspace);
-}
-
/// Spawn this same test binary to run [`second_process_readback`] against
/// `workspace`, and fail loudly with its output if it does not pass.
fn run_second_process_readback(workspace: &Path) {
diff --git a/tests/memory_golden_parity_e2e.rs b/tests/memory_golden_parity_e2e.rs
index 66474d0372..2cd17229bb 100644
--- a/tests/memory_golden_parity_e2e.rs
+++ b/tests/memory_golden_parity_e2e.rs
@@ -125,9 +125,6 @@ fn ensure_memory_seams(workspace: &Path) {
config_path: workspace.join("config.toml"),
..Config::default()
});
- openhuman_core::openhuman::memory::host_impls::install_memory_host_seams(
- config.clone(),
- );
#[cfg(feature = "modules")]
openhuman_core::openhuman::modules::memory::set_modules_policy(config);
})
diff --git a/tests/memory_roundtrip_e2e.rs b/tests/memory_roundtrip_e2e.rs
index 6c7499fce7..7c48ec8b32 100644
--- a/tests/memory_roundtrip_e2e.rs
+++ b/tests/memory_roundtrip_e2e.rs
@@ -81,9 +81,6 @@ fn ensure_memory_seams(workspace: &Path) {
config_path: workspace.join("config.toml"),
..openhuman_core::openhuman::config::Config::default()
});
- openhuman_core::openhuman::memory::host_impls::install_memory_host_seams(
- config.clone(),
- );
#[cfg(feature = "modules")]
openhuman_core::openhuman::modules::memory::set_modules_policy(config);
})
diff --git a/tests/memory_sources_e2e.rs b/tests/memory_sources_e2e.rs
index bea8608fcd..19a032e4db 100644
--- a/tests/memory_sources_e2e.rs
+++ b/tests/memory_sources_e2e.rs
@@ -48,9 +48,6 @@ fn ensure_memory_seams() {
.stack_size(8 * 1024 * 1024)
.spawn(|| {
let config = Arc::new(openhuman_core::openhuman::config::Config::default());
- openhuman_core::openhuman::memory::host_impls::install_memory_host_seams(
- config.clone(),
- );
#[cfg(feature = "modules")]
openhuman_core::openhuman::modules::memory::set_modules_policy(config);
})
diff --git a/tests/memory_sync_pipeline_e2e.rs b/tests/memory_sync_pipeline_e2e.rs
deleted file mode 100644
index ffdb90315a..0000000000
--- a/tests/memory_sync_pipeline_e2e.rs
+++ /dev/null
@@ -1,518 +0,0 @@
-//! End-to-end coverage for the redesigned memory-sync flow shipped in
-//! PR #3113 (issue #3116).
-//!
-//! What this proves, all offline (no network, no live LLM):
-//!
-//! 1. `run_github_sync` against a **local seed git repo** (a bare clone is
-//! pre-staged in the source's git cache dir with a `file://` origin so
-//! the offline `git fetch` succeeds) lands summaries in the source tree.
-//! 2. `ingest_summary` fills the L1 buffer and seals the cascade once the
-//! buffer crosses `SUMMARY_FANOUT`.
-//! 3. `rebuild_tree_from_raw` reads raw `.md` files seeded on disk and
-//! builds the tree from them.
-//! 4. `sync_source` is a no-op on a second concurrent call (per-source
-//! mutex), runs `retry_all_failed`, and writes the audit log.
-//! 5. `check_and_rebuild_tree` auto-detects raw-without-summaries
-//! (`max_level == 0`) and triggers a rebuild.
-//! 6. The Tree-mode graph export builds synthetic source-root nodes, hangs
-//! document leaves off L1 summaries, and links orphan summaries to their
-//! source root.
-//!
-//! ## What is stubbed and why
-//!
-//! The summariser (`memory_tree::summarise::summarise`) makes a real LLM
-//! call. Both `run_github_sync` and `rebuild_tree_from_raw` catch a
-//! summarise error and fall back to `fallback_summary` (a deterministic
-//! concat-and-truncate). With no provider configured in the test `Config`,
-//! the LLM call fails fast and the deterministic fallback runs — so the
-//! ingest/seal/rebuild machinery under test is exercised end-to-end without
-//! any network. The summary *text* is the fallback concat rather than a
-//! real model summary; everything else (file staging, DB rows, buffers,
-//! seal cascade, audit log, graph shape) is the production path.
-//!
-//! GitHub issues/PRs require the GitHub REST API (or `gh`), which is not
-//! reachable offline; the seeded local repo only carries commits. That is
-//! fine — `run_github_sync` treats issue/PR listing failures as non-fatal
-//! as long as commits list successfully, which is the path asserted here.
-
-use std::path::Path;
-use std::process::Command;
-use std::sync::{Arc, OnceLock};
-
-use chrono::Utc;
-use tempfile::TempDir;
-
-use openhuman_core::openhuman::config::Config;
-// The engine's own source pipeline. `memory::sources::sync` is host-side now and
-// carries only `derive_scopes`; `sync_source` stayed upstream because nothing in
-// `src/` calls it any more (#5560).
-use openhuman_core::openhuman::memory::sources::types::{MemorySourceEntry, SourceKind};
-use tinymemory_core::sources::sync::sync_source;
-use tinymemory_core::store::content::raw::{raw_kind_dir, raw_source_dir, RawKind};
-use tinymemory_core::store::trees::store as tree_store;
-use tinymemory_core::store::trees::types::SUMMARY_FANOUT;
-use tinymemory_core::tinycortex::read_audit_log;
-use tinymemory_core::tinycortex::run_github_sync;
-use tinymemory_core::tinycortex::{needs_rebuild, rebuild_tree_from_raw};
-use tinymemory_core::tree::ingest::{ingest_summary, SummaryIngestInput};
-use tinymemory_core::tree_source::get_or_create_source_tree;
-
-// ── Shared harness ────────────────────────────────────────────────────────
-
-static MEMORY_SEAMS_INIT: OnceLock<()> = OnceLock::new();
-
-fn ensure_memory_seams() {
- MEMORY_SEAMS_INIT.get_or_init(|| {
- std::thread::Builder::new()
- .name("memory-sync-pipeline-e2e-seams".to_string())
- .stack_size(8 * 1024 * 1024)
- .spawn(|| {
- openhuman_core::openhuman::memory::host_impls::install_memory_host_seams(Arc::new(
- Config::default(),
- ));
- })
- .expect("spawn memory sync pipeline seam installer")
- .join()
- .expect("memory sync pipeline seam installer panicked");
- });
-}
-
-/// Build a `Config` rooted at a temp workspace with no LLM provider and no
-/// embedder, so every test runs fully offline and deterministically.
-fn test_config(tmp: &TempDir) -> Config {
- ensure_memory_seams();
- let workspace_dir = tmp.path().join("workspace");
- std::fs::create_dir_all(&workspace_dir).expect("create workspace dir");
- let mut cfg = Config {
- workspace_dir: workspace_dir.clone(),
- config_path: tmp.path().join("config.toml"),
- ..Config::default()
- };
- // Inert embedder — no Ollama, no network.
- cfg.memory_tree.embedding_endpoint = None;
- cfg.memory_tree.embedding_model = None;
- cfg.memory_tree.embedding_strict = false;
- cfg
-}
-
-/// Build a `SummaryIngestInput` with the given content + token count and
-/// otherwise inert metadata.
-fn summary_input(content: &str, tokens: u32) -> SummaryIngestInput {
- SummaryIngestInput {
- content: content.to_string(),
- token_count: tokens,
- entities: Vec::new(),
- topics: vec!["test".to_string()],
- time_range_start: Utc::now(),
- time_range_end: Utc::now(),
- score: 0.5,
- child_labels: Vec::new(),
- child_basenames: Vec::new(),
- }
-}
-
-fn run_git(args: &[&str], cwd: &Path) {
- let status = Command::new("git")
- // A contributor with `commit.gpgsign = true` set globally otherwise
- // gets a `git commit` here that blocks forever on a pinentry prompt
- // with no tty behind it — the whole file hangs rather than failing.
- .args(["-c", "commit.gpgsign=false"])
- .args(args)
- .current_dir(cwd)
- .env("GIT_AUTHOR_NAME", "Test")
- .env("GIT_AUTHOR_EMAIL", "test@example.com")
- .env("GIT_COMMITTER_NAME", "Test")
- .env("GIT_COMMITTER_EMAIL", "test@example.com")
- .status()
- .unwrap_or_else(|e| panic!("git {args:?} failed to spawn: {e}"));
- assert!(status.success(), "git {args:?} exited {status}");
-}
-
-// ── Test 1: run_github_sync against a seeded local repo ────────────────────
-
-/// Seed a working repo with N commits, make it a bare repo, then bare-clone
-/// it into the source's git cache dir with a `file://` origin so the
-/// offline `git fetch` inside `ensure_bare_clone` succeeds. `run_github_sync`
-/// then lists/read commits via local git and lands a summary in the tree.
-#[tokio::test]
-async fn github_sync_lands_summaries_in_tree() {
- let tmp = TempDir::new().unwrap();
- let cfg = test_config(&tmp);
-
- // 1. Build a seed working repo with a handful of commits.
- let seed = tmp.path().join("seed-work");
- std::fs::create_dir_all(&seed).unwrap();
- run_git(&["init", "--quiet"], &seed);
- run_git(&["checkout", "-q", "-b", "main"], &seed);
- for i in 0..4 {
- std::fs::write(seed.join(format!("file{i}.txt")), format!("content {i}\n")).unwrap();
- run_git(&["add", "."], &seed);
- run_git(
- &[
- "commit",
- "--quiet",
- "-m",
- &format!("feat: change number {i}"),
- ],
- &seed,
- );
- }
-
- // 2. Make a bare mirror of the seed repo to act as the "remote".
- let remote_bare = tmp.path().join("seed-remote.git");
- run_git(
- &[
- "clone",
- "--bare",
- "--quiet",
- seed.to_str().unwrap(),
- remote_bare.to_str().unwrap(),
- ],
- tmp.path(),
- );
-
- // 3. Pre-stage the source's git cache as a bare clone of the local
- // remote, so `ensure_bare_clone` sees HEAD and the offline `git
- // fetch` (against the file:// origin) succeeds.
- let owner = "tinyhumansai";
- let repo = "seedrepo";
- let cache_dir = cfg
- .workspace_dir
- .join("git_cache")
- .join(owner)
- .join(format!("{repo}.git"));
- std::fs::create_dir_all(cache_dir.parent().unwrap()).unwrap();
- run_git(
- &[
- "clone",
- "--bare",
- "--quiet",
- remote_bare.to_str().unwrap(),
- cache_dir.to_str().unwrap(),
- ],
- tmp.path(),
- );
- assert!(
- cache_dir.join("HEAD").exists(),
- "seeded bare clone must have HEAD"
- );
-
- // 4. Run the sync. Commits resolve via local git; issues/PRs fail
- // offline but are non-fatal because commits succeeded.
- let source = MemorySourceEntry {
- id: "gh-seed".to_string(),
- kind: SourceKind::GithubRepo,
- label: "Seed repo".to_string(),
- enabled: true,
- url: Some(format!("https://github.com/{owner}/{repo}")),
- max_commits: Some(50),
- max_issues: Some(0),
- max_prs: Some(0),
- toolkit: None,
- connection_id: None,
- path: None,
- glob: None,
- branch: None,
- paths: Vec::new(),
- query: None,
- since_days: None,
- max_items: None,
- selector: None,
- max_tokens_per_sync: None,
- max_cost_per_sync_usd: None,
- sync_depth_days: None,
- };
-
- let outcome = run_github_sync(&source, &cfg)
- .await
- .expect("run_github_sync should succeed with local commits");
-
- assert!(
- outcome.records_ingested >= 4,
- "expected >= 4 commits ingested, got {}",
- outcome.records_ingested
- );
-
- // The source tree now has an L1 summary buffered.
- let scope = format!("github:{owner}/{repo}");
- let tree = get_or_create_source_tree(&cfg, &scope).unwrap();
- let buf = tree_store::get_buffer(&cfg, &tree.id, 1).unwrap();
- assert!(
- !buf.item_ids.is_empty(),
- "L1 buffer should hold the ingested summary"
- );
-
- // A success audit entry was written for the github sync.
- let audit = read_audit_log(&cfg);
- assert!(
- audit
- .iter()
- .any(|e| e.source_kind == "github_repo" && e.success),
- "github sync should write a successful audit entry; got {audit:?}"
- );
-}
-
-// ── Test 2: ingest_summary fills the buffer and seals at SUMMARY_FANOUT ─────
-
-#[tokio::test]
-async fn ingest_summary_seals_l1_buffer_at_fanout() {
- let tmp = TempDir::new().unwrap();
- let cfg = test_config(&tmp);
- let tree = get_or_create_source_tree(&cfg, "github:org/fanout-repo").unwrap();
-
- // First SUMMARY_FANOUT - 1 ingests should NOT seal.
- for i in 0..(SUMMARY_FANOUT - 1) {
- let outcome = ingest_summary(&cfg, &tree, summary_input(&format!("summary {i}"), 10))
- .await
- .unwrap();
- assert!(
- outcome.sealed_ids.is_empty(),
- "ingest {i} should not seal before reaching fanout"
- );
- }
-
- let buf = tree_store::get_buffer(&cfg, &tree.id, 1).unwrap();
- assert_eq!(
- buf.item_ids.len() as u32,
- SUMMARY_FANOUT - 1,
- "buffer should hold FANOUT-1 items before the sealing ingest"
- );
-
- // The SUMMARY_FANOUT-th ingest crosses the gate and seals the cascade.
- let sealing = ingest_summary(&cfg, &tree, summary_input("the tenth summary", 10))
- .await
- .unwrap();
- assert!(
- !sealing.sealed_ids.is_empty(),
- "ingest at SUMMARY_FANOUT should trigger a seal cascade"
- );
-
- // After sealing, the L1 buffer is drained and the tree grew a level.
- let buf_after = tree_store::get_buffer(&cfg, &tree.id, 1).unwrap();
- assert!(
- (buf_after.item_ids.len() as u32) < SUMMARY_FANOUT,
- "L1 buffer should be drained after the seal cascade, got {}",
- buf_after.item_ids.len()
- );
- let tree_after = get_or_create_source_tree(&cfg, "github:org/fanout-repo").unwrap();
- assert!(
- tree_after.max_level >= 2,
- "tree should have grown to L2 after sealing, max_level={}",
- tree_after.max_level
- );
-}
-
-// ── Test 3: rebuild_tree_from_raw reads seeded raw files ───────────────────
-
-#[tokio::test]
-async fn rebuild_tree_from_raw_builds_from_disk() {
- let tmp = TempDir::new().unwrap();
- let cfg = test_config(&tmp);
- let scope = "gmail:test-at-example-dot-com";
-
- // Seed raw markdown files on disk under raw//emails/.
- let content_root = cfg.memory_tree_content_root();
- let emails_dir = raw_kind_dir(&content_root, scope, RawKind::Email);
- std::fs::create_dir_all(&emails_dir).unwrap();
- for i in 0..3 {
- let ts = 1_700_000_000_000i64 + i;
- std::fs::write(
- emails_dir.join(format!("{ts}_msg-{i}.md")),
- format!("# Email {i}\n\nBody of message number {i}.\n"),
- )
- .unwrap();
- }
- // A `_source.md` sidecar that must be skipped by the collector.
- std::fs::write(
- raw_source_dir(&content_root, scope).join("_source.md"),
- "scope: gmail:test-at-example-dot-com\n",
- )
- .unwrap();
-
- // Tree has raw but no summaries yet → max_level 0.
- let before = get_or_create_source_tree(&cfg, scope).unwrap();
- assert_eq!(before.max_level, 0, "fresh tree should be at level 0");
-
- let outcome = rebuild_tree_from_raw(&cfg, scope, scope).await.unwrap();
- assert_eq!(outcome.files_read, 3, "should read the 3 seeded emails");
- assert!(outcome.batches >= 1, "should produce at least one batch");
-
- // The rebuild produced an L1 summary in the buffer.
- let tree = get_or_create_source_tree(&cfg, scope).unwrap();
- let buf = tree_store::get_buffer(&cfg, &tree.id, 1).unwrap();
- assert!(
- !buf.item_ids.is_empty(),
- "rebuild should have ingested at least one L1 summary"
- );
-
- // Rebuild wrote its own audit entry tagged "rebuild".
- let audit = read_audit_log(&cfg);
- assert!(
- audit
- .iter()
- .any(|e| e.source_kind == "rebuild" && e.scope == scope),
- "rebuild should write a rebuild audit entry; got {audit:?}"
- );
-}
-
-// ── Test 4: sync_source mutex no-op, retry_all_failed, audit ───────────────
-
-#[tokio::test]
-async fn sync_source_second_concurrent_call_is_noop_and_audits() {
- ensure_memory_seams();
- let tmp = TempDir::new().unwrap();
- let cfg = test_config(&tmp);
-
- // A Folder source pointed at a small on-disk directory: this exercises
- // the dispatcher's per-item path (no network) so the audit + retry +
- // rebuild branches all run for real.
- let docs = tmp.path().join("docs");
- std::fs::create_dir_all(&docs).unwrap();
- std::fs::write(docs.join("note.md"), "# Note\n\nHello world.\n").unwrap();
-
- let source = MemorySourceEntry {
- id: "folder-1".to_string(),
- kind: SourceKind::Folder,
- label: "Docs".to_string(),
- enabled: true,
- path: Some(docs.to_string_lossy().to_string()),
- glob: Some("**/*.md".to_string()),
- url: None,
- toolkit: None,
- connection_id: None,
- branch: None,
- paths: Vec::new(),
- max_commits: None,
- max_issues: None,
- max_prs: None,
- query: None,
- since_days: None,
- max_items: None,
- selector: None,
- max_tokens_per_sync: None,
- max_cost_per_sync_usd: None,
- sync_depth_days: None,
- };
-
- // First call kicks off the background task and returns Ok immediately.
- sync_source(source.clone(), Arc::new(cfg.clone()))
- .await
- .expect("first sync_source should return Ok");
-
- // While the source id may already be released by the time the spawned
- // task finishes, the contract under test is: a call that observes the
- // id already in ACTIVE_SYNCS no-ops. We verify the public contract by
- // hammering several concurrent calls and asserting none error and the
- // audit log records at most as many runs as calls (mutex dedups
- // overlapping work rather than double-processing).
- let mut handles = Vec::new();
- for _ in 0..5 {
- let s = source.clone();
- let c = Arc::new(cfg.clone());
- handles.push(tokio::spawn(async move { sync_source(s, c).await }));
- }
- for h in handles {
- assert!(
- h.await.unwrap().is_ok(),
- "concurrent sync_source calls must all return Ok (no-op when locked)"
- );
- }
-
- // Disabled sources are rejected outright (separate guard, same fn).
- let mut disabled = source.clone();
- disabled.enabled = false;
- let err = sync_source(disabled, Arc::new(cfg.clone()))
- .await
- .unwrap_err();
- assert!(
- err.contains("disabled"),
- "disabled source should be rejected, got: {err}"
- );
-
- // Let the spawned background tasks finish (ingest + audit write). The
- // dispatcher audits Folder syncs; retry_all_failed runs inside the task
- // (zero failed jobs on a clean workspace, so it's a no-op but covered).
- tokio::time::sleep(std::time::Duration::from_millis(800)).await;
-
- let audit = read_audit_log(&cfg);
- assert!(
- audit.iter().any(|e| e.source_kind == "folder"),
- "folder sync should produce a folder audit entry; got {audit:?}"
- );
- // The mutex must prevent runaway duplicate processing: with 6 calls for
- // the same source id, far fewer than 6 audit entries should exist.
- let folder_runs = audit.iter().filter(|e| e.source_kind == "folder").count();
- assert!(
- folder_runs <= 6,
- "mutex should dedup overlapping syncs, saw {folder_runs} folder runs"
- );
-}
-
-// ── Test 5: check_and_rebuild_tree auto-detect (via needs_rebuild) ─────────
-
-/// `check_and_rebuild_tree` is private to the dispatcher; its decision gate
-/// is the public `needs_rebuild`, and its action is `rebuild_tree_from_raw`.
-/// This test drives the same auto-detect → rebuild path the dispatcher runs:
-/// seed raw with no summaries (max_level 0) → `needs_rebuild` returns true →
-/// rebuild → `needs_rebuild` returns false (tree now has summaries).
-#[tokio::test]
-async fn check_and_rebuild_auto_detects_raw_without_summaries() {
- let tmp = TempDir::new().unwrap();
- let cfg = test_config(&tmp);
- let scope = "gmail:auto-at-example-dot-com";
-
- let content_root = cfg.memory_tree_content_root();
- let emails_dir = raw_kind_dir(&content_root, scope, RawKind::Email);
- std::fs::create_dir_all(&emails_dir).unwrap();
- std::fs::write(
- emails_dir.join("1700000000000_a.md"),
- "# A\n\nFirst email.\n",
- )
- .unwrap();
- std::fs::write(
- emails_dir.join("1700000000001_b.md"),
- "# B\n\nSecond email.\n",
- )
- .unwrap();
-
- // Before: raw exists, tree at level 0 → rebuild needed.
- assert!(
- needs_rebuild(&cfg, scope, scope),
- "needs_rebuild must be true when raw files exist with no coverage"
- );
-
- // Drive the rebuild (what check_and_rebuild_tree calls).
- rebuild_tree_from_raw(&cfg, scope, scope).await.unwrap();
-
- // After: tree now has summaries → no further rebuild needed.
- let tree = get_or_create_source_tree(&cfg, scope).unwrap();
- assert!(
- tree.max_level > 0,
- "tree should have summaries after rebuild, max_level={}",
- tree.max_level
- );
- assert!(
- !needs_rebuild(&cfg, scope, scope),
- "needs_rebuild must be false once every raw file is covered"
- );
-
- // A scope with no raw files on disk never triggers a rebuild.
- assert!(
- !needs_rebuild(
- &cfg,
- "gmail:empty-at-example-dot-com",
- "gmail:empty-at-example-dot-com"
- ),
- "needs_rebuild must be false when no raw directory exists"
- );
-}
-
-// ── Test 6: graph export — source roots, doc leaves, orphan linking ────────
-
-// `graph_export_builds_source_roots_doc_leaves_and_orphan_links` used to sit
-// here. `graph_export_rpc` reads the forest through `summary_forest`, which the
-// memory module serves now (#5560), so the case could only run against a loaded
-// module — and what it actually asserted was the host's own shaping of that
-// forest, not the store underneath it. Those assertions moved to
-// `src/openhuman/memory/read_rpc/graph_tests.rs`, where they are a pure
-// function of a hand-built forest and cover more cases than this one could.
diff --git a/tests/ollama_embeddings_fallback_e2e.rs b/tests/ollama_embeddings_fallback_e2e.rs
index 83f6046451..79fe663c53 100644
--- a/tests/ollama_embeddings_fallback_e2e.rs
+++ b/tests/ollama_embeddings_fallback_e2e.rs
@@ -46,9 +46,6 @@ fn ensure_memory_seams() {
.name("ollama-embeddings-fallback-e2e-seams".to_string())
.stack_size(8 * 1024 * 1024)
.spawn(|| {
- openhuman_core::openhuman::memory::host_impls::install_memory_host_seams(Arc::new(
- Config::default(),
- ));
})
.expect("spawn ollama embeddings seam installer")
.join()
diff --git a/tests/raw_coverage/agent_archivist_debug_round21_raw_coverage_e2e.rs b/tests/raw_coverage/agent_archivist_debug_round21_raw_coverage_e2e.rs
index b6698e3696..9bbd587c14 100644
--- a/tests/raw_coverage/agent_archivist_debug_round21_raw_coverage_e2e.rs
+++ b/tests/raw_coverage/agent_archivist_debug_round21_raw_coverage_e2e.rs
@@ -179,9 +179,6 @@ impl Tool for EchoTool {
fn setup_provider() -> (TempDir, Arc, Arc) {
// The cfg(test)-only installer is out of reach for an external test
// target; the public boot-shaped seam does the same job here.
- openhuman_core::openhuman::memory::host_impls::install_memory_host_seams(Arc::new(
- openhuman_core::openhuman::config::Config::default(),
- ));
let tmp = TempDir::new().expect("tempdir");
let workspace = tmp.path().join("ws");
std::fs::create_dir_all(&workspace).expect("workspace dir");
diff --git a/tests/raw_coverage/agent_orchestration_e2e.rs b/tests/raw_coverage/agent_orchestration_e2e.rs
index 01b26839c9..9348fed4e0 100644
--- a/tests/raw_coverage/agent_orchestration_e2e.rs
+++ b/tests/raw_coverage/agent_orchestration_e2e.rs
@@ -129,9 +129,6 @@ fn ensure_memory_seams() {
workspace_dir: workspace,
..openhuman_core::openhuman::config::Config::default()
});
- openhuman_core::openhuman::memory::host_impls::install_memory_host_seams(
- Arc::clone(&config),
- );
#[cfg(feature = "modules")]
openhuman_core::openhuman::modules::memory::set_modules_policy(config);
})
diff --git a/tests/raw_coverage/agent_session_turn_raw_coverage_e2e.rs b/tests/raw_coverage/agent_session_turn_raw_coverage_e2e.rs
index 4ee2067060..5cfbf9e715 100644
--- a/tests/raw_coverage/agent_session_turn_raw_coverage_e2e.rs
+++ b/tests/raw_coverage/agent_session_turn_raw_coverage_e2e.rs
@@ -75,9 +75,6 @@ fn ensure_memory_seams() {
.name("agent-session-turn-raw-coverage-seams".to_string())
.stack_size(8 * 1024 * 1024)
.spawn(|| {
- openhuman_core::openhuman::memory::host_impls::install_memory_host_seams(
- Arc::new(Config::default()),
- );
})
.expect("spawn agent session turn raw coverage seam installer")
.join()
diff --git a/tests/raw_coverage/app_credentials_threads_memory_sources_raw_coverage_e2e.rs b/tests/raw_coverage/app_credentials_threads_memory_sources_raw_coverage_e2e.rs
index 6d9be1bc06..ba886453b8 100644
--- a/tests/raw_coverage/app_credentials_threads_memory_sources_raw_coverage_e2e.rs
+++ b/tests/raw_coverage/app_credentials_threads_memory_sources_raw_coverage_e2e.rs
@@ -39,9 +39,6 @@ fn ensure_memory_seams() {
.name("round19-memory-source-seams".to_string())
.stack_size(8 * 1024 * 1024)
.spawn(|| {
- openhuman_core::openhuman::memory::host_impls::install_memory_host_seams(
- Arc::new(Config::default()),
- );
})
.expect("spawn round19 memory source seam installer")
.join()
diff --git a/tests/raw_coverage/channels_web_startup_raw_coverage_e2e.rs b/tests/raw_coverage/channels_web_startup_raw_coverage_e2e.rs
index 7cb7411ce2..26aa01da90 100644
--- a/tests/raw_coverage/channels_web_startup_raw_coverage_e2e.rs
+++ b/tests/raw_coverage/channels_web_startup_raw_coverage_e2e.rs
@@ -28,9 +28,6 @@ fn ensure_memory_seams() {
.name("channels-web-startup-raw-coverage-seams".to_string())
.stack_size(8 * 1024 * 1024)
.spawn(|| {
- openhuman_core::openhuman::memory::host_impls::install_memory_host_seams(
- Arc::new(Config::default()),
- );
})
.expect("spawn channels web startup raw coverage seam installer")
.join()
diff --git a/tests/raw_coverage/learning_facets_e2e.rs b/tests/raw_coverage/learning_facets_e2e.rs
index ea07cac16d..33da607427 100644
--- a/tests/raw_coverage/learning_facets_e2e.rs
+++ b/tests/raw_coverage/learning_facets_e2e.rs
@@ -115,9 +115,6 @@ fn ensure_memory_seams() {
.stack_size(8 * 1024 * 1024)
.spawn(|| {
let config = Arc::new(shared_config_at(learning_workspace()));
- openhuman_core::openhuman::memory::host_impls::install_memory_host_seams(
- config.clone(),
- );
#[cfg(feature = "modules")]
openhuman_core::openhuman::modules::memory::set_modules_policy(config);
})
diff --git a/tests/raw_coverage/memory_core_threads_raw_coverage_e2e.rs b/tests/raw_coverage/memory_core_threads_raw_coverage_e2e.rs
deleted file mode 100644
index 21cb28f1a4..0000000000
--- a/tests/raw_coverage/memory_core_threads_raw_coverage_e2e.rs
+++ /dev/null
@@ -1,670 +0,0 @@
-//! Round 16 raw integration coverage for memory-core and threads.
-//!
-//! These tests keep all state under temp workspaces and call public Rust
-//! surfaces directly. Run with `--test-threads=1`; thread ops resolve the
-//! workspace through process environment.
-
-use chrono::{Duration, TimeZone, Utc};
-use serde_json::json;
-use std::ffi::OsString;
-use std::fs;
-use std::path::{Path, PathBuf};
-use tempfile::TempDir;
-
-use openhuman_core::openhuman::agent::progress::AgentProgress;
-use openhuman_core::openhuman::config::Config;
-use openhuman_core::openhuman::memory::conversations::{
- ensure_thread, list_threads, CreateConversationThread,
-};
-use openhuman_core::openhuman::memory::read_rpc::{self, ChunkFilter, GraphMode};
-use openhuman_core::openhuman::memory::{
- AppendConversationMessageRequest, ConversationMessageRecord, ConversationMessagesRequest,
- CreateConversationThreadRequest, DeleteConversationThreadRequest, EmptyRequest,
- GenerateConversationThreadTitleRequest, UpdateConversationMessageRequest,
- UpdateConversationThreadLabelsRequest, UpdateConversationThreadTitleRequest,
-};
-use openhuman_core::openhuman::threads::ops as thread_ops;
-use openhuman_core::openhuman::threads::turn_state::{
- self, ClearTurnStateRequest, GetTurnStateRequest, TurnLifecycle, TurnStateMirror,
- TurnStateStore,
-};
-use openhuman_core::openhuman::threads::welcome_migration::migrate_welcome_agent_artifacts;
-use tinymemory_core::store::chunks::store::{upsert_chunks, with_connection};
-use tinymemory_core::store::chunks::types::{
- approx_token_count, chunk_id, Chunk, Metadata, SourceKind, SourceRef,
-};
-use tinymemory_core::store::content;
-use tinymemory_core::store::trees::store as tree_store;
-use tinymemory_core::store::trees::types::{SummaryNode, TreeKind};
-use tinymemory_core::tree::score::embed::pack_embedding;
-use tinymemory_core::tree::score::extract::EntityKind;
-use tinymemory_core::tree::score::resolver::CanonicalEntity;
-use tinymemory_core::tree::score::signals::ScoreSignals;
-use tinymemory_core::tree::score::store::{index_entity, upsert_score, ScoreRow};
-use tinymemory_core::tree_source::get_or_create_source_tree;
-
-struct EnvGuard {
- key: &'static str,
- old: Option,
-}
-
-impl EnvGuard {
- fn set_path(key: &'static str, value: &Path) -> Self {
- let old = std::env::var_os(key);
- unsafe {
- std::env::set_var(key, value);
- }
- Self { key, old }
- }
-}
-
-impl Drop for EnvGuard {
- fn drop(&mut self) {
- unsafe {
- match &self.old {
- Some(value) => std::env::set_var(self.key, value),
- None => std::env::remove_var(self.key),
- }
- }
- }
-}
-
-fn config_in(tmp: &TempDir) -> Config {
- let mut cfg = Config {
- workspace_dir: tmp.path().to_path_buf(),
- embeddings_provider: Some("none".into()),
- memory_provider: Some("cloud".into()),
- ..Config::default()
- };
- cfg.memory_tree.embedding_endpoint = None;
- cfg.memory_tree.embedding_model = None;
- cfg.memory_tree.embedding_strict = false;
- cfg
-}
-
-fn test_chunk(source_id: &str, seq: u32, content: &str, ts_ms: i64) -> Chunk {
- let ts = Utc.timestamp_millis_opt(ts_ms).single().unwrap();
- let mut metadata = Metadata::point_in_time(SourceKind::Chat, source_id, "owner@example", ts);
- metadata.tags = vec!["round16".into(), format!("seq-{seq}")];
- metadata.source_ref = Some(SourceRef::new(format!("chat://{source_id}/{seq}")));
- Chunk {
- id: chunk_id(SourceKind::Chat, source_id, seq, content),
- content: content.to_string(),
- metadata,
- token_count: approx_token_count(content),
- seq_in_source: seq,
- created_at: ts,
- partial_message: false,
- }
-}
-
-fn seed_content_paths(cfg: &Config, chunks: &[Chunk]) {
- let root = cfg.memory_tree_content_root();
- fs::create_dir_all(&root).unwrap();
- let staged = content::stage_chunks(&root, chunks).unwrap();
- with_connection(cfg, |conn| {
- for staged_chunk in &staged {
- conn.execute(
- "UPDATE mem_tree_chunks
- SET content_path = ?1, content_sha256 = ?2
- WHERE id = ?3",
- rusqlite::params![
- staged_chunk.content_path,
- staged_chunk.content_sha256,
- staged_chunk.chunk.id,
- ],
- )?;
- }
- Ok(())
- })
- .unwrap();
-}
-
-fn insert_summary(cfg: &Config, node: &SummaryNode, content_path: Option<&str>) {
- let embedding = node
- .embedding
- .as_ref()
- .map(|v| pack_embedding(v))
- .unwrap_or_default();
- with_connection(cfg, |conn| {
- conn.execute(
- "INSERT OR REPLACE INTO mem_tree_summaries (
- id, tree_id, tree_kind, level, parent_id, child_ids_json,
- content, token_count, entities_json, topics_json,
- time_range_start_ms, time_range_end_ms, score, sealed_at_ms,
- deleted, embedding, content_path
- ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17)",
- rusqlite::params![
- node.id,
- node.tree_id,
- node.tree_kind.as_str(),
- node.level as i64,
- node.parent_id,
- serde_json::to_string(&node.child_ids)?,
- node.content,
- node.token_count as i64,
- serde_json::to_string(&node.entities)?,
- serde_json::to_string(&node.topics)?,
- node.time_range_start.timestamp_millis(),
- node.time_range_end.timestamp_millis(),
- node.score,
- node.sealed_at.timestamp_millis(),
- i32::from(node.deleted),
- if embedding.is_empty() {
- None
- } else {
- Some(embedding)
- },
- content_path,
- ],
- )?;
- Ok(())
- })
- .unwrap();
-}
-
-fn daily_node(id: &str, tree_id: &str, day: chrono::DateTime) -> SummaryNode {
- SummaryNode {
- id: id.into(),
- tree_id: tree_id.into(),
- tree_kind: TreeKind::Global,
- level: 0,
- parent_id: None,
- child_ids: Vec::new(),
- content: format!("Daily digest for {id} with Alice and Phoenix planning."),
- token_count: 64,
- entities: vec!["person:alice".into()],
- topics: vec!["phoenix".into()],
- time_range_start: day,
- time_range_end: day + Duration::hours(1),
- score: 0.7,
- sealed_at: day + Duration::hours(2),
- deleted: false,
- embedding: Some(vec![0.0; 1024]),
- doc_id: None,
- version_ms: None,
- }
-}
-
-// Serialize env mutation against every other aggregated suite via the
-// single crate-wide SHARED_ENV_LOCK (these tests use an `EnvGuard` struct
-// that does not itself hold a lock). Poison is recovered so a panic
-// elsewhere cannot wedge the suite.
-fn __shared_env_lock() -> std::sync::MutexGuard<'static, ()> {
- crate::SHARED_ENV_LOCK
- .get_or_init(|| std::sync::Mutex::new(()))
- .lock()
- .unwrap_or_else(|poisoned| poisoned.into_inner())
-}
-
-#[tokio::test]
-async fn memory_read_rpc_filters_graphs_scores_reset_and_wipe_seeded_rows() {
- let _env_lock = __shared_env_lock();
- let tmp = TempDir::new().unwrap();
- let cfg = config_in(&tmp);
- // The `read_rpc` listings below go through the bound memory driver, which
- // under the `modules` gate is the loaded tinymemory artifact. That driver
- // resolves its config from the process-wide boot policy, so a test binary
- // has to publish one the way boot does. The policy is first-call-wins and
- // the module takes its `workspace_dir` at load time, so it is built from
- // THIS test's `cfg`: the rows seeded in-process below and the rows the
- // module lists must name the same store. No other case in this aggregated
- // binary routes through the module, so nothing contends for the slot.
- #[cfg(feature = "modules")]
- openhuman_core::openhuman::modules::memory::set_modules_policy(std::sync::Arc::new(
- cfg.clone(),
- ));
- let ts0 = Utc.with_ymd_and_hms(2026, 5, 20, 9, 0, 0).unwrap();
- let chunks = vec![
- test_chunk(
- "gmail:me@example.com|alice@example.com",
- 0,
- "Alice shared the Phoenix launch checklist and budget.",
- ts0.timestamp_millis(),
- ),
- test_chunk(
- "slack:#ops",
- 1,
- "Bob asked Alice for the deploy window in Phoenix.",
- (ts0 + Duration::hours(1)).timestamp_millis(),
- ),
- ];
- upsert_chunks(&cfg, &chunks).unwrap();
- seed_content_paths(&cfg, &chunks);
-
- let alice = CanonicalEntity {
- canonical_id: "person:alice".into(),
- kind: EntityKind::Person,
- surface: "Alice".into(),
- span_start: 0,
- span_end: 5,
- score: 0.95,
- };
- let topic = CanonicalEntity {
- canonical_id: "topic:phoenix".into(),
- kind: EntityKind::Topic,
- surface: "Phoenix".into(),
- span_start: 0,
- span_end: 7,
- score: 0.8,
- };
- for chunk in &chunks {
- index_entity(
- &cfg,
- &alice,
- &chunk.id,
- "leaf",
- chunk.metadata.timestamp.timestamp_millis(),
- Some("source:chat"),
- )
- .unwrap();
- index_entity(
- &cfg,
- &topic,
- &chunk.id,
- "leaf",
- chunk.metadata.timestamp.timestamp_millis(),
- Some("source:chat"),
- )
- .unwrap();
- }
- upsert_score(
- &cfg,
- &ScoreRow {
- chunk_id: chunks[0].id.clone(),
- total: 4.5,
- signals: ScoreSignals {
- token_count: 0.4,
- unique_words: 0.8,
- metadata_weight: 1.0,
- source_weight: 0.9,
- interaction: 0.7,
- entity_density: 0.6,
- llm_importance: 0.5,
- },
- dropped: false,
- reason: Some("coverage fixture".into()),
- computed_at_ms: ts0.timestamp_millis(),
- llm_importance_reason: Some("important planning".into()),
- },
- )
- .unwrap();
-
- let tree = get_or_create_source_tree(&cfg, "gmail:me@example.com|alice@example.com").unwrap();
- let summary = SummaryNode {
- id: "summary:L1:round16".into(),
- tree_id: tree.id.clone(),
- tree_kind: TreeKind::Source,
- level: 1,
- parent_id: None,
- child_ids: chunks.iter().map(|c| c.id.clone()).collect(),
- content: "Alice and Bob discussed Phoenix launch operations.".into(),
- token_count: 80,
- entities: vec!["person:alice".into()],
- topics: vec!["phoenix".into()],
- time_range_start: ts0,
- time_range_end: ts0 + Duration::hours(2),
- score: 0.9,
- sealed_at: ts0 + Duration::hours(3),
- deleted: false,
- embedding: Some(vec![0.0; 1024]),
- doc_id: None,
- version_ms: None,
- };
- insert_summary(
- &cfg,
- &summary,
- Some("wiki/summaries/source/summary-L1-round16.md"),
- );
-
- let listed = read_rpc::list_chunks_rpc(
- &cfg,
- ChunkFilter {
- source_kinds: Some(vec!["chat".into()]),
- entity_ids: Some(vec!["person:alice".into()]),
- query: Some("Phoenix".into()),
- limit: Some(10),
- ..ChunkFilter::default()
- },
- )
- .await
- .unwrap();
- assert_eq!(listed.value.total, 2);
- assert!(listed.value.chunks[0].content_preview.is_some());
-
- let sources = read_rpc::list_sources_rpc(&cfg, Some("me@example.com".into()))
- .await
- .unwrap();
- assert!(sources.value.iter().any(|source| source.source_id
- == "gmail:me@example.com|alice@example.com"
- && source.chunk_count == 1));
-
- assert_eq!(
- read_rpc::search_rpc(&cfg, "deploy".into(), 5)
- .await
- .unwrap()
- .value
- .len(),
- 1
- );
- assert_eq!(
- read_rpc::entity_index_for_rpc(&cfg, chunks[0].id.clone())
- .await
- .unwrap()
- .value
- .len(),
- 2
- );
- assert_eq!(
- read_rpc::chunks_for_entity_rpc(&cfg, "person:alice".into())
- .await
- .unwrap()
- .value
- .len(),
- 2
- );
- assert_eq!(
- read_rpc::top_entities_rpc(&cfg, Some("person".into()), 5)
- .await
- .unwrap()
- .value[0]
- .entity_id,
- "person:alice"
- );
- let score = read_rpc::chunk_score_rpc(&cfg, chunks[0].id.clone())
- .await
- .unwrap()
- .value
- .unwrap();
- assert!(score.kept);
- assert!(
- !score.llm_consulted,
- "TinyCortex does not persist admission-time LLM importance"
- );
-
- let tree_graph = read_rpc::graph_export_rpc(&cfg, GraphMode::Tree)
- .await
- .unwrap();
- assert!(tree_graph
- .value
- .nodes
- .iter()
- .any(|node| node.kind == "summary"));
- let contacts_graph = read_rpc::graph_export_rpc(&cfg, GraphMode::Contacts)
- .await
- .unwrap();
- assert!(contacts_graph.value.edges.len() >= 2);
- assert!(
- !read_rpc::obsidian_vault_status_rpc(&cfg, Some(" ".into()))
- .await
- .unwrap()
- .value
- .registered
- );
-
- let deleted = read_rpc::delete_chunk_rpc(&cfg, chunks[1].id.clone())
- .await
- .unwrap();
- assert!(deleted.value.deleted);
- assert_eq!(deleted.value.entity_index_rows_removed, 2);
- assert!(
- !read_rpc::delete_chunk_rpc(&cfg, "missing-chunk".into())
- .await
- .unwrap()
- .value
- .deleted
- );
-
- // `reset_tree` and `flush_now` are deliberately no longer exercised here.
- // Both read and mutate through the bound memory driver now
- // (`Maintenance::reset_derived_index` / `flush_pending`), and an
- // integration test cannot bind one: with nothing bound the resolve
- // refuses, and with a real module on the path it answers from the
- // module's own store rather than the rows staged above. Their behaviour
- // is pinned where a real store exists — the driver's conformance suite
- // (`resetting_the_derived_index_keeps_the_chunks_it_derives_from`,
- // `flushing_twice_in_a_window_schedules_the_work_once`) — and the host's
- // wire mapping in `read_rpc_tests`.
-
- fs::create_dir_all(cfg.memory_tree_content_root().join("raw")).unwrap();
- fs::write(cfg.memory_tree_content_root().join("raw").join("x.md"), "x").unwrap();
- let wipe = read_rpc::wipe_all_rpc(&cfg).await.unwrap().value;
- assert!(wipe.rows_deleted >= 1);
- assert!(wipe.dirs_removed.iter().any(|dir| dir == "raw"));
-}
-
-#[tokio::test]
-async fn thread_ops_welcome_migration_and_turn_state_cover_error_and_cleanup_paths() {
- let _env_lock = __shared_env_lock();
- let tmp = TempDir::new().unwrap();
- let _env = EnvGuard::set_path("OPENHUMAN_WORKSPACE", tmp.path());
- let workspace = Config::load_or_init().await.unwrap().workspace_dir;
-
- ensure_thread(
- workspace.clone(),
- CreateConversationThread {
- id: "legacy-thread".into(),
- title: "Legacy".into(),
- created_at: "2026-05-01T00:00:00Z".into(),
- parent_thread_id: None,
- labels: Some(vec!["onboarding".into(), "inbox".into()]),
- personality_id: None,
- },
- )
- .unwrap();
- write_welcome_transcript(&workspace, "20260501_welcome", "welcome", "legacy-thread");
- fs::create_dir_all(workspace.join("sessions").join("legacy-thread")).unwrap();
- fs::write(
- workspace
- .join("sessions")
- .join("legacy-thread")
- .join("20260501_welcome.md"),
- "markdown",
- )
- .unwrap();
-
- let migration = migrate_welcome_agent_artifacts(&workspace).unwrap();
- assert_eq!(migration.threads_updated, 1);
- assert_eq!(migration.transcripts_updated, 1);
- assert_eq!(migration.transcript_files_renamed, 1);
- assert!(
- migrate_welcome_agent_artifacts(&workspace)
- .unwrap()
- .already_done
- );
- assert!(list_threads(workspace.clone())
- .unwrap()
- .into_iter()
- .find(|thread| thread.id == "legacy-thread")
- .unwrap()
- .labels
- .iter()
- .all(|label| label != "onboarding"));
-
- let created = thread_ops::thread_create_new(CreateConversationThreadRequest {
- labels: Some(vec!["chat".into()]),
- personality_id: Some("default".into()),
- })
- .await
- .unwrap()
- .value
- .data
- .unwrap();
- let thread_id = created.id;
-
- let msg_id = "msg-round16".to_string();
- let appended = thread_ops::message_append(AppendConversationMessageRequest {
- thread_id: thread_id.clone(),
- message: ConversationMessageRecord {
- id: msg_id.clone(),
- content: "Please summarize the Phoenix budget risks for Alice.".into(),
- message_type: "text".into(),
- extra_metadata: json!({"draft": true}),
- sender: "user".into(),
- created_at: "2026-05-21T10:00:00Z".into(),
- },
- })
- .await
- .unwrap()
- .value
- .data
- .unwrap();
- assert_eq!(appended.id, msg_id);
-
- let generated = thread_ops::thread_generate_title(GenerateConversationThreadTitleRequest {
- thread_id: thread_id.clone(),
- assistant_message: None,
- })
- .await
- .unwrap()
- .value
- .data
- .unwrap();
- assert!(generated.title.contains("Phoenix") || generated.title.contains("budget"));
-
- assert!(
- thread_ops::thread_update_title(UpdateConversationThreadTitleRequest {
- thread_id: thread_id.clone(),
- title: " ".into(),
- })
- .await
- .is_err()
- );
- assert_eq!(
- thread_ops::thread_update_labels(UpdateConversationThreadLabelsRequest {
- thread_id: thread_id.clone(),
- labels: vec!["starred".into(), "archive".into()],
- })
- .await
- .unwrap()
- .value
- .data
- .unwrap()
- .labels,
- vec!["starred", "archive"]
- );
- let updated_msg = thread_ops::message_update(UpdateConversationMessageRequest {
- thread_id: thread_id.clone(),
- message_id: msg_id.clone(),
- extra_metadata: Some(json!({"draft": false, "edited": true})),
- })
- .await
- .unwrap()
- .value
- .data
- .unwrap();
- assert_eq!(updated_msg.extra_metadata["edited"], true);
- assert_eq!(
- thread_ops::messages_list(ConversationMessagesRequest {
- thread_id: thread_id.clone()
- })
- .await
- .unwrap()
- .value
- .data
- .unwrap()
- .count,
- 1
- );
-
- let store = TurnStateStore::new(workspace.clone());
- let mut mirror = TurnStateMirror::new(store, &thread_id, "request-round16");
- assert!(!mirror.observe(&AgentProgress::ToolCallArgsDelta {
- call_id: "call-1".into(),
- tool_name: "memory.search".into(),
- delta: "{\"q\":\"phoenix\"}".into(),
- iteration: 1,
- }));
- assert!(mirror.observe(&AgentProgress::ToolCallStarted {
- call_id: "call-1".into(),
- tool_name: "memory.search".into(),
- arguments: json!({"q": "phoenix"}),
- iteration: 1,
- display_label: None,
- display_detail: None,
- }));
- assert!(mirror.observe(&AgentProgress::SubagentSpawned {
- agent_id: "researcher".into(),
- task_id: "task-1".into(),
- mode: "typed".into(),
- dedicated_thread: true,
- prompt_chars: 42,
- prompt: String::new(),
- worker_thread_id: None,
- display_name: Some("Researcher".into()),
- }));
- assert!(mirror.observe(&AgentProgress::SubagentCompleted {
- agent_id: "researcher".into(),
- task_id: "task-1".into(),
- elapsed_ms: 50,
- iterations: 2,
- output_chars: 100,
- output: String::new(),
- worktree_path: None,
- changed_files: vec![],
- dirty_status: None,
- }));
- mirror.finish();
-
- let turn_get = thread_ops::turn_state_get(GetTurnStateRequest {
- thread_id: thread_id.clone(),
- })
- .await
- .unwrap()
- .value
- .data
- .unwrap();
- assert_eq!(
- turn_get.turn_state.unwrap().lifecycle,
- TurnLifecycle::Interrupted
- );
- assert!(
- thread_ops::turn_state_clear(ClearTurnStateRequest {
- thread_id: thread_id.clone()
- })
- .await
- .unwrap()
- .value
- .data
- .unwrap()
- .cleared
- );
- assert!(turn_state::store::get(workspace.clone(), &thread_id)
- .unwrap()
- .is_none());
-
- let deleted = thread_ops::thread_delete(DeleteConversationThreadRequest {
- thread_id: thread_id.clone(),
- deleted_at: "2026-05-21T12:00:00Z".into(),
- })
- .await
- .unwrap()
- .value
- .data
- .unwrap();
- assert!(deleted.deleted);
- assert!(
- thread_ops::thread_generate_title(GenerateConversationThreadTitleRequest {
- thread_id,
- assistant_message: Some("unused".into()),
- })
- .await
- .is_err()
- );
-
- let purged = thread_ops::threads_purge(EmptyRequest {}).await.unwrap();
- assert_eq!(purged.value.data.unwrap().agent_threads_deleted, 1);
-}
-
-fn write_welcome_transcript(workspace: &Path, stem: &str, agent: &str, thread_id: &str) -> PathBuf {
- let path = workspace.join("session_raw").join(format!("{stem}.jsonl"));
- fs::create_dir_all(path.parent().unwrap()).unwrap();
- fs::write(
- &path,
- format!(
- "{{\"_meta\":{{\"agent\":\"{agent}\",\"dispatcher\":\"native\",\"created\":\"2026-05-01T00:00:00Z\",\"updated\":\"2026-05-01T00:00:00Z\",\"turn_count\":1,\"input_tokens\":0,\"output_tokens\":0,\"cached_input_tokens\":0,\"charged_amount_usd\":0.0,\"thread_id\":\"{thread_id}\"}}}}\n{{\"role\":\"user\",\"content\":\"hi\"}}\n"
- ),
- )
- .unwrap();
- path
-}
diff --git a/tests/raw_coverage/memory_goals_people_e2e.rs b/tests/raw_coverage/memory_goals_people_e2e.rs
index 00b0f515e4..c87b7979ac 100644
--- a/tests/raw_coverage/memory_goals_people_e2e.rs
+++ b/tests/raw_coverage/memory_goals_people_e2e.rs
@@ -105,9 +105,6 @@ fn ensure_memory_seams() {
.stack_size(8 * 1024 * 1024)
.spawn(|| {
let config = Arc::new(shared_config_at(memory_workspace()));
- openhuman_core::openhuman::memory::host_impls::install_memory_host_seams(
- config.clone(),
- );
#[cfg(feature = "modules")]
openhuman_core::openhuman::modules::memory::set_modules_policy(config);
})
diff --git a/tests/raw_coverage/memory_raw_coverage_e2e.rs b/tests/raw_coverage/memory_raw_coverage_e2e.rs
deleted file mode 100644
index 4c66f97f55..0000000000
--- a/tests/raw_coverage/memory_raw_coverage_e2e.rs
+++ /dev/null
@@ -1,685 +0,0 @@
-//! Focused raw integration coverage for memory-family modules.
-//!
-//! These tests avoid network and keep all state in per-test tempdirs. Run with
-//! `--test-threads=1` because several memory surfaces use process-global
-//! stores or cached SQLite connections.
-
-use chrono::{TimeZone, Utc};
-use serde_json::json;
-use tempfile::TempDir;
-
-use openhuman_core::openhuman::config::Config;
-use openhuman_core::openhuman::memory::NamespaceDocumentInput;
-// The engine's own ingest request/config — what `UnifiedMemory::ingest_document`
-// takes. `memory::MemoryIngestion*` are the host's WIRE shapes now
-// (`rpc_models`), distinct types (#5560).
-use tinycortex::memory::ingest::{ExtractionMode, MemoryIngestionConfig, MemoryIngestionRequest};
-// The in-process ingest queue is the engine's and has no bus representation, so
-// `memory::mod` stopped re-exporting it for a consumer that was only ever this
-// test (#5560). Named on the crate directly, exactly as `upsert_chunks` below
-// already is — `tinymemory-core` is a dev-dependency, which this target links.
-use tinymemory_core::ingestion::IngestionState;
-// The engine's per-source SQL read, which is what this suite seeds a store for.
-// `memory::sources::status` is host-side now and asks the bound driver, which an
-// integration test has no module to load (#5560).
-use tinymemory_core::sources::status::{source_status, FreshnessLabel};
-use openhuman_core::openhuman::memory::sources::{MemorySourceEntry, SourceKind};
-use tinymemory_core::store::chunks::store::upsert_chunks;
-use tinymemory_core::store::chunks::types::{
- approx_token_count, chunk_id, Chunk, Metadata, SourceKind as ChunkSourceKind, SourceRef,
-};
-use tinycortex::memory::ingest::canonicalize::chat::{
- canonicalise as canonicalise_chat, ChatBatch, ChatMessage,
-};
-use tinycortex::memory::ingest::canonicalize::document::{
- canonicalise as canonicalise_document, DocumentInput,
-};
-use tinycortex::memory::ingest::canonicalize::email::{
- canonicalise as canonicalise_email, EmailMessage, EmailThread,
-};
-// These scope/catalog helpers moved off `memory::sync::composio::providers`
-// (the deleted engine registry's former home) onto
-// `integrations::composio::providers`, which re-exports them straight from
-// the `tinymemory-api` contract crate — see that module's doc comment.
-use openhuman_core::openhuman::integrations::composio::providers::{
- classify_unknown, find_curated, toolkit_from_slug, CuratedTool, ToolScope,
-};
-use tinycortex::memory::sync::{SyncOutcome, SyncPipelineKind};
-use tinymemory_core::tree::summarise::{
- fallback_summary, SummaryContext, SummaryInput,
-};
-use tinymemory_core::tree::tree_runtime::store as tree_store;
-use openhuman_core::openhuman::memory::tree::tree_runtime::{
- derive_node_ids, estimate_tokens, level_from_node_id, node_id_to_path, NodeLevel, TreeNode,
-};
-use openhuman_core::openhuman::threads::turn_state::{
- SubagentActivity, SubagentToolCall, ToolTimelineEntry, ToolTimelineStatus, TurnLifecycle,
- TurnPhase, TurnState, TurnStateStore,
-};
-
-fn config_in(tmp: &TempDir) -> Config {
- Config {
- workspace_dir: tmp.path().to_path_buf(),
- ..Config::default()
- }
-}
-
-fn source_entry(kind: SourceKind, id: &str) -> MemorySourceEntry {
- MemorySourceEntry {
- id: id.to_string(),
- kind,
- label: format!("{id} label"),
- enabled: true,
- toolkit: None,
- connection_id: None,
- path: None,
- glob: None,
- url: None,
- branch: None,
- paths: Vec::new(),
- query: None,
- since_days: None,
- max_items: None,
- max_commits: None,
- max_issues: None,
- max_prs: None,
- selector: None,
- max_tokens_per_sync: None,
- max_cost_per_sync_usd: None,
- sync_depth_days: None,
- }
-}
-
-fn tree_node(namespace: &str, node_id: &str, summary: &str) -> TreeNode {
- let ts = Utc.with_ymd_and_hms(2026, 5, 29, 12, 30, 0).unwrap();
- TreeNode {
- node_id: node_id.to_string(),
- namespace: namespace.to_string(),
- level: level_from_node_id(node_id),
- parent_id: openhuman_core::openhuman::memory::tree::tree_runtime::derive_parent_id(node_id),
- summary: summary.to_string(),
- token_count: estimate_tokens(summary),
- child_count: 0,
- created_at: ts,
- updated_at: ts,
- metadata: Some(json!({ "test": "memory_raw_coverage", "node": node_id }).to_string()),
- }
-}
-
-fn chunk(source_id: &str, seq: u32, timestamp_ms: i64, embedding_pending: bool) -> Chunk {
- let content = format!("memory raw coverage chunk {source_id} #{seq}");
- let ts = Utc.timestamp_millis_opt(timestamp_ms).unwrap();
- let mut metadata = Metadata::point_in_time(ChunkSourceKind::Document, source_id, "owner", ts);
- metadata.tags = vec!["coverage".into()];
- metadata.source_ref = Some(SourceRef::new(format!("file:///{source_id}/{seq}")));
- let mut chunk = Chunk {
- id: chunk_id(ChunkSourceKind::Document, source_id, seq, &content),
- content,
- metadata,
- token_count: approx_token_count(source_id),
- seq_in_source: seq,
- created_at: ts,
- partial_message: false,
- };
- if !embedding_pending {
- chunk.partial_message = true;
- }
- chunk
-}
-
-#[test]
-fn memory_tree_store_round_trips_nodes_buffers_and_validation_edges() {
- let tmp = TempDir::new().expect("tempdir");
- let config = config_in(&tmp);
- let ns = "raw/coverage:tree";
-
- assert!(tree_store::validate_namespace("personal").is_ok());
- assert!(tree_store::validate_namespace(" ").is_err());
- assert!(tree_store::validate_namespace("../escape").is_err());
- assert!(tree_store::validate_namespace("/absolute").is_err());
- assert!(tree_store::validate_node_id("root").is_ok());
- assert!(tree_store::validate_node_id("2026/05/29/23").is_ok());
- assert!(tree_store::validate_node_id("2026/13").is_err());
- assert!(tree_store::validate_node_id("2026/05/32").is_err());
- assert!(tree_store::validate_node_id("2026/05/29/24").is_err());
- assert!(tree_store::validate_node_id("../root").is_err());
-
- for (node_id, summary) in [
- ("root", "Root summary for the workspace"),
- ("2026", "Year summary"),
- ("2026/05", "Month summary"),
- ("2026/05/29", "Day summary"),
- ("2026/05/29/12", "Hour leaf summary"),
- ] {
- tree_store::write_node(&config, &tree_node(ns, node_id, summary)).expect("write node");
- }
-
- let root = tree_store::read_node(&config, ns, "root")
- .expect("read root")
- .expect("root exists");
- assert_eq!(root.level, NodeLevel::Root);
- assert_eq!(root.parent_id, None);
-
- let root_children = tree_store::read_children(&config, ns, "root").expect("root children");
- assert_eq!(root_children.len(), 1);
- assert_eq!(root_children[0].node_id, "2026");
- let day_children = tree_store::read_children(&config, ns, "2026/05/29").expect("day children");
- assert_eq!(day_children[0].node_id, "2026/05/29/12");
-
- let ancestors = tree_store::read_ancestors(&config, ns, "2026/05/29/12").expect("ancestors");
- assert_eq!(
- ancestors
- .iter()
- .map(|n| n.node_id.as_str())
- .collect::>(),
- vec!["2026/05/29", "2026/05", "2026", "root"]
- );
-
- let status = tree_store::get_tree_status(&config, ns).expect("status");
- assert_eq!(status.total_nodes, 5);
- assert_eq!(status.depth, 5);
- assert!(status.oldest_entry.is_some());
- assert!(status.newest_entry.is_some());
-
- let ts = Utc.with_ymd_and_hms(2026, 5, 29, 13, 0, 0).unwrap();
- let first =
- tree_store::buffer_write(&config, ns, "plain buffer", &ts, None).expect("buffer write");
- let second = tree_store::buffer_write(
- &config,
- ns,
- "frontmatter buffer",
- &ts,
- Some(&json!({ "source": "test" })),
- )
- .expect("buffer write with metadata");
- assert!(first.exists());
- assert!(second.exists());
-
- let buffered = tree_store::buffer_read(&config, ns).expect("buffer read");
- assert_eq!(buffered.len(), 2);
- assert!(buffered.iter().any(|(_, body)| body == "plain buffer"));
- assert!(buffered
- .iter()
- .any(|(_, body)| body == "frontmatter buffer"));
-
- let drained = tree_store::buffer_drain(&config, ns).expect("buffer drain");
- assert_eq!(drained.len(), 2);
- assert!(tree_store::buffer_read(&config, ns)
- .expect("buffer empty")
- .is_empty());
-
- let collected = tree_store::collect_root_summaries_with_caps(tmp.path(), 10, 12);
- assert_eq!(collected.len(), 1);
- assert!(collected[0].1.contains("Root summa"));
- assert!(collected[0].1.contains("truncated"));
-
- let deleted = tree_store::delete_tree(&config, ns).expect("delete tree");
- assert_eq!(deleted, 5);
- assert_eq!(
- tree_store::delete_tree(&config, ns).expect("delete missing"),
- 0
- );
- assert!(tree_store::read_node(&config, ns, "root")
- .expect("read missing")
- .is_none());
-}
-
-#[test]
-fn memory_tree_types_and_fallback_summary_cover_budget_and_legacy_parse_paths() {
- let ts = Utc.with_ymd_and_hms(2026, 5, 29, 9, 8, 7).unwrap();
- let (hour, day, month, year, root) = derive_node_ids(&ts);
- assert_eq!(root, "root");
- assert_eq!(year, "2026");
- assert_eq!(month, "2026/05");
- assert_eq!(day, "2026/05/29");
- assert_eq!(hour, "2026/05/29/09");
- assert_eq!(node_id_to_path("root").to_string_lossy(), "root.md");
- assert!(node_id_to_path("2026/05/29/09")
- .to_string_lossy()
- .ends_with("2026/05/29/09.md"));
- assert_eq!(NodeLevel::Hour.parent_level(), Some(NodeLevel::Day));
- assert!(NodeLevel::Hour.is_leaf());
- assert_eq!(NodeLevel::Root.max_tokens(), 20_000);
- assert_eq!(NodeLevel::from_str_label("month"), Some(NodeLevel::Month));
- assert_eq!(NodeLevel::from_str_label("bogus"), None);
-
- let legacy = "---\nlevel: hour\nparent_id: \"2026/05/29\"\ntoken_count: 3\n---\n\nlegacy body";
- let parsed = tree_store::parse_node_markdown_pub(legacy, "legacy", "2026/05/29/09")
- .expect("legacy parse");
- assert_eq!(parsed.created_at.timestamp(), 0);
- assert_eq!(parsed.updated_at, parsed.created_at);
- assert_eq!(parsed.summary, "legacy body");
-
- let inputs = vec![
- SummaryInput {
- id: "blank".into(),
- content: " ".into(),
- token_count: 0,
- entities: vec!["ignored".into()],
- topics: vec![],
- time_range_start: ts,
- time_range_end: ts,
- score: 0.1,
- },
- SummaryInput {
- id: "long".into(),
- content: "alpha beta gamma delta epsilon zeta eta theta".repeat(20),
- token_count: 200,
- entities: vec![],
- topics: vec!["planning".into()],
- time_range_start: ts,
- time_range_end: ts,
- score: 0.9,
- },
- ];
- let out = fallback_summary(&inputs, 8);
- assert!(out.content.starts_with("— alpha"));
- assert!(out.token_count <= 9);
- assert!(out.entities.is_empty());
- assert!(out.topics.is_empty());
-
- let ctx = SummaryContext {
- tree_id: "tree-coverage",
- tree_kind: tinymemory_core::store::trees::types::TreeKind::Global,
- target_level: 2,
- token_budget: 128,
- input_token_budget: tinycortex::memory::config::INPUT_TOKEN_BUDGET,
- overhead_reserve_tokens: tinycortex::memory::config::SUMMARY_OVERHEAD_RESERVE_TOKENS,
- ask: None,
- };
- assert_eq!(ctx.tree_id, "tree-coverage");
- assert_eq!(ctx.target_level, 2);
-}
-
-#[tokio::test]
-async fn memory_sources_status_counts_folder_and_composio_prefixes() {
- let tmp = TempDir::new().expect("tempdir");
- let config = config_in(&tmp);
- let folder_source_id = "mem_src:folder-alpha:file-a.md";
- let gmail_source_id = "gmail:conn-1:message-1";
-
- let now = Utc::now().timestamp_millis();
- upsert_chunks(
- &config,
- &[
- chunk(folder_source_id, 0, now - 1_000, true),
- chunk(folder_source_id, 1, now - 400_000, false),
- chunk(gmail_source_id, 0, now - 2_000, true),
- ],
- )
- .expect("upsert chunks");
-
- let mut folder = source_entry(SourceKind::Folder, "folder-alpha");
- folder.path = Some(tmp.path().to_string_lossy().into_owned());
- let folder_status = source_status(&config, &folder)
- .await
- .expect("folder status");
- assert_eq!(folder_status.source_id, "folder-alpha");
- assert_eq!(folder_status.chunks_synced, 2);
- assert_eq!(folder_status.chunks_pending, 2);
- assert_eq!(folder_status.freshness, FreshnessLabel::Active);
-
- let mut composio = source_entry(SourceKind::Composio, "gmail-source");
- composio.toolkit = Some("gmail".into());
- composio.connection_id = Some("conn-1".into());
- let composio_status = source_status(&config, &composio)
- .await
- .expect("composio status");
- assert_eq!(composio_status.chunks_synced, 1);
- assert_eq!(composio_status.freshness, FreshnessLabel::Active);
-
- let mut missing_toolkit = composio.clone();
- missing_toolkit.id = "missing-toolkit".into();
- missing_toolkit.toolkit = None;
- let missing = source_status(&config, &missing_toolkit)
- .await
- .expect("missing toolkit status");
- assert_eq!(missing.chunks_synced, 0);
- assert_eq!(missing.freshness, FreshnessLabel::Idle);
-
- assert_eq!(FreshnessLabel::from_age_ms(None, now), FreshnessLabel::Idle);
- assert_eq!(
- FreshnessLabel::from_age_ms(Some(now - 60_000), now),
- FreshnessLabel::Recent
- );
- assert_eq!(
- FreshnessLabel::from_age_ms(Some(now - 600_000), now),
- FreshnessLabel::Idle
- );
-}
-
-#[test]
-fn memory_sources_validation_and_sync_classification_edges() {
- let mut entry = source_entry(SourceKind::Folder, "src-folder");
- assert_eq!(entry.kind.as_str(), "folder");
- assert!(entry.validate().is_err());
- entry.path = Some("/tmp/notes".into());
- assert!(entry.validate().is_ok());
-
- let mut github = source_entry(SourceKind::GithubRepo, "src-github");
- assert!(github.validate().is_err());
- github.url = Some("https://github.com/tinyhumansai/openhuman".into());
- assert!(github.validate().is_ok());
-
- let mut twitter = source_entry(SourceKind::TwitterQuery, "src-twitter");
- assert!(twitter.validate().is_err());
- twitter.query = Some("openhuman".into());
- assert!(twitter.validate().is_ok());
-
- let mut rss = source_entry(SourceKind::RssFeed, "src-rss");
- rss.url = Some("https://example.com/feed.xml".into());
- assert_eq!(rss.kind.as_str(), "rss_feed");
- assert!(rss.validate().is_ok());
-
- let mut web = source_entry(SourceKind::WebPage, "src-web");
- web.url = Some("https://example.com/page".into());
- assert_eq!(web.kind.as_str(), "web_page");
- assert!(web.validate().is_ok());
-
- let mut composio = source_entry(SourceKind::Composio, "src-composio");
- composio.toolkit = Some("gmail".into());
- assert!(composio.validate().is_err());
- composio.connection_id = Some("conn".into());
- assert!(composio.validate().is_ok());
-
- assert_eq!(classify_unknown("GMAIL_DELETE_DRAFT"), ToolScope::Admin);
- assert_eq!(classify_unknown("NOTION_CREATE_PAGE"), ToolScope::Write);
- assert_eq!(classify_unknown("GMAIL_FETCH_EMAILS"), ToolScope::Read);
- assert_eq!(
- toolkit_from_slug(" MICROSOFT_TEAMS_SEND "),
- Some("microsoft_teams".into())
- );
- assert_eq!(toolkit_from_slug(""), None);
- let catalog = [CuratedTool {
- slug: "GMAIL_SEND_EMAIL",
- scope: ToolScope::Write,
- }];
- assert_eq!(
- find_curated(&catalog, "gmail_send_email").unwrap().scope,
- ToolScope::Write
- );
- assert!(find_curated(&catalog, "GMAIL_DELETE_EMAIL").is_none());
-
- assert_eq!(ToolScope::Admin.as_str(), "admin");
- assert_eq!(SyncPipelineKind::Composio.as_str(), "composio");
- assert_eq!(SyncPipelineKind::Workspace.as_str(), "workspace");
- assert_eq!(SyncPipelineKind::Mcp.as_str(), "mcp");
- let outcome = SyncOutcome {
- records_ingested: 3,
- more_pending: true,
- note: Some("paged".into()),
- ..SyncOutcome::default()
- };
- let encoded = serde_json::to_value(&outcome).expect("sync outcome json");
- assert_eq!(encoded["records_ingested"], 3);
- assert_eq!(encoded["more_pending"], true);
-}
-
-#[test]
-fn memory_sync_canonicalizers_sort_clean_and_preserve_provenance() {
- let t1 = Utc.timestamp_millis_opt(1_700_000_000_000).unwrap();
- let t2 = Utc.timestamp_millis_opt(1_700_000_010_000).unwrap();
-
- assert!(canonicalise_chat(
- "slack:empty",
- "alice",
- &[],
- ChatBatch {
- platform: "slack".into(),
- channel_label: "#empty".into(),
- messages: vec![],
- },
- )
- .expect("empty chat")
- .is_none());
-
- let chat = canonicalise_chat(
- "slack:#eng",
- "alice@example.com",
- &["eng".into()],
- ChatBatch {
- platform: "slack".into(),
- channel_label: "#eng".into(),
- messages: vec![
- ChatMessage {
- author: "Bob".into(),
- timestamp: t2,
- text: "second".into(),
- source_ref: Some("slack://second".into()),
- },
- ChatMessage {
- author: "Alice".into(),
- timestamp: t1,
- text: " first ".into(),
- source_ref: Some("slack://first".into()),
- },
- ],
- },
- )
- .expect("chat")
- .expect("chat output");
- assert!(chat.markdown.find("first").unwrap() < chat.markdown.find("second").unwrap());
- assert_eq!(chat.metadata.time_range, (t1, t2));
- assert_eq!(chat.metadata.source_ref.unwrap().value, "slack://first");
-
- let email = canonicalise_email(
- "gmail:thread",
- "alice@example.com",
- &["inbox".into()],
- EmailThread {
- provider: "gmail".into(),
- thread_subject: "Launch".into(),
- messages: vec![
- EmailMessage {
- from: "bob@example.com".into(),
- to: vec!["alice@example.com".into()],
- cc: vec!["carol@example.com".into()],
- subject: "Launch".into(),
- sent_at: t2,
- body: "Reply body\n\nUnsubscribe https://example.com".into(),
- source_ref: Some("".into()),
- list_unsubscribe: Some("".into()),
- },
- EmailMessage {
- from: "alice@example.com".into(),
- to: vec!["bob@example.com".into()],
- cc: vec![],
- subject: "Re: Launch".into(),
- sent_at: t1,
- body: "Original body".into(),
- source_ref: Some(" ".into()),
- list_unsubscribe: None,
- },
- ],
- },
- )
- .expect("email")
- .expect("email output");
- assert!(
- email.markdown.find("Original body").unwrap() < email.markdown.find("Reply body").unwrap()
- );
- assert!(email.markdown.contains("Cc: carol@example.com"));
- assert!(email
- .markdown
- .contains("List-Unsubscribe: "));
- assert!(!email.markdown.contains("https://example.com"));
- assert!(email.metadata.source_ref.is_none());
-
- assert!(canonicalise_document(
- "doc-empty",
- "alice",
- &[],
- DocumentInput {
- provider: "notion".into(),
- title: " ".into(),
- body: " ".into(),
- modified_at: t1,
- source_ref: None,
- },
- None,
- )
- .expect("empty doc")
- .is_none());
-
- let doc_json = json!({
- "title": "Plan",
- "body": "Plan body",
- "modified_at": "1700000000000",
- "source_ref": "notion://page/1"
- });
- let doc_input: DocumentInput = serde_json::from_value(doc_json).expect("document input");
- assert_eq!(doc_input.provider, "unknown");
- let doc = canonicalise_document("doc-1", "alice", &["plans".into()], doc_input, None)
- .expect("document")
- .expect("document output");
- assert_eq!(doc.metadata.timestamp.timestamp_millis(), 1_700_000_000_000);
- assert_eq!(doc.metadata.source_ref.unwrap().value, "notion://page/1");
- assert_eq!(doc.markdown, "Plan body\n");
-}
-
-#[tokio::test]
-async fn memory_ingestion_state_and_request_models_report_edges() {
- let state = IngestionState::new();
- state.enqueue();
- state.enqueue();
- {
- let _guard = state.acquire().await;
- state.dequeue();
- state.mark_running("doc-1", "Coverage Doc", "coverage-ns");
- let running = state.snapshot();
- assert!(running.running);
- assert_eq!(running.queue_depth, 1);
- assert_eq!(running.current_title.as_deref(), Some("Coverage Doc"));
- }
- state.mark_completed("doc-1", false, 1_700_000_000_000);
- let completed = state.snapshot();
- assert!(!completed.running);
- assert_eq!(completed.last_document_id.as_deref(), Some("doc-1"));
- assert_eq!(completed.last_success, Some(false));
-
- let cfg = MemoryIngestionConfig {
- model_name: "local-model".into(),
- extraction_mode: ExtractionMode::Chunk,
- entity_threshold: 0.42,
- relation_threshold: 0.37,
- adjacency_threshold: 0.51,
- batch_size: 7,
- };
- let req = MemoryIngestionRequest {
- document: NamespaceDocumentInput {
- namespace: "coverage".into(),
- key: "doc-key".into(),
- title: "Coverage Doc".into(),
- content: "Alice collaborates with Bob on OpenHuman memory tests.".into(),
- source_type: "test".into(),
- priority: "medium".into(),
- tags: vec!["coverage".into()],
- metadata: json!({ "kind": "test" }),
- category: "core".into(),
- session_id: Some("session-1".into()),
- document_id: Some("doc-1".into()),
- taint: openhuman_core::openhuman::memory::MemoryTaint::Internal,
- },
- config: cfg.clone(),
- };
- assert_eq!(req.document.document_id.as_deref(), Some("doc-1"));
- assert_eq!(req.config.batch_size, 7);
- assert_eq!(req.config.extraction_mode, cfg.extraction_mode);
-}
-
-#[test]
-fn threads_turn_state_store_skips_corrupt_entries_and_marks_interrupted() {
- let tmp = TempDir::new().expect("tempdir");
- let store = TurnStateStore::new(tmp.path().to_path_buf());
- assert!(store.list().expect("initial list").is_empty());
- assert!(!store.delete("missing").expect("delete missing"));
- assert_eq!(store.clear_all().expect("clear missing"), 0);
-
- let mut first = TurnState::started("thread-a", "req-a", 4, "2026-05-29T12:00:00Z");
- first.lifecycle = TurnLifecycle::Streaming;
- first.iteration = 2;
- first.phase = Some(TurnPhase::ToolUse);
- first.active_tool = Some("memory.search".into());
- first.tool_timeline.push(ToolTimelineEntry {
- id: "tool-1".into(),
- name: "memory.search".into(),
- round: 1,
- status: ToolTimelineStatus::Running,
- failure: None,
- args_buffer: Some("{\"query\":\"coverage\"}".into()),
- display_name: Some("Search Memory".into()),
- detail: None,
- source_tool_name: Some("memory.search".into()),
- subagent: Some(SubagentActivity {
- task_id: "task-1".into(),
- agent_id: "researcher".into(),
- status: Some("running".into()),
- mode: Some("read".into()),
- dedicated_thread: Some(false),
- child_iteration: Some(1),
- child_max_iterations: Some(3),
- iterations: Some(1),
- elapsed_ms: Some(25),
- output_chars: Some(128),
- worker_thread_id: None,
- tool_calls: vec![SubagentToolCall {
- call_id: "call-1".into(),
- tool_name: "memory.search".into(),
- status: ToolTimelineStatus::Success,
- iteration: Some(1),
- elapsed_ms: Some(20),
- output_chars: Some(64),
- display_name: None,
- output: None,
- detail: None,
- args: None,
- failure: None,
- }],
- transcript: vec![],
- }),
- output: None,
- seq: None,
- });
- let second = TurnState::started("thread-b", "req-b", 2, "2026-05-29T12:01:00Z");
- store.put(&first).expect("put first");
- store.put(&second).expect("put second");
-
- let loaded = store
- .get("thread-a")
- .expect("get first")
- .expect("first exists");
- assert_eq!(loaded.active_tool.as_deref(), Some("memory.search"));
- assert_eq!(loaded.tool_timeline[0].status, ToolTimelineStatus::Running);
-
- let dir = tmp
- .path()
- .join("memory")
- .join("conversations")
- .join("turn_states");
- std::fs::write(dir.join("corrupt.json"), "{not-json").expect("write corrupt snapshot");
- let listed = store.list().expect("list skips corrupt");
- assert_eq!(listed.len(), 2);
-
- let interrupted = store
- .mark_all_interrupted("2026-05-29T12:02:00Z")
- .expect("mark interrupted");
- assert_eq!(interrupted, 2);
- let after = store.get("thread-a").expect("get after").expect("exists");
- assert_eq!(after.lifecycle, TurnLifecycle::Interrupted);
- assert!(after.active_tool.is_none());
- assert_eq!(after.updated_at, "2026-05-29T12:02:00Z");
- assert_eq!(
- store
- .mark_all_interrupted("2026-05-29T12:03:00Z")
- .expect("idempotent mark"),
- 0
- );
-
- assert!(store.delete("thread-b").expect("delete thread-b"));
- assert!(store
- .get("thread-b")
- .expect("missing after delete")
- .is_none());
- assert_eq!(store.clear_all().expect("clear all"), 2);
- assert!(store.list().expect("empty after clear").is_empty());
-}
diff --git a/tests/raw_coverage/memory_sources_closure_round23_raw_coverage_e2e.rs b/tests/raw_coverage/memory_sources_closure_round23_raw_coverage_e2e.rs
index 711813396a..aa725f596b 100644
--- a/tests/raw_coverage/memory_sources_closure_round23_raw_coverage_e2e.rs
+++ b/tests/raw_coverage/memory_sources_closure_round23_raw_coverage_e2e.rs
@@ -18,9 +18,6 @@ fn ensure_memory_seams() {
.name("round23-memory-source-seams".to_string())
.stack_size(8 * 1024 * 1024)
.spawn(|| {
- openhuman_core::openhuman::memory::host_impls::install_memory_host_seams(
- Arc::new(Config::default()),
- );
})
.expect("spawn round23 memory source seam installer")
.join()
diff --git a/tests/raw_coverage/memory_sync_providers_raw_coverage_e2e.rs b/tests/raw_coverage/memory_sync_providers_raw_coverage_e2e.rs
index fd4c24da0f..4e8bd2ea96 100644
--- a/tests/raw_coverage/memory_sync_providers_raw_coverage_e2e.rs
+++ b/tests/raw_coverage/memory_sync_providers_raw_coverage_e2e.rs
@@ -84,9 +84,6 @@ fn ensure_memory_seams() {
.name("memory-sync-providers-raw-coverage-seams".to_string())
.stack_size(8 * 1024 * 1024)
.spawn(|| {
- openhuman_core::openhuman::memory::host_impls::install_memory_host_seams(
- std::sync::Arc::new(Config::default()),
- );
})
.expect("spawn memory sync provider seam installer")
.join()
diff --git a/tests/raw_coverage/memory_sync_round23_raw_coverage_e2e.rs b/tests/raw_coverage/memory_sync_round23_raw_coverage_e2e.rs
index 840e98afd3..ff8451b90a 100644
--- a/tests/raw_coverage/memory_sync_round23_raw_coverage_e2e.rs
+++ b/tests/raw_coverage/memory_sync_round23_raw_coverage_e2e.rs
@@ -76,9 +76,6 @@ fn ensure_memory_seams() {
.name("memory-sync-round23-raw-coverage-seams".to_string())
.stack_size(8 * 1024 * 1024)
.spawn(|| {
- openhuman_core::openhuman::memory::host_impls::install_memory_host_seams(
- std::sync::Arc::new(Config::default()),
- );
})
.expect("spawn round23 memory sync seam installer")
.join()
diff --git a/tests/raw_coverage/memory_sync_slack_bus_raw_coverage_e2e.rs b/tests/raw_coverage/memory_sync_slack_bus_raw_coverage_e2e.rs
index dfee188fa3..bfdbcbc1d1 100644
--- a/tests/raw_coverage/memory_sync_slack_bus_raw_coverage_e2e.rs
+++ b/tests/raw_coverage/memory_sync_slack_bus_raw_coverage_e2e.rs
@@ -68,9 +68,6 @@ fn ensure_memory_seams(config: Arc) {
.name("memory-sync-slack-bus-raw-coverage-seams".to_string())
.stack_size(8 * 1024 * 1024)
.spawn(move || {
- openhuman_core::openhuman::memory::host_impls::install_memory_host_seams(
- Arc::clone(&config),
- );
#[cfg(feature = "modules")]
openhuman_core::openhuman::modules::memory::set_modules_policy(config);
})
diff --git a/tests/raw_coverage/memory_sync_sources_raw_coverage_e2e.rs b/tests/raw_coverage/memory_sync_sources_raw_coverage_e2e.rs
index de4f7f9957..1ee975c8b5 100644
--- a/tests/raw_coverage/memory_sync_sources_raw_coverage_e2e.rs
+++ b/tests/raw_coverage/memory_sync_sources_raw_coverage_e2e.rs
@@ -66,9 +66,6 @@ fn ensure_memory_seams() {
.name("memory-sync-sources-raw-coverage-seams".to_string())
.stack_size(8 * 1024 * 1024)
.spawn(|| {
- openhuman_core::openhuman::memory::host_impls::install_memory_host_seams(
- Arc::new(Config::default()),
- );
})
.expect("spawn memory sync source seam installer")
.join()
diff --git a/tests/raw_coverage/memory_sync_tree_round21_raw_coverage_e2e.rs b/tests/raw_coverage/memory_sync_tree_round21_raw_coverage_e2e.rs
deleted file mode 100644
index fe59dd0f63..0000000000
--- a/tests/raw_coverage/memory_sync_tree_round21_raw_coverage_e2e.rs
+++ /dev/null
@@ -1,449 +0,0 @@
-//! Round 21 focused raw coverage for memory_sync + memory_tree gaps.
-//!
-//! Hermetic: temp workspaces, loopback Composio backend where still
-//! applicable, and no real network. Run with `--test-threads=1` because
-//! config/HOME/workspace env vars and the global memory client are
-//! process-global.
-//!
-//! # What changed here
-//!
-//! tinymemory v1.13.4 deleted the ENTIRE in-process Composio provider
-//! registry (`ComposioProvider` trait, `ProviderContext`, the concrete
-//! per-toolkit provider structs including `GmailProvider` and
-//! `LinearProvider`, `register_provider`/`init_default_providers`) — see
-//! `crate::openhuman::integrations::composio::providers`'s module docs for
-//! the full account. None of it has a replacement in this crate: it now
-//! lives in the separately-versioned `tinyconnectors` module, reachable only
-//! via a live loaded module over the bus (a real network download plus a
-//! `dlopen`), which this file's own "no real network" design rules out.
-//!
-//! `gmail_post_process_slims_wrapped_messages_and_honours_raw_flag` and the
-//! provider-internals half of
-//! `linear_provider_profile_tasks_sync_and_periodic_bookkeeping_use_loopback`
-//! (constructing a `GmailProvider`/`LinearProvider` directly and driving it
-//! against a loopback Composio execute API) tested exactly that deleted,
-//! relocated capability — Gmail's nested-payload post-processing, Linear's
-//! profile fetch, task normalization, and cursor-paginated sync. There is
-//! nothing left in this crate to assert that behaviour against; it is
-//! reported as a coverage gap rather than silently dropped. What replaces
-//! them below is the current, real, network-free entry point that stands in
-//! its place: `integrations::composio::ops::{composio_get_user_profile,
-//! composio_sync}`, which — with `modules.enabled = false` — refuse cleanly
-//! and deterministically instead of reaching a provider.
-//! `integrations::composio::periodic::record_sync_success` (moved from
-//! `memory::sync::composio::periodic`, which no longer exists) is still real
-//! and is still exercised directly.
-//!
-//! `slack_sync_status_rpc_reads_mock_connections_and_persisted_state` is
-//! rewritten rather than deleted, onto a genuinely different current
-//! behaviour: `providers::slack::rpc::sync_status_rpc`'s own doc comment
-//! (`src/openhuman/memory/sync/composio/providers/slack/rpc.rs`) explains
-//! that it is now a **deliberately degraded read** — the connector module
-//! keeps its cursor and daily-request budget internally and exposes neither
-//! outside of an actual `Sync` call, so every per-connection detail field
-//! (`per_channel_cursors`, `synced_ids_count`, `requests_used_today`,
-//! `daily_request_limit`) is hardcoded to its zero value rather than read
-//! back from anywhere. Persisting a `SyncState` before calling it (the old
-//! test's approach) no longer has any effect on the response, because
-//! nothing reads it — so this test now asserts the zero-value degraded shape
-//! and the log line that explains it, matching the RPC's own documented
-//! contract instead of a behaviour it no longer has.
-
-use std::collections::HashMap;
-use std::ffi::OsString;
-use std::path::Path;
-use std::sync::{Arc, Mutex, OnceLock};
-
-use axum::routing::get;
-use axum::{Json, Router};
-use chrono::{TimeZone, Utc};
-use serde_json::json;
-use tempfile::TempDir;
-
-use openhuman_core::openhuman::config::Config;
-use openhuman_core::openhuman::integrations::composio::ops::{
- composio_get_user_profile, composio_sync,
-};
-use openhuman_core::openhuman::integrations::composio::periodic::record_sync_success;
-use openhuman_core::openhuman::memory::sync::composio::providers::slack::rpc::{
- sync_status_rpc, SyncStatusRequest,
-};
-use openhuman_core::openhuman::security::credentials::{
- AuthService, APP_SESSION_PROVIDER, DEFAULT_AUTH_PROFILE_NAME,
-};
-use tinycortex::memory::score::embed::{pack_embedding, EMBEDDING_DIM};
-use tinymemory_core::global as memory_global;
-use tinymemory_core::store::chunks::store::with_connection;
-use tinymemory_core::store::content::atomic::stage_summary;
-use tinymemory_core::store::content::{SummaryComposeInput, SummaryTreeKind};
-use tinymemory_core::store::trees::types::{SummaryNode, Tree, TreeKind};
-use tinymemory_core::tree::retrieval::source::query_source;
-// Engine-direct for the same reason as the other retrieval e2e suites (#5560).
-use tinymemory_core::tree::tree::store as tree_store;
-use tinymemory_core::tree::tree::TreeStatus;
-
-static ENV_LOCK: &OnceLock> = &crate::SHARED_ENV_LOCK;
-static MEMORY_SEAMS_INIT: OnceLock<()> = OnceLock::new();
-
-fn ensure_memory_seams() {
- MEMORY_SEAMS_INIT.get_or_init(|| {
- std::thread::Builder::new()
- .name("memory-sync-tree-round21-raw-coverage-seams".to_string())
- .stack_size(8 * 1024 * 1024)
- .spawn(|| {
- openhuman_core::openhuman::memory::host_impls::install_memory_host_seams(Arc::new(
- Config::default(),
- ));
- })
- .expect("spawn round21 memory tree seam installer")
- .join()
- .expect("round21 memory tree seam installer panicked");
- });
-}
-
-fn env_lock() -> std::sync::MutexGuard<'static, ()> {
- ENV_LOCK
- .get_or_init(|| Mutex::new(()))
- .lock()
- .unwrap_or_else(|e| e.into_inner())
-}
-
-struct EnvGuard {
- key: &'static str,
- old: Option,
-}
-
-impl EnvGuard {
- fn set_path(key: &'static str, value: impl AsRef) -> Self {
- let old = std::env::var_os(key);
- unsafe { std::env::set_var(key, value.as_ref()) };
- Self { key, old }
- }
-
- fn unset(key: &'static str) -> Self {
- let old = std::env::var_os(key);
- unsafe { std::env::remove_var(key) };
- Self { key, old }
- }
-}
-
-impl Drop for EnvGuard {
- fn drop(&mut self) {
- unsafe {
- match &self.old {
- Some(value) => std::env::set_var(self.key, value),
- None => std::env::remove_var(self.key),
- }
- }
- }
-}
-
-fn config_in(tmp: &TempDir) -> Config {
- ensure_memory_seams();
- let mut config = Config {
- config_path: tmp.path().join("config.toml"),
- workspace_dir: tmp.path().join("workspace"),
- action_dir: tmp.path().join("workspace"),
- ..Config::default()
- };
- config.secrets.encrypt = false;
- config.memory_tree.embedding_endpoint = None;
- config.memory_tree.embedding_model = None;
- config.memory_tree.embedding_strict = false;
- config
-}
-
-async fn persist_config(config: &Config) {
- std::fs::create_dir_all(&config.workspace_dir).expect("workspace dir");
- config.save().await.expect("save config");
-}
-
-fn store_session(config: &Config) {
- AuthService::from_config(config)
- .store_provider_token(
- APP_SESSION_PROVIDER,
- DEFAULT_AUTH_PROFILE_NAME,
- "round21-session-token",
- HashMap::new(),
- true,
- )
- .expect("store app session token");
-}
-
-async fn loopback_router(router: Router) -> (String, tokio::task::JoinHandle<()>) {
- let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
- .await
- .expect("bind loopback");
- let addr = listener.local_addr().expect("loopback addr");
- let handle = tokio::spawn(async move {
- axum::serve(listener, router).await.expect("serve loopback");
- });
- (format!("http://{addr}"), handle)
-}
-
-/// What `gmail_post_process_slims_wrapped_messages_and_honours_raw_flag` and
-/// the profile/tasks half of
-/// `linear_provider_profile_tasks_sync_and_periodic_bookkeeping_use_loopback`
-/// used to cover — see the module doc comment for why that coverage cannot
-/// be expressed here any more. `composio_get_user_profile` is the real,
-/// current entry point standing in its place for "fetch a provider's
-/// profile", and it refuses cleanly and deterministically — no network, no
-/// provider — when no connectors module is loaded.
-#[tokio::test]
-async fn composio_get_user_profile_refuses_cleanly_for_gmail_and_linear_without_a_loaded_module() {
- let _guard = env_lock();
- let tmp = TempDir::new().expect("tempdir");
- let _workspace = EnvGuard::set_path("OPENHUMAN_WORKSPACE", tmp.path());
- let _home = EnvGuard::set_path("HOME", tmp.path());
- let _backend = EnvGuard::unset("BACKEND_URL");
-
- let mut config = config_in(&tmp);
- config.modules.enabled = false;
- persist_config(&config).await;
- store_session(&config);
-
- for connection_id in ["conn-gmail-round21", "conn-linear-round21"] {
- let error = composio_get_user_profile(&config, connection_id)
- .await
- .expect_err("profile fetch must refuse without a loaded connectors module");
- assert!(
- error.contains("modules are disabled in configuration"),
- "unexpected error for {connection_id}: {error}"
- );
- }
-
- // `composio_sync` — the entry point standing in for the deleted
- // `LinearProvider::sync` / periodic bookkeeping loop — refuses the same
- // way: the toolkit resolution it needs is itself module-mediated.
- let sync_error = composio_sync(&config, "conn-linear-round21", Some("manual".to_string()))
- .await
- .expect_err("sync must refuse without a loaded connectors module");
- assert!(
- sync_error.contains("modules are disabled in configuration"),
- "unexpected sync error: {sync_error}"
- );
-
- // `record_sync_success` (moved from the deleted `memory::sync::composio::
- // periodic` to `integrations::composio::periodic`) is untouched by the
- // deletion — it is a pure process-local bookkeeping call the periodic
- // scheduler uses to avoid immediately re-firing a sync it just ran. It
- // exposes no public reader, so this — like the original test — only
- // proves it is callable and does not panic.
- record_sync_success("linear", "conn-linear-round21");
- record_sync_success("linear", "conn-linear-round21");
-}
-
-#[tokio::test]
-async fn slack_sync_status_rpc_reports_the_degraded_zero_value_shape() {
- let _guard = env_lock();
- let tmp = TempDir::new().expect("tempdir");
- let _workspace = EnvGuard::set_path("OPENHUMAN_WORKSPACE", tmp.path());
- let _home = EnvGuard::set_path("HOME", tmp.path());
- let _backend = EnvGuard::unset("BACKEND_URL");
- let mut config = config_in(&tmp);
- let router = Router::new().route(
- "/agent-integrations/composio/connections",
- get(|| async {
- Json(json!({
- "success": true,
- "data": {
- "connections": [
- { "id": "conn-slack-round21", "toolkit": "slack", "status": "ACTIVE" },
- { "id": "conn-slack-pending", "toolkit": "slack", "status": "PENDING" },
- { "id": "conn-gmail-round21", "toolkit": "gmail", "status": "ACTIVE" }
- ]
- }
- }))
- }),
- );
- let (base, server) = loopback_router(router).await;
- config.api_url = Some(base);
- persist_config(&config).await;
- store_session(&config);
- memory_global::init(config.workspace_dir.clone()).expect("memory global");
-
- // `list_slack_connections` (what `sync_status_rpc` calls first) still
- // goes through the old backend HTTP client factory, untouched by the
- // tinyconnectors migration — so the loopback connections router above
- // still drives it. What changed is everything after: there is no more
- // per-connection detail to read back (see module doc comment), so this
- // no longer seeds a `SyncState` before calling the RPC — nothing would
- // read it.
- let outcome = sync_status_rpc(&config, SyncStatusRequest::default())
- .await
- .expect("status rpc");
- assert_eq!(
- outcome.value.connections.len(),
- 1,
- "only the active slack connection qualifies"
- );
- let row = &outcome.value.connections[0];
- assert_eq!(row.connection_id, "conn-slack-round21");
- assert_eq!(row.per_channel_cursors, "{}");
- assert_eq!(row.synced_ids_count, 0);
- assert_eq!(row.requests_used_today, 0);
- assert_eq!(row.daily_request_limit, 0);
- assert!(
- outcome
- .logs
- .iter()
- .any(|line| line.contains("connections=1") && line.contains("no longer available")),
- "status log should explain the degraded read: {:?}",
- outcome.logs
- );
-
- server.abort();
-}
-
-#[tokio::test]
-async fn memory_tree_source_query_filters_reranks_and_hydrates_manual_summaries() {
- let tmp = TempDir::new().expect("tempdir");
- let config = config_in(&tmp);
- std::fs::create_dir_all(config.memory_tree_content_root()).expect("content root");
- seed_source_summary(
- &config,
- "slack:#round21",
- "summary-round21-chat",
- "Full chat summary body from disk.",
- 1_780_313_600_000,
- Some(one_hot(0)),
- );
- seed_source_summary(
- &config,
- "gmail:round21@example.test",
- "summary-round21-email",
- "Full email summary body from disk.",
- 1_780_227_200_000,
- None,
- );
-
- let all = query_source(&config, None, None, None, None, 0)
- .await
- .expect("all source query");
- assert_eq!(all.total, 2);
- assert_eq!(all.hits.len(), 2);
-
- let chat = query_source(
- &config,
- None,
- Some(tinymemory_core::store::chunks::types::SourceKind::Chat),
- None,
- Some("semantic query keeps embedded rows first"),
- 10,
- )
- .await
- .expect("chat query");
- assert_eq!(chat.hits.len(), 1);
- assert_eq!(chat.hits[0].tree_scope, "slack:#round21");
- assert_eq!(chat.hits[0].content, "Full chat summary body from disk.");
-
- let missing = query_source(&config, Some("slack:#missing"), None, None, None, 10)
- .await
- .expect("missing source");
- assert!(missing.hits.is_empty());
-}
-
-fn one_hot(index: usize) -> Vec {
- let mut values = vec![0.0; EMBEDDING_DIM];
- values[index] = 1.0;
- values
-}
-
-fn seed_source_summary(
- config: &Config,
- scope: &str,
- summary_id: &str,
- body: &str,
- timestamp_ms: i64,
- embedding: Option>,
-) {
- let ts = Utc.timestamp_millis_opt(timestamp_ms).unwrap();
- let tree = Tree {
- id: format!("tree:{summary_id}"),
- kind: TreeKind::Source,
- scope: scope.to_string(),
- ask: None,
- root_id: Some(summary_id.to_string()),
- max_level: 1,
- status: TreeStatus::Active,
- created_at: ts,
- last_sealed_at: Some(ts),
- };
- tree_store::insert_tree(config, &tree).expect("insert source tree");
-
- let node = SummaryNode {
- id: summary_id.to_string(),
- tree_id: tree.id.clone(),
- tree_kind: TreeKind::Source,
- level: 1,
- parent_id: None,
- child_ids: vec!["leaf-a".to_string(), "leaf-b".to_string()],
- content: "preview only".to_string(),
- token_count: 64,
- entities: vec!["round21".to_string()],
- topics: vec!["coverage".to_string()],
- time_range_start: ts,
- time_range_end: ts,
- score: 0.75,
- sealed_at: ts,
- deleted: false,
- embedding: embedding.clone(),
- doc_id: None,
- version_ms: None,
- };
- let staged = stage_summary(
- &config.memory_tree_content_root(),
- &SummaryComposeInput {
- summary_id: &node.id,
- tree_kind: SummaryTreeKind::Source,
- tree_id: &node.tree_id,
- tree_scope: &tree.scope,
- level: node.level,
- child_ids: &node.child_ids,
- child_basenames: None,
- child_count: node.child_ids.len(),
- time_range_start: node.time_range_start,
- time_range_end: node.time_range_end,
- sealed_at: node.sealed_at,
- body,
- },
- scope,
- )
- .expect("stage summary body");
- let embedding_blob = embedding.as_ref().map(|values| pack_embedding(values));
-
- with_connection(config, |conn| {
- conn.execute(
- "INSERT INTO mem_tree_summaries (
- id, tree_id, tree_kind, level, parent_id,
- child_ids_json, content, token_count,
- entities_json, topics_json,
- time_range_start_ms, time_range_end_ms,
- score, sealed_at_ms, deleted, embedding,
- content_path, content_sha256
- ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18)",
- rusqlite::params![
- node.id,
- node.tree_id,
- node.tree_kind.as_str(),
- node.level,
- node.parent_id,
- serde_json::to_string(&node.child_ids).unwrap(),
- node.content,
- node.token_count,
- serde_json::to_string(&node.entities).unwrap(),
- serde_json::to_string(&node.topics).unwrap(),
- node.time_range_start.timestamp_millis(),
- node.time_range_end.timestamp_millis(),
- node.score,
- node.sealed_at.timestamp_millis(),
- node.deleted as i64,
- embedding_blob,
- staged.content_path,
- staged.content_sha256,
- ],
- )?;
- Ok(())
- })
- .expect("insert summary row");
-}
diff --git a/tests/raw_coverage/memory_threads_raw_coverage_e2e.rs b/tests/raw_coverage/memory_threads_raw_coverage_e2e.rs
deleted file mode 100644
index 854404c650..0000000000
--- a/tests/raw_coverage/memory_threads_raw_coverage_e2e.rs
+++ /dev/null
@@ -1,4833 +0,0 @@
-//! Raw-line oriented E2E coverage for memory, memory_tree, memory_sync,
-//! memory_sources, and threads.
-//!
-//! The tests call public Rust APIs and localhost-only readers so they stay
-//! hermetic while still exercising production code paths that are awkward to
-//! reach through full JSON-RPC flows.
-
-use axum::http::{HeaderMap, StatusCode};
-use axum::response::{Html, IntoResponse, Response};
-use axum::routing::get;
-use axum::Router;
-use chrono::{TimeZone, Utc};
-use serde_json::json;
-use serde_json::{Map, Value};
-use std::ffi::OsString;
-use std::path::{Path, PathBuf};
-use std::sync::{Arc, Mutex, OnceLock};
-use tempfile::TempDir;
-
-use openhuman_core::openhuman::agent::progress::AgentProgress;
-use openhuman_core::openhuman::agent::task_board::{TaskBoard, TaskBoardCard, TaskCardStatus};
-use openhuman_core::openhuman::config::Config;
-use openhuman_core::openhuman::inference::embeddings::NoopEmbedding;
-use openhuman_core::openhuman::memory::api::tool_memory::{
- ToolMemoryPriority as ApiToolMemoryPriority, ToolMemoryRule as ApiToolMemoryRule,
- ToolMemorySource as ApiToolMemorySource,
-};
-use openhuman_core::openhuman::memory::query::{
- MemoryQueryTool, MemoryTreeDrillDownTool, MemoryTreeFetchLeavesTool,
- MemoryTreeIngestDocumentTool, MemoryTreeQuerySourceTool, MemoryTreeSearchEntitiesTool,
-};
-use openhuman_core::openhuman::memory::sources::readers::reader_for;
-use openhuman_core::openhuman::memory::sources::registry;
-use openhuman_core::openhuman::memory::sources::rpc as memory_sources_rpc;
-// The engine's per-source SQL read, which is what this suite seeds a store for.
-// `memory::sources::status` is host-side now and asks the bound driver, which an
-// integration test has no module to load (#5560).
-use tinymemory_core::sources::status::{source_status, FreshnessLabel};
-// The engine's own source pipeline. `memory::sources::sync` is host-side now and
-// carries only `derive_scopes`; `sync_source` stayed upstream because nothing in
-// `src/` calls it any more (#5560).
-use tinymemory_core::sources::sync::sync_source;
-use openhuman_core::openhuman::memory::sources::types::{
- ContentType, MemorySourceEntry, SourceContent, SourceItem, SourceKind,
-};
-use openhuman_core::openhuman::memory::sources::{
- all_memory_sources_controller_schemas, all_memory_sources_registered_controllers,
-};
-use openhuman_core::openhuman::memory::sync::composio;
-use tinymemory_core::queue::types::ReembedBackfillPayload;
-use tinymemory_core::queue::{
- self as memory_queue, AppendBufferPayload, AppendTarget, ExtractChunkPayload,
- FlushStalePayload, JobKind, JobStatus, NewJob, NodeRef, SealPayload, DEFAULT_LOCK_DURATION_MS,
-};
-use tinymemory_core::store::chunks::store::{upsert_chunks, with_connection};
-use tinymemory_core::store::chunks::types::{
- approx_token_count, chunk_id, Chunk, DataSource, Metadata, SourceKind as ChunkSourceKind,
- SourceRef,
-};
-use tinymemory_core::store::trees::types::{
- SummaryNode, Tree, TreeKind, TreeStatus as StoredTreeStatus,
-};
-use tinymemory_core::store::{MemoryClient, NamespaceDocumentInput, UnifiedMemory};
-// `memory::sync::composio::providers::slack::schemas` is this host's own real,
-// current RPC schema module (`openhuman.slack_memory_sync_trigger` /
-// `_status`) — unrelated to the deleted per-action `post_process` (see below).
-use openhuman_core::openhuman::memory::sync::composio::providers::slack::schemas as slack_memory_schemas;
-// Everything below moved off `memory::sync::composio::providers::*` onto
-// `integrations::composio::providers`/`integrations::composio::identity_store`/
-// `integrations::composio::profile_md` (host code) or `tinymemory_api::composio`
-// (contract-crate vocabulary) — see `crate::openhuman::integrations::composio::providers`'s
-// module docs for the full account of what replaced each deleted piece.
-use openhuman_core::openhuman::integrations::composio::identity_store::{
- delete_connected_identity_facets, load_connected_identities,
-};
-use openhuman_core::openhuman::integrations::composio::ops::{
- composio_get_user_profile, composio_sync,
-};
-use openhuman_core::openhuman::integrations::composio::profile_md::{
- block_end, block_start, merge_provider_into_profile_md, remove_provider_from_profile_md,
- replace_managed_block,
-};
-use openhuman_core::openhuman::integrations::composio::providers::{
- agent_ready_toolkits, catalog_for_toolkit, classify_unknown, curated_scope_for, find_curated,
- is_action_visible_with_pref, toolkit_from_slug, toolkit_has_scope, CuratedTool, NormalizedTask,
- ProviderUserProfile, SyncOutcome as ComposioSyncOutcome, SyncReason, TaskFetchFilter,
- ToolScope, UserScopePref,
-};
-use tinymemory_api::composio::{
- canonicalize, extract_item_id, render_connected_identities_section, ConnectedIdentity,
- DailyBudget, IdentityKind, SyncState, DEFAULT_DAILY_REQUEST_LIMIT,
-};
-// The deleted engine's per-toolkit `is_self_identity(prefix, kind, value)` has
-// no replacement anywhere (confirmed by exhaustive grep of vendor/tinymemory) —
-// only the cross-toolkit matcher survived, because the memory tree's entity
-// indexer was never scoped to one toolkit to begin with. Note this is a
-// DIFFERENT (structurally identical) `IdentityKind` than
-// `tinymemory_api::composio::IdentityKind` above.
-use openhuman_core::openhuman::memory::sync::sync_status::{
- rpc as memory_sync_status_rpc, schemas as memory_sync_status_schemas,
-};
-use openhuman_core::openhuman::memory::tool_memory::prompt::{
- render_tool_memory_rules, ToolMemoryRulesSection, TOOL_MEMORY_HEADING,
-};
-use openhuman_core::openhuman::memory::tool_memory::{
- tool_memory_namespace, tool_memory_store, ToolMemoryPriority, ToolMemoryRule, ToolMemorySource,
- TOOL_MEMORY_PROMPT_CAP,
-};
-use openhuman_core::openhuman::memory::tools::tool_memory::{
- MemoryToolsListTool, MemoryToolsPutTool,
-};
-use openhuman_core::openhuman::memory::tools::{
- MemoryForgetTool, MemoryRecallTool, MemoryStoreTool,
-};
-use tinymemory_core::tree::score::embed;
-use tinymemory_core::tree::score::embed::Embedder;
-use tinymemory_core::tree::score::extract::{
- CompositeExtractor, EntityExtractor, EntityKind, ExtractedEntities, ExtractedEntity,
- ExtractedTopic,
-};
-use tinymemory_core::tree::score::resolver::CanonicalEntity;
-use tinymemory_core::tree::score::signals::{
- combine, combine_cheap_only, compute as compute_score_signals, entity_density_score,
- interaction, metadata_weight, source_weight, token_count, unique_words, ScoreSignals,
- SignalWeights,
-};
-use tinymemory_core::tree::score::store as score_store;
-use tinymemory_core::tree::score::{resolver, ScoringConfig};
-use tinymemory_core::tree::summarise::{
- fallback_summary, SummaryContext, SummaryInput,
-};
-use tinymemory_core::tree::tree::bucket_seal::LeafRef;
-use tinymemory_core::tree::tree_runtime::store as tree_runtime_store;
-use openhuman_core::openhuman::memory::tree::tree_runtime::{
- all_tree_summarizer_controller_schemas, all_tree_summarizer_registered_controllers,
- derive_node_ids, derive_parent_id, estimate_tokens, level_from_node_id, node_id_to_path,
- NodeLevel, TreeNode,
-};
-use tinymemory_core::store::identity::is_self_identity_any_toolkit;
-// `retrieval` is the engine module, not the host wrapper: the host stopped
-// re-exporting it in #5560. The `tree::score` / `tree::summarise` /
-// `tree::tree` imports above went engine-direct the same way once their host
-// re-export shims stopped serving production (#5560).
-use openhuman_core::openhuman::memory::{
- all_memory_controller_schemas, all_memory_registered_controllers,
- preferences::{
- load_general_preferences, recall_related_preferences, recall_situational_preferences,
- USER_PREF_GENERAL_NAMESPACE, USER_PREF_SITUATIONAL_NAMESPACE,
- },
- read_rpc as memory_read_rpc,
-};
-// The engine's own ingest request/config — what `UnifiedMemory::
-// ingest_document` and `extract_graph` take. `memory::MemoryIngestion*` are
-// the host's WIRE shapes now (`rpc_models`), distinct types (#5560).
-use tinycortex::memory::ingest::{MemoryIngestionConfig, MemoryIngestionRequest};
-use tinymemory_core::tree::retrieval;
-use tinymemory_core::tree_policy::TreePolicy;
-use tinymemory_core::tree_source;
-// `remember`, `rpc_models`, `traits` and `util` moved into the extracted engine
-// crate with the rest of the memory implementation; the host re-exports some of
-// their contents flat but not the modules themselves.
-// The guard exposes `store` through the contract's mandatory core trait, and
-// stamps provenance from an explicit taint argument.
-use openhuman_core::openhuman::memory::api::provider::MemoryCore;
-use openhuman_core::openhuman::memory::api::types::MemoryTaint;
-// These request/record types are consumed directly by `openhuman_core::openhuman::memory::ops`
-// and `openhuman::threads::ops` handlers below, which take the host's own `rpc_models` types,
-// not the engine crate's same-named ones — so they must come from the host, not `tinymemory_core`.
-use openhuman_core::openhuman::memory::rpc_models::{
- AppendConversationMessageRequest, ConversationMessageRecord, ConversationMessagesRequest,
- CreateConversationThreadRequest, DeleteConversationThreadRequest, DeleteDocumentRequest,
- EmptyRequest, GenerateConversationThreadTitleRequest, ListDocumentsRequest,
- ListMemoryFilesRequest, MemoryInitRequest, ReadMemoryFileRequest,
- UpdateConversationMessageRequest, UpdateConversationThreadLabelsRequest,
- UpdateConversationThreadTitleRequest, UpsertConversationThreadRequest, WriteMemoryFileRequest,
-};
-use openhuman_core::openhuman::security::{AutonomyLevel, SecurityPolicy};
-use openhuman_core::openhuman::threads::ops as thread_ops;
-use openhuman_core::openhuman::threads::title::{
- build_title_prompt, collapse_whitespace, is_auto_generated_thread_title,
- sanitize_generated_title, title_from_user_message, title_log_fingerprint,
-};
-use openhuman_core::openhuman::threads::turn_state::{
- self, ClearTurnStateRequest, GetTurnStateRequest, GetTurnStateResponse, ListTurnStatesResponse,
- SubagentActivity, SubagentToolCall, ToolTimelineEntry, ToolTimelineStatus, TurnLifecycle,
- TurnPhase, TurnState, TurnStateMirror, TurnStateStore,
-};
-use openhuman_core::openhuman::threads::ThreadsError;
-use openhuman_core::openhuman::threads::{
- all_threads_controller_schemas, all_threads_registered_controllers,
-};
-use openhuman_core::openhuman::tools::traits::{PermissionLevel, Tool, ToolCategory};
-use tinycortex::memory::ingest::canonicalize::chat::{
- canonicalise as canonicalise_chat, ChatBatch, ChatMessage,
-};
-use tinycortex::memory::ingest::canonicalize::document::{
- canonicalise as canonicalise_document, DocumentInput,
-};
-use tinycortex::memory::ingest::canonicalize::email::{
- canonicalise as canonicalise_email, EmailMessage, EmailThread,
-};
-use tinycortex::memory::ingest::canonicalize::email_clean;
-use tinycortex::memory::sync::{SyncOutcome as PipelineSyncOutcome, SyncPipelineKind};
-use tinymemory_core::{
- remember::RememberSourceKind,
- rpc_models::{
- ApiEnvelope, ApiError, ApiMeta, PaginationMeta, QueryNamespaceRequest,
- RecallContextRequest, RecallMemoriesRequest,
- },
- traits::{Memory, MemoryCategory, MemoryEntry, NamespaceSummary, RecallOpts},
- util::redact::{redact, redact_endpoint},
-};
-
-struct EnvVarGuard {
- key: &'static str,
- old: Option,
-}
-
-impl EnvVarGuard {
- fn set_to_path(key: &'static str, value: &Path) -> Self {
- let old = std::env::var_os(key);
- unsafe {
- std::env::set_var(key, value.as_os_str());
- }
- Self { key, old }
- }
-}
-
-impl Drop for EnvVarGuard {
- fn drop(&mut self) {
- unsafe {
- match &self.old {
- Some(value) => std::env::set_var(self.key, value),
- None => std::env::remove_var(self.key),
- }
- }
- }
-}
-
-static ENV_LOCK: &OnceLock> = &crate::SHARED_ENV_LOCK;
-fn ensure_memory_seams() {
- std::thread::Builder::new()
- .name("raw-coverage-memory-seams".to_string())
- .stack_size(8 * 1024 * 1024)
- .spawn(|| {
- openhuman_core::openhuman::memory::host_impls::install_memory_host_seams(Arc::new(
- Config::default(),
- ));
- })
- .expect("spawn raw coverage memory seam installer")
- .join()
- .expect("raw coverage memory seam installer panicked");
-}
-
-fn env_lock() -> std::sync::MutexGuard<'static, ()> {
- ENV_LOCK
- .get_or_init(|| Mutex::new(()))
- .lock()
- .unwrap_or_else(|poisoned| poisoned.into_inner())
-}
-
-fn config_in(tmp: &TempDir) -> Config {
- ensure_memory_seams();
- let mut config = Config::default();
- config.workspace_dir = tmp.path().to_path_buf();
- config
-}
-
-/// The one memory workspace every driver-routed case in this module shares.
-///
-/// The module host captures a workspace **once per process**: the boot policy
-/// is first-call-wins and the loaded artifact takes its `workspace_dir` at load,
-/// while each case here used to get its own `TempDir`. Whichever case published
-/// first bound the module to a directory that was deleted when that case
-/// returned, and every later read through the driver answered from the dead
-/// store: 0 rows where the case had just seeded 1. Production never has that
-/// mismatch, because boot publishes the runtime config and module and handlers
-/// name one object. This reproduces that arrangement the way
-/// `tests/json_rpc_e2e.rs` does: one leaked directory, the policy published from
-/// it exactly once, and every driver-routed case pointing its config (and
-/// `OPENHUMAN_WORKSPACE`) here. Cases keep their own `TempDir` for the files they
-/// write; only the memory workspace is shared. The path ends in `workspace` so
-/// `resolve_config_dir_for_workspace` treats it as the workspace itself rather
-/// than appending another segment, and the env var and the config agree exactly.
-fn module_workspace() -> &'static Path {
- static WORKSPACE: OnceLock = OnceLock::new();
- WORKSPACE.get_or_init(|| {
- let dir = TempDir::new().expect("module workspace tempdir");
- let path = dir.path().join("workspace");
- std::fs::create_dir_all(&path).expect("create module workspace");
- // Leaked on purpose: the module keeps this path for the process lifetime.
- std::mem::forget(dir);
- ensure_memory_seams();
- #[cfg(feature = "modules")]
- openhuman_core::openhuman::modules::memory::set_modules_policy(Arc::new(shared_config_at(
- &path,
- )));
- path
- })
-}
-
-/// A config whose memory workspace **and** source registry are the shared ones.
-///
-/// `config_path` matters as much as `workspace_dir`: the module reads its
-/// source registry from the file the host names there, and `Config::default()`
-/// names the developer's real `~/.openhuman/config.toml`. Pointing it beside
-/// the shared workspace is also where `Config::load_or_init` resolves it from
-/// `OPENHUMAN_WORKSPACE`, so env-driven cases and config-driven cases write and
-/// read one registry. Embeddings are off so no case asks the host to embed.
-fn shared_config_at(workspace: &Path) -> Config {
- let mut config = Config::default();
- config.workspace_dir = workspace.to_path_buf();
- config.config_path = workspace
- .parent()
- .expect("shared workspace has a parent")
- .join("config.toml");
- config.embeddings_provider = Some("none".into());
- config
-}
-
-/// Point `config` at the shared module workspace and registry.
-fn use_module_workspace(config: &mut Config) {
- let shared = shared_config_at(module_workspace());
- config.workspace_dir = shared.workspace_dir;
- config.config_path = shared.config_path;
- config.embeddings_provider = shared.embeddings_provider;
-}
-
-/// Empty the shared chunk store so a case that counts rows sees only its own.
-///
-/// Rows, not the file: the module holds its connection open, so replacing the
-/// file would leave it reading the old inode. Tables a fresh store lacks are
-/// skipped rather than failed.
-fn wipe_shared_store(config: &Config) {
- with_connection(config, |conn| {
- for table in [
- "mem_tree_chunk_embeddings",
- "mem_tree_chunk_reembed_skipped",
- "mem_tree_entity_edges",
- "mem_tree_entity_hotness",
- "mem_tree_entity_index",
- "mem_tree_score",
- "mem_tree_ingested_sources",
- "mem_tree_summary_embeddings",
- "mem_tree_summaries",
- "mem_tree_chunks",
- ] {
- if let Err(error) = conn.execute(&format!("DELETE FROM {table}"), []) {
- let text = error.to_string();
- assert!(text.contains("no such table"), "wipe {table}: {text}");
- }
- }
- Ok(())
- })
- .expect("wipe shared store");
-}
-
-fn source(kind: SourceKind, id: &str) -> MemorySourceEntry {
- MemorySourceEntry {
- id: id.to_string(),
- kind,
- label: format!("{id} label"),
- enabled: true,
- toolkit: None,
- connection_id: None,
- path: None,
- glob: None,
- url: None,
- branch: None,
- paths: Vec::new(),
- query: None,
- since_days: None,
- max_items: None,
- max_commits: None,
- max_issues: None,
- max_prs: None,
- selector: None,
- max_tokens_per_sync: None,
- max_cost_per_sync_usd: None,
- sync_depth_days: None,
- }
-}
-
-fn chunk(source_id: &str, seq: u32, timestamp_ms: i64) -> Chunk {
- let content = format!("chunk {source_id} {seq}");
- let ts = Utc.timestamp_millis_opt(timestamp_ms).unwrap();
- Chunk {
- id: chunk_id(ChunkSourceKind::Document, source_id, seq, &content),
- content,
- metadata: Metadata::point_in_time(ChunkSourceKind::Document, source_id, "owner", ts),
- token_count: approx_token_count(source_id),
- seq_in_source: seq,
- created_at: ts,
- partial_message: false,
- }
-}
-
-fn tree_node(namespace: &str, node_id: &str, summary: &str) -> TreeNode {
- let created_at = Utc.with_ymd_and_hms(2026, 5, 29, 12, 0, 0).unwrap();
- TreeNode {
- node_id: node_id.to_string(),
- namespace: namespace.to_string(),
- level: level_from_node_id(node_id),
- parent_id: derive_parent_id(node_id),
- summary: summary.to_string(),
- token_count: estimate_tokens(summary),
- child_count: 0,
- created_at,
- updated_at: created_at,
- metadata: Some(json!({ "kind": "coverage", "node": node_id }).to_string()),
- }
-}
-
-async fn serve_routes(router: Router) -> String {
- let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
- .await
- .expect("bind test server");
- let addr = listener.local_addr().expect("local addr");
- tokio::spawn(async move {
- let _ = axum::serve(listener, router).await;
- });
- format!("http://{addr}")
-}
-
-async fn html_page() -> Html<&'static str> {
- Html(
- "Raw Page