#![allow(unreachable_pub)]
use crate::store::*;
use crate::{
IndexDefinition, MemoryEvent, MemoryObject, MigrationRecord, QueueClaimOptions, QueueJob,
QueueJobStatus, QueueNackOptions, SearchOptions, StoredSchema,
};
#[cfg(feature = "vectors")]
use crate::{VectorSearchHit, VectorSearchOptions};
pub fn test_contract_object_lifecycle(engine: &mut impl ThingStore) {
let obj = engine
.put_object(MemoryObject::new("col", "a", r#"{"v":1}"#))
.unwrap();
assert_eq!(obj.version, 1);
let stored = engine.get_object("col", "a").unwrap().unwrap();
assert_eq!(stored.body, r#"{"v":1}"#);
let updated = engine
.put_object(MemoryObject::new("col", "a", r#"{"v":2}"#))
.unwrap();
assert_eq!(updated.version, 2);
assert!(engine.delete_object("col", "a").unwrap());
assert!(engine.get_object("col", "a").unwrap().is_none());
}
pub fn test_contract_vector_lifecycle(engine: &mut impl ThingStore) {
engine
.put_object(MemoryObject::new("col", "a", r#"{"v":1}"#).with_vector(vec![1.0, 0.0]))
.unwrap();
#[cfg(feature = "vectors")]
{
let hits: Vec<VectorSearchHit> = engine
.vector_search("col", &[1.0, 0.0], VectorSearchOptions::default())
.unwrap();
assert_eq!(hits.len(), 1, "vector search should find object");
}
engine
.put_object(MemoryObject::new("col", "a", r#"{"v":2}"#))
.unwrap();
#[cfg(feature = "vectors")]
{
let hits: Vec<VectorSearchHit> = engine
.vector_search("col", &[1.0, 0.0], VectorSearchOptions::default())
.unwrap();
assert_eq!(
hits.len(),
0,
"vector must be removed when object updated without vector"
);
}
}
pub fn test_contract_schema_store(engine: &mut impl ThingStore) {
assert!(engine.get_schema_document().unwrap().is_none());
engine
.put_schema_document(StoredSchema {
schema_json: "{\"version\":1}".to_string(),
hash: "sha256:test".to_string(),
updated_at: "2026-01-01T00:00:00Z".to_string(),
})
.unwrap();
assert_eq!(
engine.get_schema_document().unwrap().unwrap().hash,
"sha256:test"
);
engine
.record_migration(MigrationRecord {
id: "0001_initial.thingd".to_string(),
hash: "sha256:test".to_string(),
applied_at: "2026-01-01T00:00:00Z".to_string(),
})
.unwrap();
assert_eq!(engine.list_migrations().unwrap().len(), 1);
}
pub fn test_contract_indexes(engine: &mut impl ThingStore) {
engine
.create_index_definition(IndexDefinition {
collection: "users".to_string(),
field: "email".to_string(),
unique: true,
})
.unwrap();
engine
.put_object(MemoryObject::new(
"users",
"alice",
r#"{"email":"alice@example.com"}"#,
))
.unwrap();
let duplicate = engine.put_object(MemoryObject::new(
"users",
"other",
r#"{"email":"alice@example.com"}"#,
));
assert!(matches!(duplicate, Err(crate::ThingdError::Conflict(_))));
assert_eq!(
engine.list_indexes().unwrap(),
vec![("users".into(), "email".into())]
);
assert!(engine.delete_index("users", "email").unwrap());
assert!(engine.list_indexes().unwrap().is_empty());
}
pub fn test_contract_event_idempotency(engine: &mut impl ThingStore) {
let mut e1 = MemoryEvent::new("s", "type-a", r#"{"n":1}"#);
e1.idempotency_key = "k1".to_string();
let e1 = engine.append_event(e1).unwrap();
assert_eq!(e1.sequence, 1);
let mut e2 = MemoryEvent::new("s", "type-a", r#"{"n":1}"#);
e2.idempotency_key = "k1".to_string();
let e2 = engine.append_event(e2).unwrap();
assert_eq!(e2.sequence, 1);
let mut e3 = MemoryEvent::new("s", "type-b", r#"{"n":2}"#);
e3.idempotency_key = "k2".to_string();
let e3 = engine.append_event(e3).unwrap();
assert_eq!(e3.sequence, 2);
let e4 = engine
.append_event(MemoryEvent::new("s", "type-c", r#"{"n":3}"#))
.unwrap();
assert_eq!(e4.sequence, 3);
let e5 = engine
.append_event(MemoryEvent::new("s", "type-d", r#"{"n":4}"#))
.unwrap();
assert_eq!(e5.sequence, 4);
}
pub fn test_contract_queue_lifecycle(engine: &mut impl ThingStore) {
let job = engine
.push_job(QueueJob::new("q", "j1", r#"{"task":"test"}"#, 3))
.unwrap();
assert_eq!(job.status, QueueJobStatus::Ready);
let claimed = engine
.claim_job_with_options("q", QueueClaimOptions::default())
.unwrap()
.expect("should claim job");
assert_eq!(claimed.id, "j1");
assert_eq!(claimed.status, QueueJobStatus::Leased);
let acked = engine.ack_job("q", "j1").unwrap().unwrap();
assert_eq!(acked.status, QueueJobStatus::Completed);
assert!(
engine
.claim_job_with_options("q", QueueClaimOptions::default())
.unwrap()
.is_none()
);
}
pub fn test_contract_delayed_job(engine: &mut impl ThingStore) {
let far_future = 99_999_999_999_999i64;
let mut job = QueueJob::new("q", "d1", r"{}", 3);
job.available_at_ms = far_future;
engine.push_job(job).unwrap();
assert!(
engine
.claim_job_with_options("q", QueueClaimOptions::default())
.unwrap()
.is_none()
);
let mut now_job = QueueJob::new("q", "now1", r"{}", 3);
now_job.available_at_ms = 0;
engine.push_job(now_job).unwrap();
let claimed = engine
.claim_job_with_options("q", QueueClaimOptions::default())
.unwrap()
.expect("should claim available job");
assert_eq!(claimed.id, "now1");
}
pub fn test_contract_lease_expiration(engine: &mut impl ThingStore) {
let job = engine
.push_job(QueueJob::new("q", "l1", r#"{"x":1}"#, 3))
.unwrap();
assert_eq!(job.status, QueueJobStatus::Ready);
let _claimed = engine
.claim_job_with_options("q", QueueClaimOptions { lease_ms: 1 })
.unwrap()
.expect("should claim");
std::thread::sleep(std::time::Duration::from_millis(10));
let reclaimed = engine
.claim_job_with_options("q", QueueClaimOptions::default())
.unwrap()
.expect("lease should have expired");
assert_eq!(reclaimed.id, "l1");
}
pub fn test_contract_nack_dead_letter(engine: &mut impl ThingStore) {
let job = QueueJob::new("q", "n1", r#"{"x":1}"#, 2);
engine.push_job(job).unwrap();
let _claimed = engine
.claim_job_with_options("q", QueueClaimOptions::default())
.unwrap()
.expect("should claim");
let nacked = engine
.nack_job_with_options(
"q",
"n1",
QueueNackOptions {
delay_ms: 0,
error: "retry".to_string(),
},
)
.unwrap()
.expect("should nack");
assert_eq!(nacked.status, QueueJobStatus::Ready);
assert_eq!(nacked.attempts, 1);
let _claimed2 = engine
.claim_job_with_options("q", QueueClaimOptions::default())
.unwrap()
.expect("should claim again");
let dead = engine
.nack_job_with_options(
"q",
"n1",
QueueNackOptions {
delay_ms: 0,
error: "final".to_string(),
},
)
.unwrap()
.expect("should nack");
assert_eq!(dead.status, QueueJobStatus::Dead);
assert!(!engine.list_dead_jobs("q").unwrap().is_empty());
}
pub fn test_contract_search(engine: &mut impl ThingStore) {
engine
.put_object(MemoryObject::new("docs", "d1", r#"{"text":"hello world"}"#))
.unwrap();
engine
.put_object(MemoryObject::new(
"docs",
"d2",
r#"{"text":"goodbye world"}"#,
))
.unwrap();
let hits = engine.search("hello", SearchOptions::default()).unwrap();
assert!(
hits.iter().map(|h| h.id.as_str()).any(|id| id == "d1"),
"search should find 'hello' in d1"
);
}