use chrono::DateTime;
use chrono::Utc;
use mempill_core::{IngestClaimRequest, MemError, QueryHistoryRequest, QueryMemoryRequest};
use mempill_types::{
AgentId, BeliefStatus, Cardinality, ClaimRef, Confidence, Criticality, Disposition,
ExternalKind, ProvenanceLabel, ValidTime,
};
use crate::date::parse_lenient_date;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum MempillDxError {
#[error("Unparsable date {input:?}: {hint}")]
UnparsableDate {
input: String,
hint: &'static str,
},
#[error("Incoherent date range: start={start} end={end}. {hint}")]
IncoherentDates {
start: String,
end: String,
hint: &'static str,
},
#[error("Engine error: {0}")]
Engine(#[from] MemError),
}
#[async_trait::async_trait]
pub trait CanIngestClaim: Send + Sync {
async fn ingest_ergo(
&self,
req: IngestClaimRequest,
) -> Result<mempill_core::IngestClaimResponse, MemError>;
}
#[async_trait::async_trait]
pub trait CanQueryMemory: Send + Sync {
async fn query_ergo(
&self,
req: QueryMemoryRequest,
) -> Result<mempill_core::QueryMemoryResponse, MemError>;
}
#[async_trait::async_trait]
impl<P, O, V> CanIngestClaim for mempill_core::EngineHandle<P, O, V>
where
P: mempill_core::PersistencePort + Send + Sync + 'static,
O: mempill_core::OraclePort + Send + Sync + 'static,
V: mempill_core::VectorPort + Send + Sync + 'static,
{
async fn ingest_ergo(
&self,
req: IngestClaimRequest,
) -> Result<mempill_core::IngestClaimResponse, MemError> {
self.ingest_claim(req).await
}
}
#[async_trait::async_trait]
impl<P, O, V> CanQueryMemory for mempill_core::EngineHandle<P, O, V>
where
P: mempill_core::PersistencePort + Send + Sync + 'static,
O: mempill_core::OraclePort + Send + Sync + 'static,
V: mempill_core::VectorPort + Send + Sync + 'static,
{
async fn query_ergo(
&self,
req: QueryMemoryRequest,
) -> Result<mempill_core::QueryMemoryResponse, MemError> {
self.query_memory(req).await
}
}
#[async_trait::async_trait]
pub trait CanQueryHistory: Send + Sync {
async fn query_history_ergo(
&self,
req: QueryHistoryRequest,
) -> Result<mempill_core::QueryHistoryResponse, MemError>;
}
#[async_trait::async_trait]
impl<P, O, V> CanQueryHistory for mempill_core::EngineHandle<P, O, V>
where
P: mempill_core::PersistencePort + Send + Sync + 'static,
O: mempill_core::OraclePort + Send + Sync + 'static,
V: mempill_core::VectorPort + Send + Sync + 'static,
{
async fn query_history_ergo(
&self,
req: QueryHistoryRequest,
) -> Result<mempill_core::QueryHistoryResponse, MemError> {
self.query_history(req).await
}
}
pub use mempill_core::HistoryEntry;
pub use mempill_types::HistoryEntryStatus;
#[must_use]
#[derive(Debug, Clone, PartialEq)]
pub struct History {
pub entries: Vec<HistoryEntry>,
}
impl History {
pub fn current(&self) -> Option<&HistoryEntry> {
self.entries.iter().find(|e| e.status == HistoryEntryStatus::Current)
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
#[derive(Default, Clone, Debug)]
#[non_exhaustive]
pub struct RememberOptions {
pub valid_from: Option<String>,
pub valid_until: Option<String>,
pub confidence: Option<f32>,
pub criticality: Option<Criticality>,
pub derived_from: Vec<ClaimRef>,
}
impl RememberOptions {
pub fn new() -> Self {
Self::default()
}
pub fn valid_from(mut self, s: impl Into<String>) -> Self {
self.valid_from = Some(s.into());
self
}
pub fn valid_until(mut self, s: impl Into<String>) -> Self {
self.valid_until = Some(s.into());
self
}
pub fn confidence(mut self, c: f32) -> Self {
self.confidence = Some(c);
self
}
pub fn criticality(mut self, c: Criticality) -> Self {
self.criticality = Some(c);
self
}
pub fn derived_from(mut self, refs: Vec<ClaimRef>) -> Self {
self.derived_from = refs;
self
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct RememberReceipt {
pub claim_ref: ClaimRef,
pub disposition: Disposition,
pub contested_with: Vec<ClaimRef>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct BeliefDetail {
pub claim_ref: ClaimRef,
pub value: serde_json::Value,
pub valid_from: Option<DateTime<Utc>>,
pub valid_until: Option<DateTime<Utc>>,
pub value_confidence: f32,
pub provenance: String,
pub corroboration_count: u32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ContestCandidate {
pub value: serde_json::Value,
pub claim_ref: ClaimRef,
pub valid_from: Option<DateTime<Utc>>,
pub detail: BeliefDetail,
}
#[must_use]
#[derive(Debug, Clone)]
pub struct RecallResult {
pub value: Option<serde_json::Value>,
pub status: BeliefStatus,
pub candidates: Vec<ContestCandidate>,
pub currency: mempill_types::CurrencyState,
pub is_stale: bool,
pub primary: Option<BeliefDetail>,
}
impl RecallResult {
pub fn as_str(&self) -> Option<&str> {
self.value.as_ref().and_then(|v| v.as_str())
}
pub fn is_contested(&self) -> bool {
matches!(self.status, BeliefStatus::Contested | BeliefStatus::Conflict)
}
pub fn is_empty(&self) -> bool {
matches!(self.status, BeliefStatus::NoBelief)
}
}
#[derive(Debug)]
pub struct IngestClaimRequestBuilder {
agent_id: AgentId,
subject: String,
predicate: String,
value: serde_json::Value,
valid_from: Option<String>,
valid_until: Option<String>,
confidence: Option<f32>,
cardinality: Option<Cardinality>,
provenance: Option<ProvenanceLabel>,
criticality: Option<Criticality>,
derived_from: Vec<ClaimRef>,
}
impl IngestClaimRequestBuilder {
pub fn valid_from(mut self, s: impl Into<String>) -> Self {
self.valid_from = Some(s.into());
self
}
pub fn valid_until(mut self, s: impl Into<String>) -> Self {
self.valid_until = Some(s.into());
self
}
pub fn confidence(mut self, c: f32) -> Self {
self.confidence = Some(c);
self
}
pub fn cardinality(mut self, c: Cardinality) -> Self {
self.cardinality = Some(c);
self
}
pub fn provenance(mut self, p: ProvenanceLabel) -> Self {
self.provenance = Some(p);
self
}
pub fn criticality(mut self, c: Criticality) -> Self {
self.criticality = Some(c);
self
}
pub fn derived_from(mut self, refs: Vec<ClaimRef>) -> Self {
self.derived_from = refs;
self
}
pub fn build(self) -> Result<IngestClaimRequest, MempillDxError> {
let value_confidence = self.confidence.unwrap_or(1.0);
let has_dates = self.valid_from.is_some() || self.valid_until.is_some();
let vtc = if has_dates { value_confidence } else { 0.0 };
let start = self.valid_from.as_deref().map(parse_lenient_date).transpose()?;
let end = self.valid_until.as_deref().map(parse_lenient_date).transpose()?;
if let (Some(s), Some(e)) = (start, end) {
if s >= e {
return Err(MempillDxError::IncoherentDates {
start: s.to_rfc3339(),
end: e.to_rfc3339(),
hint: "valid_from must precede valid_until",
});
}
}
let valid_time = if has_dates {
Some(ValidTime { start, end, valid_time_confidence: vtc })
} else {
None
};
Ok(IngestClaimRequest {
agent_id: self.agent_id,
subject: self.subject,
predicate: self.predicate,
value: self.value,
provenance: self.provenance.unwrap_or(ProvenanceLabel::External(ExternalKind::UserAsserted)),
cardinality: self.cardinality.unwrap_or(Cardinality::Functional),
valid_time,
confidence: Confidence { value_confidence, valid_time_confidence: vtc },
criticality: self.criticality.unwrap_or(Criticality::Medium),
derived_from: self.derived_from,
})
}
}
pub trait IngestClaimRequestExt {
fn builder(
agent_id: impl Into<String>,
subject: impl Into<String>,
predicate: impl Into<String>,
value: serde_json::Value,
) -> IngestClaimRequestBuilder;
}
impl IngestClaimRequestExt for IngestClaimRequest {
fn builder(
agent_id: impl Into<String>,
subject: impl Into<String>,
predicate: impl Into<String>,
value: serde_json::Value,
) -> IngestClaimRequestBuilder {
IngestClaimRequestBuilder {
agent_id: AgentId(agent_id.into()),
subject: subject.into(),
predicate: predicate.into(),
value,
valid_from: None,
valid_until: None,
confidence: None,
cardinality: None,
provenance: None,
criticality: None,
derived_from: vec![],
}
}
}
pub async fn remember(
engine: &impl CanIngestClaim,
agent_id: impl Into<String>,
subject: impl Into<String>,
predicate: impl Into<String>,
value: impl serde::Serialize,
opts: RememberOptions,
) -> Result<RememberReceipt, MempillDxError> {
let value_json = serde_json::to_value(value)
.map_err(|e| MempillDxError::Engine(MemError::MalformedFact { reason: e.to_string() }))?;
let derived = opts.derived_from.clone();
let req = IngestClaimRequest::builder(agent_id, subject, predicate, value_json)
.then_if(opts.valid_from, |b, s| b.valid_from(s))
.then_if(opts.valid_until, |b, s| b.valid_until(s))
.then_if(opts.confidence, |b, c| b.confidence(c))
.then_if(opts.criticality, |b, c| b.criticality(c))
.derived_from(derived)
.build()?;
let resp = engine.ingest_ergo(req).await?;
Ok(RememberReceipt {
claim_ref: resp.claim_ref,
disposition: resp.disposition,
contested_with: resp.contested_with,
})
}
pub async fn recall(
engine: &impl CanQueryMemory,
agent_id: impl Into<String>,
subject: impl Into<String>,
predicate: impl Into<String>,
) -> Result<RecallResult, MempillDxError> {
let req = QueryMemoryRequest {
agent_id: AgentId(agent_id.into()),
subject: subject.into(),
predicate: predicate.into(),
as_of_tx_time: None,
};
let resp = engine.query_ergo(req).await?;
let bp = resp.belief;
let make_detail = |b: &mempill_types::Belief| -> BeliefDetail {
BeliefDetail {
claim_ref: b.claim_ref.clone(),
value: b.fact.value.clone(),
valid_from: b.valid_time.start,
valid_until: b.valid_time.end,
value_confidence: b.confidence.value_confidence,
provenance: provenance_label_str(&b.provenance),
corroboration_count: b.currency_signal.corroboration_count,
}
};
let (value, candidates, primary) = match &bp.status {
BeliefStatus::Contested | BeliefStatus::Conflict => {
let cands = std::iter::once(bp.primary.as_ref())
.flatten()
.chain(bp.alternatives.iter())
.map(|b| ContestCandidate {
value: b.fact.value.clone(),
claim_ref: b.claim_ref.clone(),
valid_from: b.valid_time.start,
detail: make_detail(b),
})
.collect();
(None, cands, None)
}
BeliefStatus::NoBelief => {
(None, vec![], None)
}
BeliefStatus::TimingUncertain | BeliefStatus::Resolved => {
let value = bp.primary.as_ref().map(|b| b.fact.value.clone());
let detail = bp.primary.as_ref().map(make_detail);
(value, vec![], detail)
}
_ => (None, vec![], None),
};
let currency = bp.currency.clone();
let is_stale = bp.staleness.is_stale;
Ok(RecallResult { value, status: bp.status, candidates, currency, is_stale, primary })
}
pub async fn history(
engine: &impl CanQueryHistory,
agent_id: impl Into<String>,
subject: impl Into<String>,
predicate: impl Into<String>,
) -> Result<History, MempillDxError> {
let req = QueryHistoryRequest {
agent_id: AgentId(agent_id.into()),
subject: subject.into(),
predicate: predicate.into(),
};
let resp = engine.query_history_ergo(req).await?;
Ok(History { entries: resp.entries })
}
fn provenance_label_str(p: &ProvenanceLabel) -> String {
match p {
ProvenanceLabel::External(mempill_types::ExternalKind::UserAsserted) => {
"External/UserAsserted".to_owned()
}
ProvenanceLabel::External(mempill_types::ExternalKind::ExternalFirstHand) => {
"External/ExternalFirstHand".to_owned()
}
ProvenanceLabel::RecallReEntry => "RecallReEntry".to_owned(),
ProvenanceLabel::ModelDerived => "ModelDerived".to_owned(),
_ => format!("{p:?}"),
}
}
trait BuilderExt: Sized {
fn then_if<T, F>(self, opt: Option<T>, f: F) -> Self
where
F: FnOnce(Self, T) -> Self;
}
impl BuilderExt for IngestClaimRequestBuilder {
fn then_if<T, F>(self, opt: Option<T>, f: F) -> Self
where
F: FnOnce(Self, T) -> Self,
{
match opt {
Some(v) => f(self, v),
None => self,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Datelike;
use mempill_types::{CurrencyState};
#[test]
fn builder_defaults_applied() {
let req = IngestClaimRequest::builder("agent", "user", "city", serde_json::json!("Berlin"))
.build()
.unwrap();
assert_eq!(req.provenance, ProvenanceLabel::External(ExternalKind::UserAsserted));
assert_eq!(req.cardinality, Cardinality::Functional);
assert_eq!(req.confidence.value_confidence, 1.0);
assert_eq!(req.confidence.valid_time_confidence, 0.0); assert!(req.valid_time.is_none());
assert_eq!(req.criticality, Criticality::Medium);
assert!(req.derived_from.is_empty());
}
#[test]
fn builder_confidence_drives_valid_time_confidence_when_dates_supplied() {
let req = IngestClaimRequest::builder("agent", "user", "city", serde_json::json!("X"))
.valid_from("2025-01-01")
.confidence(0.8)
.build()
.unwrap();
assert_eq!(req.confidence.value_confidence, 0.8);
assert_eq!(req.confidence.valid_time_confidence, 0.8); let vt = req.valid_time.unwrap();
assert_eq!(vt.valid_time_confidence, 0.8);
}
#[test]
fn builder_incoherent_dates_rejected() {
let err = IngestClaimRequest::builder("a", "s", "p", serde_json::json!(1))
.valid_from("2025-06-01")
.valid_until("2025-01-01")
.build()
.unwrap_err();
assert!(matches!(err, MempillDxError::IncoherentDates { .. }));
}
#[test]
fn builder_lenient_year_only() {
let req = IngestClaimRequest::builder("a", "s", "p", serde_json::json!(1))
.valid_from("2026")
.build()
.unwrap();
let vt = req.valid_time.unwrap();
assert_eq!(vt.start.unwrap().year(), 2026);
}
#[test]
fn builder_bad_date_gives_clear_error() {
let err = IngestClaimRequest::builder("a", "s", "p", serde_json::json!(1))
.valid_from("not-a-date")
.build()
.unwrap_err();
match err {
MempillDxError::UnparsableDate { input, hint } => {
assert_eq!(input, "not-a-date");
assert!(hint.contains("YYYY"));
assert!(!hint.contains("premature end of input"));
}
other => panic!("expected UnparsableDate, got {other:?}"),
}
}
fn make_recall_result(status: BeliefStatus, value: Option<serde_json::Value>) -> RecallResult {
RecallResult {
value,
status,
candidates: vec![],
currency: CurrencyState::Fresh,
is_stale: false,
primary: None,
}
}
fn make_belief_detail(value: serde_json::Value) -> BeliefDetail {
BeliefDetail {
claim_ref: ClaimRef::new_random(),
value,
valid_from: None,
valid_until: None,
value_confidence: 1.0,
provenance: "External/UserAsserted".to_owned(),
corroboration_count: 0,
}
}
fn make_contest_candidate(value: serde_json::Value) -> ContestCandidate {
let detail = make_belief_detail(value.clone());
ContestCandidate {
value,
claim_ref: detail.claim_ref.clone(),
valid_from: None,
detail,
}
}
#[test]
fn recall_result_as_str_resolved() {
let r = make_recall_result(BeliefStatus::Resolved, Some(serde_json::json!("Berlin")));
assert_eq!(r.as_str(), Some("Berlin"));
assert!(!r.is_contested());
assert!(!r.is_empty());
}
#[test]
fn recall_result_contested_is_none_value() {
let r = RecallResult {
value: None, status: BeliefStatus::Contested,
candidates: vec![
make_contest_candidate(serde_json::json!("Alice")),
make_contest_candidate(serde_json::json!("Bob")),
],
currency: CurrencyState::Fresh,
is_stale: false,
primary: None,
};
assert!(r.is_contested());
assert!(r.value.is_none(), "Contested must have value=None");
assert_eq!(r.candidates.len(), 2);
assert!(!r.is_empty(), "Contested must NOT be is_empty()");
}
#[test]
fn recall_result_no_belief_is_empty() {
let r = make_recall_result(BeliefStatus::NoBelief, None);
assert!(r.is_empty());
assert!(!r.is_contested());
}
#[test]
fn recall_result_conflict_is_contested() {
let r = make_recall_result(BeliefStatus::Conflict, None);
assert!(r.is_contested());
}
#[test]
fn remember_options_derived_from_builder() {
let ref1 = ClaimRef::new_random();
let ref2 = ClaimRef::new_random();
let opts = RememberOptions::new().derived_from(vec![ref1.clone(), ref2.clone()]);
assert_eq!(opts.derived_from.len(), 2);
assert_eq!(opts.derived_from[0], ref1);
}
#[test]
fn remember_options_derived_from_default_empty() {
let opts = RememberOptions::new();
assert!(opts.derived_from.is_empty());
}
#[test]
fn belief_detail_fields_accessible() {
let detail = make_belief_detail(serde_json::json!("Berlin"));
assert_eq!(detail.value, serde_json::json!("Berlin"));
assert_eq!(detail.value_confidence, 1.0);
assert_eq!(detail.provenance, "External/UserAsserted");
assert_eq!(detail.corroboration_count, 0);
assert!(detail.valid_from.is_none());
assert!(detail.valid_until.is_none());
}
#[test]
fn contest_candidate_detail_field_accessible() {
let c = make_contest_candidate(serde_json::json!("Alice"));
assert_eq!(c.detail.value, serde_json::json!("Alice"));
assert_eq!(c.detail.provenance, "External/UserAsserted");
}
#[test]
fn recall_result_primary_field_present_on_resolved() {
let detail = make_belief_detail(serde_json::json!("Berlin"));
let r = RecallResult {
value: Some(serde_json::json!("Berlin")),
status: BeliefStatus::Resolved,
candidates: vec![],
currency: CurrencyState::Fresh,
is_stale: false,
primary: Some(detail),
};
let p = r.primary.as_ref().unwrap();
assert_eq!(p.value, serde_json::json!("Berlin"));
assert_eq!(p.provenance, "External/UserAsserted");
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn remember_recall_round_trip() {
let engine = crate::open_default_in_memory().unwrap();
let receipt = remember(
&engine,
"test-agent",
"user",
"city",
serde_json::json!("Berlin"),
RememberOptions::new(),
)
.await
.unwrap();
assert!(!format!("{:?}", receipt.disposition).is_empty());
let result = recall(&engine, "test-agent", "user", "city").await.unwrap();
assert!(!result.is_empty());
assert!(!result.is_contested());
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn recall_contested_value_is_none_candidates_populated() {
let engine = crate::open_default_in_memory().unwrap();
remember(&engine, "agent", "acme", "ceo", serde_json::json!("Alice"), RememberOptions::new())
.await
.unwrap();
remember(&engine, "agent", "acme", "ceo", serde_json::json!("Bob"), RememberOptions::new())
.await
.unwrap();
let r = recall(&engine, "agent", "acme", "ceo").await.unwrap();
assert!(r.is_contested(), "expected Contested, got {:?}", r.status);
assert!(r.value.is_none(), "Contested must have value=None");
assert_eq!(r.candidates.len(), 2, "both Alice and Bob must surface");
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn remember_with_valid_from_defaults_correct() {
let engine = crate::open_default_in_memory().unwrap();
let opts = RememberOptions::new().valid_from("2025-01-01").confidence(0.9);
remember(&engine, "agent", "user", "city", "Munich", opts).await.unwrap();
let r = recall(&engine, "agent", "user", "city").await.unwrap();
assert!(!r.is_empty());
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn remember_derived_from_forwarded() {
let engine = crate::open_default_in_memory().unwrap();
let source = remember(
&engine,
"agent",
"user",
"city",
"Berlin",
RememberOptions::new(),
)
.await
.unwrap();
let derived = remember(
&engine,
"agent",
"user",
"city_note",
"Capital of Germany",
RememberOptions::new().derived_from(vec![source.claim_ref.clone()]),
)
.await
.unwrap();
assert!(!format!("{:?}", derived.claim_ref).is_empty());
let r = recall(&engine, "agent", "user", "city_note").await.unwrap();
assert!(!r.is_empty());
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn recall_primary_exposes_rich_fields() {
let engine = crate::open_default_in_memory().unwrap();
let opts = RememberOptions::new()
.valid_from("2025-01-01")
.valid_until("2026-01-01")
.confidence(0.9);
let receipt = remember(&engine, "agent", "user", "city", "Berlin", opts).await.unwrap();
let r = recall(&engine, "agent", "user", "city").await.unwrap();
let p = r.primary.as_ref().expect("primary must be set for a non-empty resolved belief");
assert_eq!(p.claim_ref, receipt.claim_ref, "claim_ref must match the stored claim");
assert_eq!(p.value, serde_json::json!("Berlin"));
assert!(p.valid_from.is_some(), "valid_from must be populated");
assert!(p.valid_until.is_some(), "valid_until must be populated");
assert!((p.value_confidence - 0.9).abs() < 1e-4, "value_confidence must be 0.9");
assert!(p.provenance.contains("UserAsserted"), "provenance must contain UserAsserted");
assert_eq!(p.corroboration_count, 0);
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn history_succession_ordered_with_correct_statuses() {
let engine = crate::open_default_in_memory().unwrap();
remember(
&engine,
"agent",
"user",
"city",
"Berlin",
RememberOptions::new().valid_from("2020-01-01").valid_until("2025-01-01"),
)
.await
.unwrap();
remember(
&engine,
"agent",
"user",
"city",
"Munich",
RememberOptions::new().valid_from("2025-01-01"),
)
.await
.unwrap();
let h = history(&engine, "agent", "user", "city").await.unwrap();
assert_eq!(h.entries.len(), 2, "two claims → two history entries");
assert!(!h.is_empty());
assert_eq!(
h.entries[0].value,
serde_json::json!("Berlin"),
"Berlin must be first (oldest)"
);
assert_eq!(
h.entries[1].value,
serde_json::json!("Munich"),
"Munich must be second (newer)"
);
assert_eq!(
h.entries[0].status,
mempill_types::HistoryEntryStatus::Superseded,
"Berlin must be Superseded"
);
assert_eq!(
h.entries[1].status,
mempill_types::HistoryEntryStatus::Current,
"Munich must be Current"
);
let current = h.current().expect("must have a Current entry");
assert_eq!(current.value, serde_json::json!("Munich"));
let r = recall(&engine, "agent", "user", "city").await.unwrap();
let primary_value = r.primary.as_ref().map(|p| &p.value);
assert_eq!(
primary_value,
Some(&serde_json::json!("Munich")),
"history().current() value must match recall().primary value"
);
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn history_empty_for_unknown_predicate() {
let engine = crate::open_default_in_memory().unwrap();
let h = history(&engine, "agent", "nobody", "nothing").await.unwrap();
assert!(h.is_empty());
assert!(h.current().is_none());
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn recall_contested_candidates_expose_rich_fields() {
let engine = crate::open_default_in_memory().unwrap();
remember(&engine, "agent", "acme", "ceo", serde_json::json!("Alice"), RememberOptions::new())
.await
.unwrap();
remember(&engine, "agent", "acme", "ceo", serde_json::json!("Bob"), RememberOptions::new())
.await
.unwrap();
let r = recall(&engine, "agent", "acme", "ceo").await.unwrap();
assert!(r.is_contested());
assert!(r.primary.is_none(), "Contested belief must not have primary set");
assert_eq!(r.candidates.len(), 2);
for c in &r.candidates {
assert!(!format!("{:?}", c.detail.claim_ref).is_empty());
assert!(!c.detail.provenance.is_empty());
assert!(
c.value == serde_json::json!("Alice") || c.value == serde_json::json!("Bob"),
"unexpected candidate value: {:?}", c.value
);
assert_eq!(c.detail.value, c.value);
}
}
}