1#[cfg(any(test, feature = "test-support"))]
40use std::time::Duration;
41
42#[cfg(any(test, feature = "test-support"))]
43use mempill_types::{
44 AdjudicationResponse, AdjudicationVerdict, AgentId, BeliefStatus, Cardinality, Confidence,
45 Criticality, Disposition, ExternalKind, LedgerEventKind, ProvenanceLabel,
46};
47
48#[cfg(any(test, feature = "test-support"))]
49use crate::{
50 application::{AuditQueryRequest, IngestClaimRequest, QueryMemoryRequest},
51 ports::OraclePort,
52 EngineConfig, EngineHandle,
53};
54
55#[cfg(any(test, feature = "test-support"))]
63pub struct TestOracle {
64 pub fixed_uuid: uuid::Uuid,
67}
68
69#[cfg(any(test, feature = "test-support"))]
70impl OraclePort for TestOracle {
71 type Error = crate::noop::NoOpError;
72 type Handle = uuid::Uuid;
73
74 fn request_adjudication(
75 &self,
76 _agent_id: &AgentId,
77 _request: mempill_types::AdjudicationRequest,
78 ) -> Result<Self::Handle, Self::Error> {
79 Ok(self.fixed_uuid)
80 }
81
82 fn handle_to_uuid(handle: &Self::Handle) -> uuid::Uuid {
83 *handle
84 }
85}
86
87#[cfg(any(test, feature = "test-support"))]
92pub fn ingest_req(agent: &AgentId, value: &str) -> IngestClaimRequest {
93 IngestClaimRequest {
94 agent_id: agent.clone(),
95 subject: "subject".into(),
96 predicate: "predicate".into(),
97 value: serde_json::json!(value),
98 provenance: ProvenanceLabel::External(ExternalKind::UserAsserted),
99 cardinality: Cardinality::Functional,
100 valid_time: None,
101 confidence: Confidence { value_confidence: 0.95, valid_time_confidence: 0.0 },
102 criticality: Criticality::High,
103 derived_from: vec![],
104 }
105}
106
107#[cfg(any(test, feature = "test-support"))]
110pub fn query_req(agent: &AgentId) -> QueryMemoryRequest {
111 QueryMemoryRequest {
112 agent_id: agent.clone(),
113 subject: "subject".into(),
114 predicate: "predicate".into(),
115 as_of_tx_time: None,
116 valid_at: None,
117 }
118}
119
120#[cfg(any(test, feature = "test-support"))]
121fn adj_response(
122 handle_id: uuid::Uuid,
123 verdict: AdjudicationVerdict,
124) -> AdjudicationResponse {
125 AdjudicationResponse {
126 handle_id,
127 verdict,
128 evidence_provenance: ProvenanceLabel::External(ExternalKind::ExternalFirstHand),
129 }
130}
131
132#[cfg(any(test, feature = "test-support"))]
139#[cfg(any(test, feature = "test-support"))]
140pub async fn scenario_affirm_challenger_wins_with_handle<P, O, V>(
141 engine: &EngineHandle<P, O, V>,
142 handle_id: uuid::Uuid,
143) where
144 P: crate::ports::PersistencePort + Send + Sync + 'static,
145 P::Error: std::fmt::Debug,
146 O: OraclePort + Send + Sync + 'static,
147 V: crate::ports::VectorPort + Send + Sync + 'static,
148{
149 let agent = AgentId("conformance-affirm-agent".into());
150
151 let resp_inc = engine.ingest_claim(ingest_req(&agent, "incumbent-value")).await
152 .expect("conformance[affirm]: ingest incumbent must succeed");
153 assert_eq!(resp_inc.disposition, Disposition::CommittedCheap,
154 "conformance[affirm]: incumbent must be CommittedCheap");
155
156 let resp_ch = engine.ingest_claim(ingest_req(&agent, "challenger-value")).await
157 .expect("conformance[affirm]: ingest challenger must succeed");
158 assert_eq!(resp_ch.disposition, Disposition::QueuedForAdjudication,
159 "conformance[affirm]: challenger with oracle present must be QueuedForAdjudication");
160
161 let challenger_ref = resp_ch.claim_ref.clone();
162 let incumbent_ref = resp_inc.claim_ref.clone();
163
164 let outcome = engine.submit_adjudication(
166 handle_id,
167 adj_response(handle_id, AdjudicationVerdict::Affirm),
168 ).await.expect("conformance[affirm]: Affirm submit must succeed");
169
170 assert_eq!(outcome.disposition, Disposition::CommittedCheap,
171 "conformance[affirm]: challenger must be CommittedCheap after Affirm");
172 assert_eq!(outcome.claim_ref, challenger_ref,
173 "conformance[affirm]: outcome.claim_ref must be challenger");
174
175 let qr = engine.query_memory(query_req(&agent)).await
177 .expect("conformance[affirm]: query must succeed");
178 let primary_val = qr.belief.primary.as_ref().map(|b| b.fact.value.clone());
179 assert_ne!(qr.belief.status, BeliefStatus::Contested,
180 "conformance[affirm]: must NOT be Contested after Affirm");
181 assert_ne!(qr.belief.status, BeliefStatus::NoBelief,
182 "conformance[affirm]: must NOT be NoBelief after Affirm");
183 assert_eq!(primary_val, Some(serde_json::json!("challenger-value")),
184 "conformance[affirm]: challenger must be surfaced as primary belief");
185
186 let audit = engine.query_audit(AuditQueryRequest {
188 agent_id: agent.clone(),
189 claim_ref: None,
190 from_tx_time: None,
191 limit: 100,
192 }).await.expect("conformance[affirm]: audit must succeed");
193
194 let ch_entry = audit.entries.iter()
195 .find(|e| e.claim_ref == challenger_ref && e.event_kind == LedgerEventKind::AdjudicationResolved)
196 .expect("conformance[affirm]: AdjudicationResolved entry for challenger must exist");
197 assert_eq!(ch_entry.disposition, Disposition::CommittedCheap,
198 "conformance[affirm]: ledger entry disposition must be CommittedCheap");
199 let rationale = ch_entry.rationale.as_ref().map(|r| r.to_string()).unwrap_or_default();
200 assert!(rationale.contains("ExternalFirstHand"),
201 "conformance[affirm]: Affirm rationale must contain ExternalFirstHand provenance");
202
203 let inc_entry = audit.entries.iter()
205 .find(|e| e.claim_ref == incumbent_ref && e.disposition == Disposition::Superseded)
206 .expect("conformance[affirm]: incumbent Superseded entry must exist");
207 assert_eq!(inc_entry.disposition, Disposition::Superseded,
208 "conformance[affirm]: incumbent must be Superseded");
209}
210
211#[cfg(any(test, feature = "test-support"))]
216pub async fn scenario_deny_incumbent_stands<P, O, V>(
217 engine: &EngineHandle<P, O, V>,
218 handle_id: uuid::Uuid,
219) where
220 P: crate::ports::PersistencePort + Send + Sync + 'static,
221 P::Error: std::fmt::Debug,
222 O: OraclePort + Send + Sync + 'static,
223 V: crate::ports::VectorPort + Send + Sync + 'static,
224{
225 let agent = AgentId("conformance-deny-agent".into());
226
227 let resp_inc = engine.ingest_claim(ingest_req(&agent, "incumbent-deny")).await
228 .expect("conformance[deny]: ingest incumbent");
229 assert_eq!(resp_inc.disposition, Disposition::CommittedCheap);
230
231 let resp_ch = engine.ingest_claim(ingest_req(&agent, "challenger-deny")).await
232 .expect("conformance[deny]: ingest challenger");
233 assert_eq!(resp_ch.disposition, Disposition::QueuedForAdjudication);
234
235 let challenger_ref = resp_ch.claim_ref.clone();
236
237 let outcome = engine.submit_adjudication(
238 handle_id,
239 adj_response(handle_id, AdjudicationVerdict::Deny),
240 ).await.expect("conformance[deny]: Deny submit must succeed");
241
242 assert_eq!(outcome.disposition, Disposition::Superseded,
243 "conformance[deny]: challenger must be Superseded after Deny");
244 assert_eq!(outcome.claim_ref, challenger_ref,
245 "conformance[deny]: outcome.claim_ref must be challenger");
246
247 let qr = engine.query_memory(query_req(&agent)).await
249 .expect("conformance[deny]: query must succeed");
250 let primary_val = qr.belief.primary.as_ref().map(|b| b.fact.value.clone());
251 assert_ne!(qr.belief.status, BeliefStatus::Contested,
252 "conformance[deny]: must NOT be Contested after Deny");
253 assert_ne!(qr.belief.status, BeliefStatus::NoBelief,
254 "conformance[deny]: must NOT be NoBelief after Deny");
255 assert_eq!(primary_val, Some(serde_json::json!("incumbent-deny")),
256 "conformance[deny]: incumbent must be surfaced after Deny");
257}
258
259#[cfg(any(test, feature = "test-support"))]
264pub async fn scenario_unknown_stays_contested<P, O, V>(
265 engine: &EngineHandle<P, O, V>,
266 handle_id: uuid::Uuid,
267) where
268 P: crate::ports::PersistencePort + Send + Sync + 'static,
269 P::Error: std::fmt::Debug,
270 O: OraclePort + Send + Sync + 'static,
271 V: crate::ports::VectorPort + Send + Sync + 'static,
272{
273 let agent = AgentId("conformance-unknown-agent".into());
274
275 let resp_inc = engine.ingest_claim(ingest_req(&agent, "incumbent-unknown")).await
276 .expect("conformance[unknown]: ingest incumbent");
277 let incumbent_ref = resp_inc.claim_ref.clone();
278 assert_eq!(resp_inc.disposition, Disposition::CommittedCheap);
279
280 let resp_ch = engine.ingest_claim(ingest_req(&agent, "challenger-unknown")).await
281 .expect("conformance[unknown]: ingest challenger");
282 let challenger_ref = resp_ch.claim_ref.clone();
283 assert_eq!(resp_ch.disposition, Disposition::QueuedForAdjudication);
284
285 let outcome = engine.submit_adjudication(
286 handle_id,
287 adj_response(handle_id, AdjudicationVerdict::Unknown),
288 ).await.expect("conformance[unknown]: Unknown submit must succeed");
289
290 assert_eq!(outcome.disposition, Disposition::Contested,
291 "conformance[unknown]: outcome must be Contested after Unknown");
292 assert_eq!(outcome.claim_ref, challenger_ref);
293
294 let qr = engine.query_memory(query_req(&agent)).await
296 .expect("conformance[unknown]: query must succeed");
297 assert_eq!(qr.belief.status, BeliefStatus::Contested,
298 "conformance[unknown]: must be Contested after Unknown");
299 let all_vals: Vec<_> = qr.belief.primary.iter()
300 .map(|b| b.fact.value.clone())
301 .chain(qr.belief.alternatives.iter().map(|b| b.fact.value.clone()))
302 .collect();
303 assert!(all_vals.contains(&serde_json::json!("incumbent-unknown")),
304 "conformance[unknown]: incumbent must be visible in Contested");
305 assert!(all_vals.contains(&serde_json::json!("challenger-unknown")),
306 "conformance[unknown]: challenger must be visible in Contested");
307
308 let second = engine.submit_adjudication(
310 handle_id,
311 adj_response(handle_id, AdjudicationVerdict::Unknown),
312 ).await;
313 assert!(
314 matches!(second, Err(crate::error::MemError::AdjudicationHandleNotFound { .. })),
315 "conformance[unknown]: second submit on consumed handle must be AdjudicationHandleNotFound; got {second:?}"
316 );
317
318 let audit = engine.query_audit(AuditQueryRequest {
320 agent_id: agent.clone(),
321 claim_ref: None,
322 from_tx_time: None,
323 limit: 100,
324 }).await.expect("conformance[unknown]: audit must succeed");
325 let resolved: Vec<_> = audit.entries.iter()
326 .filter(|e| e.event_kind == LedgerEventKind::AdjudicationResolved)
327 .collect();
328 assert_eq!(resolved.len(), 2,
329 "conformance[unknown]: Unknown must produce 2 AdjudicationResolved entries");
330 let has_inc = resolved.iter().any(|e| e.claim_ref == incumbent_ref && e.disposition == Disposition::Contested);
331 let has_ch = resolved.iter().any(|e| e.claim_ref == challenger_ref && e.disposition == Disposition::Contested);
332 assert!(has_inc, "conformance[unknown]: incumbent AdjudicationResolved/Contested must exist");
333 assert!(has_ch, "conformance[unknown]: challenger AdjudicationResolved/Contested must exist");
334}
335
336#[cfg(any(test, feature = "test-support"))]
341pub async fn scenario_queued_surfaces_contested<P, O, V>(
342 engine: &EngineHandle<P, O, V>,
343) where
344 P: crate::ports::PersistencePort + Send + Sync + 'static,
345 P::Error: std::fmt::Debug,
346 O: OraclePort + Send + Sync + 'static,
347 V: crate::ports::VectorPort + Send + Sync + 'static,
348{
349 let agent = AgentId("conformance-queued-agent".into());
350
351 let resp_inc = engine.ingest_claim(ingest_req(&agent, "queued-incumbent")).await
352 .expect("conformance[queued]: ingest incumbent");
353 assert_eq!(resp_inc.disposition, Disposition::CommittedCheap);
354
355 let resp_ch = engine.ingest_claim(ingest_req(&agent, "queued-challenger")).await
356 .expect("conformance[queued]: ingest challenger");
357 assert_eq!(resp_ch.disposition, Disposition::QueuedForAdjudication,
358 "conformance[queued]: challenger with oracle must be QueuedForAdjudication");
359
360 let qr = engine.query_memory(query_req(&agent)).await
362 .expect("conformance[queued]: query must succeed");
363 assert_eq!(qr.belief.status, BeliefStatus::Contested,
364 "conformance[queued]: BEFORE any submit, belief must be Contested (I7)");
365 let all_vals: Vec<_> = qr.belief.primary.iter()
366 .map(|b| b.fact.value.clone())
367 .chain(qr.belief.alternatives.iter().map(|b| b.fact.value.clone()))
368 .collect();
369 assert!(all_vals.contains(&serde_json::json!("queued-incumbent")),
370 "conformance[queued]: incumbent must be visible in pre-submit Contested");
371 assert!(all_vals.contains(&serde_json::json!("queued-challenger")),
372 "conformance[queued]: challenger must be visible in pre-submit Contested");
373}
374
375#[cfg(any(test, feature = "test-support"))]
380pub async fn scenario_stale_handle_not_found<P, O, V>(
381 engine: &EngineHandle<P, O, V>,
382) where
383 P: crate::ports::PersistencePort + Send + Sync + 'static,
384 P::Error: std::fmt::Debug,
385 O: OraclePort + Send + Sync + 'static,
386 V: crate::ports::VectorPort + Send + Sync + 'static,
387{
388 let random_handle = uuid::Uuid::new_v4();
389 let result = engine.submit_adjudication(
390 random_handle,
391 adj_response(random_handle, AdjudicationVerdict::Affirm),
392 ).await;
393 assert!(
394 matches!(result, Err(crate::error::MemError::AdjudicationHandleNotFound { .. })),
395 "conformance[stale-handle]: random/unknown handle must return AdjudicationHandleNotFound; got {result:?}"
396 );
397}
398
399#[cfg(any(test, feature = "test-support"))]
404pub async fn scenario_duplicate_submit_not_found<P, O, V>(
405 engine: &EngineHandle<P, O, V>,
406 handle_id: uuid::Uuid,
407) where
408 P: crate::ports::PersistencePort + Send + Sync + 'static,
409 P::Error: std::fmt::Debug,
410 O: OraclePort + Send + Sync + 'static,
411 V: crate::ports::VectorPort + Send + Sync + 'static,
412{
413 let agent = AgentId("conformance-dup-agent".into());
414
415 engine.ingest_claim(ingest_req(&agent, "dup-incumbent")).await
416 .expect("conformance[dup]: ingest incumbent");
417 engine.ingest_claim(ingest_req(&agent, "dup-challenger")).await
418 .expect("conformance[dup]: ingest challenger");
419
420 engine.submit_adjudication(handle_id, adj_response(handle_id, AdjudicationVerdict::Affirm)).await
422 .expect("conformance[dup]: first submit must succeed");
423
424 let second = engine.submit_adjudication(handle_id, adj_response(handle_id, AdjudicationVerdict::Affirm)).await;
426 assert!(
427 matches!(second, Err(crate::error::MemError::AdjudicationHandleNotFound { .. })),
428 "conformance[dup]: duplicate submit must return AdjudicationHandleNotFound; got {second:?}"
429 );
430}
431
432#[cfg(any(test, feature = "test-support"))]
437pub async fn scenario_ttl_expiry_reverts_contested<P, O, V>(
438 engine: &EngineHandle<P, O, V>,
439 handle_id: uuid::Uuid,
440) where
441 P: crate::ports::PersistencePort + Send + Sync + 'static,
442 P::Error: std::fmt::Debug,
443 O: OraclePort + Send + Sync + 'static,
444 V: crate::ports::VectorPort + Send + Sync + 'static,
445{
446 let agent = AgentId("conformance-ttl-agent".into());
447
448 let resp_inc = engine.ingest_claim(ingest_req(&agent, "ttl-incumbent")).await
449 .expect("conformance[ttl]: ingest incumbent");
450 assert_eq!(resp_inc.disposition, Disposition::CommittedCheap);
451
452 let resp_ch = engine.ingest_claim(ingest_req(&agent, "ttl-challenger")).await
453 .expect("conformance[ttl]: ingest challenger");
454 assert_eq!(resp_ch.disposition, Disposition::QueuedForAdjudication);
455
456 tokio::time::sleep(tokio::time::Duration::from_millis(5)).await;
458
459 let result = engine.submit_adjudication(
461 handle_id,
462 adj_response(handle_id, AdjudicationVerdict::Affirm),
463 ).await;
464 assert!(
465 matches!(result, Err(crate::error::MemError::AdjudicationHandleNotFound { .. })),
466 "conformance[ttl]: expired handle must return AdjudicationHandleNotFound; got {result:?}"
467 );
468
469 let qr = engine.query_memory(query_req(&agent)).await
471 .expect("conformance[ttl]: query must succeed");
472 assert_eq!(qr.belief.status, BeliefStatus::Contested,
473 "conformance[ttl]: after TTL expiry, must be Contested");
474 let all_vals: Vec<_> = qr.belief.primary.iter()
475 .map(|b| b.fact.value.clone())
476 .chain(qr.belief.alternatives.iter().map(|b| b.fact.value.clone()))
477 .collect();
478 assert!(all_vals.contains(&serde_json::json!("ttl-incumbent")),
479 "conformance[ttl]: incumbent must be visible in Contested after expiry");
480 assert!(all_vals.contains(&serde_json::json!("ttl-challenger")),
481 "conformance[ttl]: challenger must be visible in Contested after expiry");
482
483 let audit = engine.query_audit(AuditQueryRequest {
485 agent_id: agent.clone(),
486 claim_ref: None,
487 from_tx_time: None,
488 limit: 100,
489 }).await.expect("conformance[ttl]: audit must succeed");
490 let has_expiry_entry = audit.entries.iter().any(|e| {
492 e.claim_ref == resp_ch.claim_ref
493 && (e.event_kind == LedgerEventKind::AdjudicationExpired
494 || e.disposition == Disposition::Contested)
495 });
496 assert!(has_expiry_entry,
497 "conformance[ttl]: ledger must have an expiry entry for the challenger; entries={:?}",
498 audit.entries.iter().map(|e| (&e.claim_ref, &e.event_kind, &e.disposition)).collect::<Vec<_>>()
499 );
500}
501
502#[cfg(any(test, feature = "test-support"))]
507pub async fn scenario_sweep_reverts_expired<P, O, V>(
508 engine: &EngineHandle<P, O, V>,
509) where
510 P: crate::ports::PersistencePort + Send + Sync + 'static,
511 P::Error: std::fmt::Debug,
512 O: OraclePort + Send + Sync + 'static,
513 V: crate::ports::VectorPort + Send + Sync + 'static,
514{
515 let agent = AgentId("conformance-sweep-exp-agent".into());
516
517 engine.ingest_claim(ingest_req(&agent, "sweep-exp-incumbent")).await
518 .expect("conformance[sweep-exp]: ingest incumbent");
519 let resp_ch = engine.ingest_claim(ingest_req(&agent, "sweep-exp-challenger")).await
520 .expect("conformance[sweep-exp]: ingest challenger");
521 assert_eq!(resp_ch.disposition, Disposition::QueuedForAdjudication);
522
523 tokio::time::sleep(tokio::time::Duration::from_millis(5)).await;
524
525 let swept = engine.sweep_expired_adjudications().await
526 .expect("conformance[sweep-exp]: sweep must succeed");
527 assert!(swept >= 1,
528 "conformance[sweep-exp]: sweep must revert at least 1 expired row; got {swept}");
529
530 let qr = engine.query_memory(query_req(&agent)).await
531 .expect("conformance[sweep-exp]: query must succeed");
532 assert_eq!(qr.belief.status, BeliefStatus::Contested,
533 "conformance[sweep-exp]: after sweep, must be Contested");
534 let all_vals: Vec<_> = qr.belief.primary.iter()
535 .map(|b| b.fact.value.clone())
536 .chain(qr.belief.alternatives.iter().map(|b| b.fact.value.clone()))
537 .collect();
538 assert!(all_vals.contains(&serde_json::json!("sweep-exp-incumbent")),
539 "conformance[sweep-exp]: incumbent must be visible after sweep");
540 assert!(all_vals.contains(&serde_json::json!("sweep-exp-challenger")),
541 "conformance[sweep-exp]: challenger must be visible after sweep");
542}
543
544#[cfg(any(test, feature = "test-support"))]
558pub async fn scenario_sweep_recovers_orphan<P, O, V>(
559 engine: &EngineHandle<P, O, V>,
560 agent_name: &str,
561) where
562 P: crate::ports::PersistencePort + Send + Sync + 'static,
563 P::Error: std::fmt::Debug,
564 O: OraclePort + Send + Sync + 'static,
565 V: crate::ports::VectorPort + Send + Sync + 'static,
566{
567 let agent = AgentId(agent_name.into());
569
570 let swept = engine.sweep_expired_adjudications().await
571 .expect("conformance[sweep-orphan]: sweep must succeed");
572 assert!(swept >= 1,
573 "conformance[sweep-orphan]: sweep must recover at least 1 orphaned claim; got {swept}");
574
575 let qr = engine.query_memory(query_req(&agent)).await
576 .expect("conformance[sweep-orphan]: query must succeed");
577 assert_eq!(qr.belief.status, BeliefStatus::Contested,
578 "conformance[sweep-orphan]: after orphan recovery, must be Contested");
579 let all_vals: Vec<_> = qr.belief.primary.iter()
580 .map(|b| b.fact.value.clone())
581 .chain(qr.belief.alternatives.iter().map(|b| b.fact.value.clone()))
582 .collect();
583 assert!(all_vals.contains(&serde_json::json!("orphan-incumbent")),
584 "conformance[sweep-orphan]: incumbent must be visible; got {all_vals:?}");
585 assert!(all_vals.contains(&serde_json::json!("orphan-challenger")),
586 "conformance[sweep-orphan]: orphaned challenger must be visible; got {all_vals:?}");
587}
588
589#[cfg(any(test, feature = "test-support"))]
598pub async fn scenario_durable_store_survives_reopen<P, O, V>(
599 engine1: EngineHandle<P, O, V>,
600 build_engine2: impl FnOnce() -> EngineHandle<P, O, V>,
601 handle_id: uuid::Uuid,
602) where
603 P: crate::ports::PersistencePort + Send + Sync + 'static,
604 P::Error: std::fmt::Debug,
605 O: OraclePort + Send + Sync + 'static,
606 V: crate::ports::VectorPort + Send + Sync + 'static,
607{
608 let agent = AgentId("conformance-reopen-agent".into());
609
610 let resp_inc = engine1.ingest_claim(ingest_req(&agent, "reopen-incumbent")).await
612 .expect("conformance[reopen]: ingest incumbent on engine-1");
613 assert_eq!(resp_inc.disposition, Disposition::CommittedCheap);
614
615 let resp_ch = engine1.ingest_claim(ingest_req(&agent, "reopen-challenger")).await
616 .expect("conformance[reopen]: ingest challenger on engine-1");
617 assert_eq!(resp_ch.disposition, Disposition::QueuedForAdjudication);
618
619 let challenger_ref = resp_ch.claim_ref.clone();
620
621 drop(engine1);
623
624 let engine2 = build_engine2();
626
627 let outcome = engine2.submit_adjudication(
629 handle_id,
630 adj_response(handle_id, AdjudicationVerdict::Affirm),
631 ).await.expect("conformance[reopen]: Affirm on pre-restart handle must succeed");
632 assert_eq!(outcome.disposition, Disposition::CommittedCheap,
633 "conformance[reopen]: challenger must be CommittedCheap after cross-restart Affirm");
634 assert_eq!(outcome.claim_ref, challenger_ref,
635 "conformance[reopen]: outcome.claim_ref must be challenger");
636
637 let qr = engine2.query_memory(query_req(&agent)).await
639 .expect("conformance[reopen]: query on engine-2 must succeed");
640 let primary_val = qr.belief.primary.as_ref().map(|b| b.fact.value.clone());
641 assert_eq!(primary_val, Some(serde_json::json!("reopen-challenger")),
642 "conformance[reopen]: challenger must be surfaced after cross-restart Affirm");
643}
644
645#[cfg(any(test, feature = "test-support"))]
653pub async fn scenario_atomicity_no_torn_write<P, O, V>(
654 engine: &EngineHandle<P, O, V>,
655 handle_id: uuid::Uuid,
656) where
657 P: crate::ports::PersistencePort + Send + Sync + 'static,
658 P::Error: std::fmt::Debug,
659 O: OraclePort + Send + Sync + 'static,
660 V: crate::ports::VectorPort + Send + Sync + 'static,
661{
662 let agent = AgentId("conformance-atomicity-agent".into());
663
664 engine.ingest_claim(ingest_req(&agent, "atom-incumbent")).await
665 .expect("conformance[atomicity]: ingest incumbent");
666 let resp_ch = engine.ingest_claim(ingest_req(&agent, "atom-challenger")).await
667 .expect("conformance[atomicity]: ingest challenger");
668 let challenger_ref = resp_ch.claim_ref.clone();
669
670 let outcome = engine.submit_adjudication(
671 handle_id,
672 adj_response(handle_id, AdjudicationVerdict::Affirm),
673 ).await.expect("conformance[atomicity]: Affirm submit must succeed");
674
675 assert_eq!(outcome.disposition, Disposition::CommittedCheap,
677 "conformance[atomicity]: challenger disposition must be CommittedCheap");
678 assert_eq!(outcome.claim_ref, challenger_ref);
679
680 let second = engine.submit_adjudication(
682 handle_id,
683 adj_response(handle_id, AdjudicationVerdict::Affirm),
684 ).await;
685 assert!(
686 matches!(second, Err(crate::error::MemError::AdjudicationHandleNotFound { .. })),
687 "conformance[atomicity]: pending row must be consumed (not found on second submit)"
688 );
689
690 let qr = engine.query_memory(query_req(&agent)).await
692 .expect("conformance[atomicity]: query must succeed");
693 assert_ne!(qr.belief.status, BeliefStatus::Contested,
694 "conformance[atomicity]: after Affirm, must NOT be Contested");
695 assert_ne!(qr.belief.status, BeliefStatus::NoBelief,
696 "conformance[atomicity]: after Affirm, must NOT be NoBelief");
697
698 let audit = engine.query_audit(AuditQueryRequest {
700 agent_id: agent.clone(),
701 claim_ref: None,
702 from_tx_time: None,
703 limit: 100,
704 }).await.expect("conformance[atomicity]: audit must succeed");
705 let resolved = audit.entries.iter()
706 .find(|e| e.claim_ref == challenger_ref && e.event_kind == LedgerEventKind::AdjudicationResolved)
707 .expect("conformance[atomicity]: AdjudicationResolved ledger entry must exist");
708 assert_eq!(resolved.disposition, Disposition::CommittedCheap,
709 "conformance[atomicity]: ledger entry must be CommittedCheap");
710}
711
712#[cfg(any(test, feature = "test-support"))]
719pub async fn scenario_ledger_entry_expectations<P, O, V>(
720 engine: &EngineHandle<P, O, V>,
721 handle_id: uuid::Uuid,
722 verdict: AdjudicationVerdict,
723 expected_ch_disposition: Disposition,
724 expected_ch_event_kind: LedgerEventKind,
725) where
726 P: crate::ports::PersistencePort + Send + Sync + 'static,
727 P::Error: std::fmt::Debug,
728 O: OraclePort + Send + Sync + 'static,
729 V: crate::ports::VectorPort + Send + Sync + 'static,
730{
731 let label = format!("{verdict:?}");
732 let agent = AgentId(format!("conformance-ledger-{label}-agent"));
733
734 engine.ingest_claim(ingest_req(&agent, "ledger-incumbent")).await
735 .expect("conformance[ledger]: ingest incumbent");
736 let resp_ch = engine.ingest_claim(ingest_req(&agent, "ledger-challenger")).await
737 .expect("conformance[ledger]: ingest challenger");
738 let challenger_ref = resp_ch.claim_ref.clone();
739
740 engine.submit_adjudication(handle_id, adj_response(handle_id, verdict)).await
741 .expect("conformance[ledger]: submit must succeed");
742
743 let audit = engine.query_audit(AuditQueryRequest {
744 agent_id: agent.clone(),
745 claim_ref: None,
746 from_tx_time: None,
747 limit: 100,
748 }).await.expect("conformance[ledger]: audit must succeed");
749
750 let ch_entry = audit.entries.iter()
752 .find(|e| e.claim_ref == challenger_ref && e.event_kind == expected_ch_event_kind)
753 .unwrap_or_else(|| panic!(
754 "conformance[ledger/{label}]: expected {:?} event kind for challenger; entries={:?}",
755 expected_ch_event_kind,
756 audit.entries.iter().map(|e| (&e.event_kind, &e.disposition)).collect::<Vec<_>>()
757 ));
758 assert_eq!(ch_entry.disposition, expected_ch_disposition,
759 "conformance[ledger/{label}]: challenger disposition must be {expected_ch_disposition:?}");
760}
761
762#[cfg(any(test, feature = "test-support"))]
767pub async fn scenario_b11_oracle_absent_contested<P, O, V>(
768 engine: &EngineHandle<P, O, V>,
769) where
770 P: crate::ports::PersistencePort + Send + Sync + 'static,
771 P::Error: std::fmt::Debug,
772 O: OraclePort + Send + Sync + 'static,
773 V: crate::ports::VectorPort + Send + Sync + 'static,
774{
775 let agent = AgentId("conformance-b11-agent".into());
776
777 let resp_inc = engine.ingest_claim(ingest_req(&agent, "b11-incumbent")).await
778 .expect("conformance[b11]: ingest incumbent");
779 assert_eq!(resp_inc.disposition, Disposition::CommittedCheap);
780
781 let resp_ch = engine.ingest_claim(ingest_req(&agent, "b11-challenger")).await
782 .expect("conformance[b11]: ingest challenger");
783 assert_eq!(resp_ch.disposition, Disposition::Contested,
784 "conformance[b11]: oracle-absent External conflict MUST be Contested immediately");
785
786 let qr = engine.query_memory(query_req(&agent)).await
787 .expect("conformance[b11]: query must succeed");
788 assert_eq!(qr.belief.status, BeliefStatus::Contested,
789 "conformance[b11]: query_memory after oracle-absent conflict must be Contested");
790 let all_vals: Vec<_> = qr.belief.primary.iter()
791 .map(|b| b.fact.value.clone())
792 .chain(qr.belief.alternatives.iter().map(|b| b.fact.value.clone()))
793 .collect();
794 assert!(all_vals.contains(&serde_json::json!("b11-incumbent")),
795 "conformance[b11]: incumbent must be visible in Contested");
796 assert!(all_vals.contains(&serde_json::json!("b11-challenger")),
797 "conformance[b11]: challenger must be visible in Contested");
798}
799
800#[cfg(any(test, feature = "test-support"))]
805pub fn tiny_ttl_config() -> EngineConfig {
806 EngineConfig {
807 default_adjudication_ttl: Some(Duration::from_nanos(1)),
808 ..EngineConfig::default()
809 }
810}