#[allow(clippy::expect_used)]
mod inner {
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Mutex;
use std::time::Duration;
use crate::config::AgentConfig;
use crate::config_provider::ConfigProvider;
use crate::error::{ConfigError, LlmError, StateError, TerminationReason};
use crate::knowledge::{
IngestOutcome as KnowledgeIngestOutcome, Knowledge, KnowledgeChunk, KnowledgeQuery,
KnowledgeResult, RetrievedChunk,
};
use crate::llm::{LlmBackend, LlmRequest, LlmResponse};
use crate::long_term::{
EpisodeIngest, IngestOutcome, LongTermMemory, LongTermMemoryError, RecallQuery,
RecalledFact,
};
use crate::state::{AgentStateStore, ConversationState, SessionLock, SessionLockInner};
use crate::telemetry::{StepTelemetryCtx, Telemetry};
use crate::tenant::TenantContext;
use greentic_types::TenantCtx;
use tokio::sync::Notify;
pub struct MockLlmBackend {
pub responses: Mutex<Vec<Result<LlmResponse, LlmError>>>,
pub seen_system_prompts: Mutex<Vec<String>>,
pub seen_tool_names: Mutex<Vec<Vec<String>>>,
}
impl MockLlmBackend {
pub fn new(responses: Vec<Result<LlmResponse, LlmError>>) -> Self {
Self {
responses: Mutex::new(responses),
seen_system_prompts: Mutex::new(Vec::new()),
seen_tool_names: Mutex::new(Vec::new()),
}
}
}
impl LlmBackend for MockLlmBackend {
fn complete<'a>(
&'a self,
req: LlmRequest,
) -> Pin<Box<dyn Future<Output = Result<LlmResponse, LlmError>> + Send + 'a>> {
self.seen_system_prompts
.lock()
.expect("mock llm prompt lock poisoned")
.push(req.system_prompt.clone());
self.seen_tool_names
.lock()
.expect("mock llm tools lock poisoned")
.push(req.tools.iter().map(|t| t.tool_name.clone()).collect());
let next = {
let mut queue = self.responses.lock().expect("mock LLM mutex poisoned");
if queue.is_empty() {
Err(LlmError::Transport("mock queue exhausted".into()))
} else {
queue.remove(0)
}
};
Box::pin(async move { next })
}
}
pub struct MockLongTermMemory {
scripted_facts: Vec<RecalledFact>,
ingested: Mutex<Vec<EpisodeIngest>>,
ingested_notify: Notify,
}
impl MockLongTermMemory {
pub fn new(scripted_facts: Vec<RecalledFact>) -> Self {
Self {
scripted_facts,
ingested: Mutex::new(Vec::new()),
ingested_notify: Notify::new(),
}
}
pub fn ingested(&self) -> Vec<EpisodeIngest> {
self.ingested
.lock()
.expect("mock ingest lock poisoned")
.clone()
}
pub async fn wait_for_ingests(&self, n: usize) {
loop {
let notified = self.ingested_notify.notified();
if self
.ingested
.lock()
.expect("mock ingest lock poisoned")
.len()
>= n
{
return;
}
notified.await;
}
}
}
#[async_trait::async_trait]
impl LongTermMemory for MockLongTermMemory {
async fn ingest_episode(
&self,
_tenant: &TenantCtx,
episode: EpisodeIngest,
) -> Result<IngestOutcome, LongTermMemoryError> {
let id = format!("ep-{}", episode.name);
self.ingested
.lock()
.expect("mock ingest lock poisoned")
.push(episode);
self.ingested_notify.notify_waiters();
Ok(IngestOutcome {
episode_id: id,
fact_count: 0,
entity_count: 0,
})
}
async fn recall(
&self,
_tenant: &TenantCtx,
_query: RecallQuery,
) -> Result<Vec<RecalledFact>, LongTermMemoryError> {
Ok(self.scripted_facts.clone())
}
}
pub struct MockKnowledge {
scripted_chunks: Vec<RetrievedChunk>,
ingested: Mutex<Vec<KnowledgeChunk>>,
}
impl MockKnowledge {
pub fn new(scripted_chunks: Vec<RetrievedChunk>) -> Self {
Self {
scripted_chunks,
ingested: Mutex::new(Vec::new()),
}
}
pub fn ingested(&self) -> Vec<KnowledgeChunk> {
self.ingested
.lock()
.expect("mock knowledge ingest lock poisoned")
.clone()
}
}
#[async_trait::async_trait]
impl Knowledge for MockKnowledge {
async fn ingest(
&self,
_tenant: &TenantCtx,
chunks: Vec<KnowledgeChunk>,
) -> KnowledgeResult<KnowledgeIngestOutcome> {
let ids = chunks
.iter()
.map(|c| format!("{}#{}", c.doc_id, c.chunk_index))
.collect();
self.ingested
.lock()
.expect("mock knowledge ingest lock poisoned")
.extend(chunks);
Ok(KnowledgeIngestOutcome { chunk_ids: ids })
}
async fn search(
&self,
_tenant: &TenantCtx,
query: KnowledgeQuery,
) -> KnowledgeResult<Vec<RetrievedChunk>> {
let mut hits = self.scripted_chunks.clone();
if let Some(limit) = query.limit {
hits.truncate(limit);
}
Ok(hits)
}
}
pub struct MockAgentStateStore {
entries: Mutex<HashMap<String, ConversationState>>,
}
impl MockAgentStateStore {
pub fn new() -> Self {
Self {
entries: Mutex::new(HashMap::new()),
}
}
fn build_key(tenant: &TenantContext, session_id: &str) -> String {
format!("{}:{}", tenant.key_prefix(), session_id)
}
}
impl Default for MockAgentStateStore {
fn default() -> Self {
Self::new()
}
}
impl AgentStateStore for MockAgentStateStore {
fn load<'a>(
&'a self,
tenant: &'a TenantContext,
session_id: &'a str,
) -> Pin<Box<dyn Future<Output = Result<ConversationState, StateError>> + Send + 'a>>
{
let key = Self::build_key(tenant, session_id);
let state = self
.entries
.lock()
.expect("mock state mutex poisoned")
.get(&key)
.cloned()
.unwrap_or_else(|| ConversationState::empty(tenant, session_id));
Box::pin(async move { Ok(state) })
}
fn save<'a>(
&'a self,
tenant: &'a TenantContext,
session_id: &'a str,
state: &'a ConversationState,
) -> Pin<Box<dyn Future<Output = Result<(), StateError>> + Send + 'a>> {
let key = Self::build_key(tenant, session_id);
let cloned = state.clone();
Box::pin(async move {
self.entries
.lock()
.expect("mock state mutex poisoned")
.insert(key, cloned);
Ok(())
})
}
fn acquire_lock<'a>(
&'a self,
_tenant: &'a TenantContext,
_session_id: &'a str,
_wait: Duration,
) -> Pin<Box<dyn Future<Output = Result<SessionLock, StateError>> + Send + 'a>> {
Box::pin(async move { Ok(SessionLock::new(Box::new(NoopLockInner))) })
}
}
struct NoopLockInner;
impl SessionLockInner for NoopLockInner {
fn refresh<'a>(
&'a self,
) -> Pin<Box<dyn Future<Output = Result<(), StateError>> + Send + 'a>> {
Box::pin(async { Ok(()) })
}
fn release(&self) {}
}
pub struct MockTelemetry {
pub recorded: Mutex<Vec<StepTelemetryCtx>>,
}
impl MockTelemetry {
pub fn new() -> Self {
Self {
recorded: Mutex::new(Vec::new()),
}
}
}
impl Default for MockTelemetry {
fn default() -> Self {
Self::new()
}
}
impl Telemetry for MockTelemetry {
fn record_step(&self, ctx: &StepTelemetryCtx) {
self.recorded
.lock()
.expect("mock telemetry mutex poisoned")
.push(ctx.clone());
}
}
pub struct MockConfigProvider {
pub configs: Mutex<HashMap<String, AgentConfig>>,
}
impl MockConfigProvider {
pub fn new() -> Self {
Self {
configs: Mutex::new(HashMap::new()),
}
}
pub fn insert(&self, tenant: &TenantContext, agent_id: &str, cfg: AgentConfig) {
self.configs
.lock()
.expect("mock config mutex poisoned")
.insert(format!("{}:{agent_id}", tenant.key_prefix()), cfg);
}
}
impl Default for MockConfigProvider {
fn default() -> Self {
Self::new()
}
}
impl ConfigProvider for MockConfigProvider {
fn agent_config<'a>(
&'a self,
tenant: &'a TenantContext,
agent_id: &'a str,
) -> Pin<Box<dyn Future<Output = Result<AgentConfig, ConfigError>> + Send + 'a>> {
let key = format!("{}:{agent_id}", tenant.key_prefix());
let agent_id_owned = agent_id.to_string();
let entry = self
.configs
.lock()
.expect("mock config mutex poisoned")
.get(&key)
.cloned();
Box::pin(async move { entry.ok_or(ConfigError::AgentNotFound(agent_id_owned)) })
}
}
pub struct NoopToolLedger;
impl crate::tools::ToolLedger for NoopToolLedger {
fn get<'a>(
&'a self,
_tenant: &'a TenantContext,
_session_id: &'a str,
_call_id: &'a str,
) -> Pin<Box<dyn Future<Output = Result<Option<serde_json::Value>, StateError>> + Send + 'a>>
{
Box::pin(async { Ok(None) })
}
fn record<'a>(
&'a self,
_tenant: &'a TenantContext,
_session_id: &'a str,
_call_id: &'a str,
_result: serde_json::Value,
) -> Pin<Box<dyn Future<Output = Result<(), StateError>> + Send + 'a>> {
Box::pin(async { Ok(()) })
}
}
pub fn assert_terminated_by(actual: &TerminationReason, expected: &TerminationReason) {
assert_eq!(actual, expected, "expected {expected:?}, got {actual:?}");
}
}
pub use inner::{
MockAgentStateStore, MockConfigProvider, MockKnowledge, MockLlmBackend, MockLongTermMemory,
MockTelemetry, NoopToolLedger, assert_terminated_by,
};