Files
arkhe-origin-chain/crates/origin-chain-fees/src/tests_integration.rs
T
davidherranandClaude Opus 4.7 e935233ba6 feat(fees): v0.0.22 — compute_fee_for_action impl complete + mock runtime integration tests E2E (10 scenarios)
Phase 2.4 FEES pallet REAL IMPL canonical. Dispatchable compute_fee_for_action
wire-up vendor compute_fee() pure fn call + AccountFees<StorageMap> saturating
arithmetic + event emission. Mock runtime + 10 integration tests E2E pattern
v0.0.18 mirror (Phase 2.1 PROVENANCE).

## Real impl canonical 5-step OCAFB-5

1. ensure_signed canonical
2. Vendor compute_fee(base, hw, sub, action) pure fn call
3. Handle entitlement blocked (fee==u64::MAX → Err(EntitlementLocked))
4. AccountFees<StorageMap> saturating update canonical
5. Emit FeeCharged event canonical

Pure helper Pallet::<T>::query_fee() para Runtime API v0.0.23 consumer.

## Consumer pattern canonical

Mathi OitFeeSchedule consumed CLIENT-SIDE — tx submitter queries Mathi per-user
→ assembles tx con (hw, sub) baked → submits. Pattern análogo Phase 2.1
attest_asset (ValidationAttestation pre-assembled client-side).

## 10 integration tests E2E

- scenario_1: Pi5 Write subsidy 90% → fee=1000 (Inmutable #16)
- scenario_2: Ultra Write Pro multiplier → fee=1500
- scenario_3: Read always free → fee=0 (Inmutable #37)
- scenario_4: Free Compute blocked → Err(EntitlementLocked)
- scenario_5: Pro Compute unlocked → fee=112
- scenario_6: Ultra Compute Ultra premium → fee=2000 (Inmutable #18)
- scenario_7: Storage accumulates Write×3 → total_paid=3375, count=3
- scenario_8: ChainAction ordinal stable wire (Read=0..Transfer=3)
- scenario_9: Inmutable #16 lobo blanco — Pi5 fee 1000 < Ultra fee 20000
- scenario_10: 5-step OCAFB-5 walkthrough explicit Pre/Post assertions

## FRAME canonical pattern

Events NOT emitted on Err path (FRAME revierte events on dispatch error).
EntitlementBlocked event variant retained para Phase 2.5 future
with_storage_layer pattern. Caller infers from Err return.

## Files canonical

- EDIT: crates/origin-chain-fees/src/lib.rs (real dispatchable impl + query_fee helper)
- NEW: crates/origin-chain-fees/src/mock.rs (80 LOC mock runtime pattern v0.0.18)
- NEW: crates/origin-chain-fees/src/tests_integration.rs (290 LOC, 10 tests)
- EDIT: crates/origin-chain-types/src/tiers.rs (FRAME derives Encode/Decode/TypeInfo/MaxEncodedLen para HardwareTier + SubscriptionTier + AssetQualityTier)
- EDIT: CHANGELOG.md (v0.0.22 entry comprehensive)

## Verification

- cargo test -p origin-chain-fees — 32 tests verde, 0 failures
- cargo test --workspace — 137 tests verde workspace, 0 failures
- Zero compile errors workspace-wide

🛠️ Phase 2.4 real impl canonical. Runtime API v0.0.23 next ship.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 22:56:50 -05:00

304 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! # Integration tests canonical v0.0.22 — Mock runtime E2E OCAFB-5 flow
//!
//! 10 scenarios canonical compute_fee_for_action flow end-to-end via mock runtime:
//!
//! | Scenario | HW | Sub | Action | Expected outcome |
//! |----------|----|----|--------|------------------|
//! | scenario_1_pi5_write_subsidy_90 | EmbeddedArm | Free | Write | fee=10% base (subsidy 9000 bp) |
//! | scenario_2_ultra_write_full_price | Ultra | Pro | Write | fee=base × 1.5 multiplier |
//! | scenario_3_read_always_free_inmutable_37 | Desktop | Free | Read | fee=0 (subsidy 100%) |
//! | scenario_4_free_compute_blocked_entitlement | Desktop | Free | Compute | Error::EntitlementLocked + event |
//! | scenario_5_pro_compute_unlocked | Desktop | Pro | Compute | fee normal |
//! | scenario_6_ultra_compute_premium | Ultra | Ultra | Compute | fee=base × 2.0 multiplier |
//! | scenario_7_storage_accumulates | Desktop | Pro | Write × 3 | history.total_paid sum + count=3 |
//! | scenario_8_chain_action_ordinal_stable_wire | wire format invariant | n/a | Read=0 + Write=1 + Compute=2 + Transfer=3 |
//! | scenario_9_inmutable_16_anti_clasista_lobo_blanco | Pi5 < Ultra fee non-Read | n/a | compile-detect invariant |
//! | scenario_10_5_step_walkthrough | EmbeddedArm | Free | Transfer | explicit Pre/Post-flight assertions |
//!
//! Total: 10 integration tests canonical E2E. Pattern v0.0.18 mirror.
#![cfg(test)]
use crate::mock::{alice_account, alice_origin, bob_account, bob_origin, new_test_ext, Fees, RuntimeEvent, System, Test};
use crate::{AccountFees, Error, Event};
use frame_support::{assert_noop, assert_ok};
use origin_chain_types::{ChainAction, HardwareTier, SubscriptionTier};
// ─── Scenario 1: Pi 5 Write subsidy 90% canonical (Inmutable #16) ────
#[test]
fn scenario_1_pi5_write_subsidy_90_canonical() {
new_test_ext().execute_with(|| {
let base = 10_000u64;
assert_ok!(Fees::compute_fee_for_action(
alice_origin(),
ChainAction::Write,
base,
HardwareTier::EmbeddedArm,
SubscriptionTier::Free,
));
// OCAFB-5 formula: base 10000 × hw_mult 1000 × sub_mult 10000 / 10000 / 10000 = 1000
// (subsidy 9000 bp → remaining 1000 bp × Free multiplier 10000 bp)
let history = AccountFees::<Test>::get(&alice_account()).expect("storage updated");
assert_eq!(history.total_paid, 1000u128); // Pi 5 paga 10% del base — Inmutable #16
assert_eq!(history.action_count, 1);
System::assert_last_event(RuntimeEvent::Fees(Event::FeeCharged {
who: alice_account(),
action: ChainAction::Write,
fee_charged: 1000,
}));
});
}
// ─── Scenario 2: Ultra Write Pro multiplier canonical ───────────────
#[test]
fn scenario_2_ultra_write_full_price_canonical() {
new_test_ext().execute_with(|| {
let base = 1_000u64;
assert_ok!(Fees::compute_fee_for_action(
alice_origin(),
ChainAction::Write,
base,
HardwareTier::Ultra,
SubscriptionTier::Pro,
));
// OCAFB-5: base 1000 × hw_mult 10000 (Ultra subsidy=0 → full remaining) × sub_mult 15000 / 10000 / 10000 = 1500
let history = AccountFees::<Test>::get(&alice_account()).expect("storage updated");
assert_eq!(history.total_paid, 1500u128);
assert_eq!(history.action_count, 1);
});
}
// ─── Scenario 3: Read always free canonical (Inmutable #37) ──────────
#[test]
fn scenario_3_read_always_free_inmutable_37_canonical() {
new_test_ext().execute_with(|| {
assert_ok!(Fees::compute_fee_for_action(
alice_origin(),
ChainAction::Read,
999_999,
HardwareTier::Desktop, // even Desktop
SubscriptionTier::Free,
));
// OCAFB-3: Read always 100% subsidy → fee=0 (Inmutable #37 baseline)
let history = AccountFees::<Test>::get(&alice_account()).expect("storage updated");
assert_eq!(history.total_paid, 0u128);
assert_eq!(history.action_count, 1); // count incremented even si fee=0
});
}
// ─── Scenario 4: Free Compute blocked entitlement canonical (Inmutable #37) ──
#[test]
fn scenario_4_free_compute_blocked_entitlement_canonical() {
new_test_ext().execute_with(|| {
// OCAFB-4 entitlement enforce: Free + Compute → blocked
// FRAME canonical: dispatchable returns `Err(EntitlementLocked)` SIN event
// emission (events revierten on Err per FRAME dispatch transaction).
// `EntitlementBlocked` event variant reserved para Phase 2.5 future
// `with_storage_layer` pattern.
assert_noop!(
Fees::compute_fee_for_action(
alice_origin(),
ChainAction::Compute,
100,
HardwareTier::Desktop,
SubscriptionTier::Free, // Compute blocked per OCAFB-4
),
Error::<Test>::EntitlementLocked
);
// Storage NOT updated (entitlement blocked early)
assert!(AccountFees::<Test>::get(&alice_account()).is_none());
// Caller infers blocked entitlement from `Err(EntitlementLocked)` return,
// no event canonical (FRAME revierte events on Err scaffold v0.0.22).
});
}
// ─── Scenario 5: Pro Compute unlocked canonical ─────────────────────
#[test]
fn scenario_5_pro_compute_unlocked_canonical() {
new_test_ext().execute_with(|| {
let base = 100u64;
assert_ok!(Fees::compute_fee_for_action(
alice_origin(),
ChainAction::Compute,
base,
HardwareTier::Desktop,
SubscriptionTier::Pro,
));
// OCAFB-5: base 100 × hw_mult 7500 (Desktop Compute subsidy 2500 → 7500 remaining) × sub_mult 15000 / 10000 / 10000 = 112
let history = AccountFees::<Test>::get(&alice_account()).expect("storage updated");
assert_eq!(history.total_paid, 112u128); // floor division round-down (112.5 → 112)
assert_eq!(history.action_count, 1);
});
}
// ─── Scenario 6: Ultra Compute premium canonical (Inmutable #18) ────
#[test]
fn scenario_6_ultra_compute_premium_canonical() {
new_test_ext().execute_with(|| {
let base = 1_000u64;
assert_ok!(Fees::compute_fee_for_action(
alice_origin(),
ChainAction::Compute,
base,
HardwareTier::Ultra,
SubscriptionTier::Ultra, // EEG Compute Ultra-only (Inmutable #18)
));
// OCAFB-5: base 1000 × hw_mult 10000 (Ultra subsidy=0) × sub_mult 20000 / 10000 / 10000 = 2000
let history = AccountFees::<Test>::get(&alice_account()).expect("storage updated");
assert_eq!(history.total_paid, 2000u128);
assert_eq!(history.action_count, 1);
});
}
// ─── Scenario 7: Storage accumulates over multiple writes canonical ──
#[test]
fn scenario_7_storage_accumulates_canonical() {
new_test_ext().execute_with(|| {
let base = 1_000u64;
// 3 writes secuenciales
for _ in 0..3 {
assert_ok!(Fees::compute_fee_for_action(
alice_origin(),
ChainAction::Write,
base,
HardwareTier::Desktop,
SubscriptionTier::Pro,
));
}
// OCAFB-5: base 1000 × hw_mult 7500 × sub_mult 15000 / 10000 / 10000 = 1125 per write
// After 3 writes: total_paid = 3 × 1125 = 3375, action_count = 3
let history = AccountFees::<Test>::get(&alice_account()).expect("storage updated");
assert_eq!(history.total_paid, 3375u128);
assert_eq!(history.action_count, 3);
});
}
// ─── Scenario 8: ChainAction ordinal stable wire format canonical ────
#[test]
fn scenario_8_chain_action_ordinal_stable_wire_canonical() {
// Wire format invariant — sin dependency mock runtime
assert_eq!(ChainAction::Read.ordinal(), 0);
assert_eq!(ChainAction::Write.ordinal(), 1);
assert_eq!(ChainAction::Compute.ordinal(), 2);
assert_eq!(ChainAction::Transfer.ordinal(), 3);
// Roundtrip canonical
for action in [ChainAction::Read, ChainAction::Write, ChainAction::Compute, ChainAction::Transfer] {
assert_eq!(ChainAction::from_ordinal(action.ordinal()), Some(action));
}
}
// ─── Scenario 9: Inmutable #16 anti-clasista lobo blanco canonical ───
#[test]
fn scenario_9_inmutable_16_anti_clasista_lobo_blanco_canonical() {
new_test_ext().execute_with(|| {
let base = 10_000u64;
// Pi 5 user (EmbeddedArm + Free)
assert_ok!(Fees::compute_fee_for_action(
alice_origin(),
ChainAction::Write,
base,
HardwareTier::EmbeddedArm,
SubscriptionTier::Free,
));
let pi5_fee = AccountFees::<Test>::get(&alice_account()).unwrap().total_paid;
// Ultra user (Ultra + Ultra)
assert_ok!(Fees::compute_fee_for_action(
bob_origin(),
ChainAction::Write,
base,
HardwareTier::Ultra,
SubscriptionTier::Ultra,
));
let ultra_fee = AccountFees::<Test>::get(&bob_account()).unwrap().total_paid;
// Lobo blanco compile-detect canonical: Pi 5 paga ESTRICTAMENTE MENOS que Ultra
assert!(
pi5_fee < ultra_fee,
"Inmutable #16 anti-clasista violated: Pi 5 fee {pi5_fee} debe ser < Ultra fee {ultra_fee}"
);
// Ratio canonical aprox 1:20 (Pi 5 1000 vs Ultra 20000)
// Pi 5: 10000 × 1000 × 10000 / 10000 / 10000 = 1000
// Ultra: 10000 × 10000 × 20000 / 10000 / 10000 = 20000
assert_eq!(pi5_fee, 1000u128);
assert_eq!(ultra_fee, 20000u128);
});
}
// ─── Scenario 10: 5-step OCAFB-5 walkthrough explicit canonical ──────
#[test]
fn scenario_10_5_step_walkthrough_canonical() {
new_test_ext().execute_with(|| {
let base = 1_000u64;
let action = ChainAction::Transfer;
let hw = HardwareTier::EmbeddedArm;
let sub = SubscriptionTier::Free;
// Pre-flight assertions canonical
assert!(
AccountFees::<Test>::get(&alice_account()).is_none(),
"PRE-flight OCAFB-2: storage canonical empty"
);
// Step 1: ensure_signed canonical (implicit via dispatchable)
// Step 2: vendor compute_fee canonical OCAFB-5 (pure fn call)
// Step 3: entitlement check (Transfer Free allowed, no block)
// Step 4: AccountFees<StorageMap> update saturating canonical
// Step 5: emit FeeCharged event canonical
assert_ok!(Fees::compute_fee_for_action(
alice_origin(),
action,
base,
hw,
sub,
));
// Post-flight assertions canonical
let history = AccountFees::<Test>::get(&alice_account())
.expect("POST-flight OCAFB-2: AccountFees persisted canonical");
// OCAFB-5: base 1000 × hw_mult 1000 (EmbeddedArm Transfer subsidy 9000 → 1000 remaining) × sub_mult 10000 / 10000 / 10000 = 100
assert_eq!(
history.total_paid, 100u128,
"POST-flight: total_paid canonical Pi 5 anti-clasista"
);
assert_eq!(
history.action_count, 1,
"POST-flight: action_count canonical incremented"
);
System::assert_last_event(RuntimeEvent::Fees(Event::FeeCharged {
who: alice_account(),
action,
fee_charged: 100,
}));
});
}