use super::*;
use async_trait::async_trait;
use roboticus_agent::capability::{Capability, CapabilitySource};
use roboticus_agent::tools::{ToolContext, ToolError, ToolResult};
use roboticus_core::RiskLevel;
use serde_json::json;
use std::sync::Arc;
fn inference_input<'a>(
state: &'a AppState,
session_id: &'a str,
turn_id: &'a str,
user_content: &'a str,
) -> super::core::InferenceInput<'a> {
super::core::InferenceInput {
state,
session_id,
user_content,
turn_id,
channel_label: "api",
agent_name: "TestBot".to_string(),
agent_id: "test-agent".to_string(),
os_text: String::new(),
firmware_text: String::new(),
primary_model: "ollama/qwen3:8b".to_string(),
tier_adapt: roboticus_core::config::TierAdaptConfig::default(),
delegation_workflow_note: None,
inject_diagnostics: false,
gate_system_note: None,
delegated_execution_note: None,
delegated_execution_result: None,
behavioral_note: None,
content_parts: None,
topic_tag: None,
}
}
#[tokio::test]
async fn prepare_inference_autonomously_includes_working_memory_without_explicit_recall_prompt() {
let state = crate::api::routes::tests::test_state();
let session_id =
roboticus_db::sessions::find_or_create(&state.db, "memory-auto-working", None).unwrap();
let user_prompt = "Draft a concise status update for this week.";
let turn_id =
roboticus_db::sessions::create_turn(&state.db, &session_id, None, None, None, None)
.unwrap();
roboticus_db::sessions::append_message(&state.db, &session_id, "user", user_prompt).unwrap();
roboticus_db::memory::store_working(
&state.db,
&session_id,
"goal",
"Prioritize telegram stability and avoid duplicate replies",
9,
)
.unwrap();
let prepared = super::core::prepare_inference(&inference_input(
&state,
&session_id,
&turn_id,
user_prompt,
))
.await
.unwrap();
assert!(
prepared.context_snapshot.memory_tokens > 0,
"memory token bucket should be non-zero when memory is auto-injected"
);
let memory_block = prepared
.request
.messages
.iter()
.find(|m| m.role == "system" && m.content.contains("[Working Memory]"))
.map(|m| m.content.clone())
.expect("prepared context should contain a directly-injected [Working Memory] block");
assert!(memory_block.contains("telegram stability"));
}
#[tokio::test]
async fn prepare_inference_autonomously_uses_relevant_semantic_memories() {
let state = crate::api::routes::tests::test_state();
let session_id =
roboticus_db::sessions::find_or_create(&state.db, "memory-auto-relevant", None).unwrap();
let user_prompt = "Give me a short post-incident update with the SLO target.";
let turn_id =
roboticus_db::sessions::create_turn(&state.db, &session_id, None, None, None, None)
.unwrap();
roboticus_db::sessions::append_message(&state.db, &session_id, "user", user_prompt).unwrap();
let sem_id = roboticus_db::memory::store_semantic(
&state.db,
"facts",
"slo_target",
"Service-level objective target is 99.95% for core APIs",
0.9,
)
.unwrap();
roboticus_db::memory_index::upsert_index_entry(
&state.db,
"semantic_memory",
&sem_id,
"SLO target 99.95% for core APIs",
Some("facts"),
)
.unwrap();
let prepared = super::core::prepare_inference(&inference_input(
&state,
&session_id,
&turn_id,
user_prompt,
))
.await
.unwrap();
let memory_block = prepared
.request
.messages
.iter()
.find(|m| m.role == "system" && m.content.contains("[Memory Index"))
.map(|m| m.content.to_ascii_lowercase())
.expect("prepared context should contain a [Memory Index block for semantic memories");
assert!(
memory_block.contains("99.95"),
"index entry summary should contain the SLO target"
);
}
#[tokio::test]
async fn prepare_inference_autonomously_uses_relevant_episodic_memories() {
let state = crate::api::routes::tests::test_state();
let session_id =
roboticus_db::sessions::find_or_create(&state.db, "memory-auto-episodic", None).unwrap();
let user_prompt = "Give me a short update on incident stabilization and rollback status.";
let turn_id =
roboticus_db::sessions::create_turn(&state.db, &session_id, None, None, None, None)
.unwrap();
roboticus_db::sessions::append_message(&state.db, &session_id, "user", user_prompt).unwrap();
let ep_id = roboticus_db::memory::store_episodic(
&state.db,
"incident",
"Incident bridge stabilized after rollback and mitigation",
8,
)
.unwrap();
roboticus_db::memory_index::upsert_index_entry(
&state.db,
"episodic_memory",
&ep_id,
"Incident bridge stabilized after rollback",
Some("incident"),
)
.unwrap();
let prepared = super::core::prepare_inference(&inference_input(
&state,
&session_id,
&turn_id,
user_prompt,
))
.await
.unwrap();
let memory_block = prepared
.request
.messages
.iter()
.find(|m| m.role == "system" && m.content.contains("[Memory Index"))
.map(|m| m.content.to_ascii_lowercase())
.expect("prepared context should contain a [Memory Index block for episodic memories");
assert!(
memory_block.contains("incident bridge stabilized"),
"index entry summary should contain the incident description"
);
}
#[tokio::test]
async fn prepare_inference_includes_operational_introspection_policy() {
let state = crate::api::routes::tests::test_state();
let session_id =
roboticus_db::sessions::find_or_create(&state.db, "memory-introspection-policy", None)
.unwrap();
let user_prompt = "Do we already know enough to delegate this migration review?";
let turn_id =
roboticus_db::sessions::create_turn(&state.db, &session_id, None, None, None, None)
.unwrap();
roboticus_db::sessions::append_message(&state.db, &session_id, "user", user_prompt).unwrap();
let prepared = super::core::prepare_inference(&inference_input(
&state,
&session_id,
&turn_id,
user_prompt,
))
.await
.unwrap();
let system_prompt = prepared
.request
.messages
.iter()
.find(|m| m.role == "system" && m.content.contains("Operational Introspection"))
.map(|m| m.content.clone())
.expect("prepared inference should include operational introspection guidance");
assert!(system_prompt.contains("get_memory_stats"));
assert!(system_prompt.contains("get_runtime_context"));
assert!(system_prompt.contains("hippocampus-backed"));
assert!(system_prompt.contains("list-subagent-roster"));
assert!(system_prompt.contains("compose-skill"));
assert!(system_prompt.contains("compose-subagent"));
}
struct TestCapability {
name: String,
description: String,
}
#[async_trait]
impl Capability for TestCapability {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
&self.description
}
fn risk_level(&self) -> RiskLevel {
RiskLevel::Safe
}
fn parameters_schema(&self) -> serde_json::Value {
json!({"type": "object"})
}
fn source(&self) -> CapabilitySource {
CapabilitySource::BuiltIn
}
async fn execute(
&self,
_params: serde_json::Value,
_ctx: &ToolContext,
) -> Result<ToolResult, ToolError> {
Ok(ToolResult {
output: "ok".into(),
metadata: None,
})
}
}
#[tokio::test]
async fn prepare_inference_prunes_tool_list_with_tool_search() {
let state = crate::api::routes::tests::test_state();
for idx in 0..20 {
let (name, description) = if idx == 0 {
(
"incident_tool".to_string(),
"Analyze incident rollback status and summarize service recovery".to_string(),
)
} else {
(
format!("irrelevant_tool_{idx}"),
format!("Generate botanical poetry about rare orchids variant {idx}"),
)
};
state
.capabilities
.register(Arc::new(TestCapability { name, description }))
.await
.unwrap();
}
let session_id =
roboticus_db::sessions::find_or_create(&state.db, "tool-search-pruning", None).unwrap();
let user_prompt = "Give me an incident rollback status update and recovery summary.";
let turn_id =
roboticus_db::sessions::create_turn(&state.db, &session_id, None, None, None, None)
.unwrap();
roboticus_db::sessions::append_message(&state.db, &session_id, "user", user_prompt).unwrap();
let prepared = super::core::prepare_inference(&inference_input(
&state,
&session_id,
&turn_id,
user_prompt,
))
.await
.unwrap();
assert!(
prepared.request.tools.len() <= 15,
"tool-search should prune tool definitions to top-k, got {}",
prepared.request.tools.len()
);
assert!(
prepared
.request
.tools
.iter()
.any(|tool| tool.name == "incident_tool"),
"relevant tool should survive pruning"
);
let stats = prepared
.tool_search_stats
.as_ref()
.expect("tool search stats should be populated when query embedding exists");
assert!(stats.candidates_considered > prepared.request.tools.len());
assert!(stats.candidates_pruned > 0);
}