diff --git a/apexchainx_calculator/src/lib.rs b/apexchainx_calculator/src/lib.rs index a8d11bd..764c8ed 100644 --- a/apexchainx_calculator/src/lib.rs +++ b/apexchainx_calculator/src/lib.rs @@ -546,6 +546,71 @@ impl SLACalculatorContract { Ok(()) } + // Initialise any storage keys that may be missing from older schema + // versions. This is intentionally conservative: only set a value when + // the key is absent so migration is idempotent and does not overwrite + // existing state. + fn init_missing_storage_defaults(env: &Env) { + let inst = env.storage().instance(); + + if !inst.has(&PAUSED_KEY) { + inst.set(&PAUSED_KEY, &false); + } + + if !inst.has(&STATS_KEY) { + inst.set( + &STATS_KEY, + &SLAStats { + total_calculations: 0, + total_violations: 0, + total_rewards: 0, + total_penalties: 0, + }, + ); + } + + if !inst.has(&HISTORY_KEY) { + inst.set(&HISTORY_KEY, &Vec::::new(env)); + } + + if !inst.has(&CONFIG_KEY) { + let mut configs = Map::::new(env); + configs.set( + symbol_short!("critical"), + SLAConfig { + threshold_minutes: 15, + penalty_per_minute: 100, + reward_base: 750, + }, + ); + configs.set( + symbol_short!("high"), + SLAConfig { + threshold_minutes: 30, + penalty_per_minute: 50, + reward_base: 750, + }, + ); + configs.set( + symbol_short!("medium"), + SLAConfig { + threshold_minutes: 60, + penalty_per_minute: 25, + reward_base: 750, + }, + ); + configs.set( + symbol_short!("low"), + SLAConfig { + threshold_minutes: 120, + penalty_per_minute: 10, + reward_base: 600, + }, + ); + inst.set(&CONFIG_KEY, &configs); + } + } + // ------------------------------------------------------------------- // #61 – Storage migration harness // ------------------------------------------------------------------- @@ -593,6 +658,11 @@ impl SLACalculatorContract { // v0 → v1: stamp the version; all other fields were set by initialize if current == 0 { + // Ensure any storage keys that might be missing from older + // deployments are initialised to deterministic defaults before + // we mark the storage version as migrated. This codifies the + // contract: migration arms must initialise newly-added keys. + Self::init_missing_storage_defaults(&env); env.storage().instance().set(&STORAGE_VERSION_KEY, &1u32); current = 1; } diff --git a/apexchainx_calculator/src/tests.rs b/apexchainx_calculator/src/tests.rs index 79d299e..fff7611 100644 --- a/apexchainx_calculator/src/tests.rs +++ b/apexchainx_calculator/src/tests.rs @@ -3200,6 +3200,39 @@ fn test_get_migration_state_after_migrate_shows_no_migration_needed() { assert!(!info.needs_migration); } +#[test] +fn test_migrate_initialises_missing_fields() { + // Simulate a frozen older snapshot that lacks keys added in later + // schemas, then run migrate and verify deterministic defaults are set. + let env = Env::default(); + let cid = env.register_contract(None, SLACalculatorContract); + let client = SLACalculatorContractClient::new(&env, &cid); + let admin = soroban_sdk::Address::generate(&env); + let op = soroban_sdk::Address::generate(&env); + client.initialize(&admin, &op); + + // Remove some keys to emulate an older schema state and force stored + // version to 0 so migrate will exercise the v0->v1 path. + env.as_contract(&cid, || { + env.storage().instance().remove(&CONFIG_KEY); + env.storage().instance().remove(&STATS_KEY); + env.storage().instance().set(&STORAGE_VERSION_KEY, &0u32); + }); + + // Run migration as admin + client.migrate(&admin); + + // After migrate the keys should exist again and the stored version + // should match the binary's STORAGE_VERSION. + let version = client.get_storage_version(); + assert_eq!(version, STORAGE_VERSION); + + env.as_contract(&cid, || { + assert!(env.storage().instance().has(&CONFIG_KEY)); + assert!(env.storage().instance().has(&STATS_KEY)); + }); +} + // ============================================================ // SC-011 – Latest result by outage (issue #131) – additional coverage // ============================================================