use either::Either;
use freenet_stdlib::prelude::*;
use std::sync::Arc;
use crate::config::ConfigArgs;
use crate::contract::executor::{ContractExecutor, Executor, OperationMode};
use crate::node::OpManager;
use crate::ring::MIN_DEFAULT_HOSTING_BUDGET_BYTES;
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 install_tight_budget(
op_manager: &OpManager,
seed_key: ContractKey,
state_seed: u64,
budget: u64,
) {
assert!(
budget >= MIN_DEFAULT_HOSTING_BUDGET_BYTES,
"budget must be >= MIN or the recompute clamps it up"
);
op_manager
.ring
.seed_disk_tracker_for_test([(seed_key, state_seed)]);
op_manager.ring.configure_disk_budget_for_test(1.0, budget);
let available = budget.saturating_sub(state_seed);
op_manager
.ring
.recompute_effective_budget_for_test(available);
assert_eq!(
op_manager.ring.disk_budget_bytes_for_test(),
budget,
"test setup: disk_budget_bytes must be exactly the target budget"
);
}
fn blob_len_of(contract: &ContractContainer) -> u64 {
contract.data().len() as u64
}
#[tokio::test(flavor = "current_thread")]
async fn v1_put_over_budget_rejects_and_reverses_wasm_charge() {
let (op_manager, _guards) = build_op_manager("disk-budget-put-reject").await;
let contract = test_contract(b"disk_budget_put_reject_contract");
let key = contract.key();
let blob_len = blob_len_of(&contract);
let state = WrappedState::new(vec![7u8; 32]);
let state_len = state.as_ref().len() as u64;
let seed_key = test_contract(b"disk_budget_seed_key").key();
let budget = MIN_DEFAULT_HOSTING_BUDGET_BYTES;
let state_seed = budget - blob_len - 1;
assert!(
state_seed + blob_len + state_len > budget,
"test math: wasm+state must exceed budget"
);
install_tight_budget(&op_manager, seed_key, state_seed, budget);
let wasm_before = op_manager
.ring
.disk_usage_stats_for_test()
.expect("tracker seeded")
.wasm_bytes;
let mut executor =
Executor::new_mock_wasm("t", MockStateStorage::new(), None, Some(op_manager.clone()))
.await
.expect("build mock-wasm executor");
let result = executor
.upsert_contract_state(
key,
Either::Left(state.clone()),
RelatedContracts::default(),
Some(contract.clone()),
)
.await;
match result {
Err(err) => {
let msg = err.to_string().to_lowercase();
assert!(
msg.contains("disk budget"),
"PUT rejection must cite the disk budget, got: {msg}"
);
}
Ok(other) => panic!("over-budget PUT must be rejected, got Ok({other:?})"),
}
let wasm_after = op_manager
.ring
.disk_usage_stats_for_test()
.expect("tracker still seeded")
.wasm_bytes;
assert_eq!(
wasm_after, wasm_before,
"rejected PUT must reverse the wasm charge exactly once (leak or \
double-reverse would move wasm_bytes off its pre-PUT value)"
);
let stored = executor.fetch_contract(key, false).await;
assert!(
matches!(stored, Ok((None, _)) | Err(_)),
"rejected PUT must leave no state on disk, got {stored:?}"
);
}
#[tokio::test(flavor = "current_thread")]
async fn v1_put_under_budget_admits_and_charges_wasm() {
let (op_manager, _guards) = build_op_manager("disk-budget-put-admit").await;
let contract = test_contract(b"disk_budget_put_admit_contract");
let key = contract.key();
let blob_len = blob_len_of(&contract);
let state = WrappedState::new(vec![9u8; 32]);
let seed_key = test_contract(b"disk_budget_admit_seed_key").key();
let budget = MIN_DEFAULT_HOSTING_BUDGET_BYTES;
install_tight_budget(&op_manager, seed_key, 1_000, budget);
let wasm_before = op_manager
.ring
.disk_usage_stats_for_test()
.expect("seeded")
.wasm_bytes;
let mut executor =
Executor::new_mock_wasm("t", MockStateStorage::new(), None, Some(op_manager.clone()))
.await
.expect("build mock-wasm executor");
let result = executor
.upsert_contract_state(
key,
Either::Left(state.clone()),
RelatedContracts::default(),
Some(contract.clone()),
)
.await
.expect("under-budget PUT must be admitted");
assert!(
matches!(result, crate::contract::UpsertResult::Updated(_)),
"admitted PUT must return Updated, got {result:?}"
);
let wasm_after = op_manager
.ring
.disk_usage_stats_for_test()
.expect("seeded")
.wasm_bytes;
assert_eq!(
wasm_after,
wasm_before + blob_len,
"admitted PUT must charge the wasm blob and keep it charged"
);
let (fetched, _) = executor
.fetch_contract(key, false)
.await
.expect("fetch after admitted PUT");
assert_eq!(
fetched.map(|s| s.as_ref().to_vec()),
Some(state.as_ref().to_vec()),
"admitted PUT state must be readable"
);
}