test(rust): expand smart harness coverage (#1961)

This commit is contained in:
Steven Enamakel
2026-05-16 13:22:49 -07:00
committed by GitHub
parent 30a4a0706f
commit 351d561318
5 changed files with 490 additions and 12 deletions
+1 -1
View File
@@ -56,7 +56,7 @@ mod bughunt_tests;
#[cfg(test)]
pub(crate) mod test_support;
#[cfg(test)]
mod test_support_tests;
mod test_support_test;
#[cfg(test)]
mod harness_gap_tests;
+65 -4
View File
@@ -334,6 +334,37 @@ pub struct ComposioFixture {
pub tools: Vec<serde_json::Value>,
/// Per-action canned execute responses, keyed by action slug.
pub execute_responses: std::collections::HashMap<String, serde_json::Value>,
/// Ordered request-aware execute overrides. The first matching rule wins.
pub execute_rules: Vec<ComposioExecuteRule>,
}
#[derive(Debug, Clone)]
pub struct ComposioExecuteRule {
pub action: String,
pub argument_path: Option<String>,
pub argument_contains: Option<String>,
pub response: serde_json::Value,
}
impl ComposioExecuteRule {
pub fn new(action: impl Into<String>, response: serde_json::Value) -> Self {
Self {
action: action.into(),
argument_path: None,
argument_contains: None,
response,
}
}
pub fn when_argument_contains(
mut self,
path: impl Into<String>,
needle: impl Into<String>,
) -> Self {
self.argument_path = Some(path.into());
self.argument_contains = Some(needle.into());
self
}
}
impl ComposioFixture {
@@ -448,10 +479,42 @@ impl ComposioFixture {
]
.into_iter()
.collect(),
execute_rules: Vec::new(),
}
}
}
fn json_path<'a>(value: &'a serde_json::Value, path: &str) -> Option<&'a serde_json::Value> {
path.split('.')
.filter(|segment| !segment.is_empty())
.try_fold(value, |current, segment| current.get(segment))
}
fn match_execute_rule(
rules: &[ComposioExecuteRule],
action: &str,
body: &serde_json::Value,
) -> Option<serde_json::Value> {
rules.iter().find_map(|rule| {
if rule.action != action {
return None;
}
if let Some(path) = rule.argument_path.as_deref() {
let actual = json_path(body, path)?;
if let Some(needle) = rule.argument_contains.as_deref() {
if !actual
.to_string()
.to_ascii_lowercase()
.contains(&needle.to_ascii_lowercase())
{
return None;
}
}
}
Some(rule.response.clone())
})
}
#[derive(Clone)]
struct FakeComposioState {
fixture: Arc<Mutex<ComposioFixture>>,
@@ -635,10 +698,8 @@ pub async fn spawn_fake_composio_backend(fixture: ComposioFixture) -> FakeCompos
.unwrap_or("")
.to_string();
let fx = st.fixture.lock();
let response = fx
.execute_responses
.get(&action)
.cloned()
let response = match_execute_rule(&fx.execute_rules, &action, &body)
.or_else(|| fx.execute_responses.get(&action).cloned())
.unwrap_or_else(|| json!({"ok": true, "action": action.clone()}));
// Wrap in the BackendResponse envelope expected by
// IntegrationClient, with the inner shape matching
@@ -5,11 +5,11 @@
//! and history threading.
use super::test_support::{
spawn_fake_composio_backend, ComposioFixture, KeywordRule, KeywordScriptedProvider,
ScriptedToolCall,
spawn_fake_composio_backend, ComposioExecuteRule, ComposioFixture, KeywordRule,
KeywordScriptedProvider, ScriptedToolCall,
};
use super::tool_loop::run_tool_call_loop;
use crate::openhuman::providers::{ChatMessage, ChatResponse};
use crate::openhuman::providers::{ChatMessage, ChatRequest, ChatResponse, Provider};
use crate::openhuman::tools::traits::{PermissionLevel, Tool, ToolCategory, ToolResult, ToolScope};
use async_trait::async_trait;
use serde_json::json;
@@ -22,6 +22,244 @@ fn mm() -> crate::openhuman::config::MultimodalConfig {
crate::openhuman::config::MultimodalConfig::default()
}
#[tokio::test]
async fn keyword_provider_records_forced_then_fallback_turns() {
let provider =
KeywordScriptedProvider::new(vec![KeywordRule::final_reply("matched", "final answer")])
.with_native_tools(true)
.with_vision(true)
.with_fallback("fallback reply");
let caps = provider.capabilities();
assert!(caps.native_tool_calling);
assert!(caps.vision);
provider.push_forced_response(ChatResponse {
text: Some("forced reply".into()),
tool_calls: vec![],
usage: None,
});
let messages = vec![ChatMessage::user("nothing should match here")];
let forced = provider
.chat(
ChatRequest {
messages: &messages,
tools: None,
stream: None,
},
"test-model",
0.0,
)
.await
.expect("forced response");
assert_eq!(forced.text.as_deref(), Some("forced reply"));
let fallback = provider
.chat(
ChatRequest {
messages: &messages,
tools: None,
stream: None,
},
"test-model",
0.0,
)
.await
.expect("fallback response");
assert_eq!(fallback.text.as_deref(), Some("fallback reply"));
let turns = provider.turns();
assert_eq!(turns.len(), 2);
assert_eq!(turns[0].rule_keyword, None);
assert_eq!(turns[0].emitted_text.as_deref(), Some("forced reply"));
assert_eq!(turns[1].rule_keyword, None);
assert_eq!(turns[1].emitted_text.as_deref(), Some("fallback reply"));
}
#[tokio::test]
async fn keyword_provider_prompt_guided_text_wraps_tool_calls_and_honors_fire_limit() {
let provider = KeywordScriptedProvider::new(vec![KeywordRule::tool_call(
"search please",
ScriptedToolCall::new("search_tool", json!({"q": "rust"})),
)
.with_text("Looking it up.")]);
let messages = vec![
ChatMessage::assistant("earlier assistant turn"),
ChatMessage::tool("search please from a tool result"),
];
let first = provider
.chat(
ChatRequest {
messages: &messages,
tools: None,
stream: None,
},
"test-model",
0.0,
)
.await
.expect("prompt-guided response");
let text = first.text.expect("prompt-guided text body");
assert!(first.tool_calls.is_empty());
assert!(text.starts_with("Looking it up.\n"));
assert!(text.contains("<tool_call>"));
assert!(text.contains("\"name\":\"search_tool\""));
assert!(text.contains("\"q\":\"rust\""));
let second = provider
.chat(
ChatRequest {
messages: &messages,
tools: None,
stream: None,
},
"test-model",
0.0,
)
.await
.expect("fallback after max_fires");
assert_eq!(second.text.as_deref(), Some("done"));
assert_eq!(provider.turn_count(), 2);
}
#[tokio::test]
async fn fake_composio_backend_serves_routes_and_uses_response_fallbacks() {
let mut fixture = ComposioFixture::realistic();
fixture.execute_rules = vec![ComposioExecuteRule::new(
"GMAIL_FETCH_EMAILS",
json!({"messages": [{"id": "gmail-priority-2"}]}),
)
.when_argument_contains("arguments.query", "release blocker")];
let backend = spawn_fake_composio_backend(fixture).await;
let http = reqwest::Client::new();
let toolkits: serde_json::Value = http
.get(format!(
"{}/agent-integrations/composio/toolkits",
backend.base_url
))
.send()
.await
.expect("toolkits request")
.json()
.await
.expect("toolkits json");
assert_eq!(toolkits["data"]["toolkits"][0], "gmail");
let authorize: serde_json::Value = http
.post(format!(
"{}/agent-integrations/composio/authorize",
backend.base_url
))
.json(&json!({"toolkit": "gmail"}))
.send()
.await
.expect("authorize request")
.json()
.await
.expect("authorize json");
assert_eq!(authorize["data"]["connectionId"], "conn_gmail_pending",);
let rule_match: serde_json::Value = http
.post(format!(
"{}/agent-integrations/composio/execute",
backend.base_url
))
.json(&json!({
"tool": "GMAIL_FETCH_EMAILS",
"arguments": {"query": "Need RELEASE BLOCKER updates"},
}))
.send()
.await
.expect("execute request")
.json()
.await
.expect("execute json");
assert_eq!(
rule_match["data"]["data"]["messages"][0]["id"],
"gmail-priority-2",
);
let execute_fallback: serde_json::Value = http
.post(format!(
"{}/agent-integrations/composio/execute",
backend.base_url
))
.json(&json!({
"tool": "GMAIL_FETCH_EMAILS",
"arguments": {"page": 1},
}))
.send()
.await
.expect("execute fallback request")
.json()
.await
.expect("execute fallback json");
assert_eq!(execute_fallback["data"]["data"]["messages"][0]["id"], "m1",);
let default_execute: serde_json::Value = http
.post(format!(
"{}/agent-integrations/composio/execute",
backend.base_url
))
.json(&json!({
"tool": "UNKNOWN_ACTION",
"arguments": {"topic": "ops"},
}))
.send()
.await
.expect("default execute request")
.json()
.await
.expect("default execute json");
assert_eq!(default_execute["data"]["data"]["ok"], true);
assert_eq!(default_execute["data"]["data"]["action"], "UNKNOWN_ACTION");
let delete: serde_json::Value = http
.delete(format!(
"{}/agent-integrations/composio/connections/conn_gmail_1",
backend.base_url
))
.send()
.await
.expect("delete request")
.json()
.await
.expect("delete json");
assert_eq!(delete["data"]["deleted"], true);
let requests = backend.requests();
assert!(
requests
.iter()
.any(|(m, p, _)| m == "GET" && p == "/toolkits"),
"expected toolkits route to be recorded"
);
assert!(
requests
.iter()
.any(|(m, p, _)| m == "POST" && p == "/authorize"),
"expected authorize route to be recorded"
);
assert!(
requests
.iter()
.any(|(m, p, body)| m == "POST" && p == "/execute" && body["tool"] == "UNKNOWN_ACTION"),
"expected execute route to record unknown action body"
);
assert!(
requests
.iter()
.any(|(m, p, _)| m == "DELETE" && p == "/connections/conn_gmail_1"),
"expected delete route to be recorded"
);
}
/// Generic test tool: records the args it was called with and returns
/// whatever was wired at construction.
struct RecordingTool {
@@ -623,6 +861,55 @@ async fn fake_composio_backend_serves_realistic_toolkits() {
assert!(reqs.iter().any(|(_, p, _)| p == "/execute"));
}
#[tokio::test]
async fn fake_composio_backend_can_match_execute_rules_by_argument_content() {
let mut fixture = ComposioFixture::realistic();
fixture.execute_rules.push(
ComposioExecuteRule::new(
"GMAIL_FETCH_EMAILS",
json!({
"messages": [
{
"id": "gmail-priority-1",
"subject": "Release blocker",
"snippet": "The release blocker is the broken memory recall spec."
}
]
}),
)
.when_argument_contains("arguments.query", "release blocker"),
);
let backend = spawn_fake_composio_backend(fixture).await;
let client = backend.client();
let exec = client
.execute_tool(
"GMAIL_FETCH_EMAILS",
Some(json!({
"query": "label:inbox release blocker",
"max_results": 5
})),
)
.await
.unwrap();
assert!(exec.successful, "execute should report success");
let resp_json = serde_json::to_value(&exec.data).unwrap();
assert!(
resp_json.to_string().contains("gmail-priority-1"),
"expected rule-driven response, got: {resp_json}"
);
let reqs = backend.requests();
let exec_req = reqs
.iter()
.find(|(method, path, _)| method == "POST" && path == "/execute")
.expect("execute request should be recorded");
assert_eq!(
exec_req.2["arguments"]["query"],
"label:inbox release blocker"
);
}
// ── 12. End-to-end: harness drives a Composio tool against fake backend
#[tokio::test]
+93 -2
View File
@@ -1,6 +1,6 @@
use super::super::context::{
build_memory_context, conversation_memory_key, ChannelRuntimeContext,
CHANNEL_MESSAGE_TIMEOUT_SECS,
build_memory_context, clear_sender_history, conversation_history_key, conversation_memory_key,
ChannelRuntimeContext, CHANNEL_MESSAGE_TIMEOUT_SECS,
};
use super::super::runtime::process_channel_message;
use super::super::{traits, Channel};
@@ -205,4 +205,95 @@ async fn process_channel_message_restores_per_sender_history_on_follow_ups() {
assert!(calls[1][3].1.contains("follow up"));
}
#[tokio::test]
async fn process_channel_message_uses_autosaved_memory_after_history_is_cleared() {
let _bus_guard = super::common::use_real_agent_handler().await;
let channel_impl = Arc::new(RecordingChannel::default());
let channel: Arc<dyn Channel> = channel_impl.clone();
let mut channels_by_name = HashMap::new();
channels_by_name.insert(channel.name().to_string(), channel);
let provider_impl = Arc::new(HistoryCaptureProvider::default());
let tmp = TempDir::new().unwrap();
let memory = Arc::new(UnifiedMemory::new(tmp.path(), Arc::new(NoopEmbedding), None).unwrap());
let runtime_ctx = Arc::new(ChannelRuntimeContext {
channels_by_name: Arc::new(channels_by_name),
provider: provider_impl.clone(),
default_provider: Arc::new("test-provider".to_string()),
memory,
tools_registry: Arc::new(vec![]),
system_prompt: Arc::new("test-system-prompt".to_string()),
model: Arc::new("test-model".to_string()),
temperature: 0.0,
auto_save_memory: true,
max_tool_iterations: 5,
min_relevance_score: 0.0,
conversation_histories: Arc::new(Mutex::new(HashMap::new())),
provider_cache: Arc::new(Mutex::new(HashMap::new())),
route_overrides: Arc::new(Mutex::new(HashMap::new())),
api_url: None,
inference_url: None,
reliability: Arc::new(crate::openhuman::config::ReliabilityConfig::default()),
provider_runtime_options: providers::ProviderRuntimeOptions::default(),
workspace_dir: Arc::new(std::env::temp_dir()),
message_timeout_secs: CHANNEL_MESSAGE_TIMEOUT_SECS,
multimodal: crate::openhuman::config::MultimodalConfig::default(),
});
let first = traits::ChannelMessage {
id: "msg-memory-a".to_string(),
sender: "alice".to_string(),
reply_target: "chat-1".to_string(),
content: "My launch code is phoenix-773.".to_string(),
channel: "test-channel".to_string(),
timestamp: 1,
thread_ts: None,
};
let history_key = conversation_history_key(&first);
process_channel_message(runtime_ctx.clone(), first).await;
clear_sender_history(&runtime_ctx, &history_key);
process_channel_message(
runtime_ctx,
traits::ChannelMessage {
id: "msg-memory-b".to_string(),
sender: "alice".to_string(),
reply_target: "chat-1".to_string(),
content: "What is my launch code?".to_string(),
channel: "test-channel".to_string(),
timestamp: 2,
thread_ts: None,
},
)
.await;
let calls = provider_impl
.calls
.lock()
.unwrap_or_else(|e| e.into_inner());
assert_eq!(calls.len(), 2);
assert_eq!(calls[1].len(), 2);
assert_eq!(calls[1][0].0, "system");
assert_eq!(calls[1][1].0, "user");
assert!(
calls[1][1].1.contains("[Memory context]"),
"second turn should include recalled memory context: {:?}",
calls[1][1]
);
assert!(
calls[1][1].1.contains("phoenix-773"),
"second turn should surface the autosaved fact: {:?}",
calls[1][1]
);
assert!(
calls[1][1].1.contains("What is my launch code?"),
"current user question should remain in the final prompt: {:?}",
calls[1][1]
);
}
// ── AIEOS Identity Tests (Issue #168) ─────────────────────────
+41 -2
View File
@@ -16,9 +16,10 @@ use std::sync::{Mutex, OnceLock};
use tempfile::tempdir;
use openhuman_core::openhuman::memory::ops::{
clear_namespace, doc_put, memory_recall_memories, ClearNamespaceParams, PutDocParams,
clear_namespace, doc_put, memory_recall_context, memory_recall_memories, ClearNamespaceParams,
PutDocParams,
};
use openhuman_core::openhuman::memory::rpc_models::RecallMemoriesRequest;
use openhuman_core::openhuman::memory::rpc_models::{RecallContextRequest, RecallMemoriesRequest};
// ── Env isolation ────────────────────────────────────────────────────
@@ -91,6 +92,15 @@ fn recall_request() -> RecallMemoriesRequest {
}
}
fn recall_context_request() -> RecallContextRequest {
RecallContextRequest {
namespace: NS.to_string(),
include_references: Some(true),
limit: Some(10),
max_chunks: None,
}
}
// ── Tests ────────────────────────────────────────────────────────────
/// 8.1.1 store + 8.1.2 recall — the happy-path round-trip.
@@ -122,6 +132,35 @@ async fn doc_put_then_recall_memories_returns_canary() {
);
}
/// `recall_context` should surface the same document as an LLM-ready prompt
/// block, not only in the raw memory list view.
#[tokio::test]
async fn doc_put_then_recall_context_renders_llm_context_message() {
let _lock = env_lock();
let tmp = tempdir().expect("tempdir");
let _home = EnvVarGuard::set_to_path("HOME", tmp.path());
let workspace_path = tmp.path().join("workspace");
std::fs::create_dir_all(&workspace_path).expect("create workspace dir");
let _ws = EnvVarGuard::set_to_path("OPENHUMAN_WORKSPACE", &workspace_path);
doc_put(put_params()).await.expect("doc_put rpc");
let recall_outcome = memory_recall_context(recall_context_request())
.await
.expect("memory_recall_context rpc");
let llm_context = recall_outcome
.value
.data
.as_ref()
.and_then(|data| data.llm_context_message.as_ref())
.cloned()
.unwrap_or_default();
assert!(
llm_context.contains(CONTENT) || llm_context.contains(KEY),
"llm context should reference the canary content/key — got {llm_context}"
);
}
/// 8.1.3 forget — clear_namespace must scrub the namespace so subsequent
/// recalls do not see the canary content. Failure-path / edge-case assertion
/// required by gitbooks/developing/testing-strategy.md.