Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/core/src/decode/mappings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ pub mod budget;
pub mod context;
pub mod storage;
pub mod value;

#[cfg(test)]
mod severity_tests;
241 changes: 241 additions & 0 deletions crates/core/src/decode/mappings/severity_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
/// Tests that verify each HostError variant maps to the correct `Severity` level.
///
/// Coverage: Fatal, Error, and Warning are each exercised at least once.
#[cfg(test)]
mod tests {
use crate::decode::mappings::{budget, context, value};
use crate::taxonomy::loader::TaxonomyDatabase;
use crate::taxonomy::schema::ErrorCategory;
use crate::types::report::Severity;

// ------------------------------------------------------------------
// ErrorSeverity → Severity conversion
// ------------------------------------------------------------------

#[test]
fn error_severity_critical_maps_to_fatal() {
// Budget code 0 (CPUExceeded) is ErrorSeverity::Critical → Severity::Fatal
let detail = budget::lookup(0).expect("budget code 0 exists");
let severity: Severity = detail.severity.clone().into();
assert_eq!(severity, Severity::Fatal);
}

#[test]
fn error_severity_error_maps_to_error() {
// Budget code 8 (ExceededLimit) is ErrorSeverity::Error → Severity::Error
let detail = budget::lookup(8).expect("budget code 8 exists");
let severity: Severity = detail.severity.clone().into();
assert_eq!(severity, Severity::Error);
}

#[test]
fn error_severity_warning_maps_to_warning() {
// The ErrorSeverity → Severity conversion must preserve the Warning level.
use crate::decode::mappings::budget::ErrorSeverity;
let severity: Severity = ErrorSeverity::Warning.into();
assert_eq!(severity, Severity::Warning);
}

#[test]
fn error_severity_info_maps_to_info() {
use crate::decode::mappings::budget::ErrorSeverity;
let severity: Severity = ErrorSeverity::Info.into();
assert_eq!(severity, Severity::Info);
}

// ------------------------------------------------------------------
// Value mapping severity checks
// ------------------------------------------------------------------

#[test]
fn value_internal_error_maps_to_fatal() {
// Value code 4 (InternalError) is ErrorSeverity::Critical → Fatal
let detail = value::lookup(4).expect("value code 4 exists");
let severity: Severity = detail.severity.clone().into();
assert_eq!(severity, Severity::Fatal);
}

#[test]
fn value_invalid_input_maps_to_error() {
let detail = value::lookup(6).expect("value code 6 exists");
let severity: Severity = detail.severity.clone().into();
assert_eq!(severity, Severity::Error);
}

// ------------------------------------------------------------------
// Context mapping severity checks (uses Severity directly)
// ------------------------------------------------------------------

#[test]
fn context_internal_error_is_fatal() {
let detail = context::lookup(7).expect("context code 7 exists");
assert_eq!(detail.severity, Severity::Fatal);
}

#[test]
fn context_invalid_action_is_error() {
let detail = context::lookup(6).expect("context code 6 exists");
assert_eq!(detail.severity, Severity::Error);
}

// ------------------------------------------------------------------
// Taxonomy-driven build_report severity checks
// ------------------------------------------------------------------

#[test]
fn build_report_context_internal_error_is_fatal() {
use crate::decode::host_error::ClassifiedError;
use crate::decode::report::build_report;

let classified = ClassifiedError {
category: ErrorCategory::Context,
error_code: 7,
is_contract_error: false,
contract_id: None,
raw_data: serde_json::Value::Null,
};
let report = build_report(&classified).expect("report should build");
assert_eq!(report.severity, Severity::Fatal);
}

#[test]
fn build_report_auth_missing_auth_is_error() {
use crate::decode::host_error::ClassifiedError;
use crate::decode::report::build_report;

let classified = ClassifiedError {
category: ErrorCategory::Auth,
error_code: 2,
is_contract_error: false,
contract_id: None,
raw_data: serde_json::Value::Null,
};
let report = build_report(&classified).expect("report should build");
assert_eq!(report.severity, Severity::Error);
}

#[test]
fn build_report_unknown_code_defaults_to_error() {
use crate::decode::host_error::ClassifiedError;
use crate::decode::report::build_report;

let classified = ClassifiedError {
category: ErrorCategory::Budget,
error_code: 9999,
is_contract_error: false,
contract_id: None,
raw_data: serde_json::Value::Null,
};
let report = build_report(&classified).expect("report should build");
assert_eq!(report.severity, Severity::Error);
}

// ------------------------------------------------------------------
// Exhaustive: every mapping-table entry has an expected severity
// ------------------------------------------------------------------

#[test]
fn all_budget_entries_have_valid_severity() {
use crate::decode::mappings::budget::BUDGET_ERROR_DETAILS;

for entry in BUDGET_ERROR_DETAILS {
let sev: Severity = entry.severity.clone().into();
assert!(
matches!(sev, Severity::Fatal | Severity::Error | Severity::Warning | Severity::Info),
"Unexpected severity for budget code {}: {:?}",
entry.code,
sev
);
}
}

#[test]
fn all_value_entries_have_valid_severity() {
use crate::decode::mappings::value::VALUE_ERROR_DETAILS;

for entry in VALUE_ERROR_DETAILS {
let sev: Severity = entry.severity.clone().into();
assert!(
matches!(sev, Severity::Fatal | Severity::Error | Severity::Warning | Severity::Info),
"Unexpected severity for value code {}: {:?}",
entry.code,
sev
);
}
}

#[test]
fn taxonomy_fatal_severity_is_correctly_parsed() {
let db = TaxonomyDatabase::load_embedded().expect("taxonomy loads");
// Context code 7 is the only Fatal entry in the embedded taxonomy.
let entry = db
.lookup(&ErrorCategory::Context, 7)
.expect("context code 7 in taxonomy");
assert_eq!(entry.severity, "Fatal");
}

#[test]
fn taxonomy_error_severity_is_correctly_parsed() {
let db = TaxonomyDatabase::load_embedded().expect("taxonomy loads");
let entry = db
.lookup(&ErrorCategory::Auth, 1)
.expect("auth code 1 in taxonomy");
assert_eq!(entry.severity, "Error");
}

// ------------------------------------------------------------------
// Warning severity — mapping table and build_report
// ------------------------------------------------------------------

#[test]
fn storage_near_expiry_maps_to_warning() {
use crate::decode::mappings::storage;
// Storage code 4 (NearExpiry) is the canonical Warning entry.
let detail = storage::lookup(4).expect("storage code 4 exists");
assert_eq!(detail.severity, Severity::Warning);
}

#[test]
fn build_report_storage_near_expiry_is_warning() {
use crate::decode::host_error::ClassifiedError;
use crate::decode::report::build_report;

let classified = ClassifiedError {
category: ErrorCategory::Storage,
error_code: 4,
is_contract_error: false,
contract_id: None,
raw_data: serde_json::Value::Null,
};
let report = build_report(&classified).expect("report should build");
assert_eq!(report.severity, Severity::Warning);
}

#[test]
fn taxonomy_warning_severity_is_correctly_parsed() {
let db = TaxonomyDatabase::load_embedded().expect("taxonomy loads");
// Storage code 4 (NearExpiry) is the canonical Warning entry in the taxonomy.
let entry = db
.lookup(&ErrorCategory::Storage, 4)
.expect("storage code 4 in taxonomy");
assert_eq!(entry.severity, "Warning");
}

// ------------------------------------------------------------------
// Exhaustive: every storage mapping-table entry has a valid severity
// ------------------------------------------------------------------

#[test]
fn all_storage_entries_have_valid_severity() {
use crate::decode::mappings::storage::STORAGE_ERROR_DETAILS;

for entry in STORAGE_ERROR_DETAILS {
assert!(
matches!(entry.severity, Severity::Fatal | Severity::Error | Severity::Warning | Severity::Info),
"Unexpected severity for storage code {}: {:?}",
entry.code,
entry.severity
);
}
}
}
12 changes: 8 additions & 4 deletions crates/core/src/decode/mappings/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ pub const STORAGE_ERROR_DETAILS: &[StorageErrorDetail] = &[
summary: "An internal storage failure occurred. This code alone reveals nothing; check the diagnostic events to get more signal on the underlying issue.",
severity: Severity::Error,
},
StorageErrorDetail {
code: 4,
name: "NearExpiry",
summary: "The accessed ledger entry is approaching its expiration ledger and will soon become read-only unless extended.",
severity: Severity::Warning,
},
];

pub fn lookup(code: u32) -> Option<&'static StorageErrorDetail> {
Expand All @@ -58,10 +64,8 @@ mod tests {

#[test]
fn table_covers_known_storage_codes() {
assert_eq!(STORAGE_ERROR_DETAILS.len(), 4);
assert!(STORAGE_ERROR_DETAILS
.iter()
.all(|detail| detail.severity == Severity::Error));
assert_eq!(STORAGE_ERROR_DETAILS.len(), 5);
assert!(lookup(4).is_some());
assert!(lookup(99).is_none());
}
}
27 changes: 27 additions & 0 deletions crates/core/src/taxonomy/data/storage.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,30 @@ requires_upgrade = false

related_errors = ["host.storage.missing_key"]
source_file = "soroban-env-host/src/storage.rs"

[[errors]]
id = "host.storage.near_expiry"
category = "storage"
code = 4
name = "NearExpiry"
severity = "Warning"
since_protocol = 20
summary = "The accessed ledger entry is approaching its expiration ledger and will soon become read-only unless extended."
detailed_explanation = """
Soroban ledger entries have a time-to-live measured in ledgers. This warning indicates that a \
contract read or written an entry that is close to expiring. The operation succeeded, but the \
entry must be extended with extendTTL before it archives or future transactions will fail with \
EntryNotFound. This is not a blocking error — it is a signal to act before the entry is lost.
"""

[[errors.common_causes]]
description = "Contract data was created or last extended many ledgers ago and TTL refresh was missed"
likelihood = "high"

[[errors.suggested_fixes]]
description = "Call extendTTL on the entry to push the expiration ledger further into the future"
difficulty = "easy"
requires_upgrade = false

related_errors = ["host.storage.entry_not_found"]
source_file = "soroban-env-host/src/storage.rs"