diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83904fa..e7e50b6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,13 +1,6 @@ # ============================================================================= # ApexChainx Contracts — CI Pipeline # ============================================================================= -# -# Triggered on push/PR to main. Essential checks: -# 1. Formatting (rustfmt) -# 2. Linting (clippy) -# 3. Build (cargo build) -# 4. Tests (cargo test) -# ============================================================================= name: CI @@ -22,31 +15,102 @@ concurrency: cancel-in-progress: true jobs: - test: - name: Build, Test & Verify + client-checks: + name: Client Checks runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout code + uses: actions/checkout@v4 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable - name: Install Rust toolchain uses: dtolnay/rust-toolchain@1.94.1 with: + targets: wasm32-unknown-unknown components: rustfmt, clippy - - name: Cache Cargo dependencies - uses: Swatinem/rust-cache@v2 - with: - workspaces: apexchainx_calculator + - name: Debug environment + run: | + echo "=== Rust toolchain ===" + rustc --version + cargo --version + rustup show + echo "=== Project structure ===" + ls -la + ls -la apexchainx_calculator/ + echo "=== Cargo.toml ===" + cat apexchainx_calculator/Cargo.toml + + - name: Cargo check + working-directory: apexchainx_calculator + run: | + cargo check 2>&1 | tee check.log || (echo "=== Cargo check failed ===" && cat check.log && exit 1) + + - name: Format check + working-directory: apexchainx_calculator + run: cargo fmt --check + + - name: Clippy + working-directory: apexchainx_calculator + run: cargo clippy --all-targets -- -D warnings - - name: Check formatting - run: cargo fmt --manifest-path apexchainx_calculator/Cargo.toml -- --check + - name: Build native + working-directory: apexchainx_calculator + run: cargo build - - name: Clippy linting - run: cargo clippy --manifest-path apexchainx_calculator/Cargo.toml + - name: Build WASM + working-directory: apexchainx_calculator + run: cargo build --target wasm32-unknown-unknown - - name: Build - run: cargo build --manifest-path apexchainx_calculator/Cargo.toml + e2e-tests: + name: E2E Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable - name: Run tests - run: cargo test --manifest-path apexchainx_calculator/Cargo.toml + working-directory: apexchainx_calculator + run: cargo test --lib + + provenance-hashes: + name: Provenance & Hashes + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@1.94.1 + with: + targets: wasm32-unknown-unknown + + - name: Build WASM release + working-directory: apexchainx_calculator + run: cargo build --target wasm32-unknown-unknown --release + + - name: Generate hash + run: | + WASM=target/wasm32-unknown-unknown/release/apexchainx_calculator.wasm + if [ -f "$WASM" ]; then + sha256sum "$WASM" | awk '{print $1 " apexchainx_calculator.wasm"}' > provenance.sha256 + echo "Hash generated successfully" + cat provenance.sha256 + else + echo "WASM file not found at $WASM" + exit 1 + fi + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: pr-provenance-hash + path: provenance.sha256 + retention-days: 30 diff --git a/.github/workflows/release-hash.yml b/.github/workflows/release-hash.yml index 1d955c2..1714b39 100644 --- a/.github/workflows/release-hash.yml +++ b/.github/workflows/release-hash.yml @@ -4,6 +4,7 @@ # # Produces a SHA-256 hash manifest for the release WASM artifact. # Only runs on version tags (v*), not on every PR/push. +# Optimized with WASM-specific caching for faster rebuilds. # ============================================================================= name: Release Artifact Hash Manifest @@ -26,13 +27,26 @@ jobs: with: targets: wasm32-unknown-unknown - - name: Cache Cargo dependencies + - name: Cache Cargo dependencies (WASM-optimized for release) uses: Swatinem/rust-cache@v2 - with: - workspaces: apexchainx_calculator - name: Build release WASM - run: cargo build --release --manifest-path apexchainx_calculator/Cargo.toml --target wasm32-unknown-unknown + run: | + echo "🔨 Building WASM release artifact..." + cd apexchainx_calculator + + START_TIME=$(date +%s) + time cargo build --release --target wasm32-unknown-unknown + END_TIME=$(date +%s) + BUILD_DURATION=$((END_TIME - START_TIME)) + + echo "✅ WASM build completed in ${BUILD_DURATION} seconds" + + # Log final artifact info for verification + WASM=target/wasm32-unknown-unknown/release/apexchainx_calculator.wasm + if [ -f "$WASM" ]; then + echo "📦 Final WASM size: $(wc -c < "$WASM") bytes" + fi - name: Generate SHA-256 manifest run: | diff --git a/apexchainx_calculator/Cargo.toml b/apexchainx_calculator/Cargo.toml index 1df2c82..d093845 100644 --- a/apexchainx_calculator/Cargo.toml +++ b/apexchainx_calculator/Cargo.toml @@ -11,10 +11,10 @@ publish = false crate-type = ["cdylib"] [features] -export-snapshots = [] +default = [] [dependencies] -soroban-sdk = "21.0.0" +soroban-sdk = { version = "21.1.0", features = ["alloc"] } [dev-dependencies] -soroban-sdk = { version = "21.0.0", features = ["testutils"] } +soroban-sdk = { version = "21.1.0", features = ["testutils", "alloc"] } diff --git a/apexchainx_calculator/src/auth_matrix_tests.rs b/apexchainx_calculator/src/auth_matrix_tests.rs index 1a8c196..7701475 100644 --- a/apexchainx_calculator/src/auth_matrix_tests.rs +++ b/apexchainx_calculator/src/auth_matrix_tests.rs @@ -1,7 +1,7 @@ #[cfg(test)] mod auth_matrix_tests { use soroban_sdk::{symbol_short, testutils::Address as _, Address, Env}; - use crate::{SLACalculatorContract, SLACalculatorContractClient, SLAConfig, SLAError}; + use crate::{SLACalculatorContract, SLACalculatorContractClient}; fn setup(env: &Env) -> (Address, Address, SLACalculatorContractClient) { let contract_id = env.register_contract(None, SLACalculatorContract); @@ -27,7 +27,9 @@ mod auth_matrix_tests { client.set_config( &admin, &symbol_short!("high"), - &SLAConfig { threshold_minutes: 30, penalty_per_minute: 50, reward_base: 500 }, + &30, + &50, + &500, ); } @@ -127,7 +129,9 @@ mod auth_matrix_tests { client.set_config( &stranger, &symbol_short!("high"), - &SLAConfig { threshold_minutes: 30, penalty_per_minute: 50, reward_base: 500 }, + &30, + &50, + &500, ); } @@ -139,10 +143,11 @@ mod auth_matrix_tests { client.set_config( &operator, &symbol_short!("high"), - &SLAConfig { threshold_minutes: 30, penalty_per_minute: 50, reward_base: 500 }, + &30, + &50, + &500, ); } - #[test] #[should_panic] fn test_stranger_cannot_pause() { diff --git a/apexchainx_calculator/src/coordination_harness.rs b/apexchainx_calculator/src/coordination_harness.rs index ee223be..8480a39 100644 --- a/apexchainx_calculator/src/coordination_harness.rs +++ b/apexchainx_calculator/src/coordination_harness.rs @@ -26,13 +26,11 @@ mod coordination_harness_tests { use soroban_sdk::testutils::Address as _; use soroban_sdk::{symbol_short, Address, Env, Symbol, Vec}; - use crate::cross_contract_safety::{ - self, CrossContractCallStatus, CrossContractSafety, SafeCallResult, - }; + use crate::cross_contract_safety::{self, CrossContractCallStatus, CrossContractSafety}; use crate::event_correlation; use crate::version_negotiation::{ - self, build_negotiation_info, negotiate_contract_versions, NegotiationOutcome, - VersionMismatchDetail, VersionNegotiationInfo, + build_negotiation_info, negotiate_contract_versions, NegotiationOutcome, + VersionNegotiationInfo, }; // ----------------------------------------------------------------------- @@ -285,7 +283,7 @@ mod coordination_harness_tests { assert_ne!(corr_id, 0, "Step 2: Correlation ID must be non-zero"); // Step 3: Prepare safety tracker for calls - let mut safety = CrossContractSafety::new(&env); + let safety = CrossContractSafety::new(&env); assert!(!safety.has_pending(), "Step 3: Safety tracker starts empty"); // Step 4: Verify correlation topics propagate diff --git a/apexchainx_calculator/src/cross_contract_safety.rs b/apexchainx_calculator/src/cross_contract_safety.rs index 3749ed8..b3c9750 100644 --- a/apexchainx_calculator/src/cross_contract_safety.rs +++ b/apexchainx_calculator/src/cross_contract_safety.rs @@ -403,7 +403,7 @@ mod tests { #[test] fn test_safe_call_result_debug() { - let env = Env::default(); + let _env = Env::default(); let result = SafeCallResult { status: CrossContractCallStatus::Success, raw_output: Val::from(true), diff --git a/apexchainx_calculator/src/event_schema.rs b/apexchainx_calculator/src/event_schema.rs index 9ef1827..f3fa2b3 100644 --- a/apexchainx_calculator/src/event_schema.rs +++ b/apexchainx_calculator/src/event_schema.rs @@ -99,7 +99,7 @@ //! end) are NOT considered breaking and do not require a version bump as long //! as old consumers ignore unrecognised trailing fields. -#![expect(dead_code)] +#![allow(dead_code)] use soroban_sdk::{symbol_short, Symbol}; @@ -132,7 +132,6 @@ pub fn current_event_version() -> Symbol { mod tests { use super::*; use alloc::format; - use soroban_sdk::Env; #[test] fn test_event_version_is_stable() { diff --git a/apexchainx_calculator/src/event_state_tests.rs b/apexchainx_calculator/src/event_state_tests.rs index 204fa2a..4f9bad7 100644 --- a/apexchainx_calculator/src/event_state_tests.rs +++ b/apexchainx_calculator/src/event_state_tests.rs @@ -6,7 +6,7 @@ mod event_state_tests { }; use crate::{ EVENT_CONFIG_UPD, EVENT_PRUNED, EVENT_PRUNED_AGE, EVENT_SETTLE_INTENT, EVENT_SLA_CALC, - EVENT_VERSION, SLACalculatorContract, SLACalculatorContractClient, SLAConfig, SLAError, + EVENT_VERSION, SLACalculatorContract, SLACalculatorContractClient, SLAConfig, }; fn setup(env: &Env) -> (Address, Address, SLACalculatorContractClient) { diff --git a/apexchainx_calculator/src/tests.rs b/apexchainx_calculator/src/tests.rs index 08b8185..89574b5 100644 --- a/apexchainx_calculator/src/tests.rs +++ b/apexchainx_calculator/src/tests.rs @@ -129,7 +129,7 @@ fn test_result_schema_is_explicit_and_stable() { assert_eq!(schema.rating_excellent, symbol_short!("excel")); assert_eq!(schema.rating_good, symbol_short!("good")); assert_eq!(schema.rating_poor, symbol_short!("poor")); - assert_eq!(schema.includes_config_version_hash, true); + assert!(schema.includes_config_version_hash); } #[test] @@ -374,7 +374,7 @@ fn test_old_operator_locked_out_after_rotation() { #[test] fn test_contract_starts_unpaused() { let (_env, client, _actors) = setup(); - assert_eq!(client.is_paused(), false); + assert!(!client.is_paused()); } #[test] @@ -382,10 +382,10 @@ fn test_admin_can_pause_and_unpause() { let (_env, client, actors) = setup(); client.pause(&actors.admin, &soroban_sdk::String::from_str(&_env, "test")); - assert_eq!(client.is_paused(), true); + assert!(client.is_paused()); client.unpause(&actors.admin); - assert_eq!(client.is_paused(), false); + assert!(!client.is_paused()); } #[test] @@ -1386,108 +1386,6 @@ fn test_repeated_config_updates_across_severities_are_independent() { assert_eq!(low.threshold_minutes, 120); } -// ============================================================ -// #50 – Canonical SLA vector snapshot export -// ============================================================ - -#[cfg(feature = "export-snapshots")] -mod snapshots { - use super::*; - use std::fs; - use std::path::Path; - - fn write_snapshot(name: &str, json: &str) { - let dir = Path::new("test_snapshots/tests"); - fs::create_dir_all(dir).unwrap(); - fs::write(dir.join(format!("{}.json", name)), json).unwrap(); - } - - #[test] - fn test_backend_parity_threshold_boundary_cases_snapshot() { - let (env, client, actors) = setup(); - let cases = [ - ("critical", 15u32, "met", "rew", "good", 750i128), - ("critical", 16, "viol", "pen", "poor", -100), - ("high", 30, "met", "rew", "good", 750), - ("high", 31, "viol", "pen", "poor", -50), - ("medium", 60, "met", "rew", "good", 750), - ("medium", 61, "viol", "pen", "poor", -25), - ("low", 120, "met", "rew", "good", 600), - ("low", 121, "viol", "pen", "poor", -10), - ]; - - let mut entries = Vec::new(); - for (sev, mttr, status, ptype, rating, amount) in cases { - let result = - client.calculate_sla_view(&symbol(&env, "SNAP_B"), &symbol(&env, sev), &mttr); - assert_eq!(result.status, symbol(&env, status)); - assert_eq!(result.payment_type, symbol(&env, ptype)); - assert_eq!(result.rating, symbol(&env, rating)); - assert_eq!(result.amount, amount); - entries.push(format!( - r#"{{"severity":"{sev}","mttr_minutes":{mttr},"status":"{status}","payment_type":"{ptype}","rating":"{rating}","amount":{amount}}}"# - )); - } - write_snapshot( - "test_backend_parity_threshold_boundary_cases", - &format!("[{}]", entries.join(",")), - ); - } - - #[test] - fn test_backend_parity_reward_tier_cases_snapshot() { - let (env, client, _actors) = setup(); - let cases = [ - ("critical", 7u32, "met", "rew", "top", 1500i128), - ("critical", 10, "met", "rew", "excel", 1125), - ("critical", 15, "met", "rew", "good", 750), - ("low", 59, "met", "rew", "top", 1200), - ("low", 89, "met", "rew", "excel", 900), - ("low", 120, "met", "rew", "good", 600), - ]; - - let mut entries = Vec::new(); - for (sev, mttr, status, ptype, rating, amount) in cases { - let result = - client.calculate_sla_view(&symbol(&env, "SNAP_R"), &symbol(&env, sev), &mttr); - assert_eq!(result.status, symbol(&env, status)); - assert_eq!(result.payment_type, symbol(&env, ptype)); - assert_eq!(result.rating, symbol(&env, rating)); - assert_eq!(result.amount, amount); - entries.push(format!( - r#"{{"severity":"{sev}","mttr_minutes":{mttr},"status":"{status}","payment_type":"{ptype}","rating":"{rating}","amount":{amount}}}"# - )); - } - write_snapshot( - "test_backend_parity_reward_tier_cases", - &format!("[{}]", entries.join(",")), - ); - } - - #[test] - fn test_config_snapshot_is_deterministic_and_complete_snapshot() { - let (_env, client, _actors) = setup(); - let snap = client.get_config_snapshot(); - assert_eq!(snap.entries.len(), 4); - - let mut entries = Vec::new(); - for i in 0..snap.entries.len() { - let e = snap.entries.get(i).unwrap(); - entries.push(format!( - r#"{{"severity":"{}","threshold_minutes":{},"penalty_per_minute":{},"reward_base":{}}}"#, - ["critical", "high", "medium", "low"][i as usize], - e.config.threshold_minutes, - e.config.penalty_per_minute, - e.config.reward_base, - )); - } - write_snapshot( - "test_config_snapshot_is_deterministic_and_complete", - &format!("[{}]", entries.join(",")), - ); - } -} - // ============================================================ // #94 – Fixture helpers for repeated actor and contract setup // ============================================================ @@ -1850,9 +1748,9 @@ fn test_backend_smoke_violation_path() { #[test] #[should_panic] fn test_admin_gated_call_fails_after_renounce() { - let (env, client, actors) = setup(); + let (_env, client, actors) = setup(); client.renounce_admin(&actors.admin); - // set_config must now panic – no admin exists + client.set_config(&actors.admin, &symbol_short!("critical"), &20, &200, &1000); } @@ -3848,7 +3746,7 @@ fn test_renounce_while_paused_succeeds() { &actors.admin, &soroban_sdk::String::from_str(&env, "maintenance"), ); - assert_eq!(client.is_paused(), true); + assert!(client.is_paused()); // Renounce must succeed regardless of pause state client.renounce_admin(&actors.admin); @@ -3996,12 +3894,12 @@ fn test_repeated_pause_unpause_cycles_is_paused_state_consistent() { let (env, client, actors) = setup(); for _ in 0..5u32 { - assert_eq!(client.is_paused(), false); + assert!(!client.is_paused()); client.pause(&actors.admin, &soroban_sdk::String::from_str(&env, "cycle")); - assert_eq!(client.is_paused(), true); + assert!(client.is_paused()); client.unpause(&actors.admin); } - assert_eq!(client.is_paused(), false); + assert!(!client.is_paused()); } #[test] @@ -4113,7 +4011,7 @@ fn test_storage_growth_history_grows_linearly_then_caps() { // Set a small cap and verify it holds client.set_retention_limit(&admin, &10); - let oid = Symbol::new(&env, &alloc::format!("GRW_last")); + let oid = Symbol::new(&env, "GRW_last"); client.calculate_sla(&op, &oid, &symbol_short!("low"), &10); assert_eq!( client.get_history().len(), @@ -4509,8 +4407,8 @@ fn test_invariance_boundary_mttr_zero() { .enumerate() { let oid = Symbol::new(&_env, &alloc::format!("Z_{}", idx)); - let view = client.calculate_sla_view(&oid, &sev, &0); - let mutating = client.calculate_sla(&actors.operator, &oid, &sev, &0); + let view = client.calculate_sla_view(&oid, sev, &0); + let mutating = client.calculate_sla(&actors.operator, &oid, sev, &0); assert_eq!(view.status, symbol_short!("met")); assert_eq!(view.status, mutating.status); assert_eq!(view.amount, mutating.amount); @@ -5049,7 +4947,7 @@ fn test_error_invalid_reward_is_terminal() { #[test] fn test_error_invalid_severity_is_terminal() { - let (env, client, actors) = setup(); + let (_env, client, actors) = setup(); let result = client.try_set_config(&actors.admin, &symbol_short!("bogus"), &30, &50, &500); assert_eq!(result.unwrap_err().unwrap(), SLAError::InvalidSeverity); } @@ -5241,11 +5139,11 @@ fn test_failed_set_retention_limit_leaves_limit_unchanged() { #[test] fn test_failed_pause_unauthorized_leaves_pause_state_unchanged() { let (_env, client, actors) = setup(); - assert_eq!(client.is_paused(), false); + assert!(!client.is_paused()); let _ = client.try_pause(&actors.stranger, &soroban_sdk::String::from_str(&_env, "x")); - assert_eq!(client.is_paused(), false); + assert!(!client.is_paused()); } // ============================================================ @@ -6100,8 +5998,7 @@ fn test_257_calculate_sla_view_hash_matches_standalone() { #[test] fn test_257_config_snapshot_entry_order_is_canonical() { - // get_config_snapshot must return entries in canonical order: critical, high, medium, low. - let (env, client, _actors) = setup(); + let (_env, client, _actors) = setup(); let snapshot = client.get_config_snapshot(); let expected = [ symbol_short!("critical"), @@ -6193,8 +6090,8 @@ fn test_exclusivity_at_exact_threshold_boundary_is_met() { assert!(result.amount > 0); } +#[test] fn test_257_result_schema_fields_are_stable() { - // get_result_schema must return the expected symbol constants. let (_env, client, _actors) = setup(); let schema = client.get_result_schema(); assert_eq!(schema.status_met, symbol_short!("met")); diff --git a/apexchainx_calculator/src/threshold_config.rs b/apexchainx_calculator/src/threshold_config.rs index 83e369d..6949b8a 100644 --- a/apexchainx_calculator/src/threshold_config.rs +++ b/apexchainx_calculator/src/threshold_config.rs @@ -6,9 +6,6 @@ //! //! # Test Scenarios //! -//! - `test_zero_threshold_always_violated`: A threshold of 0 minutes means -//! any positive MTTR is a violation. This tests the boundary condition -//! where even 1 minute of repair time exceeds the threshold. //! - `test_near_zero_threshold_one_minute`: A 1-minute threshold creates a //! razor-thin boundary where MTTR of 1 minute meets the SLA but MTTR of //! 2 minutes violates it.