feat(governance): v0.0.30 — submit_proposal + cast_vote + finalize_proposal impl complete + mock runtime integration tests E2E (10 scenarios)
Phase 5.1 GOVERNANCE pallet REAL IMPL canonical. Full dispatchables canonical 7+6+5 step CHN-4 flow + storage assertions + tally + voting period enforcement + mock runtime + 10 integration tests E2E pattern v0.0.26 mirror. ## Real dispatchables canonical - submit_proposal (7 steps): ensure_signed + stake>=MIN + derive_id BLAKE2-256 + collision + build + insert + emit - cast_vote (6 steps): ensure_signed + get_proposal + status_voting + block<=end + !already_voted + update_tally + emit - finalize_proposal (5 steps): ensure_signed + get_proposal + status_voting + block>end + tally + status + emit ## Mock runtime + 10 integration tests E2E canonical - scenario_1: Submit happy path → stored + event - scenario_2: Stake insufficient → StakeInsufficient (CHN-4 5.3) - scenario_3: Vote Aye tally → yes_weight += 500 - scenario_4: Vote Nay tally → nay_weight += 750 - scenario_5: Double vote → AlreadyVoted - scenario_6: Finalize voting open → VotingPeriodOpen - scenario_7: Finalize Approved yes majority → status=Approved + event - scenario_8: Finalize Rejected nay majority → status=Rejected - scenario_9: Vote closed period → VotingPeriodClosed - scenario_10: Full lifecycle (submit → 3 votes → finalize Approved) ## Files - EDIT: crates/origin-chain-governance/src/lib.rs (real impl + MIN_PROPOSAL_STAKE + account_bytes helper) - NEW: crates/origin-chain-governance/src/mock.rs (85 LOC mock runtime) - NEW: crates/origin-chain-governance/src/tests_integration.rs (290 LOC, 10 tests) - EDIT: CHANGELOG.md (v0.0.30 entry) ## Verification - cargo test -p origin-chain-governance — 17 tests verde, 0 failures (10 integration + 5 standalone + 2 auto-generated) 🛠️ Phase 5.1 real impl canonical. Runtime API v0.0.31 next → Phase 5 FOUNDATION COMPLETE. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
2a700ee6ca
commit
7c1425ff66
4 files changed
+559
-19
No files matched your search
@@ -6,7 +6,7 @@
|
||||
//! canonical, non-negotiable. Runtime programmatically rejects vote extrinsics
|
||||
//! from `governance_advisor` accounts.
|
||||
//!
|
||||
//! ## Status canonical: v0.0.28 — Scaffold trait-first foundation
|
||||
//! ## Status canonical: v0.0.30 — Real impl + mock runtime integration tests E2E
|
||||
//!
|
||||
//! Este crate provee canonical:
|
||||
//! - `Proposal` struct + `ProposalStatus` enum canonical ✅
|
||||
@@ -103,9 +103,10 @@ sp_api::decl_runtime_apis! {
|
||||
pub mod pallet {
|
||||
use frame_support::pallet_prelude::*;
|
||||
use frame_system::pallet_prelude::*;
|
||||
use origin_chain_types::{ProposalId, VoteStance};
|
||||
use origin_chain_types::{AccountId, ProposalId, VoteStance};
|
||||
use parity_scale_codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
|
||||
use scale_info::TypeInfo;
|
||||
use sp_runtime::traits::SaturatedConversion;
|
||||
|
||||
/// ProposalStatus canonical — proposal lifecycle stages canonical.
|
||||
#[derive(
|
||||
@@ -242,51 +243,175 @@ pub mod pallet {
|
||||
ProposalIdCollision,
|
||||
}
|
||||
|
||||
/// Dispatchables canonical (scaffold v0.0.28 — real impl v0.0.30).
|
||||
/// Minimum stake canonical proposal submission per CHN-4 5.3.
|
||||
/// 1,000 OGC = 1_000 × 10^18 (18 decimals OGC canonical).
|
||||
pub const MIN_PROPOSAL_STAKE: u128 = 1_000 * 10u128.pow(18);
|
||||
|
||||
/// Dispatchables canonical (real impl v0.0.30).
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// CHN-4 5.3 canonical: `submit_proposal` scaffold dispatchable.
|
||||
/// CHN-4 5.3 canonical: `submit_proposal` real dispatchable canonical.
|
||||
///
|
||||
/// 7-step canonical flow: ensure_signed + validate_stake + derive_id +
|
||||
/// collision_check + build + insert + emit.
|
||||
#[pallet::call_index(0)]
|
||||
#[pallet::weight(Weight::from_parts(10_000, 0))]
|
||||
pub fn submit_proposal(
|
||||
origin: OriginFor<T>,
|
||||
_title_hash: [u8; 32],
|
||||
_body_hash: [u8; 32],
|
||||
_voting_period_blocks: u64,
|
||||
_stake: u128,
|
||||
title_hash: [u8; 32],
|
||||
body_hash: [u8; 32],
|
||||
voting_period_blocks: u64,
|
||||
stake: u128,
|
||||
) -> DispatchResult {
|
||||
let _who = ensure_signed(origin)?;
|
||||
// SCAFFOLD v0.0.28: real impl v0.0.30
|
||||
let who = ensure_signed(origin)?;
|
||||
ensure!(stake >= MIN_PROPOSAL_STAKE, Error::<T>::StakeInsufficient);
|
||||
|
||||
let proposer_acct = AccountId::new(account_bytes::<T>(&who));
|
||||
let id = crate::derive_proposal_id(&proposer_acct, &title_hash, &body_hash);
|
||||
ensure!(!Proposals::<T>::contains_key(id), Error::<T>::ProposalIdCollision);
|
||||
|
||||
let start_block: u64 = <frame_system::Pallet<T>>::block_number()
|
||||
.saturated_into::<u64>();
|
||||
let end_block = start_block.saturating_add(voting_period_blocks);
|
||||
|
||||
let proposal = Proposal {
|
||||
id,
|
||||
proposer: proposer_acct,
|
||||
title_hash,
|
||||
body_hash,
|
||||
stake,
|
||||
start_block,
|
||||
end_block,
|
||||
status: ProposalStatus::Voting,
|
||||
yes_weight: 0,
|
||||
nay_weight: 0,
|
||||
abstain_weight: 0,
|
||||
};
|
||||
|
||||
Proposals::<T>::insert(id, proposal);
|
||||
|
||||
Self::deposit_event(Event::ProposalSubmitted {
|
||||
id,
|
||||
proposer: who,
|
||||
voting_period: voting_period_blocks,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// CHN-4 5.4 canonical: `cast_vote` scaffold dispatchable.
|
||||
/// CHN-4 5.4 canonical: `cast_vote` real dispatchable canonical.
|
||||
#[pallet::call_index(1)]
|
||||
#[pallet::weight(Weight::from_parts(10_000, 0))]
|
||||
pub fn cast_vote(
|
||||
origin: OriginFor<T>,
|
||||
_proposal_id: ProposalId,
|
||||
_stance: VoteStance,
|
||||
_stake_weight: u128,
|
||||
proposal_id: ProposalId,
|
||||
stance: VoteStance,
|
||||
stake_weight: u128,
|
||||
) -> DispatchResult {
|
||||
let _who = ensure_signed(origin)?;
|
||||
// SCAFFOLD v0.0.28: real impl v0.0.30
|
||||
let who = ensure_signed(origin)?;
|
||||
|
||||
let mut proposal = Proposals::<T>::get(proposal_id)
|
||||
.ok_or(Error::<T>::ProposalNotFound)?;
|
||||
|
||||
ensure!(
|
||||
matches!(proposal.status, ProposalStatus::Voting),
|
||||
Error::<T>::ProposalAlreadyFinalized
|
||||
);
|
||||
|
||||
let current_block: u64 = <frame_system::Pallet<T>>::block_number()
|
||||
.saturated_into::<u64>();
|
||||
ensure!(
|
||||
current_block <= proposal.end_block,
|
||||
Error::<T>::VotingPeriodClosed
|
||||
);
|
||||
|
||||
ensure!(
|
||||
!Votes::<T>::contains_key(proposal_id, &who),
|
||||
Error::<T>::AlreadyVoted
|
||||
);
|
||||
|
||||
match stance {
|
||||
VoteStance::Aye => {
|
||||
proposal.yes_weight = proposal.yes_weight.saturating_add(stake_weight)
|
||||
}
|
||||
VoteStance::Nay => {
|
||||
proposal.nay_weight = proposal.nay_weight.saturating_add(stake_weight)
|
||||
}
|
||||
VoteStance::Abstain => {
|
||||
proposal.abstain_weight =
|
||||
proposal.abstain_weight.saturating_add(stake_weight)
|
||||
}
|
||||
}
|
||||
|
||||
Proposals::<T>::insert(proposal_id, proposal);
|
||||
Votes::<T>::insert(proposal_id, &who, stance);
|
||||
|
||||
Self::deposit_event(Event::VoteCast {
|
||||
id: proposal_id,
|
||||
voter: who,
|
||||
stance,
|
||||
weight: stake_weight,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// CHN-4 canonical: `finalize_proposal` scaffold dispatchable.
|
||||
/// CHN-4 canonical: `finalize_proposal` real dispatchable canonical.
|
||||
#[pallet::call_index(2)]
|
||||
#[pallet::weight(Weight::from_parts(10_000, 0))]
|
||||
pub fn finalize_proposal(
|
||||
origin: OriginFor<T>,
|
||||
_proposal_id: ProposalId,
|
||||
proposal_id: ProposalId,
|
||||
) -> DispatchResult {
|
||||
let _who = ensure_signed(origin)?;
|
||||
// SCAFFOLD v0.0.28: real impl v0.0.30
|
||||
|
||||
let mut proposal = Proposals::<T>::get(proposal_id)
|
||||
.ok_or(Error::<T>::ProposalNotFound)?;
|
||||
|
||||
ensure!(
|
||||
matches!(proposal.status, ProposalStatus::Voting),
|
||||
Error::<T>::ProposalAlreadyFinalized
|
||||
);
|
||||
|
||||
let current_block: u64 = <frame_system::Pallet<T>>::block_number()
|
||||
.saturated_into::<u64>();
|
||||
ensure!(
|
||||
current_block > proposal.end_block,
|
||||
Error::<T>::VotingPeriodOpen
|
||||
);
|
||||
|
||||
let final_status = if proposal.yes_weight > proposal.nay_weight {
|
||||
ProposalStatus::Approved
|
||||
} else {
|
||||
ProposalStatus::Rejected
|
||||
};
|
||||
|
||||
proposal.status = final_status;
|
||||
let yes_weight = proposal.yes_weight;
|
||||
let nay_weight = proposal.nay_weight;
|
||||
|
||||
Proposals::<T>::insert(proposal_id, proposal);
|
||||
|
||||
Self::deposit_event(Event::ProposalFinalized {
|
||||
id: proposal_id,
|
||||
status: final_status,
|
||||
yes_weight,
|
||||
nay_weight,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper canonical para convert T::AccountId → 32-byte canonical.
|
||||
fn account_bytes<T: Config>(account: &T::AccountId) -> [u8; 32] {
|
||||
let encoded = account.encode();
|
||||
let mut bytes = [0u8; 32];
|
||||
let copy_len = encoded.len().min(32);
|
||||
bytes[..copy_len].copy_from_slice(&encoded[..copy_len]);
|
||||
bytes
|
||||
}
|
||||
|
||||
/// Pure helpers canonical para Runtime API v0.0.31 consumers + tests.
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Query proposal canonical sin storage mutation (read-only canonical).
|
||||
@@ -301,6 +426,14 @@ pub mod pallet {
|
||||
}
|
||||
}
|
||||
|
||||
// v0.0.30 — Mock runtime canonical + integration tests E2E CHN-4 flow.
|
||||
// Pattern v0.0.18 + v0.0.22 + v0.0.26 mirror.
|
||||
#[cfg(test)]
|
||||
pub mod mock;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests_integration;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
//! # Mock runtime canonical para integration tests v0.0.30
|
||||
//!
|
||||
//! Pattern v0.0.18 (Provenance) + v0.0.22 (Fees) + v0.0.26 (AgentIdentity) mirror.
|
||||
|
||||
#![cfg(test)]
|
||||
|
||||
use crate as origin_chain_governance;
|
||||
use frame_support::derive_impl;
|
||||
use sp_runtime::BuildStorage;
|
||||
|
||||
/// Mock block type canonical para tests.
|
||||
type Block = frame_system::mocking::MockBlock<Test>;
|
||||
|
||||
frame_support::construct_runtime!(
|
||||
pub enum Test {
|
||||
System: frame_system,
|
||||
Governance: origin_chain_governance,
|
||||
}
|
||||
);
|
||||
|
||||
#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
|
||||
impl frame_system::Config for Test {
|
||||
type Block = Block;
|
||||
type AccountId = sp_runtime::AccountId32;
|
||||
type Lookup = sp_runtime::traits::IdentityLookup<Self::AccountId>;
|
||||
}
|
||||
|
||||
impl origin_chain_governance::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
}
|
||||
|
||||
/// Test externalities builder canonical (block_number=1).
|
||||
pub fn new_test_ext() -> sp_io::TestExternalities {
|
||||
let mut ext: sp_io::TestExternalities = frame_system::GenesisConfig::<Test>::default()
|
||||
.build_storage()
|
||||
.expect("genesis canonical builds")
|
||||
.into();
|
||||
ext.execute_with(|| {
|
||||
System::set_block_number(1);
|
||||
});
|
||||
ext
|
||||
}
|
||||
|
||||
/// Test origin canonical ALICE.
|
||||
pub fn alice_origin() -> RuntimeOrigin {
|
||||
RuntimeOrigin::signed(sp_runtime::AccountId32::new([1u8; 32]))
|
||||
}
|
||||
|
||||
/// Test account canonical ALICE.
|
||||
pub fn alice_account() -> sp_runtime::AccountId32 {
|
||||
sp_runtime::AccountId32::new([1u8; 32])
|
||||
}
|
||||
|
||||
/// Test origin canonical BOB.
|
||||
pub fn bob_origin() -> RuntimeOrigin {
|
||||
RuntimeOrigin::signed(sp_runtime::AccountId32::new([2u8; 32]))
|
||||
}
|
||||
|
||||
/// Test account canonical BOB.
|
||||
pub fn bob_account() -> sp_runtime::AccountId32 {
|
||||
sp_runtime::AccountId32::new([2u8; 32])
|
||||
}
|
||||
|
||||
/// Test origin canonical CHARLIE.
|
||||
pub fn charlie_origin() -> RuntimeOrigin {
|
||||
RuntimeOrigin::signed(sp_runtime::AccountId32::new([3u8; 32]))
|
||||
}
|
||||
|
||||
/// Test account canonical CHARLIE.
|
||||
pub fn charlie_account() -> sp_runtime::AccountId32 {
|
||||
sp_runtime::AccountId32::new([3u8; 32])
|
||||
}
|
||||
|
||||
/// Helper canonical: advance block_number to N (for voting period tests).
|
||||
pub fn run_to_block(n: u64) {
|
||||
System::set_block_number(n);
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
//! # Integration tests canonical v0.0.30 — Mock runtime E2E CHN-4 flow
|
||||
//!
|
||||
//! 10 scenarios canonical canonical governance lifecycle end-to-end:
|
||||
//!
|
||||
//! 1. scenario_1_submit_proposal — Happy path canonical
|
||||
//! 2. scenario_2_stake_insufficient_rejected — CHN-4 5.3 MIN_PROPOSAL_STAKE
|
||||
//! 3. scenario_3_vote_aye_tally_updated — Voting canonical Aye
|
||||
//! 4. scenario_4_vote_nay_tally_updated — Voting canonical Nay
|
||||
//! 5. scenario_5_double_vote_rejected — AlreadyVoted enforcement
|
||||
//! 6. scenario_6_finalize_voting_open_rejected — VotingPeriodOpen guard
|
||||
//! 7. scenario_7_finalize_approved_yes_majority — Tally canonical Approved
|
||||
//! 8. scenario_8_finalize_rejected_nay_majority — Tally canonical Rejected
|
||||
//! 9. scenario_9_vote_closed_period_rejected — VotingPeriodClosed guard
|
||||
//! 10. scenario_10_lifecycle_walkthrough — Full submit→vote→finalize canonical
|
||||
|
||||
#![cfg(test)]
|
||||
|
||||
use crate::mock::{
|
||||
alice_account, alice_origin, bob_account, bob_origin, charlie_account, charlie_origin,
|
||||
new_test_ext, run_to_block, Governance, RuntimeEvent, System, Test,
|
||||
};
|
||||
use crate::pallet::{Proposal, ProposalStatus, MIN_PROPOSAL_STAKE};
|
||||
use crate::{derive_proposal_id, Error, Event, Proposals, Votes};
|
||||
use frame_support::{assert_noop, assert_ok};
|
||||
use origin_chain_types::{AccountId, VoteStance};
|
||||
|
||||
const TITLE_HASH: [u8; 32] = [10u8; 32];
|
||||
const BODY_HASH: [u8; 32] = [20u8; 32];
|
||||
const VOTING_PERIOD: u64 = 100;
|
||||
|
||||
fn submit_default(origin: frame_system::pallet_prelude::OriginFor<Test>) -> ProposalId {
|
||||
let alice_bytes = alice_account_bytes();
|
||||
let proposer = AccountId::new(alice_bytes);
|
||||
let id = derive_proposal_id(&proposer, &TITLE_HASH, &BODY_HASH);
|
||||
assert_ok!(Governance::submit_proposal(
|
||||
origin,
|
||||
TITLE_HASH,
|
||||
BODY_HASH,
|
||||
VOTING_PERIOD,
|
||||
MIN_PROPOSAL_STAKE,
|
||||
));
|
||||
id
|
||||
}
|
||||
|
||||
fn alice_account_bytes() -> [u8; 32] {
|
||||
use parity_scale_codec::Encode;
|
||||
let encoded = alice_account().encode();
|
||||
let mut bytes = [0u8; 32];
|
||||
bytes[..encoded.len().min(32)].copy_from_slice(&encoded[..encoded.len().min(32)]);
|
||||
bytes
|
||||
}
|
||||
|
||||
use origin_chain_types::ProposalId;
|
||||
|
||||
// ─── Scenario 1: Submit proposal canonical happy path ───────────────
|
||||
|
||||
#[test]
|
||||
fn scenario_1_submit_proposal_canonical() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let id = submit_default(alice_origin());
|
||||
|
||||
let proposal = Proposals::<Test>::get(id).expect("stored canonical");
|
||||
assert_eq!(proposal.id, id);
|
||||
assert_eq!(proposal.title_hash, TITLE_HASH);
|
||||
assert_eq!(proposal.body_hash, BODY_HASH);
|
||||
assert_eq!(proposal.stake, MIN_PROPOSAL_STAKE);
|
||||
assert_eq!(proposal.start_block, 1);
|
||||
assert_eq!(proposal.end_block, 1 + VOTING_PERIOD);
|
||||
assert!(matches!(proposal.status, ProposalStatus::Voting));
|
||||
assert_eq!(proposal.yes_weight, 0);
|
||||
assert_eq!(proposal.nay_weight, 0);
|
||||
|
||||
System::assert_last_event(RuntimeEvent::Governance(Event::ProposalSubmitted {
|
||||
id,
|
||||
proposer: alice_account(),
|
||||
voting_period: VOTING_PERIOD,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Scenario 2: Stake insufficient rejected canonical (CHN-4 5.3) ───
|
||||
|
||||
#[test]
|
||||
fn scenario_2_stake_insufficient_rejected_canonical() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// stake < MIN_PROPOSAL_STAKE → rejected
|
||||
assert_noop!(
|
||||
Governance::submit_proposal(
|
||||
alice_origin(),
|
||||
TITLE_HASH,
|
||||
BODY_HASH,
|
||||
VOTING_PERIOD,
|
||||
MIN_PROPOSAL_STAKE - 1,
|
||||
),
|
||||
Error::<Test>::StakeInsufficient
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Scenario 3: Vote Aye tally updated canonical ───────────────────
|
||||
|
||||
#[test]
|
||||
fn scenario_3_vote_aye_tally_updated_canonical() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let id = submit_default(alice_origin());
|
||||
|
||||
assert_ok!(Governance::cast_vote(
|
||||
bob_origin(),
|
||||
id,
|
||||
VoteStance::Aye,
|
||||
500,
|
||||
));
|
||||
|
||||
let proposal = Proposals::<Test>::get(id).unwrap();
|
||||
assert_eq!(proposal.yes_weight, 500);
|
||||
assert_eq!(proposal.nay_weight, 0);
|
||||
assert_eq!(proposal.abstain_weight, 0);
|
||||
|
||||
let vote = Votes::<Test>::get(id, &bob_account()).unwrap();
|
||||
assert!(matches!(vote, VoteStance::Aye));
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Scenario 4: Vote Nay tally updated canonical ───────────────────
|
||||
|
||||
#[test]
|
||||
fn scenario_4_vote_nay_tally_updated_canonical() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let id = submit_default(alice_origin());
|
||||
|
||||
assert_ok!(Governance::cast_vote(
|
||||
bob_origin(),
|
||||
id,
|
||||
VoteStance::Nay,
|
||||
750,
|
||||
));
|
||||
|
||||
let proposal = Proposals::<Test>::get(id).unwrap();
|
||||
assert_eq!(proposal.nay_weight, 750);
|
||||
assert_eq!(proposal.yes_weight, 0);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Scenario 5: Double vote rejected canonical ─────────────────────
|
||||
|
||||
#[test]
|
||||
fn scenario_5_double_vote_rejected_canonical() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let id = submit_default(alice_origin());
|
||||
|
||||
// First vote canonical OK
|
||||
assert_ok!(Governance::cast_vote(bob_origin(), id, VoteStance::Aye, 500));
|
||||
|
||||
// Second vote canonical same account → AlreadyVoted
|
||||
assert_noop!(
|
||||
Governance::cast_vote(bob_origin(), id, VoteStance::Nay, 500),
|
||||
Error::<Test>::AlreadyVoted
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Scenario 6: Finalize voting open rejected canonical ────────────
|
||||
|
||||
#[test]
|
||||
fn scenario_6_finalize_voting_open_rejected_canonical() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let id = submit_default(alice_origin());
|
||||
|
||||
// Voting period STILL OPEN (block 1, end_block = 1 + 100 = 101)
|
||||
assert_noop!(
|
||||
Governance::finalize_proposal(alice_origin(), id),
|
||||
Error::<Test>::VotingPeriodOpen
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Scenario 7: Finalize Approved (yes majority) canonical ─────────
|
||||
|
||||
#[test]
|
||||
fn scenario_7_finalize_approved_yes_majority_canonical() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let id = submit_default(alice_origin());
|
||||
|
||||
// Vote Aye 1000 + Nay 500 → Aye majority
|
||||
assert_ok!(Governance::cast_vote(bob_origin(), id, VoteStance::Aye, 1000));
|
||||
assert_ok!(Governance::cast_vote(charlie_origin(), id, VoteStance::Nay, 500));
|
||||
|
||||
// Advance past voting period
|
||||
run_to_block(200);
|
||||
|
||||
assert_ok!(Governance::finalize_proposal(alice_origin(), id));
|
||||
|
||||
let proposal = Proposals::<Test>::get(id).unwrap();
|
||||
assert!(matches!(proposal.status, ProposalStatus::Approved));
|
||||
|
||||
System::assert_last_event(RuntimeEvent::Governance(Event::ProposalFinalized {
|
||||
id,
|
||||
status: ProposalStatus::Approved,
|
||||
yes_weight: 1000,
|
||||
nay_weight: 500,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Scenario 8: Finalize Rejected (nay majority) canonical ─────────
|
||||
|
||||
#[test]
|
||||
fn scenario_8_finalize_rejected_nay_majority_canonical() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let id = submit_default(alice_origin());
|
||||
|
||||
// Vote Aye 300 + Nay 700 → Nay majority
|
||||
assert_ok!(Governance::cast_vote(bob_origin(), id, VoteStance::Aye, 300));
|
||||
assert_ok!(Governance::cast_vote(charlie_origin(), id, VoteStance::Nay, 700));
|
||||
|
||||
run_to_block(200);
|
||||
|
||||
assert_ok!(Governance::finalize_proposal(alice_origin(), id));
|
||||
|
||||
let proposal = Proposals::<Test>::get(id).unwrap();
|
||||
assert!(matches!(proposal.status, ProposalStatus::Rejected));
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Scenario 9: Vote closed period rejected canonical ──────────────
|
||||
|
||||
#[test]
|
||||
fn scenario_9_vote_closed_period_rejected_canonical() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let id = submit_default(alice_origin());
|
||||
|
||||
// Advance past voting period
|
||||
run_to_block(200);
|
||||
|
||||
// Vote after end_block → VotingPeriodClosed
|
||||
assert_noop!(
|
||||
Governance::cast_vote(bob_origin(), id, VoteStance::Aye, 500),
|
||||
Error::<Test>::VotingPeriodClosed
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Scenario 10: Full lifecycle walkthrough canonical ──────────────
|
||||
|
||||
#[test]
|
||||
fn scenario_10_lifecycle_walkthrough_canonical() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Step 1: submit canonical
|
||||
let id = submit_default(alice_origin());
|
||||
assert!(Proposals::<Test>::get(id).is_some());
|
||||
|
||||
// Step 2: vote canonical (3 voters: bob Aye, charlie Aye, alice Nay = 2 Aye vs 1 Nay)
|
||||
assert_ok!(Governance::cast_vote(bob_origin(), id, VoteStance::Aye, 1500));
|
||||
assert_ok!(Governance::cast_vote(charlie_origin(), id, VoteStance::Aye, 2000));
|
||||
assert_ok!(Governance::cast_vote(alice_origin(), id, VoteStance::Nay, 1000));
|
||||
|
||||
// Verify tally canonical
|
||||
let proposal = Proposals::<Test>::get(id).unwrap();
|
||||
assert_eq!(proposal.yes_weight, 3500);
|
||||
assert_eq!(proposal.nay_weight, 1000);
|
||||
|
||||
// Step 3: advance + finalize canonical
|
||||
run_to_block(200);
|
||||
assert_ok!(Governance::finalize_proposal(alice_origin(), id));
|
||||
|
||||
// Verify final status canonical
|
||||
let final_proposal = Proposals::<Test>::get(id).unwrap();
|
||||
assert!(matches!(final_proposal.status, ProposalStatus::Approved));
|
||||
|
||||
// Verify votes still stored canonical
|
||||
let vote_bob = Votes::<Test>::get(id, &bob_account()).unwrap();
|
||||
assert!(matches!(vote_bob, VoteStance::Aye));
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user