use std::sync::Arc;
use exocortex_ingest::IngestServer;
use exocortex_pack_dev_v1::pack_def;
use exocortex_storage::InMemoryStorage;
use exocortex_wire::ingest::v1::{
ingest_service_server::IngestService, MemoryDraft, ProducerIdentity, RegisterSourceRequest,
};
use hmac::{Hmac, Mac};
use sha2::Sha256;
fn server() -> IngestServer<InMemoryStorage> {
let onto = Arc::new(exocortex_kernel::Ontology::from_packs(vec![pack_def()]).unwrap());
IngestServer::new(
Arc::new(InMemoryStorage::new(onto.clone())),
onto,
[5u8; 32],
)
}
fn row(key: &str, mt: &str, vis: i32) -> MemoryDraft {
MemoryDraft {
draft_key: key.into(),
id: String::new(),
memory_type: mt.into(),
title: format!("row {key}"),
content: format!("content {key} mentions src/main.rs and cargo build"),
tags: vec![],
visibility: vis,
valid_from: None,
valid_until: None,
external_key: None,
}
}
#[tokio::test]
async fn fifty_row_batch_accepted_lsn_monotonic() {
let srv = server();
use tonic::Request;
srv.register_source(Request::new(RegisterSourceRequest {
org_id: "org".into(),
source_uri: "session://it".into(),
producer_id: "test-adapter".into(),
ceiling: 3,
source_flavor: "custom".into(),
}))
.await
.unwrap();
let rows: Vec<MemoryDraft> = (0..50)
.map(|i| row(&format!("k{i}"), "Solution", 3))
.collect();
let mut b = exocortex_wire::ingest::v1::IngestBatch {
org_id: "org".into(),
source_uri: "session://it".into(),
producer_id: "test-adapter".into(),
batch_id: "big-batch".into(),
mapping_version: "1".into(),
ontology_fingerprint: srv.ontology.fingerprint.0.to_vec(),
ceiling: 3,
checksum: String::new(),
observed_at: None,
recorded_at: None,
snapshot: None,
memories: rows,
relationships: vec![],
producer: Some(ProducerIdentity {
node_id: "n".into(),
agent_id: String::new(),
adapter_id: String::new(),
hmac_signature: vec![],
}),
};
let mut mac = <Hmac<Sha256> as Mac>::new_from_slice(&[5u8; 32]).unwrap();
mac.update(&prost::Message::encode_to_vec(&b));
if let Some(p) = b.producer.as_mut() {
p.hmac_signature = mac.finalize().into_bytes().to_vec();
}
let ack = srv.submit(Request::new(b)).await.unwrap().into_inner();
assert_eq!(ack.accepted, 50);
assert_eq!(ack.rejected, 0);
assert!(ack.assigned_lsn >= 50, "monotonic LSN covers every row");
}
#[tokio::test]
async fn bad_triple_rejects_whole_batch_naming_the_key() {
let srv = server();
use tonic::Request;
srv.register_source(Request::new(RegisterSourceRequest {
org_id: "org".into(),
source_uri: "session://it2".into(),
producer_id: "test-adapter".into(),
ceiling: 3,
source_flavor: "custom".into(),
}))
.await
.unwrap();
let mut b = exocortex_wire::ingest::v1::IngestBatch {
org_id: "org".into(),
source_uri: "session://it2".into(),
producer_id: "test-adapter".into(),
batch_id: "bad-triple".into(),
mapping_version: "1".into(),
ontology_fingerprint: srv.ontology.fingerprint.0.to_vec(),
ceiling: 3,
checksum: String::new(),
observed_at: None,
recorded_at: None,
snapshot: None,
memories: vec![row("ok", "Fix", 3), row("bad", "Problem", 3)],
relationships: vec![exocortex_wire::ingest::v1::RelationshipDraft {
from_draft_key: "bad".into(),
to_draft_key: "ok".into(),
kind: "Solves".into(),
strength: 0.0,
confidence: 0.0,
context: String::new(),
visibility: 3,
}],
producer: Some(ProducerIdentity {
node_id: "n".into(),
agent_id: String::new(),
adapter_id: String::new(),
hmac_signature: vec![],
}),
};
let mut mac = <Hmac<Sha256> as Mac>::new_from_slice(&[5u8; 32]).unwrap();
mac.update(&prost::Message::encode_to_vec(&b));
if let Some(p) = b.producer.as_mut() {
p.hmac_signature = mac.finalize().into_bytes().to_vec();
}
let ack = srv.submit(Request::new(b)).await.unwrap().into_inner();
assert_eq!(
ack.accepted, 0,
"atomic: one bad row rejects the batch (R-T17)"
);
assert!(
ack.rejections.iter().any(|r| {
r.code == exocortex_wire::ingest::v1::RejectCode::InvalidTypeTriple as i32
&& r.draft_key.contains("bad->ok")
}),
"the ack names the offending draft keys: {:?}",
ack.rejections
);
}
#[tokio::test]
async fn client_side_batch_size_gate() {
use exocortex_client::tools::end_session::EndSessionArgs;
let args = |n: usize| EndSessionArgs {
session_id: "s".into(),
project_id: "p".into(),
memories: (0..n)
.map(|i| exocortex_client::tools::end_session::MemoryDraftInput {
draft_key: format!("k{i}"),
memory_type: "Fix".into(),
title: format!("t{i}"),
content: "c".into(),
visibility: "org".into(),
tags: vec![],
})
.collect(),
edges: vec![],
};
assert!(args(0).memories.is_empty());
assert_eq!(args(6).memories.len(), 6);
assert_eq!(args(5).memories.len(), 5);
}
#[test]
fn checksum_is_order_independent() {
let mk = |k: &str, t: &str| exocortex_wire::ingest::v1::MemoryDraft {
draft_key: k.into(),
id: String::new(),
memory_type: "Fix".into(),
title: t.into(),
content: "c".into(),
tags: vec![],
visibility: 3,
valid_from: None,
valid_until: None,
external_key: None,
};
let a = vec![mk("k1", "t1"), mk("k2", "t2")];
let b = vec![mk("k2", "t2"), mk("k1", "t1")];
assert_eq!(
exocortex_client::tools::end_session::compute_checksum(&a),
exocortex_client::tools::end_session::compute_checksum(&b)
);
let c = vec![mk("k1", "changed")];
assert_ne!(
exocortex_client::tools::end_session::compute_checksum(&a),
exocortex_client::tools::end_session::compute_checksum(&c)
);
}
#[tokio::test]
async fn inverse_materialized_on_write() {
use exocortex_storage::Storage;
use exocortex_wire::ingest::v1::RelationshipDraft;
use futures::StreamExt;
let srv = server();
use tonic::Request;
srv.register_source(Request::new(RegisterSourceRequest {
org_id: "org".into(),
source_uri: "session://inv".into(),
producer_id: "test-adapter".into(),
ceiling: 3,
source_flavor: "custom".into(),
}))
.await
.unwrap();
let submit = |rel_kind: &'static str, batch_id: &'static str| {
let srv = srv.clone();
async move {
let rows = vec![row("sol", "Solution", 3), row("prob", "Problem", 3)];
let mut b = exocortex_wire::ingest::v1::IngestBatch {
org_id: "org".into(),
source_uri: "session://inv".into(),
producer_id: "test-adapter".into(),
batch_id: batch_id.into(),
mapping_version: "1".into(),
ontology_fingerprint: srv.ontology.fingerprint.0.to_vec(),
ceiling: 3,
checksum: String::new(),
observed_at: None,
recorded_at: None,
snapshot: None,
memories: rows,
relationships: vec![RelationshipDraft {
from_draft_key: "sol".into(),
to_draft_key: "prob".into(),
kind: rel_kind.into(),
strength: 0.9,
confidence: 0.8,
context: String::new(),
visibility: 3,
}],
producer: Some(ProducerIdentity {
node_id: "n".into(),
agent_id: String::new(),
adapter_id: String::new(),
hmac_signature: vec![],
}),
};
let mut mac = <Hmac<Sha256> as Mac>::new_from_slice(&[5u8; 32]).unwrap();
mac.update(&prost::Message::encode_to_vec(&b));
if let Some(p) = b.producer.as_mut() {
p.hmac_signature = mac.finalize().into_bytes().to_vec();
}
srv.submit(Request::new(b)).await.unwrap().into_inner()
}
};
let ack = submit("Solves", "inv-1").await;
assert_eq!(ack.rejected, 0, "{:?}", ack.rejections);
assert_eq!(ack.accepted, 4, "2 memories + authored edge + companion");
let rels: Vec<_> = {
let mut out = vec![];
let mut rs = srv.storage.stream_all_relationships().await;
while let Some(Ok(r)) = rs.next().await {
out.push(r);
}
out
};
let solved_by = rels.iter().find(|r| {
srv.ontology
.kinds_by_id
.get(&r.kind)
.map(|m| m.display_name == "SolvedBy")
.unwrap_or(false)
});
let Some(inv) = solved_by else {
panic!("SolvedBy companion missing: {rels:?}");
};
assert!(inv.valid_until.is_none(), "companion mirrors validity");
assert_eq!(inv.visibility, exocortex_kernel::Visibility::Org);
let sol = rels
.iter()
.find(|r| {
srv.ontology
.kinds_by_id
.get(&r.kind)
.map(|m| m.display_name == "Solves")
.unwrap_or(false)
})
.expect("authored Solves");
assert_eq!(inv.from, sol.to);
assert_eq!(inv.to, sol.from);
let ack2 = submit("Solves", "inv-2").await;
assert_eq!(ack2.rejected, 0);
assert_eq!(ack2.accepted, 4);
let ack3 = submit("SolvedBy", "inv-3").await;
assert_eq!(ack3.accepted, 0, "companion kind not authorable");
assert!(
ack3.rejections
.iter()
.any(|r| { r.code == exocortex_wire::ingest::v1::RejectCode::UnknownKind as i32 }),
"UnknownKind rejection: {:?}",
ack3.rejections
);
}
#[tokio::test]
async fn computed_only_kind_rejected_at_ingest() {
use exocortex_wire::ingest::v1::RelationshipDraft;
use tonic::Request;
let srv = server();
srv.register_source(Request::new(RegisterSourceRequest {
org_id: "org".into(),
source_uri: "session://sim".into(),
producer_id: "test-adapter".into(),
ceiling: 3,
source_flavor: "custom".into(),
}))
.await
.unwrap();
let rows = vec![row("a", "Solution", 3), row("b", "Solution", 3)];
let mut b = exocortex_wire::ingest::v1::IngestBatch {
org_id: "org".into(),
source_uri: "session://sim".into(),
producer_id: "test-adapter".into(),
batch_id: "sim-forged".into(),
mapping_version: "1".into(),
ontology_fingerprint: srv.ontology.fingerprint.0.to_vec(),
ceiling: 3,
checksum: String::new(),
observed_at: None,
recorded_at: None,
snapshot: None,
memories: rows,
relationships: vec![RelationshipDraft {
from_draft_key: "a".into(),
to_draft_key: "b".into(),
kind: "SimilarTo".into(),
strength: 0.95,
confidence: 0.9,
context: String::new(),
visibility: 3,
}],
producer: Some(ProducerIdentity {
node_id: "n".into(),
agent_id: String::new(),
adapter_id: String::new(),
hmac_signature: vec![],
}),
};
let mut mac = <Hmac<Sha256> as Mac>::new_from_slice(&[5u8; 32]).unwrap();
mac.update(&prost::Message::encode_to_vec(&b));
if let Some(p) = b.producer.as_mut() {
p.hmac_signature = mac.finalize().into_bytes().to_vec();
}
let ack = srv.submit(Request::new(b)).await.unwrap().into_inner();
assert_eq!(ack.accepted, 0, "nothing from the forged batch commits");
assert!(
ack.rejections
.iter()
.any(|r| r.code == exocortex_wire::ingest::v1::RejectCode::ComputedKindRejected as i32),
"forged SimilarTo must be rejected with ComputedKindRejected: {:?}",
ack.rejections
);
}
#[tokio::test]
async fn storage_inverse_idempotent() {
use exocortex_kernel::{
materialize_inverse, Relationship, RelationshipProperties, Visibility, LSN,
};
use exocortex_storage::{InMemoryStorage, Storage};
let onto =
std::sync::Arc::new(exocortex_kernel::Ontology::from_packs(vec![pack_def()]).unwrap());
let store = InMemoryStorage::new(onto.clone());
let now = chrono::Utc::now();
let a = exocortex_kernel::MemoryId([1; 16]);
let b = exocortex_kernel::MemoryId([2; 16]);
let rel = Relationship {
id: exocortex_kernel::RelationshipId::derive(a, exocortex_kernel::kinds::SOLVES, b, None),
kind: exocortex_kernel::kinds::SOLVES,
from: a,
to: b,
visibility: Visibility::Org,
provenance: exocortex_kernel::Provenance::Asserted { author: "t".into() },
properties: RelationshipProperties {
strength: 0.9,
confidence: 0.8,
context: None,
evidence_count: 1,
success_rate: None,
validation_count: 0,
counter_evidence_count: 0,
last_validated: now,
},
description: None,
bidirectional: false,
valid_from: now,
valid_until: None,
recorded_at: now,
invalidated_by: None,
lsn: LSN::new_local(0),
};
let companion = materialize_inverse(&onto, &rel).expect("SolvedBy companion");
assert_eq!(companion.from, b, "companion reverses endpoints");
assert_eq!(companion.to, a);
store.upsert_relationship(&rel).await.unwrap();
store.upsert_relationship(&rel).await.unwrap();
let mut ids = std::collections::HashSet::new();
let mut rs = store.stream_all_relationships().await;
use futures::StreamExt;
while let Some(Ok(r)) = rs.next().await {
assert!(r.valid_until.is_none(), "rows stay current");
ids.insert(r.id);
}
assert_eq!(ids.len(), 2, "exactly Solves + SolvedBy, no duplicates");
assert!(ids.contains(&rel.id));
assert!(ids.contains(&companion.id));
}