#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::needless_borrows_for_generic_args,
missing_docs
)]
use honcho_ai::Honcho;
use honcho_ai::session::Session;
use honcho_ai::types::session::SessionContextOptions;
use serde_json::{Value, json};
use wiremock::matchers::{body_json, method, path, query_param};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn workspace_response_json() -> Value {
json!({
"id": "ws1",
"metadata": {},
"configuration": {},
"created_at": "2025-01-15T10:30:00Z"
})
}
fn session_response_json() -> Value {
json!({
"id": "sess1",
"workspace_id": "ws1",
"is_active": true,
"metadata": {},
"configuration": {},
"created_at": "2025-01-15T10:30:00Z"
})
}
async fn make_session(server: &MockServer) -> Session {
Mock::given(method("POST"))
.and(path("/v3/workspaces"))
.and(body_json(json!({"id": "ws1"})))
.respond_with(ResponseTemplate::new(200).set_body_json(workspace_response_json()))
.up_to_n_times(1)
.mount(server)
.await;
Mock::given(method("POST"))
.and(path("/v3/workspaces/ws1/sessions"))
.and(body_json(json!({"id": "sess1"})))
.respond_with(ResponseTemplate::new(200).set_body_json(session_response_json()))
.up_to_n_times(1)
.mount(server)
.await;
let honcho = Honcho::new(&server.uri(), "ws1").unwrap();
honcho.session("sess1", None, None, None).await.unwrap()
}
fn context_response_json() -> Value {
json!({
"id": "sess1",
"messages": [
{
"id": "m1",
"content": "hello",
"peer_id": "user1",
"session_id": "sess1",
"metadata": {},
"created_at": "2025-01-15T10:30:00Z",
"workspace_id": "ws1",
"token_count": 1
}
],
"summary": {
"content": "a summary",
"message_id": "msg0",
"summary_type": "short",
"created_at": "2025-01-15T10:30:00Z",
"token_count": 5
},
"peer_representation": "some rep",
"peer_card": ["fact1"]
})
}
#[tokio::test]
async fn session_context_returns_session_context() {
let server = MockServer::start().await;
let session = make_session(&server).await;
Mock::given(method("GET"))
.and(path("/v3/workspaces/ws1/sessions/sess1/context"))
.and(query_param("summary", "true"))
.and(query_param("limit_to_session", "false"))
.respond_with(ResponseTemplate::new(200).set_body_json(context_response_json()))
.mount(&server)
.await;
let ctx = session.context().await.unwrap();
assert_eq!(ctx.id, "sess1");
assert_eq!(ctx.messages.len(), 1);
assert_eq!(ctx.messages[0].content, "hello");
assert!(ctx.summary.is_some());
assert_eq!(ctx.summary.unwrap().content, "a summary");
assert_eq!(ctx.peer_representation, Some("some rep".to_string()));
assert_eq!(ctx.peer_card, Some(vec!["fact1".to_string()]));
}
#[tokio::test]
async fn session_summaries_returns_both() {
let server = MockServer::start().await;
let session = make_session(&server).await;
Mock::given(method("GET"))
.and(path("/v3/workspaces/ws1/sessions/sess1/summaries"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"id": "sess1",
"short_summary": {
"content": "short one",
"message_id": "msg1",
"summary_type": "short",
"created_at": "2025-01-15T10:30:00Z",
"token_count": 3
},
"long_summary": {
"content": "long one",
"message_id": "msg2",
"summary_type": "long",
"created_at": "2025-01-15T10:30:00Z",
"token_count": 10
}
})))
.mount(&server)
.await;
let summaries = session.summaries().await.unwrap();
assert_eq!(summaries.id, "sess1");
assert!(summaries.short_summary.is_some());
assert_eq!(summaries.short_summary.unwrap().content, "short one");
assert!(summaries.long_summary.is_some());
assert_eq!(summaries.long_summary.unwrap().content, "long one");
}
#[tokio::test]
async fn session_summaries_none_when_not_available() {
let server = MockServer::start().await;
let session = make_session(&server).await;
Mock::given(method("GET"))
.and(path("/v3/workspaces/ws1/sessions/sess1/summaries"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"id": "sess1"
})))
.mount(&server)
.await;
let summaries = session.summaries().await.unwrap();
assert_eq!(summaries.id, "sess1");
assert!(summaries.short_summary.is_none());
assert!(summaries.long_summary.is_none());
}
#[tokio::test]
async fn session_search_returns_messages() {
let server = MockServer::start().await;
let session = make_session(&server).await;
Mock::given(method("POST"))
.and(path("/v3/workspaces/ws1/sessions/sess1/search"))
.and(body_json(json!({
"query": "hello",
"limit": 10
})))
.respond_with(ResponseTemplate::new(200).set_body_json(json!([
{
"id": "m1",
"content": "hello world",
"peer_id": "user1",
"session_id": "sess1",
"metadata": {},
"created_at": "2025-01-15T10:30:00Z",
"workspace_id": "ws1",
"token_count": 2
}
])))
.mount(&server)
.await;
let results = session.search("hello").await.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].content(), "hello world");
}
#[tokio::test]
async fn session_search_validates_empty_query() {
let server = MockServer::start().await;
let session = make_session(&server).await;
let err = session.search("").await.unwrap_err();
assert_eq!(err.code(), "validation_error");
}
#[tokio::test]
async fn session_representation_posts_to_peer_representation() {
let server = MockServer::start().await;
let session = make_session(&server).await;
Mock::given(method("POST"))
.and(path("/v3/workspaces/ws1/peers/alice/representation"))
.and(body_json(json!({"session_id": "sess1"})))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"representation": "Alice likes Rust"
})))
.expect(1)
.mount(&server)
.await;
let rep = session.representation("alice").await.unwrap();
assert_eq!(rep, "Alice likes Rust");
}
#[tokio::test]
async fn session_queue_status_gets_with_session_id() {
let server = MockServer::start().await;
let session = make_session(&server).await;
Mock::given(method("GET"))
.and(path("/v3/workspaces/ws1/queue/status"))
.and(query_param("session_id", "sess1"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"total_work_units": 5,
"completed_work_units": 3,
"in_progress_work_units": 1,
"pending_work_units": 1
})))
.expect(1)
.mount(&server)
.await;
let status = session.queue_status(None, None).await.unwrap();
assert_eq!(status.total_work_units, 5);
assert_eq!(status.completed_work_units, 3);
assert_eq!(status.in_progress_work_units, 1);
assert_eq!(status.pending_work_units, 1);
}
#[tokio::test]
async fn session_context_with_options_sends_all_query_params() {
let server = MockServer::start().await;
let session = make_session(&server).await;
Mock::given(method("GET"))
.and(path("/v3/workspaces/ws1/sessions/sess1/context"))
.and(query_param("summary", "false"))
.and(query_param("limit_to_session", "true"))
.and(query_param("tokens", "4096"))
.and(query_param("peer_target", "bob"))
.and(query_param("peer_perspective", "alice"))
.and(query_param("search_query", "preferences"))
.and(query_param("search_top_k", "10"))
.and(query_param("search_max_distance", "0.5"))
.and(query_param("include_most_frequent", "true"))
.and(query_param("max_conclusions", "20"))
.respond_with(ResponseTemplate::new(200).set_body_json(context_response_json()))
.expect(1)
.mount(&server)
.await;
let opts = SessionContextOptions::builder()
.summary(false)
.limit_to_session(true)
.tokens(4096)
.peer_target("bob")
.peer_perspective("alice")
.search_query("preferences")
.search_top_k(10)
.search_max_distance(0.5)
.include_most_frequent(true)
.max_conclusions(20)
.build();
let ctx = session.context_with_options(&opts).await.unwrap();
assert_eq!(ctx.id, "sess1");
}
#[tokio::test]
async fn session_context_with_options_sends_only_set_params() {
let server = MockServer::start().await;
let session = make_session(&server).await;
Mock::given(method("GET"))
.and(path("/v3/workspaces/ws1/sessions/sess1/context"))
.and(query_param("summary", "true"))
.and(query_param("limit_to_session", "false"))
.respond_with(ResponseTemplate::new(200).set_body_json(context_response_json()))
.expect(1)
.mount(&server)
.await;
let opts = SessionContextOptions::builder()
.summary(true)
.limit_to_session(false)
.build();
let ctx = session.context_with_options(&opts).await.unwrap();
assert_eq!(ctx.id, "sess1");
}
#[test]
fn session_context_options_peer_perspective_requires_peer_target() {
let opts = SessionContextOptions::builder()
.peer_perspective("alice")
.build();
let err = opts.validate().unwrap_err();
assert_eq!(err.code(), "validation_error");
}
#[test]
fn session_context_options_both_set_succeeds() {
let opts = SessionContextOptions::builder()
.peer_perspective("alice")
.peer_target("bob")
.build();
assert_eq!(opts.peer_perspective.as_deref(), Some("alice"));
assert_eq!(opts.peer_target.as_deref(), Some("bob"));
}
#[test]
fn session_context_options_no_perspective_no_target_succeeds() {
let opts = SessionContextOptions::builder().build();
assert!(opts.peer_perspective.is_none());
assert!(opts.peer_target.is_none());
}