use nodedb_client::NodeDb;
use nodedb_lite::{NodeDbLite, PagedbStorageMem};
use nodedb_types::document::Document;
use nodedb_types::id::NodeId;
use nodedb_types::value::Value;
async fn open_db() -> std::sync::Arc<NodeDbLite<PagedbStorageMem>> {
let storage = PagedbStorageMem::open_in_memory().await.unwrap();
NodeDbLite::open(storage).await.unwrap()
}
#[tokio::test]
async fn e2e_memory_stays_within_budget() {
let db = open_db().await;
let vectors: Vec<(String, Vec<f32>)> = (0..50)
.map(|i| {
let emb: Vec<f32> = (0..32).map(|d| ((i * 32 + d) as f32) * 0.001).collect();
(format!("v{i}"), emb)
})
.collect();
let refs: Vec<(&str, &[f32])> = vectors
.iter()
.map(|(id, e)| (id.as_str(), e.as_slice()))
.collect();
db.batch_vector_insert("vecs", &refs).await.unwrap();
let used = db.governor().total_allocated();
let budget = db.governor().global_ceiling();
assert!(used <= budget, "memory {used} exceeds budget {budget}");
}
#[tokio::test]
async fn e2e_two_lite_instances_crdt_convergence() {
let db1 = {
let s = PagedbStorageMem::open_in_memory().await.unwrap();
NodeDbLite::open(s).await.unwrap()
};
let db2 = {
let s = PagedbStorageMem::open_in_memory().await.unwrap();
NodeDbLite::open(s).await.unwrap()
};
let mut doc1 = Document::new("shared-doc");
doc1.set("author", Value::String("alice".into()));
db1.document_put("notes", doc1).await.unwrap();
let mut doc2 = Document::new("shared-doc");
doc2.set("reviewer", Value::String("bob".into()));
db2.document_put("notes", doc2).await.unwrap();
let deltas1 = db1.pending_crdt_deltas().unwrap();
let deltas2 = db2.pending_crdt_deltas().unwrap();
assert!(!deltas1.is_empty());
assert!(!deltas2.is_empty());
for d in &deltas2 {
db1.import_remote_deltas(&d.collection, &d.delta_bytes)
.unwrap();
}
for d in &deltas1 {
db2.import_remote_deltas(&d.collection, &d.delta_bytes)
.unwrap();
}
let doc_from_1 = db1
.document_get("notes", "shared-doc")
.await
.unwrap()
.unwrap();
let doc_from_2 = db2
.document_get("notes", "shared-doc")
.await
.unwrap()
.unwrap();
assert!(doc_from_1.get_str("author").is_some() || doc_from_1.get_str("reviewer").is_some());
assert!(doc_from_2.get_str("author").is_some() || doc_from_2.get_str("reviewer").is_some());
}
#[tokio::test]
async fn e2e_compensation_reject_rollback() {
let db = open_db().await;
let mut doc = Document::new("user-1");
doc.set("username", Value::String("alice".into()));
db.document_put("users", doc).await.unwrap();
assert!(db.document_get("users", "user-1").await.unwrap().is_some());
let deltas = db.pending_crdt_deltas().unwrap();
assert!(!deltas.is_empty());
let mutation_id = deltas[0].mutation_id;
db.reject_delta(mutation_id).unwrap();
let doc = db.document_get("users", "user-1").await.unwrap();
assert!(doc.is_none(), "rejected document should be rolled back");
}
#[tokio::test]
async fn e2e_delta_acknowledge_clears_pending() {
let db = open_db().await;
db.document_put("a", Document::new("d1")).await.unwrap();
db.document_put("a", Document::new("d2")).await.unwrap();
db.document_put("a", Document::new("d3")).await.unwrap();
let deltas = db.pending_crdt_deltas().unwrap();
assert_eq!(deltas.len(), 3);
db.acknowledge_deltas(deltas[1].mutation_id).unwrap();
let remaining: Vec<u64> = db
.pending_crdt_deltas()
.unwrap()
.iter()
.map(|d| d.mutation_id)
.collect();
assert_eq!(
remaining,
vec![deltas[0].mutation_id, deltas[2].mutation_id]
);
}
#[tokio::test]
async fn e2e_native_full_suite_passes() {
let db = open_db().await;
db.vector_insert("test", "v1", &[1.0, 0.0], None)
.await
.unwrap();
let r = db
.vector_search("test", &[1.0, 0.0], 1, None, None)
.await
.unwrap();
assert_eq!(r.len(), 1);
db.graph_insert_edge(
"test",
&NodeId::from_validated("a".to_string()),
&NodeId::from_validated("b".to_string()),
"L",
None,
)
.await
.unwrap();
let sg = db
.graph_traverse(
"test",
&NodeId::from_validated("a".to_string()),
1,
nodedb_types::graph::Direction::Out,
None,
)
.await
.unwrap();
assert!(sg.node_count() >= 2);
db.document_put("d", Document::new("d1")).await.unwrap();
assert!(db.document_get("d", "d1").await.unwrap().is_some());
}