use std::sync::Arc;
use synaptic_core::{MemoryStore, Message, RunnableConfig, SynapticError};
use synaptic_memory::{ChatMessageHistory, RunnableWithMessageHistory};
use synaptic_runnables::Runnable;
struct EchoRunnable;
#[async_trait::async_trait]
impl Runnable<Vec<Message>, String> for EchoRunnable {
async fn invoke(
&self,
input: Vec<Message>,
_config: &RunnableConfig,
) -> Result<String, SynapticError> {
let last_human = input
.iter()
.rev()
.find(|m| m.is_human())
.map(|m| format!("Echo: {}", m.content()))
.unwrap_or_else(|| "No human message".to_string());
Ok(last_human)
}
}
#[tokio::test]
async fn runnable_with_message_history_loads_and_saves() {
let store = Arc::new(ChatMessageHistory::new(Arc::new(
synaptic_store::InMemoryStore::new(),
)));
let inner = EchoRunnable.boxed();
let history = RunnableWithMessageHistory::new(inner, store.clone());
let config = RunnableConfig::default().with_metadata(
"session_id",
serde_json::Value::String("test-session".to_string()),
);
let output = history.invoke("hello".to_string(), &config).await.unwrap();
assert_eq!(output, "Echo: hello");
let messages = store.load("test-session").await.unwrap();
assert_eq!(messages.len(), 2);
assert!(messages[0].is_human());
assert_eq!(messages[0].content(), "hello");
assert!(messages[1].is_ai());
assert_eq!(messages[1].content(), "Echo: hello");
let output2 = history.invoke("world".to_string(), &config).await.unwrap();
assert_eq!(output2, "Echo: world");
let messages = store.load("test-session").await.unwrap();
assert_eq!(messages.len(), 4);
assert_eq!(messages[2].content(), "world");
assert_eq!(messages[3].content(), "Echo: world");
}
#[tokio::test]
async fn session_isolation() {
let store = Arc::new(ChatMessageHistory::new(Arc::new(
synaptic_store::InMemoryStore::new(),
)));
let inner = EchoRunnable.boxed();
let history = RunnableWithMessageHistory::new(inner, store.clone());
let config_a = RunnableConfig::default().with_metadata(
"session_id",
serde_json::Value::String("session-a".to_string()),
);
let config_b = RunnableConfig::default().with_metadata(
"session_id",
serde_json::Value::String("session-b".to_string()),
);
history
.invoke("hello-a".to_string(), &config_a)
.await
.unwrap();
history
.invoke("hello-b".to_string(), &config_b)
.await
.unwrap();
let msgs_a = store.load("session-a").await.unwrap();
let msgs_b = store.load("session-b").await.unwrap();
assert_eq!(msgs_a.len(), 2); assert_eq!(msgs_b.len(), 2);
assert_eq!(msgs_a[0].content(), "hello-a");
assert_eq!(msgs_b[0].content(), "hello-b");
}
#[tokio::test]
async fn message_roles_preserved() {
let store = Arc::new(ChatMessageHistory::new(Arc::new(
synaptic_store::InMemoryStore::new(),
)));
let inner = EchoRunnable.boxed();
let history = RunnableWithMessageHistory::new(inner, store.clone());
let config = RunnableConfig::default().with_metadata(
"session_id",
serde_json::Value::String("roles-test".to_string()),
);
history.invoke("test".to_string(), &config).await.unwrap();
let msgs = store.load("roles-test").await.unwrap();
assert!(msgs[0].is_human());
assert!(msgs[1].is_ai());
}
#[tokio::test]
async fn system_message_in_history() {
let store = Arc::new(ChatMessageHistory::new(Arc::new(
synaptic_store::InMemoryStore::new(),
)));
store
.append("sys-test", Message::system("You are helpful"))
.await
.unwrap();
let inner = EchoRunnable.boxed();
let history = RunnableWithMessageHistory::new(inner, store.clone());
let config = RunnableConfig::default().with_metadata(
"session_id",
serde_json::Value::String("sys-test".to_string()),
);
history.invoke("hi".to_string(), &config).await.unwrap();
let msgs = store.load("sys-test").await.unwrap();
assert_eq!(msgs.len(), 3); assert!(msgs[0].is_system());
assert!(msgs[1].is_human());
assert!(msgs[2].is_ai());
}