use std::sync::Arc;
use khive_pack_kg::KgPack;
use khive_pack_session::SessionPack;
use khive_runtime::{
AllowAllGate, BackendId, KhiveRuntime, Namespace, RuntimeConfig, VerbRegistry,
VerbRegistryBuilder,
};
use serde_json::{json, Value};
use tempfile::TempDir;
fn file_rt(db_path: std::path::PathBuf) -> KhiveRuntime {
KhiveRuntime::new(RuntimeConfig {
db_path: Some(db_path),
default_namespace: Namespace::local(),
embedding_model: None,
additional_embedding_models: vec![],
gate: Arc::new(AllowAllGate),
packs: vec!["kg".to_string(), "session".to_string()],
backend_id: BackendId::main(),
brain_profile: None,
visible_namespaces: vec![],
allowed_outbound_namespaces: vec![],
actor_id: None,
})
.expect("file-backed runtime")
}
fn build_registry(rt: KhiveRuntime) -> VerbRegistry {
let mut builder = VerbRegistryBuilder::new();
builder.register(KgPack::new(rt.clone()));
builder.register(SessionPack::new(rt.clone()));
let registry = builder.build().expect("registry builds");
rt.install_edge_rules(registry.all_edge_rules());
registry
}
async fn store_session(registry: &VerbRegistry, content: &str) -> Value {
registry
.dispatch("session.store", json!({"content": content}))
.await
.expect("store ok")
}
#[tokio::test]
async fn pack_metadata_matches_trait_consts() {
let dir = TempDir::new().expect("tempdir");
let rt = file_rt(dir.path().join("meta.db"));
let registry = build_registry(rt);
let surface: Vec<&str> = registry.all_verbs().iter().map(|v| v.name).collect();
for verb in ["session.store", "session.list", "session.get"] {
assert!(
!surface.contains(&verb),
"{verb} must NOT be on the agent-facing verb surface (subhandler)"
);
assert!(
registry.is_subhandler_verb(verb),
"{verb} must be registered as an internal subhandler"
);
}
assert!(
registry.all_note_kinds().contains(&"session"),
"session not in note kinds"
);
}
#[tokio::test]
async fn store_returns_session_envelope() {
let dir = TempDir::new().expect("tempdir");
let rt = file_rt(dir.path().join("store.db"));
let registry = build_registry(rt);
let result = registry
.dispatch("session.store", json!({"content": "hello session"}))
.await
.expect("store ok");
assert_eq!(result["kind"], "session");
assert!(result["id"].as_str().is_some(), "id must be a string UUID");
assert_eq!(result["content"], "hello session");
assert!(
result["created_at"].as_str().is_some(),
"created_at present"
);
}
#[tokio::test]
async fn store_with_agent_id_stored_in_properties() {
let dir = TempDir::new().expect("tempdir");
let rt = file_rt(dir.path().join("store_agent.db"));
let registry = build_registry(rt);
let result = registry
.dispatch(
"session.store",
json!({"content": "agent session", "agent_id": "lambda:khive"}),
)
.await
.expect("store ok");
assert_eq!(result["agent_id"], "lambda:khive");
}
#[tokio::test]
async fn store_with_metadata_merged_into_properties() {
let dir = TempDir::new().expect("tempdir");
let rt = file_rt(dir.path().join("store_meta.db"));
let registry = build_registry(rt);
let result = registry
.dispatch(
"session.store",
json!({
"content": "metadata session",
"metadata": {"source": "test", "version": 2}
}),
)
.await
.expect("store ok");
assert_eq!(result["properties"]["source"], "test");
assert_eq!(result["properties"]["version"], 2);
}
#[tokio::test]
async fn store_explicit_agent_id_wins_over_metadata() {
let dir = TempDir::new().expect("tempdir");
let rt = file_rt(dir.path().join("store_win.db"));
let registry = build_registry(rt);
let result = registry
.dispatch(
"session.store",
json!({
"content": "priority test",
"agent_id": "explicit",
"metadata": {"agent_id": "from_metadata"}
}),
)
.await
.expect("store ok");
assert_eq!(result["agent_id"], "explicit", "explicit param must win");
}
#[tokio::test]
async fn store_empty_content_returns_error() {
let dir = TempDir::new().expect("tempdir");
let rt = file_rt(dir.path().join("store_empty.db"));
let registry = build_registry(rt);
let err = registry
.dispatch("session.store", json!({"content": ""}))
.await;
assert!(err.is_err(), "empty content must return error");
}
#[tokio::test]
async fn store_unknown_field_returns_error() {
let dir = TempDir::new().expect("tempdir");
let rt = file_rt(dir.path().join("store_unknown.db"));
let registry = build_registry(rt);
let err = registry
.dispatch(
"session.store",
json!({"content": "x", "unknown_field": true}),
)
.await;
assert!(err.is_err(), "unknown field must be rejected");
}
#[tokio::test]
async fn store_metadata_non_object_returns_error() {
let dir = TempDir::new().expect("tempdir");
let rt = file_rt(dir.path().join("store_meta_bad.db"));
let registry = build_registry(rt);
let err = registry
.dispatch(
"session.store",
json!({"content": "x", "metadata": "not-an-object"}),
)
.await;
assert!(err.is_err(), "non-object metadata must be rejected");
}
#[tokio::test]
async fn list_returns_all_sessions_newest_first() {
let dir = TempDir::new().expect("tempdir");
let rt = file_rt(dir.path().join("list.db"));
let registry = build_registry(rt);
store_session(®istry, "first").await;
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
store_session(®istry, "second").await;
let all = registry
.dispatch("session.list", json!({}))
.await
.expect("list ok");
let arr = all.as_array().expect("list returns array");
assert_eq!(arr.len(), 2);
assert!(
arr[0]["created_at"].as_str().unwrap() >= arr[1]["created_at"].as_str().unwrap(),
"list must be newest-first"
);
}
#[tokio::test]
async fn list_filter_by_agent_id() {
let dir = TempDir::new().expect("tempdir");
let rt = file_rt(dir.path().join("list_agent.db"));
let registry = build_registry(rt);
registry
.dispatch(
"session.store",
json!({"content": "alpha session", "agent_id": "alpha"}),
)
.await
.expect("store alpha");
registry
.dispatch(
"session.store",
json!({"content": "beta session", "agent_id": "beta"}),
)
.await
.expect("store beta");
let alpha_only = registry
.dispatch("session.list", json!({"agent_id": "alpha"}))
.await
.expect("list alpha");
let arr = alpha_only.as_array().expect("list returns array");
assert_eq!(arr.len(), 1);
assert_eq!(arr[0]["agent_id"], "alpha");
}
#[tokio::test]
async fn list_limit_respected() {
let dir = TempDir::new().expect("tempdir");
let rt = file_rt(dir.path().join("list_limit.db"));
let registry = build_registry(rt);
for i in 0..5 {
store_session(®istry, &format!("session {i}")).await;
}
let limited = registry
.dispatch("session.list", json!({"limit": 3}))
.await
.expect("list limited");
assert_eq!(limited.as_array().expect("array").len(), 3);
}
#[tokio::test]
async fn list_since_filter() {
let dir = TempDir::new().expect("tempdir");
let rt = file_rt(dir.path().join("list_since.db"));
let registry = build_registry(rt);
store_session(®istry, "before").await;
let future = "2099-01-01T00:00:00Z";
let filtered = registry
.dispatch("session.list", json!({"since": future}))
.await
.expect("list since ok");
assert_eq!(
filtered.as_array().expect("array").len(),
0,
"since=far-future must return empty"
);
let past = "2020-01-01T00:00:00Z";
let all = registry
.dispatch("session.list", json!({"since": past}))
.await
.expect("list since past ok");
assert_eq!(all.as_array().expect("array").len(), 1);
}
#[tokio::test]
async fn list_since_invalid_format_returns_error() {
let dir = TempDir::new().expect("tempdir");
let rt = file_rt(dir.path().join("list_since_bad.db"));
let registry = build_registry(rt);
let err = registry
.dispatch("session.list", json!({"since": "not-a-date"}))
.await;
assert!(err.is_err(), "invalid since must return error");
}
#[tokio::test]
async fn list_offset_pagination() {
let dir = TempDir::new().expect("tempdir");
let rt = file_rt(dir.path().join("list_offset.db"));
let registry = build_registry(rt);
for i in 0..5 {
store_session(®istry, &format!("session {i}")).await;
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
}
let all = registry
.dispatch("session.list", json!({"limit": 5}))
.await
.expect("full list ok");
let all_arr = all.as_array().expect("all array");
assert_eq!(all_arr.len(), 5, "expected 5 sessions");
let paged = registry
.dispatch("session.list", json!({"offset": 2, "limit": 2}))
.await
.expect("paged list ok");
let paged_arr = paged.as_array().expect("paged array");
assert_eq!(
paged_arr.len(),
2,
"expected exactly 2 items with offset=2 limit=2"
);
assert_eq!(
paged_arr[0]["id"], all_arr[2]["id"],
"offset=2 first item must match all[2]"
);
assert_eq!(
paged_arr[1]["id"], all_arr[3]["id"],
"offset=2 second item must match all[3]"
);
}
#[tokio::test]
async fn get_returns_full_record() {
let dir = TempDir::new().expect("tempdir");
let rt = file_rt(dir.path().join("resume.db"));
let registry = build_registry(rt);
let stored = store_session(®istry, "resumeable content").await;
let id = stored["id"].as_str().expect("id present").to_string();
let resumed = registry
.dispatch("session.get", json!({"id": id}))
.await
.expect("resume ok");
assert_eq!(resumed["id"], id);
assert_eq!(resumed["kind"], "session");
assert_eq!(resumed["content"], "resumeable content");
assert!(resumed["created_at"].as_str().is_some());
}
#[tokio::test]
async fn get_not_found_returns_error() {
let dir = TempDir::new().expect("tempdir");
let rt = file_rt(dir.path().join("resume_404.db"));
let registry = build_registry(rt);
let err = registry
.dispatch(
"session.get",
json!({"id": "00000000-0000-0000-0000-000000000001"}),
)
.await;
assert!(err.is_err(), "missing session must return NotFound error");
}
#[tokio::test]
async fn get_invalid_uuid_returns_error() {
let dir = TempDir::new().expect("tempdir");
let rt = file_rt(dir.path().join("resume_bad_id.db"));
let registry = build_registry(rt);
let err = registry
.dispatch("session.get", json!({"id": "not-a-uuid"}))
.await;
assert!(err.is_err(), "invalid UUID must return error");
}
#[tokio::test]
async fn get_soft_deleted_returns_error() {
let dir = TempDir::new().expect("tempdir");
let rt = file_rt(dir.path().join("resume_soft_del.db"));
let registry = build_registry(rt);
let stored = store_session(®istry, "soft-delete me").await;
let id = stored["id"].as_str().expect("id present").to_string();
registry
.dispatch("delete", json!({"id": id, "kind": "session"}))
.await
.expect("soft-delete ok");
let err = registry.dispatch("session.get", json!({"id": id})).await;
assert!(
err.is_err(),
"resume of a soft-deleted session must return an error"
);
}