use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use serde_json::{Value, json};
use eli::framework::EliFramework;
use eli::hooks::{EliHookSpec, HookError};
use eli::types::{Envelope, PromptValue, State};
struct MockModelPlugin {
canned_response: String,
}
#[async_trait]
impl EliHookSpec for MockModelPlugin {
fn plugin_name(&self) -> &str {
"mock-model"
}
async fn run_model(
&self,
prompt: &PromptValue,
_session_id: &str,
_state: &State,
) -> Result<Option<String>, HookError> {
Ok(Some(format!(
"{}: {}",
self.canned_response,
prompt.as_text()
)))
}
}
struct CapturePlugin {
captured: Arc<Mutex<Vec<Value>>>,
}
#[async_trait]
impl EliHookSpec for CapturePlugin {
fn plugin_name(&self) -> &str {
"capture"
}
async fn dispatch_outbound(&self, message: &Envelope) -> Option<bool> {
self.captured.lock().unwrap().push(message.clone());
Some(true)
}
}
struct MockSessionPlugin;
#[async_trait]
impl EliHookSpec for MockSessionPlugin {
fn plugin_name(&self) -> &str {
"mock-session"
}
async fn resolve_session(&self, message: &Envelope) -> Result<Option<String>, HookError> {
Ok(message
.as_object()
.and_then(|o| o.get("session_id"))
.and_then(|v| v.as_str())
.map(|s| s.to_owned()))
}
}
struct MockStatePlugin;
#[async_trait]
impl EliHookSpec for MockStatePlugin {
fn plugin_name(&self) -> &str {
"mock-state"
}
async fn load_state(
&self,
_message: &Envelope,
_session_id: &str,
) -> Result<Option<State>, HookError> {
let mut s = State::new();
s.insert("test_key".into(), Value::String("test_value".into()));
Ok(Some(s))
}
}
struct StateEchoModelPlugin;
#[async_trait]
impl EliHookSpec for StateEchoModelPlugin {
fn plugin_name(&self) -> &str {
"state-echo-model"
}
fn build_system_prompt(&self, _prompt_text: &str, _state: &State) -> Option<String> {
Some("system-prompt-from-hook".into())
}
async fn run_model(
&self,
_prompt: &PromptValue,
_session_id: &str,
state: &State,
) -> Result<Option<String>, HookError> {
let sys = state
.get("_runtime_system_prompt")
.and_then(|v| v.as_str())
.unwrap_or("no-system-prompt");
let test_key = state
.get("test_key")
.and_then(|v| v.as_str())
.unwrap_or("no-test-key");
Ok(Some(format!("sys={sys}|key={test_key}")))
}
}
#[tokio::test]
async fn test_full_pipeline_inbound_to_outbound() {
let captured = Arc::new(Mutex::new(Vec::new()));
let fw = EliFramework::with_workspace("/tmp/test-pipeline".into());
fw.register_plugin(Arc::new(MockSessionPlugin) as Arc<dyn EliHookSpec>)
.await;
fw.register_plugin(Arc::new(MockModelPlugin {
canned_response: "REPLY".into(),
}) as Arc<dyn EliHookSpec>)
.await;
fw.register_plugin(Arc::new(CapturePlugin {
captured: captured.clone(),
}) as Arc<dyn EliHookSpec>)
.await;
let inbound = json!({
"content": "hello world",
"channel": "test",
"chat_id": "42",
"session_id": "sess-001",
});
let result = fw.process_inbound(inbound).await.unwrap();
assert_eq!(result.session_id, "sess-001");
assert!(
result.model_output.contains("REPLY"),
"expected model output to contain 'REPLY', got: {}",
result.model_output
);
assert!(
result.model_output.contains("hello world"),
"expected model output to contain original prompt, got: {}",
result.model_output
);
assert!(
!result.outbounds.is_empty(),
"expected at least one outbound envelope"
);
let dispatched = captured.lock().unwrap();
assert!(
!dispatched.is_empty(),
"expected capture plugin to receive dispatched outbounds"
);
let first = &dispatched[0];
let content = first.get("content").and_then(|v| v.as_str()).unwrap_or("");
assert!(
content.contains("REPLY"),
"dispatched outbound should contain model response, got: {content}"
);
}
#[tokio::test]
async fn test_pipeline_default_session_id() {
let fw = EliFramework::with_workspace("/tmp/test-pipeline-default".into());
fw.register_plugin(Arc::new(MockModelPlugin {
canned_response: "OK".into(),
}) as Arc<dyn EliHookSpec>)
.await;
let inbound = json!({
"content": "ping",
"channel": "cli",
"chat_id": "99",
});
let result = fw.process_inbound(inbound).await.unwrap();
assert_eq!(result.session_id, "cli:99");
assert!(result.model_output.contains("OK"));
}
#[tokio::test]
async fn test_pipeline_state_flows_to_model() {
let fw = EliFramework::with_workspace("/tmp/test-pipeline-state".into());
fw.register_plugin(Arc::new(MockStatePlugin) as Arc<dyn EliHookSpec>)
.await;
fw.register_plugin(Arc::new(StateEchoModelPlugin) as Arc<dyn EliHookSpec>)
.await;
let inbound = json!({
"content": "test",
"session_id": "state-test",
});
let result = fw.process_inbound(inbound).await.unwrap();
assert!(
result.model_output.contains("sys=system-prompt-from-hook"),
"expected system prompt in model output, got: {}",
result.model_output
);
assert!(
result.model_output.contains("key=test_value"),
"expected test_key in model output, got: {}",
result.model_output
);
}
#[tokio::test]
async fn test_pipeline_no_model_returns_error() {
let fw = EliFramework::with_workspace("/tmp/test-pipeline-no-model".into());
let inbound = json!({
"content": "hello",
"session_id": "no-model",
});
let result = fw.process_inbound(inbound).await.unwrap();
assert!(
result.model_output.contains("Error"),
"expected error indicator in model output, got: {}",
result.model_output
);
}
#[tokio::test]
async fn test_pipeline_independent_sessions() {
let fw = EliFramework::with_workspace("/tmp/test-pipeline-sessions".into());
fw.register_plugin(Arc::new(MockSessionPlugin) as Arc<dyn EliHookSpec>)
.await;
fw.register_plugin(Arc::new(MockModelPlugin {
canned_response: "ECHO".into(),
}) as Arc<dyn EliHookSpec>)
.await;
let msg_a = json!({
"content": "alpha",
"session_id": "sess-a",
"channel": "test",
"chat_id": "1",
});
let msg_b = json!({
"content": "beta",
"session_id": "sess-b",
"channel": "test",
"chat_id": "2",
});
let result_a = fw.process_inbound(msg_a).await.unwrap();
let result_b = fw.process_inbound(msg_b).await.unwrap();
assert_eq!(result_a.session_id, "sess-a");
assert_eq!(result_b.session_id, "sess-b");
assert!(result_a.model_output.contains("alpha"));
assert!(result_b.model_output.contains("beta"));
}