use either::Either;
use freenet_stdlib::prelude::*;
use std::sync::Arc;
use std::sync::atomic::AtomicU64;
use crate::config::ConfigArgs;
use crate::contract::UpsertResult;
use crate::contract::executor::mock_wasm_runtime::UpdateOverride;
use crate::contract::executor::{ContractExecutor, Executor, OperationMode};
use crate::node::OpManager;
use crate::wasm_runtime::MockStateStorage;
use super::super::mock_runtime::test::create_test_contract as test_contract;
async fn build_op_manager(id: &str) -> (Arc<OpManager>, Box<dyn std::any::Any>) {
let config_args = ConfigArgs {
id: Some(id.to_string()),
mode: Some(OperationMode::Local),
..Default::default()
};
let node_config =
crate::node::NodeConfig::new(config_args.build().await.expect("build Config"))
.await
.expect("build NodeConfig");
let (notification_rx, notification_tx) = crate::node::event_loop_notification_channel();
let (ops_ch_channel, ch_channel, wait_for_event) = crate::contract::contract_handler_channel();
let connection_manager = crate::ring::ConnectionManager::new(&node_config);
let (result_router_tx, result_router_rx) = tokio::sync::mpsc::channel(100);
let task_monitor = crate::node::background_task_monitor::BackgroundTaskMonitor::new();
let op_manager = Arc::new(
OpManager::new(
notification_tx,
ops_ch_channel,
&node_config,
crate::tracing::DynamicRegister::new(vec![]),
connection_manager,
result_router_tx,
&task_monitor,
)
.expect("build OpManager"),
);
op_manager.ring.attach_op_manager(&op_manager);
let guards: Box<dyn std::any::Any> = Box::new((
notification_rx,
ch_channel,
wait_for_event,
result_router_rx,
task_monitor,
));
(op_manager, guards)
}
fn distinct_bytes_state() -> WrappedState {
WrappedState::new((1u8..=32).collect::<Vec<u8>>())
}
async fn build_executor(
op_manager: &Arc<OpManager>,
) -> Executor<crate::contract::executor::mock_wasm_runtime::MockWasmRuntime, MockStateStorage> {
Executor::new_mock_wasm("t", MockStateStorage::new(), None, Some(op_manager.clone()))
.await
.expect("build mock-wasm executor")
}
#[tokio::test(flavor = "current_thread")]
async fn identical_input_reapply_flags_non_idempotent_contract_on_first_apply() {
let (op_manager, _guards) = build_op_manager("nonidem-identity-flag").await;
let contract = test_contract(b"nonidem_identity_contract");
let key = contract.key();
let state = distinct_bytes_state();
let mut executor = build_executor(&op_manager).await;
let counter = Arc::new(AtomicU64::new(0));
executor
.mock_runtime_mut()
.update_overrides
.insert(*key.id(), UpdateOverride::NonIdempotent(counter));
executor
.upsert_contract_state(
key,
Either::Left(state.clone()),
RelatedContracts::default(),
Some(contract.clone()),
)
.await
.expect("initial PUT");
assert!(
!op_manager.ring.is_contract_broken(&key),
"no flag before any identical re-apply — the install path proves nothing"
);
let result = executor
.upsert_contract_state(
key,
Either::Left(state.clone()),
RelatedContracts::default(),
None,
)
.await
.expect("identical re-apply");
assert!(
matches!(result, UpsertResult::NoChange),
"identical re-apply must report NoChange, got {result:?}"
);
assert!(
op_manager.ring.is_contract_broken(&key),
"identity probe must flag the non-idempotent contract on the FIRST \
identical apply (no 1/32 sampling)"
);
}
#[tokio::test(flavor = "current_thread")]
async fn identical_input_reapply_does_not_flag_healthy_contract() {
let (op_manager, _guards) = build_op_manager("nonidem-identity-healthy").await;
let contract = test_contract(b"healthy_identity_contract");
let key = contract.key();
let state = distinct_bytes_state();
let mut executor = build_executor(&op_manager).await;
executor
.upsert_contract_state(
key,
Either::Left(state.clone()),
RelatedContracts::default(),
Some(contract.clone()),
)
.await
.expect("initial PUT");
let result = executor
.upsert_contract_state(
key,
Either::Left(state.clone()),
RelatedContracts::default(),
None,
)
.await
.expect("identical re-apply");
assert!(matches!(result, UpsertResult::NoChange));
assert!(
!op_manager.ring.is_contract_broken(&key),
"a correctly-idempotent contract must NOT be flagged by the identity probe"
);
}
#[tokio::test(flavor = "current_thread")]
async fn identical_input_reorder_only_is_not_flagged() {
let (op_manager, _guards) = build_op_manager("nonidem-identity-reorder").await;
let contract = test_contract(b"reorder_identity_contract");
let key = contract.key();
let state = distinct_bytes_state();
let mut executor = build_executor(&op_manager).await;
executor
.mock_runtime_mut()
.update_overrides
.insert(*key.id(), UpdateOverride::ReorderBytes);
executor
.upsert_contract_state(
key,
Either::Left(state.clone()),
RelatedContracts::default(),
Some(contract.clone()),
)
.await
.expect("initial PUT");
let result = executor
.upsert_contract_state(
key,
Either::Left(state.clone()),
RelatedContracts::default(),
None,
)
.await
.expect("identical re-apply");
assert!(matches!(result, UpsertResult::NoChange));
assert!(
!op_manager.ring.is_contract_broken(&key),
"a reordering-only re-merge (same byte multiset) is the #4295 \
false-positive shape and must NOT be flagged"
);
}
#[tokio::test(flavor = "current_thread")]
async fn identical_input_canonicalize_once_is_not_flagged() {
let (op_manager, _guards) = build_op_manager("nonidem-identity-canonicalize").await;
let contract = test_contract(b"canonicalize_identity_contract");
let key = contract.key();
let mut raw = vec![0xFFu8, 0xFF];
raw.extend(1u8..=16);
let state = WrappedState::new(raw);
let mut executor = build_executor(&op_manager).await;
executor
.mock_runtime_mut()
.update_overrides
.insert(*key.id(), UpdateOverride::CanonicalizeOnce);
executor
.upsert_contract_state(
key,
Either::Left(state.clone()),
RelatedContracts::default(),
Some(contract.clone()),
)
.await
.expect("initial PUT");
let result = executor
.upsert_contract_state(
key,
Either::Left(state.clone()),
RelatedContracts::default(),
None,
)
.await
.expect("identical re-apply");
assert!(matches!(result, UpsertResult::NoChange));
assert!(
!op_manager.ring.is_contract_broken(&key),
"a canonicalize-once-then-stable contract must NOT be flagged by the \
identity probe (F3 false-positive fix)"
);
}
#[tokio::test(flavor = "current_thread")]
async fn flagged_contract_subsequent_apply_is_suppressed() {
let (op_manager, _guards) = build_op_manager("nonidem-identity-suppress").await;
let contract = test_contract(b"suppressed_identity_contract");
let key = contract.key();
let state = distinct_bytes_state();
let mut executor = build_executor(&op_manager).await;
let counter = Arc::new(AtomicU64::new(0));
executor
.mock_runtime_mut()
.update_overrides
.insert(*key.id(), UpdateOverride::NonIdempotent(counter));
executor
.upsert_contract_state(
key,
Either::Left(state.clone()),
RelatedContracts::default(),
Some(contract.clone()),
)
.await
.expect("initial PUT");
executor
.upsert_contract_state(
key,
Either::Left(state.clone()),
RelatedContracts::default(),
None,
)
.await
.expect("identical re-apply");
assert!(op_manager.ring.is_contract_broken(&key));
let different = WrappedState::new((101u8..=132).collect::<Vec<u8>>());
let result = executor
.upsert_contract_state(
key,
Either::Left(different),
RelatedContracts::default(),
None,
)
.await
.expect("non-identical apply while flagged");
assert!(
matches!(result, UpsertResult::NoChange),
"apply while flagged must be suppressed to NoChange, got {result:?}"
);
let (stored, _) = executor
.fetch_contract(key, false)
.await
.expect("fetch after suppressed apply");
assert_eq!(
stored.map(|s| s.as_ref().to_vec()),
Some(state.as_ref().to_vec()),
"suppressed apply must not have touched the stored state"
);
}