From 21d1bc2375f9e0fbb85ada072c9c9dc963a02dd0 Mon Sep 17 00:00:00 2001 From: Kurdistan Tech Ministry Date: Thu, 19 Feb 2026 00:10:21 +0300 Subject: [PATCH] test(governance): add OpenGov track configuration unit tests Verify all 18 tracks (15 standard + 3 welati) have correct production periods, unique IDs/names, proper origin mappings, and no leftover test values. --- pezkuwi/runtime/pezkuwichain/src/tests.rs | 184 ++++++++++++++++++++++ 1 file changed, 184 insertions(+) diff --git a/pezkuwi/runtime/pezkuwichain/src/tests.rs b/pezkuwi/runtime/pezkuwichain/src/tests.rs index a58385e1..d28a9267 100644 --- a/pezkuwi/runtime/pezkuwichain/src/tests.rs +++ b/pezkuwi/runtime/pezkuwichain/src/tests.rs @@ -128,3 +128,187 @@ fn location_conversion_works() { assert_eq!(got, expected, "{}", tc.description); } } + +// ============================================================================= +// OpenGov Track Configuration Tests +// ============================================================================= + +use governance::TracksInfo; +use pezkuwichain_runtime_constants::time::{DAYS, HOURS, MINUTES}; +use pezpallet_referenda::TracksInfo as TracksInfoTrait; +use std::collections::HashMap; + +#[test] +fn governance_tracks_total_count() { + let count = >::tracks().count(); + assert_eq!(count, 18, "Expected 18 tracks (15 standard + 3 welati), got {count}"); +} + +#[test] +fn governance_track_ids_are_unique() { + let mut seen = HashSet::new(); + for track in >::tracks() { + assert!( + seen.insert(track.id), + "Duplicate track ID: {}", + track.id + ); + } +} + +#[test] +fn governance_track_names_are_unique() { + let mut seen = HashSet::new(); + for track in >::tracks() { + let name = String::from_utf8_lossy(&track.info.name).to_string(); + assert!(seen.insert(name.clone()), "Duplicate track name: {name}"); + } +} + +#[test] +fn governance_no_test_periods_remain() { + // Ensure no track still uses the old test values (< 1 HOURS for decision_period). + // All production decision periods should be at least 7 DAYS. + for track in >::tracks() { + let name = String::from_utf8_lossy(&track.info.name).to_string(); + assert!( + track.info.decision_period >= 7 * DAYS, + "Track '{name}' (id={}) has decision_period={} blocks, expected >= {} (7 DAYS)", + track.id, + track.info.decision_period, + 7 * DAYS + ); + } +} + +#[test] +fn governance_production_periods_match_spec() { + // Build expected values: (track_id, prepare, decision, confirm, enact) + let expected: Vec<(u16, &str, BlockNumber, BlockNumber, BlockNumber, BlockNumber)> = vec![ + (0, "root", 2 * HOURS, 28 * DAYS, 24 * HOURS, 24 * HOURS), + (1, "whitelisted_caller", 30 * MINUTES, 28 * DAYS, 10 * MINUTES, 10 * MINUTES), + (10, "staking_admin", 2 * HOURS, 14 * DAYS, 3 * HOURS, 10 * MINUTES), + (11, "treasurer", 2 * HOURS, 28 * DAYS, 3 * HOURS, 24 * HOURS), + (12, "lease_admin", 2 * HOURS, 14 * DAYS, 3 * HOURS, 10 * MINUTES), + (13, "fellowship_admin", 2 * HOURS, 14 * DAYS, 3 * HOURS, 10 * MINUTES), + (14, "general_admin", 2 * HOURS, 14 * DAYS, 3 * HOURS, 10 * MINUTES), + (15, "auction_admin", 2 * HOURS, 14 * DAYS, 3 * HOURS, 10 * MINUTES), + (20, "referendum_canceller", 2 * HOURS, 7 * DAYS, 3 * HOURS, 10 * MINUTES), + (21, "referendum_killer", 2 * HOURS, 14 * DAYS, 3 * HOURS, 10 * MINUTES), + (30, "small_tipper", 1 * MINUTES, 7 * DAYS, 10 * MINUTES, 1 * MINUTES), + (31, "big_tipper", 10 * MINUTES, 7 * DAYS, 1 * HOURS, 10 * MINUTES), + (32, "small_spender", 4 * HOURS, 28 * DAYS, 12 * HOURS, 24 * HOURS), + (33, "medium_spender", 4 * HOURS, 28 * DAYS, 24 * HOURS, 24 * HOURS), + (34, "big_spender", 4 * HOURS, 28 * DAYS, 48 * HOURS, 24 * HOURS), + (40, "welati_election", 2 * HOURS, 14 * DAYS, 12 * HOURS, 24 * HOURS), + (41, "welati_admin", 2 * HOURS, 7 * DAYS, 3 * HOURS, 10 * MINUTES), + (42, "citizenship_admin", 2 * HOURS, 14 * DAYS, 6 * HOURS, 24 * HOURS), + ]; + + let tracks: HashMap = >::tracks() + .map(|t| (t.id, t.into_owned())) + .collect(); + + for (id, name, prepare, decision, confirm, enact) in &expected { + let track = tracks.get(id).unwrap_or_else(|| panic!("Track id={id} '{name}' not found")); + let got_name = String::from_utf8_lossy(&track.info.name).trim_end_matches('\0').to_string(); + assert_eq!(&got_name, name, "Track id={id} name mismatch"); + assert_eq!( + track.info.prepare_period, *prepare, + "Track '{name}' prepare_period: got={}, expected={prepare}", + track.info.prepare_period + ); + assert_eq!( + track.info.decision_period, *decision, + "Track '{name}' decision_period: got={}, expected={decision}", + track.info.decision_period + ); + assert_eq!( + track.info.confirm_period, *confirm, + "Track '{name}' confirm_period: got={}, expected={confirm}", + track.info.confirm_period + ); + assert_eq!( + track.info.min_enactment_period, *enact, + "Track '{name}' min_enactment_period: got={}, expected={enact}", + track.info.min_enactment_period + ); + } + + assert_eq!(expected.len(), tracks.len(), "Track count mismatch"); +} + +#[test] +fn governance_welati_tracks_exist() { + let tracks: HashMap = >::tracks() + .map(|t| (t.id, t.into_owned())) + .collect(); + + // welati_election + let t40 = tracks.get(&40).expect("welati_election track (id=40) missing"); + assert_eq!(t40.info.max_deciding, 1, "welati_election should allow only 1 deciding"); + + // welati_admin + let t41 = tracks.get(&41).expect("welati_admin track (id=41) missing"); + assert_eq!(t41.info.max_deciding, 10); + + // citizenship_admin + let t42 = tracks.get(&42).expect("citizenship_admin track (id=42) missing"); + assert_eq!(t42.info.max_deciding, 10); +} + +#[test] +fn governance_decision_periods_are_in_days() { + // Verify all decision periods are expressed as multiples of DAYS (not minutes) + for track in >::tracks() { + let name = String::from_utf8_lossy(&track.info.name).to_string(); + let period = track.info.decision_period; + let days = period / DAYS; + assert!( + period == days * DAYS, + "Track '{name}' decision_period ({period} blocks) is not a whole number of days" + ); + assert!( + days >= 7, + "Track '{name}' decision_period is only {days} days, expected at least 7" + ); + } +} + +#[test] +fn governance_track_for_origin_mapping() { + use governance::pezpallet_custom_origins::Origin; + + // Test that track_for() correctly maps each origin to its track ID + let origin_to_track: Vec<(Origin, u16)> = vec![ + (Origin::WhitelistedCaller, 1), + (Origin::StakingAdmin, 10), + (Origin::Treasurer, 11), + (Origin::LeaseAdmin, 12), + (Origin::FellowshipAdmin, 13), + (Origin::GeneralAdmin, 14), + (Origin::AuctionAdmin, 15), + (Origin::ReferendumCanceller, 20), + (Origin::ReferendumKiller, 21), + (Origin::SmallTipper, 30), + (Origin::BigTipper, 31), + (Origin::SmallSpender, 32), + (Origin::MediumSpender, 33), + (Origin::BigSpender, 34), + (Origin::WelatiElection, 40), + (Origin::WelatiAdmin, 41), + (Origin::CitizenshipAdmin, 42), + ]; + + for (origin, expected_id) in origin_to_track { + let pallet_origin: ::PalletsOrigin = + origin.clone().into(); + let result = >::track_for(&pallet_origin); + assert_eq!( + result, + Ok(expected_id), + "Origin {:?} should map to track {expected_id}", + origin + ); + } +}