use crate::agent::document::JACSDocument;
use serde_json::json;
pub fn make_test_doc(
id: &str,
version: &str,
jacs_type: &str,
agent_id: Option<&str>,
) -> JACSDocument {
let mut value = json!({
"jacsId": id,
"jacsVersion": version,
"jacsType": jacs_type,
"jacsLevel": "raw",
"data": "test content"
});
if let Some(aid) = agent_id {
value["jacsSignature"] = json!({
"jacsSignatureAgentId": aid
});
}
JACSDocument {
id: id.to_string(),
version: version.to_string(),
value,
jacs_type: jacs_type.to_string(),
}
}
#[macro_export]
macro_rules! storage_conformance_tests {
($factory:expr) => {
use jacs::storage::StorageDocumentTraits;
use jacs::testing::make_test_doc;
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn conformance_store_and_retrieve() {
let storage = $factory().await;
let doc = make_test_doc("conf-sr-1", "v1", "agent", Some("agent-alpha"));
storage.store_document(&doc).expect("store_document failed");
let retrieved = storage
.get_document("conf-sr-1:v1")
.expect("get_document failed");
assert_eq!(retrieved.id, "conf-sr-1");
assert_eq!(retrieved.version, "v1");
assert_eq!(retrieved.jacs_type, "agent");
assert_eq!(retrieved.value["jacsId"], "conf-sr-1");
assert_eq!(retrieved.value["data"], "test content");
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn conformance_document_exists() {
let storage = $factory().await;
let doc = make_test_doc("conf-de-1", "v1", "agent", None);
storage.store_document(&doc).expect("store_document failed");
assert!(
storage
.document_exists("conf-de-1:v1")
.expect("document_exists failed"),
"Stored document should exist"
);
assert!(
!storage
.document_exists("nonexistent:v1")
.expect("document_exists failed"),
"Non-existent document should not exist"
);
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn conformance_document_not_found() {
let storage = $factory().await;
let result = storage.get_document("missing-doc:v1");
assert!(result.is_err(), "get_document on missing key should error");
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn conformance_remove_document() {
let storage = $factory().await;
let doc = make_test_doc("conf-rm-1", "v1", "config", None);
storage.store_document(&doc).expect("store_document failed");
assert!(storage.document_exists("conf-rm-1:v1").unwrap());
let removed = storage
.remove_document("conf-rm-1:v1")
.expect("remove_document failed");
assert_eq!(removed.id, "conf-rm-1");
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn conformance_get_document_versions() {
let storage = $factory().await;
storage
.store_document(&make_test_doc("conf-dv-1", "v1", "agent", None))
.unwrap();
storage
.store_document(&make_test_doc("conf-dv-1", "v2", "agent", None))
.unwrap();
storage
.store_document(&make_test_doc("conf-dv-1", "v3", "agent", None))
.unwrap();
let versions = storage
.get_document_versions("conf-dv-1")
.expect("get_document_versions failed");
assert_eq!(versions.len(), 3, "Should have 3 versions");
for key in &versions {
assert!(
key.starts_with("conf-dv-1:"),
"Key '{}' should start with 'conf-dv-1:'",
key
);
}
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn conformance_get_latest_document() {
let storage = $factory().await;
storage
.store_document(&make_test_doc("conf-gl-1", "v1", "agent", None))
.unwrap();
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
storage
.store_document(&make_test_doc("conf-gl-1", "v2", "agent", None))
.unwrap();
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
storage
.store_document(&make_test_doc("conf-gl-1", "v3", "agent", None))
.unwrap();
let latest = storage
.get_latest_document("conf-gl-1")
.expect("get_latest_document failed");
assert_eq!(latest.version, "v3", "Latest should be v3");
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn conformance_merge_documents() {
let storage = $factory().await;
let result = storage.merge_documents("some-id", "v1", "v2");
assert!(result.is_err(), "merge_documents should return an error");
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn conformance_store_documents_bulk() {
let storage = $factory().await;
let docs = vec![
make_test_doc("conf-bulk-1", "v1", "agent", None),
make_test_doc("conf-bulk-2", "v1", "agent", None),
make_test_doc("conf-bulk-3", "v1", "config", None),
];
storage
.store_documents(docs)
.expect("store_documents failed");
assert!(storage.document_exists("conf-bulk-1:v1").unwrap());
assert!(storage.document_exists("conf-bulk-2:v1").unwrap());
assert!(storage.document_exists("conf-bulk-3:v1").unwrap());
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn conformance_get_documents_bulk() {
let storage = $factory().await;
let docs = vec![
make_test_doc("conf-gbulk-1", "v1", "agent", None),
make_test_doc("conf-gbulk-2", "v1", "config", None),
];
storage
.store_documents(docs)
.expect("store_documents failed");
let keys = vec!["conf-gbulk-1:v1".to_string(), "conf-gbulk-2:v1".to_string()];
let retrieved = storage.get_documents(keys).expect("get_documents failed");
assert_eq!(retrieved.len(), 2, "Should retrieve 2 documents");
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn conformance_idempotent_store() {
let storage = $factory().await;
let doc = make_test_doc("conf-idem-1", "v1", "agent", None);
storage
.store_document(&doc)
.expect("First store should succeed");
storage
.store_document(&doc)
.expect("Second store (idempotent) should not error");
let versions = storage
.get_document_versions("conf-idem-1")
.expect("get_document_versions failed");
assert_eq!(
versions.len(),
1,
"Duplicate insert should not create a second row"
);
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn conformance_invalid_key_format() {
let storage = $factory().await;
let result = storage.get_document("invalid-key-no-colon");
assert!(
result.is_err(),
"get_document with invalid key format should error"
);
}
};
}
#[macro_export]
macro_rules! database_conformance_tests {
($factory:expr) => {
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn conformance_db_list_documents() {
let storage = $factory().await;
storage
.store_document(&make_test_doc("conf-ls-a1", "v1", "agent", None))
.unwrap();
storage
.store_document(&make_test_doc("conf-ls-a2", "v1", "agent", None))
.unwrap();
storage
.store_document(&make_test_doc("conf-ls-c1", "v1", "config", None))
.unwrap();
let agent_docs = storage
.list_documents("agent")
.expect("list_documents failed");
assert_eq!(agent_docs.len(), 2, "Should list exactly 2 agent documents");
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn conformance_db_get_documents_by_agent() {
let storage = $factory().await;
storage
.store_document(&make_test_doc("conf-ba-1", "v1", "agent", Some("alice")))
.unwrap();
storage
.store_document(&make_test_doc("conf-ba-2", "v1", "config", Some("alice")))
.unwrap();
storage
.store_document(&make_test_doc("conf-ba-3", "v1", "agent", Some("bob")))
.unwrap();
let alice_docs = storage
.get_documents_by_agent("alice")
.expect("get_documents_by_agent failed");
assert_eq!(alice_docs.len(), 2, "Alice should have 2 documents");
let bob_docs = storage
.get_documents_by_agent("bob")
.expect("get_documents_by_agent failed");
assert_eq!(bob_docs.len(), 1, "Bob should have 1 document");
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn conformance_db_query_by_type() {
let storage = $factory().await;
for i in 0..5 {
let id = format!("conf-qbt-{}", i);
storage
.store_document(&make_test_doc(&id, "v1", "task", None))
.unwrap();
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
let page1 = storage
.query_by_type("task", 3, 0)
.expect("query_by_type page1 failed");
assert_eq!(page1.len(), 3, "Page 1 should have 3 results");
let page2 = storage
.query_by_type("task", 3, 3)
.expect("query_by_type page2 failed");
assert_eq!(page2.len(), 2, "Page 2 should have 2 results");
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn conformance_db_query_by_field() {
let storage = $factory().await;
let mut doc_a = make_test_doc("conf-qbf-a", "v1", "config", None);
doc_a.value["status"] = serde_json::json!("active");
storage.store_document(&doc_a).unwrap();
let mut doc_b = make_test_doc("conf-qbf-b", "v1", "config", None);
doc_b.value["status"] = serde_json::json!("inactive");
storage.store_document(&doc_b).unwrap();
let active = storage
.query_by_field("status", "active", None, 100, 0)
.expect("query_by_field failed");
assert_eq!(active.len(), 1, "Should find 1 active document");
let missing = storage
.query_by_field("status", "archived", None, 100, 0)
.expect("query_by_field for missing value failed");
assert!(missing.is_empty(), "Should find no 'archived' documents");
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn conformance_db_count_by_type() {
let storage = $factory().await;
for i in 0..4 {
storage
.store_document(&make_test_doc(
&format!("conf-cnt-{}", i),
"v1",
"message",
None,
))
.unwrap();
}
storage
.store_document(&make_test_doc("conf-cnt-other", "v1", "agent", None))
.unwrap();
let count = storage
.count_by_type("message")
.expect("count_by_type failed");
assert_eq!(count, 4, "Should count exactly 4 message documents");
let zero = storage
.count_by_type("nonexistent")
.expect("count_by_type failed");
assert_eq!(zero, 0, "Non-existent type should have count 0");
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn conformance_db_get_versions() {
let storage = $factory().await;
storage
.store_document(&make_test_doc("conf-gv-1", "v1", "agent", Some("agent-x")))
.unwrap();
tokio::time::sleep(std::time::Duration::from_millis(30)).await;
storage
.store_document(&make_test_doc("conf-gv-1", "v2", "agent", Some("agent-x")))
.unwrap();
let versions = storage
.get_versions("conf-gv-1")
.expect("get_versions failed");
assert_eq!(versions.len(), 2);
assert_eq!(versions[0].version, "v1", "Ordered by created_at ASC");
assert_eq!(versions[1].version, "v2");
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn conformance_db_get_latest() {
let storage = $factory().await;
storage
.store_document(&make_test_doc("conf-lt-1", "v1", "config", None))
.unwrap();
tokio::time::sleep(std::time::Duration::from_millis(30)).await;
storage
.store_document(&make_test_doc("conf-lt-1", "v2", "config", None))
.unwrap();
let latest = storage.get_latest("conf-lt-1").expect("get_latest failed");
assert_eq!(latest.version, "v2");
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn conformance_db_query_by_agent() {
let storage = $factory().await;
storage
.store_document(&make_test_doc("conf-qba-1", "v1", "agent", Some("alice")))
.unwrap();
storage
.store_document(&make_test_doc("conf-qba-2", "v1", "config", Some("alice")))
.unwrap();
storage
.store_document(&make_test_doc("conf-qba-3", "v1", "agent", Some("bob")))
.unwrap();
let alice_all = storage
.query_by_agent("alice", None, 100, 0)
.expect("query_by_agent failed");
assert_eq!(alice_all.len(), 2, "Alice should have 2 documents");
let alice_agents = storage
.query_by_agent("alice", Some("agent"), 100, 0)
.expect("query_by_agent with type failed");
assert_eq!(alice_agents.len(), 1, "Alice should have 1 agent document");
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn conformance_db_migrations_idempotent() {
let storage = $factory().await;
storage
.run_migrations()
.expect("Second run_migrations should not error");
}
};
}