pub mod common;
use nodedb_array::sync::op_codec;
use nodedb_lite::sync::array::inbound::outcome::InboundOutcome;
use nodedb_types::sync::wire::array::{ArrayRejectMsg, ArrayRejectReason};
async fn enqueue_and_get_hlc_bytes(harness: &common::SyncHarness, array: &str) -> [u8; 18] {
let schema_hlc = harness.schema_hlc(array);
let rep = common::replica(7);
let op = common::put_op(array, 1, 55.0, 300, schema_hlc, rep);
harness
.pending
.enqueue(&op)
.await
.expect("enqueue pending op");
op.header.hlc.to_bytes()
}
#[tokio::test(flavor = "multi_thread")]
async fn reject_array_unknown_keeps_the_op_queued() {
let harness = common::SyncHarness::new_in_memory().await;
harness.create_array("rej_arr").await;
let hlc_bytes = enqueue_and_get_hlc_bytes(&harness, "rej_arr").await;
let before = harness.pending.len().await.expect("len");
assert_eq!(before, 1, "one op must be pending before reject");
let reject_msg = ArrayRejectMsg {
array: "rej_arr".into(),
op_hlc_bytes: hlc_bytes,
reason: ArrayRejectReason::ArrayUnknown,
detail: "array not found on Origin".into(),
};
let outcome = harness
.inbound
.handle_reject(&reject_msg)
.await
.expect("handle_reject");
assert_eq!(
outcome,
InboundOutcome::RejectAcknowledged,
"reject must return RejectAcknowledged"
);
let after = harness.pending.len().await.expect("len");
assert_eq!(after, 1, "the op waits for a schema re-send");
}
#[tokio::test(flavor = "multi_thread")]
async fn reject_shape_invalid_drops_from_pending() {
let harness = common::SyncHarness::new_in_memory().await;
harness.create_array("rej_shape").await;
let hlc_bytes = enqueue_and_get_hlc_bytes(&harness, "rej_shape").await;
let reject_msg = ArrayRejectMsg {
array: "rej_shape".into(),
op_hlc_bytes: hlc_bytes,
reason: ArrayRejectReason::ShapeInvalid,
detail: "shape validation failed".into(),
};
let outcome = harness
.inbound
.handle_reject(&reject_msg)
.await
.expect("handle_reject");
assert_eq!(outcome, InboundOutcome::RejectAcknowledged);
assert_eq!(
harness.pending.len().await.expect("len"),
0,
"a permanently refused op is dropped"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn reject_retention_floor_marks_catchup_needed() {
let harness = common::SyncHarness::new_in_memory().await;
harness.create_array("ret_floor").await;
let hlc_bytes = enqueue_and_get_hlc_bytes(&harness, "ret_floor").await;
let reject_msg = ArrayRejectMsg {
array: "ret_floor".into(),
op_hlc_bytes: hlc_bytes,
reason: ArrayRejectReason::RetentionFloor,
detail: "op predates retention window".into(),
};
harness
.inbound
.handle_reject(&reject_msg)
.await
.expect("handle_reject");
let needs = harness.catchup.arrays_needing_catchup();
assert!(
needs.contains(&"ret_floor".to_owned()),
"RetentionFloor reject must flag the array as needing catchup; got: {needs:?}"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn reject_missing_op_is_idempotent() {
let harness = common::SyncHarness::new_in_memory().await;
harness.create_array("ghost").await;
let rep = common::replica(1);
let schema_hlc = harness.schema_hlc("ghost");
let op = common::put_op("ghost", 0, 0.0, 1, schema_hlc, rep);
let reject_msg = ArrayRejectMsg {
array: "ghost".into(),
op_hlc_bytes: op.header.hlc.to_bytes(),
reason: ArrayRejectReason::ArrayUnknown,
detail: "ghost op".into(),
};
let outcome = harness
.inbound
.handle_reject(&reject_msg)
.await
.expect("handle_reject must not fail for missing op");
assert_eq!(outcome, InboundOutcome::RejectAcknowledged);
}
#[tokio::test(flavor = "multi_thread")]
async fn schema_too_new_surfaces_as_rejected_outcome() {
use nodedb_array::sync::apply::ApplyRejection;
use nodedb_array::sync::hlc::Hlc;
use nodedb_array::sync::op::{ArrayOp, ArrayOpHeader, ArrayOpKind};
use nodedb_array::sync::replica_id::ReplicaId;
use nodedb_array::types::cell_value::value::CellValue;
use nodedb_array::types::coord::value::CoordValue;
use nodedb_types::sync::wire::array::ArrayDeltaMsg;
let harness = common::SyncHarness::new_in_memory().await;
harness.create_array("schema_rej").await;
let future_schema_hlc = Hlc::new(u64::MAX >> 16, 0, ReplicaId::new(99)).expect("valid HLC");
let op = ArrayOp {
header: ArrayOpHeader {
array: "schema_rej".into(),
hlc: common::hlc1(10),
schema_hlc: future_schema_hlc,
valid_from_ms: 0,
valid_until_ms: -1,
system_from_ms: 10,
},
kind: ArrayOpKind::Put,
coord: vec![CoordValue::Int64(0)],
attrs: Some(vec![CellValue::Float64(0.0)]),
};
let payload = op_codec::encode_op(&op).expect("encode_op");
let msg = ArrayDeltaMsg {
array: "schema_rej".into(),
op_payload: payload,
producer_id: 0,
epoch: 0,
seq: 0,
};
let outcome = harness.inbound.handle_delta(&msg).expect("handle_delta");
assert!(
matches!(
outcome,
InboundOutcome::Rejected(ApplyRejection::SchemaTooNew { .. })
),
"future schema_hlc must yield SchemaTooNew rejection, got: {outcome:?}"
);
}