Move MCP session tests out of source
This commit is contained in:
@@ -331,220 +331,3 @@ fn is_expired(session: &SessionState, now: OffsetDateTime) -> bool {
|
|||||||
.expires_at
|
.expires_at
|
||||||
.is_some_and(|expires_at| expires_at <= now)
|
.is_some_and(|expires_at| expires_at <= now)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use std::env;
|
|
||||||
|
|
||||||
use sqlx::{
|
|
||||||
Executor,
|
|
||||||
postgres::{PgConnectOptions, PgPoolOptions},
|
|
||||||
};
|
|
||||||
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use super::{
|
|
||||||
InMemorySessionStore, PostgresTransportSessionStore, SessionStoreError,
|
|
||||||
TransportSessionStore,
|
|
||||||
};
|
|
||||||
use crank_registry::PostgresPoolConfig;
|
|
||||||
|
|
||||||
fn timestamp(value: &str) -> time::OffsetDateTime {
|
|
||||||
time::OffsetDateTime::parse(value, &Rfc3339).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn truncate_to_micros(value: OffsetDateTime) -> OffsetDateTime {
|
|
||||||
value
|
|
||||||
.replace_nanosecond((value.nanosecond() / 1_000) * 1_000)
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn creates_and_reads_transport_sessions() {
|
|
||||||
let store = InMemorySessionStore::default();
|
|
||||||
let created_at = timestamp("2026-05-01T10:00:00Z");
|
|
||||||
|
|
||||||
let session_id = store
|
|
||||||
.create("2025-11-25", "default", "sales", created_at, None)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let session = store.get(&session_id).await.unwrap().unwrap();
|
|
||||||
|
|
||||||
assert_eq!(session.id, session_id);
|
|
||||||
assert_eq!(session.protocol_version, "2025-11-25");
|
|
||||||
assert_eq!(session.workspace_slug, "default");
|
|
||||||
assert_eq!(session.agent_slug, "sales");
|
|
||||||
assert!(!session.initialized);
|
|
||||||
assert_eq!(session.created_at, created_at);
|
|
||||||
assert_eq!(session.updated_at, created_at);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn marks_transport_sessions_initialized() {
|
|
||||||
let store = InMemorySessionStore::default();
|
|
||||||
let created_at = timestamp("2026-05-01T10:00:00Z");
|
|
||||||
let initialized_at = timestamp("2026-05-01T10:00:05Z");
|
|
||||||
|
|
||||||
let session_id = store
|
|
||||||
.create("2025-11-25", "default", "sales", created_at, None)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
store
|
|
||||||
.mark_initialized(&session_id, initialized_at)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
let session = store.get(&session_id).await.unwrap().unwrap();
|
|
||||||
assert!(session.initialized);
|
|
||||||
assert_eq!(session.updated_at, initialized_at);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn drops_expired_in_memory_transport_sessions_on_read() {
|
|
||||||
let store = InMemorySessionStore::default();
|
|
||||||
let created_at = timestamp("2026-05-01T10:00:00Z");
|
|
||||||
let expires_at = timestamp("2026-05-01T10:00:01Z");
|
|
||||||
|
|
||||||
let session_id = store
|
|
||||||
.create(
|
|
||||||
"2025-11-25",
|
|
||||||
"default",
|
|
||||||
"sales",
|
|
||||||
created_at,
|
|
||||||
Some(expires_at),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(store.get(&session_id).await.unwrap().is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn formats_transport_session_store_error() {
|
|
||||||
let error = SessionStoreError {
|
|
||||||
details: "postgres is unavailable".to_owned(),
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
error.to_string(),
|
|
||||||
"transport session store is unavailable: postgres is unavailable"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn postgres_transport_sessions_survive_store_reconnect() {
|
|
||||||
let database_url = env::var("TEST_DATABASE_URL")
|
|
||||||
.unwrap_or_else(|_| "postgres://crank:crank@127.0.0.1:15432/crank".to_owned());
|
|
||||||
let admin_pool = PgPoolOptions::new()
|
|
||||||
.max_connections(1)
|
|
||||||
.connect(&database_url)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let schema = format!("test_mcp_transport_{}", Uuid::now_v7().simple());
|
|
||||||
|
|
||||||
admin_pool
|
|
||||||
.execute(sqlx::query(&format!("create schema {schema}")))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let connect_options = format!("{database_url}?options=-csearch_path%3D{schema}")
|
|
||||||
.parse::<PgConnectOptions>()
|
|
||||||
.unwrap();
|
|
||||||
let pool_config = PostgresPoolConfig::default();
|
|
||||||
let store_a = PostgresTransportSessionStore::connect_with_options_and_pool_config(
|
|
||||||
connect_options.clone(),
|
|
||||||
pool_config,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let created_at = OffsetDateTime::now_utc() - time::Duration::hours(1);
|
|
||||||
let initialized_at = created_at + time::Duration::seconds(5);
|
|
||||||
let session_id = store_a
|
|
||||||
.create(
|
|
||||||
"2025-11-25",
|
|
||||||
"default",
|
|
||||||
"sales",
|
|
||||||
created_at,
|
|
||||||
Some(created_at + time::Duration::days(30)),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
store_a
|
|
||||||
.mark_initialized(&session_id, initialized_at)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let store_b = PostgresTransportSessionStore::connect_with_options_and_pool_config(
|
|
||||||
connect_options,
|
|
||||||
pool_config,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let session = store_b.get(&session_id).await.unwrap().unwrap();
|
|
||||||
|
|
||||||
assert_eq!(session.id, session_id);
|
|
||||||
assert!(session.initialized);
|
|
||||||
assert_eq!(session.updated_at, truncate_to_micros(initialized_at));
|
|
||||||
assert_eq!(session.workspace_slug, "default");
|
|
||||||
assert_eq!(session.agent_slug, "sales");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn postgres_transport_sessions_evict_expired_rows_on_read() {
|
|
||||||
let database_url = env::var("TEST_DATABASE_URL")
|
|
||||||
.unwrap_or_else(|_| "postgres://crank:crank@127.0.0.1:15432/crank".to_owned());
|
|
||||||
let admin_pool = PgPoolOptions::new()
|
|
||||||
.max_connections(1)
|
|
||||||
.connect(&database_url)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let schema = format!("test_mcp_transport_{}", Uuid::now_v7().simple());
|
|
||||||
|
|
||||||
admin_pool
|
|
||||||
.execute(sqlx::query(&format!("create schema {schema}")))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let connect_options = format!("{database_url}?options=-csearch_path%3D{schema}")
|
|
||||||
.parse::<PgConnectOptions>()
|
|
||||||
.unwrap();
|
|
||||||
let store = PostgresTransportSessionStore::connect_with_options_and_pool_config(
|
|
||||||
connect_options.clone(),
|
|
||||||
PostgresPoolConfig::default(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let session_id = store
|
|
||||||
.create(
|
|
||||||
"2025-11-25",
|
|
||||||
"default",
|
|
||||||
"sales",
|
|
||||||
timestamp("2026-05-01T10:00:00Z"),
|
|
||||||
Some(timestamp("2026-05-01T10:00:01Z")),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(store.get(&session_id).await.unwrap().is_none());
|
|
||||||
|
|
||||||
let pool = PgPoolOptions::new()
|
|
||||||
.max_connections(1)
|
|
||||||
.connect_with(connect_options)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let remaining = sqlx::query_scalar::<_, i64>(
|
|
||||||
"select count(*) from mcp_transport_sessions where id = $1",
|
|
||||||
)
|
|
||||||
.bind(&session_id)
|
|
||||||
.fetch_one(&pool)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(remaining, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#[path = "integration/session.rs"]
|
||||||
|
mod session;
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
use std::env;
|
||||||
|
|
||||||
|
use crank_community_mcp::session::{PostgresTransportSessionStore, TransportSessionStore};
|
||||||
|
use crank_registry::PostgresPoolConfig;
|
||||||
|
use sqlx::{
|
||||||
|
Executor,
|
||||||
|
postgres::{PgConnectOptions, PgPoolOptions},
|
||||||
|
};
|
||||||
|
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
fn timestamp(value: &str) -> time::OffsetDateTime {
|
||||||
|
time::OffsetDateTime::parse(value, &Rfc3339).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn truncate_to_micros(value: OffsetDateTime) -> OffsetDateTime {
|
||||||
|
value
|
||||||
|
.replace_nanosecond((value.nanosecond() / 1_000) * 1_000)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn postgres_transport_sessions_survive_store_reconnect() {
|
||||||
|
let database_url = env::var("TEST_DATABASE_URL")
|
||||||
|
.unwrap_or_else(|_| "postgres://crank:crank@127.0.0.1:15432/crank".to_owned());
|
||||||
|
let admin_pool = PgPoolOptions::new()
|
||||||
|
.max_connections(1)
|
||||||
|
.connect(&database_url)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let schema = format!("test_mcp_transport_{}", Uuid::now_v7().simple());
|
||||||
|
|
||||||
|
admin_pool
|
||||||
|
.execute(sqlx::query(&format!("create schema {schema}")))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let connect_options = format!("{database_url}?options=-csearch_path%3D{schema}")
|
||||||
|
.parse::<PgConnectOptions>()
|
||||||
|
.unwrap();
|
||||||
|
let pool_config = PostgresPoolConfig::default();
|
||||||
|
let store_a = PostgresTransportSessionStore::connect_with_options_and_pool_config(
|
||||||
|
connect_options.clone(),
|
||||||
|
pool_config,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let created_at = OffsetDateTime::now_utc() - time::Duration::hours(1);
|
||||||
|
let initialized_at = created_at + time::Duration::seconds(5);
|
||||||
|
let session_id = store_a
|
||||||
|
.create(
|
||||||
|
"2025-11-25",
|
||||||
|
"default",
|
||||||
|
"sales",
|
||||||
|
created_at,
|
||||||
|
Some(created_at + time::Duration::days(30)),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
store_a
|
||||||
|
.mark_initialized(&session_id, initialized_at)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let store_b = PostgresTransportSessionStore::connect_with_options_and_pool_config(
|
||||||
|
connect_options,
|
||||||
|
pool_config,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let session = store_b.get(&session_id).await.unwrap().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(session.id, session_id);
|
||||||
|
assert!(session.initialized);
|
||||||
|
assert_eq!(session.updated_at, truncate_to_micros(initialized_at));
|
||||||
|
assert_eq!(session.workspace_slug, "default");
|
||||||
|
assert_eq!(session.agent_slug, "sales");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn postgres_transport_sessions_evict_expired_rows_on_read() {
|
||||||
|
let database_url = env::var("TEST_DATABASE_URL")
|
||||||
|
.unwrap_or_else(|_| "postgres://crank:crank@127.0.0.1:15432/crank".to_owned());
|
||||||
|
let admin_pool = PgPoolOptions::new()
|
||||||
|
.max_connections(1)
|
||||||
|
.connect(&database_url)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let schema = format!("test_mcp_transport_{}", Uuid::now_v7().simple());
|
||||||
|
|
||||||
|
admin_pool
|
||||||
|
.execute(sqlx::query(&format!("create schema {schema}")))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let connect_options = format!("{database_url}?options=-csearch_path%3D{schema}")
|
||||||
|
.parse::<PgConnectOptions>()
|
||||||
|
.unwrap();
|
||||||
|
let store = PostgresTransportSessionStore::connect_with_options_and_pool_config(
|
||||||
|
connect_options.clone(),
|
||||||
|
PostgresPoolConfig::default(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let session_id = store
|
||||||
|
.create(
|
||||||
|
"2025-11-25",
|
||||||
|
"default",
|
||||||
|
"sales",
|
||||||
|
timestamp("2026-05-01T10:00:00Z"),
|
||||||
|
Some(timestamp("2026-05-01T10:00:01Z")),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(store.get(&session_id).await.unwrap().is_none());
|
||||||
|
|
||||||
|
let pool = PgPoolOptions::new()
|
||||||
|
.max_connections(1)
|
||||||
|
.connect_with(connect_options)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let remaining =
|
||||||
|
sqlx::query_scalar::<_, i64>("select count(*) from mcp_transport_sessions where id = $1")
|
||||||
|
.bind(&session_id)
|
||||||
|
.fetch_one(&pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(remaining, 0);
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
#[path = "unit/manifest.rs"]
|
#[path = "unit/manifest.rs"]
|
||||||
mod manifest;
|
mod manifest;
|
||||||
|
#[path = "unit/session.rs"]
|
||||||
|
mod session;
|
||||||
#[path = "unit/tool_error.rs"]
|
#[path = "unit/tool_error.rs"]
|
||||||
mod tool_error;
|
mod tool_error;
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
use crank_community_mcp::session::{
|
||||||
|
InMemorySessionStore, SessionStoreError, TransportSessionStore,
|
||||||
|
};
|
||||||
|
use time::format_description::well_known::Rfc3339;
|
||||||
|
|
||||||
|
fn timestamp(value: &str) -> time::OffsetDateTime {
|
||||||
|
time::OffsetDateTime::parse(value, &Rfc3339).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn creates_and_reads_transport_sessions() {
|
||||||
|
let store = InMemorySessionStore::default();
|
||||||
|
let created_at = timestamp("2026-05-01T10:00:00Z");
|
||||||
|
|
||||||
|
let session_id = store
|
||||||
|
.create("2025-11-25", "default", "sales", created_at, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let session = store.get(&session_id).await.unwrap().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(session.id, session_id);
|
||||||
|
assert_eq!(session.protocol_version, "2025-11-25");
|
||||||
|
assert_eq!(session.workspace_slug, "default");
|
||||||
|
assert_eq!(session.agent_slug, "sales");
|
||||||
|
assert!(!session.initialized);
|
||||||
|
assert_eq!(session.created_at, created_at);
|
||||||
|
assert_eq!(session.updated_at, created_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn marks_transport_sessions_initialized() {
|
||||||
|
let store = InMemorySessionStore::default();
|
||||||
|
let created_at = timestamp("2026-05-01T10:00:00Z");
|
||||||
|
let initialized_at = timestamp("2026-05-01T10:00:05Z");
|
||||||
|
|
||||||
|
let session_id = store
|
||||||
|
.create("2025-11-25", "default", "sales", created_at, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
store
|
||||||
|
.mark_initialized(&session_id, initialized_at)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
let session = store.get(&session_id).await.unwrap().unwrap();
|
||||||
|
assert!(session.initialized);
|
||||||
|
assert_eq!(session.updated_at, initialized_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn drops_expired_in_memory_transport_sessions_on_read() {
|
||||||
|
let store = InMemorySessionStore::default();
|
||||||
|
let created_at = timestamp("2026-05-01T10:00:00Z");
|
||||||
|
let expires_at = timestamp("2026-05-01T10:00:01Z");
|
||||||
|
|
||||||
|
let session_id = store
|
||||||
|
.create(
|
||||||
|
"2025-11-25",
|
||||||
|
"default",
|
||||||
|
"sales",
|
||||||
|
created_at,
|
||||||
|
Some(expires_at),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(store.get(&session_id).await.unwrap().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn formats_transport_session_store_error() {
|
||||||
|
let error = SessionStoreError {
|
||||||
|
details: "postgres is unavailable".to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
error.to_string(),
|
||||||
|
"transport session store is unavailable: postgres is unavailable"
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user