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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci-full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,8 @@ jobs:
# and leave a stale digest that only fails after the download. Both values
# are the ubuntu-22.04-x86_64 row of `src/openhuman/modules/registry.rs`,
# which is the authoritative pin — copy them from there, never recompute.
memory_version="1.15.2"
memory_sha256="32eb22f09c2ede0981a8462e3e4438db40455a6369ae5acc577f8d4493f1e84a"
memory_version="1.15.3"
memory_sha256="5f50715abdc1b48745282db4718027159a292b5336d4bbd258cc471bb037b78e"
module_root="/opt/openhuman-test-modules/${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
memory_dir="$module_root/tinymemory"
juice_dir="$module_root/tinyjuice"
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ci-lite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -831,8 +831,8 @@ jobs:
# and leave a stale digest that only fails after the download. Both values
# are the ubuntu-22.04-x86_64 row of `src/openhuman/modules/registry.rs`,
# which is the authoritative pin — copy them from there, never recompute.
memory_version="1.15.2"
memory_sha256="32eb22f09c2ede0981a8462e3e4438db40455a6369ae5acc577f8d4493f1e84a"
memory_version="1.15.3"
memory_sha256="5f50715abdc1b48745282db4718027159a292b5336d4bbd258cc471bb037b78e"
module_root="/opt/openhuman-test-modules/${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
memory_dir="$module_root/tinymemory"
juice_dir="$module_root/tinyjuice"
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/e2e-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,8 @@ jobs:
# and leave a stale digest that only fails after the download. Both values
# are the ubuntu-22.04-x86_64 row of `src/openhuman/modules/registry.rs`,
# which is the authoritative pin — copy them from there, never recompute.
memory_version="1.15.2"
memory_sha256="32eb22f09c2ede0981a8462e3e4438db40455a6369ae5acc577f8d4493f1e84a"
memory_version="1.15.3"
memory_sha256="5f50715abdc1b48745282db4718027159a292b5336d4bbd258cc471bb037b78e"
module_root="/opt/openhuman-test-modules/${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
memory_dir="$module_root/tinymemory"
memory_archive="$memory_dir/tinymemory-module-${memory_version}-ubuntu-22.04-x86_64.tar.gz"
Expand Down Expand Up @@ -374,8 +374,8 @@ jobs:
# and leave a stale digest that only fails after the download. Both values
# are the ubuntu-22.04-x86_64 row of `src/openhuman/modules/registry.rs`,
# which is the authoritative pin — copy them from there, never recompute.
memory_version="1.15.2"
memory_sha256="32eb22f09c2ede0981a8462e3e4438db40455a6369ae5acc577f8d4493f1e84a"
memory_version="1.15.3"
memory_sha256="5f50715abdc1b48745282db4718027159a292b5336d4bbd258cc471bb037b78e"
module_root="/opt/openhuman-test-modules/${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
memory_dir="$module_root/tinymemory"
memory_archive="$memory_dir/tinymemory-module-${memory_version}-ubuntu-22.04-x86_64.tar.gz"
Expand Down
6 changes: 0 additions & 6 deletions scripts/ci/module-pin-exemptions.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,6 @@
"submodule": "vendor/tinymcp",
"expect": "v0.3.2-2-g8b0627d",
"reason": "The host compiles the MCP contract against tinyhumansai/tinymcp#13 (Supervisor::tick returns a TickReport; needed by openhuman#5931), merged to tinymcp main but not yet in a tagged release, while the registry keeps the published v0.3.2 artifact. The drift is compile-only: the tinymcp module is registry-entered but not wired (AGENTS.md, 'step two of the extraction'), so no build downloads or loads that artifact. Delete this entry when tinymcp cuts its next release and the registry pin moves onto it."
},
{
"id": "tinymemory",
"submodule": "vendor/tinymemory",
"expect": "v1.15.2-15-g5c55431",
"reason": "The host compiles the memory contract against tinymemory main (tinyhumansai/tinymemory#148, #150 and #151 \u2014 the engine-free conformance driver openhuman#6161 binds in place of an in-process engine), merged upstream but not yet in a tagged release, while the registry keeps the published v1.15.2 artifact. The drift cannot reach a runtime mismatch, and that was checked rather than assumed: the entire v1.15.2..5c55431 delta to the crates this build ships is (a) one additive re-export, `pub use tinymemory_bus::chrono` in tinymemory-api, so a driver crate depending on the contract alone can name the `DateTime<Utc>` two MemoryTree methods already take, (b) a `#[cfg(test)] mod summarise_tests;` line in tinymemory-core, and (c) 31 lines of doc comment on MemoryDocuments::list_documents and delete_document. Zero removals, no method added or changed, no wire slot moved, CONTRACT_VERSION untouched \u2014 so the v1.15.2 artifact serves exactly the contract compiled here. Everything else in the range is tinymemory-conformance, a test-only crate that is a dev-dependency here and is not in the module artifact at all. Delete this entry when tinymemory cuts its next release and the registry pin moves onto it."
}
]
}
37 changes: 25 additions & 12 deletions src/openhuman/memory/binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -370,17 +370,20 @@ fn module_provider(
)
}

/// The configuration every test-build memory binding loads its module through.
///
/// Unit tests do not run the full boot sequence that publishes the module
/// policy. A native module is loaded once per process and therefore captures
/// the first workspace it receives. Pin every test binding to the same
/// workspace as the process-global test client so concurrent tests cannot
/// win module initialization with an unrelated tempdir and split guarded
/// writes from legacy read-back calls.
///
/// Named rather than inlined into [`module_provider`] so a test can await this
/// module's resolution through the same configuration the binding will use —
/// see [`crate::openhuman::memory::test_support::settle_memory_module`].
#[cfg(all(feature = "modules", test))]
fn module_provider(
_workspace_dir: &Path,
memory_subdir: &str,
) -> (Arc<dyn MemoryProvider>, DriverClass) {
// Unit tests do not run the full boot sequence that publishes the module
// policy. A native module is loaded once per process and therefore captures
// the first workspace it receives. Pin every test binding to the same
// workspace as the process-global test client so concurrent tests cannot
// win module initialization with an unrelated tempdir and split guarded
// writes from legacy read-back calls.
pub(crate) fn test_module_config() -> crate::openhuman::config::Config {
let workspace_dir = crate::openhuman::memory::ops::shared_memory_test_workspace();
let mut config = crate::openhuman::config::Config::default();
config.workspace_dir = workspace_dir.clone();
Expand All @@ -394,10 +397,20 @@ fn module_provider(
path: path.to_string_lossy().into_owned(),
});
}
config
}

#[cfg(all(feature = "modules", test))]
fn module_provider(
_workspace_dir: &Path,
memory_subdir: &str,
) -> (Arc<dyn MemoryProvider>, DriverClass) {
(
Arc::new(
crate::openhuman::modules::memory::ModuleMemoryProvider::new(Arc::new(config))
.in_subdir(memory_subdir),
crate::openhuman::modules::memory::ModuleMemoryProvider::new(Arc::new(
test_module_config(),
))
.in_subdir(memory_subdir),
),
DriverClass::Module,
)
Expand Down
4 changes: 4 additions & 0 deletions src/openhuman/memory/ops/provider_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ async fn status_without_a_context_reports_an_unresolved_slot() {

#[tokio::test]
async fn bound_driver_status_reports_id_class_contract_and_capabilities() {
// `health` below is `degraded` for as long as the module is loading, and
// the load is process-wide: without this the assertion races whichever
// sibling test asked for the module first (openhuman#6172).
crate::openhuman::memory::test_support::settle_memory_module().await;
let workspace = tempfile::tempdir().expect("tempdir");
let cfg = crate::openhuman::config::schema::MemorySubsystemConfig::default();
let binding = crate::openhuman::memory::binding::for_workspace(workspace.path(), &cfg)
Expand Down
148 changes: 148 additions & 0 deletions src/openhuman/memory/sources/rpc_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,151 @@ fn composio_rows_dispatch_to_the_connector_and_everything_else_to_the_driver() {
"the refusal must name the row and say what to do about it, got: {error}"
);
}

// ── what `add_rpc` owns ─────────────────────────────────────────────────────
//
// The handler generates the source id, maps the request into a
// `MemorySourceEntry`, and applies conservative per-kind caps before the
// registry sees it. Only the first two are its own code; the third is
// `tinymemory-sources`' `apply_kind_defaults`, and what belongs here is that
// this handler *calls* it — an add whose caps the user left unset must not
// reach the registry uncapped.
//
// Covered only by `tests/raw_coverage/memory_threads_raw_coverage_e2e.rs`
// before now, which went with the engine (#6161) although none of this is the
// engine's (#6172).

/// Pins `OPENHUMAN_WORKSPACE` for the duration, holding the crate's env lock so
/// concurrent tests cannot observe the change. `add_rpc` writes through
/// `registry::add_source`, which resolves its config with
/// `load_config_with_timeout` — process-global, so the workspace has to be
/// pinned rather than passed.
struct WorkspaceEnvGuard {
_env_lock: std::sync::MutexGuard<'static, ()>,
previous: Option<std::ffi::OsString>,
}

impl WorkspaceEnvGuard {
fn pin(workspace: &std::path::Path) -> Self {
let env_lock = crate::openhuman::config::TEST_ENV_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let previous = std::env::var_os("OPENHUMAN_WORKSPACE");
std::env::set_var("OPENHUMAN_WORKSPACE", workspace);
Self {
_env_lock: env_lock,
previous,
}
}
}

impl Drop for WorkspaceEnvGuard {
fn drop(&mut self) {
match self.previous.take() {
Some(value) => std::env::set_var("OPENHUMAN_WORKSPACE", value),
None => std::env::remove_var("OPENHUMAN_WORKSPACE"),
}
}
}

fn github_add_request() -> AddRequest {
AddRequest {
kind: tinymemory_sources::types::SourceKind::GithubRepo,
label: "A repository".into(),
enabled: true,
toolkit: None,
connection_id: None,
path: None,
glob: None,
url: Some("https://github.invalid/owner/repo".into()),
branch: None,
paths: Vec::new(),
// The three the caller left unset, which is the whole point.
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,
}
}

#[tokio::test]
async fn add_generates_an_id_and_caps_a_request_that_left_its_limits_unset() {
let tmp = tempfile::TempDir::new().expect("tempdir");
let workspace = tmp.path().join("workspace");
std::fs::create_dir_all(&workspace).expect("workspace dir");
let _env = WorkspaceEnvGuard::pin(&workspace);

let added = add_rpc(github_add_request())
.await
.expect("add_rpc")
.value
.source;

// ── the id is the handler's ─────────────────────────────────────────────
//
// The caller never supplies one; a request that could name its own id would
// let two sources collide by construction.
let minted = added.id.strip_prefix("src_").unwrap_or_else(|| {
panic!(
"the handler must mint a `src_`-prefixed id, got {:?}",
added.id
)
});
assert!(
minted.len() == 32
&& minted
.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)),
"the suffix must be a uuid-simple — 32 lowercase hex digits — and not \
merely 32 characters, got {minted:?}"
);

// ── the caps came from somewhere ────────────────────────────────────────
//
// Asserted as "no longer None" plus the concrete GitHub values, because the
// two say different things: the first is that this handler applies defaults
// at all, the second that it applied *these* — a handler that filled them
// with zeros would satisfy the first and cap every sync at nothing.
assert_eq!(added.max_prs, Some(10), "per-kind PR cap");
assert_eq!(added.max_issues, Some(10), "per-kind issue cap");
assert_eq!(added.max_commits, Some(50), "per-kind commit cap");

// ── and it reached the registry ─────────────────────────────────────────
//
// The response alone would be satisfied by a handler that shaped an entry
// and dropped it.
let fetched = get_rpc(GetRequest {
id: added.id.clone(),
})
.await
.expect("get_rpc")
.value
.source;
let fetched = fetched.expect("the added source is not readable back from the registry");
assert_eq!(
fetched.id, added.id,
"the registry read back a different source"
);

// The id alone would be satisfied by a registry that persisted an entry
// with every other field defaulted. These four are the request's
// non-default fields, so each one is a mapping the handler had to carry.
assert_eq!(
fetched.kind,
tinymemory_sources::types::SourceKind::GithubRepo,
"the request's kind must survive the round trip"
);
assert_eq!(fetched.label, "A repository", "the request's label");
assert!(fetched.enabled, "the request asked for an enabled source");
assert_eq!(
fetched.url.as_deref(),
Some("https://github.invalid/owner/repo"),
"the request's url"
);
}
98 changes: 98 additions & 0 deletions src/openhuman/memory/sync/composio/providers/slack/rpc_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,101 @@ async fn list_slack_connections_resolves_direct_variant_when_mode_is_direct() {
// a valid empty envelope), that's also acceptable — the
// factory still routed correctly.
}

// ── the status surface's degraded shape ─────────────────────────────────────
//
// `sync_status_rpc` does two things nothing asserted: it filters the connection
// list to slack rows that are ACTIVE, and it answers a fixed zero-value shape
// because per-connection sync detail is no longer readable — the connector
// module keeps its cursor internally.
//
// The deleted `slack_sync_status_rpc_reports_the_degraded_zero_value_shape`
// covered exactly this, in `tests/raw_coverage/memory_sync_tree_round21_*`,
// which went with the engine (#6161) although the assertion was never about the
// engine. Restored here against a local mock backend (#6172).

#[tokio::test]
async fn status_filters_to_active_slack_and_reports_the_degraded_zero_value_shape() {
use crate::openhuman::security::credentials::{
AuthService, APP_SESSION_PROVIDER, DEFAULT_AUTH_PROFILE_NAME,
};
use serde_json::json;
use std::collections::HashMap;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};

let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/agent-integrations/composio/connections"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"success": true,
"data": { "connections": [
// The one row that must survive both filters.
{ "id": "conn-slack-active", "toolkit": "slack", "status": "ACTIVE" },
// Dropped by the status filter…
{ "id": "conn-slack-pending", "toolkit": "slack", "status": "PENDING" },
// …and this one by the toolkit filter.
{ "id": "conn-gmail-active", "toolkit": "gmail", "status": "ACTIVE" },
] }
})))
.mount(&server)
.await;

let tmp = tempfile::tempdir().expect("tempdir");
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.api_url = Some(server.uri());
std::fs::create_dir_all(&config.workspace_dir).expect("workspace dir");
AuthService::from_config(&config)
.store_provider_token(
APP_SESSION_PROVIDER,
DEFAULT_AUTH_PROFILE_NAME,
"test-session-token",
HashMap::new(),
true,
)
.expect("store app session token");

let outcome = sync_status_rpc(&config, SyncStatusRequest::default())
.await
.expect("status rpc");

// ── the filter ──────────────────────────────────────────────────────────
assert_eq!(
outcome.value.connections.len(),
1,
"only the ACTIVE slack connection qualifies; got {:?}",
outcome.value.connections
);
let row = &outcome.value.connections[0];
assert_eq!(row.connection_id, "conn-slack-active");

// ── the degraded shape ──────────────────────────────────────────────────
//
// Four fixed zero values, asserted individually rather than as a struct
// comparison: each one is a separate promise to the status table, and a
// struct literal would hide which of them a future change broke.
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);

// ── and the log that explains it ────────────────────────────────────────
//
// The zeros are indistinguishable from "a connection that has synced
// nothing yet", so the log line is what tells an operator the detail is
// gone rather than empty. Without it the shape above is a silent lie.
assert!(
outcome
.logs
.iter()
.any(|line| line.contains("connections=1") && line.contains("no longer available")),
"the status log must explain the degraded read: {:?}",
outcome.logs
);
}
Loading
Loading