use crate::{
constants::WASM_PAGE_SIZE_BYTES,
declaration::AllocationDeclaration,
ledger::{AllocationLedger, AllocationRecord, GenerationRecord},
physical::CommitStoreDiagnostic,
policy::PolicyIdentity,
registry::SealedDeclarationFingerprint,
slot::{MemoryManagerAuthorityRecord, MemoryManagerRangeAuthority, MemoryManagerSlot},
};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct DiagnosticExport {
pub current_generation: u64,
pub ledger_anchor: MemoryManagerSlot,
pub records: Vec<DiagnosticRecord>,
pub generations: Vec<GenerationRecord>,
#[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
pub commit_recovery: Option<CommitStoreDiagnostic>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct DiagnosticRuntimeBinding {
pub policy_identity: PolicyIdentity,
pub declaration_fingerprint: SealedDeclarationFingerprint,
}
impl DiagnosticRuntimeBinding {
#[must_use]
pub const fn new(
policy_identity: PolicyIdentity,
declaration_fingerprint: SealedDeclarationFingerprint,
) -> Self {
Self {
policy_identity,
declaration_fingerprint,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct MemoryRuntimeDoctorReport {
pub bootstrapped: bool,
pub tested_policy_identity: Result<PolicyIdentity, DiagnosticFailure>,
pub tested_declaration_fingerprint: SealedDeclarationFingerprint,
#[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
pub established_bootstrap_binding: Option<DiagnosticRuntimeBinding>,
pub bootstrap_binding: DiagnosticCheck,
pub ledger_anchor: MemoryManagerSlot,
pub stable_cell: DiagnosticStableCell,
#[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
pub commit_recovery: Option<CommitStoreDiagnostic>,
#[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
pub ledger: Option<DiagnosticExport>,
pub registered_declarations: Vec<DiagnosticDeclaration>,
pub range_authority: DiagnosticRangeAuthority,
pub validation: DiagnosticCheck,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct DiagnosticDeclaration {
pub authority: String,
pub declaration: AllocationDeclaration,
}
impl DiagnosticDeclaration {
#[must_use]
pub fn new(authority: impl Into<String>, declaration: AllocationDeclaration) -> Self {
Self {
authority: authority.into(),
declaration,
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum DiagnosticCode {
#[serde(rename = "stable_cell")]
StableCell,
#[serde(rename = "unsupported_format")]
UnsupportedFormat,
#[serde(rename = "ledger_recovery")]
LedgerRecovery,
#[serde(rename = "allocation_validation")]
AllocationValidation,
#[serde(rename = "policy_identity")]
PolicyIdentity,
#[serde(rename = "runtime_binding")]
RuntimeBinding,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct DiagnosticFailure {
pub code: DiagnosticCode,
pub message: String,
}
impl DiagnosticFailure {
#[must_use]
pub fn new(code: DiagnosticCode, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct DiagnosticRangeAuthority {
pub registered_records: Vec<MemoryManagerAuthorityRecord>,
pub effective_authority: MemoryManagerRangeAuthority,
}
impl DiagnosticRangeAuthority {
#[must_use]
pub const fn new(
registered_records: Vec<MemoryManagerAuthorityRecord>,
effective_authority: MemoryManagerRangeAuthority,
) -> Self {
Self {
registered_records,
effective_authority,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct DiagnosticStableCell {
pub status: DiagnosticStableCellStatus,
pub memory_size: DiagnosticMemorySize,
}
impl DiagnosticStableCell {
#[must_use]
pub const fn new(
status: DiagnosticStableCellStatus,
memory_size: DiagnosticMemorySize,
) -> Self {
Self {
status,
memory_size,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub enum DiagnosticStableCellStatus {
Empty,
Readable,
Corrupt {
failure: DiagnosticFailure,
},
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub enum DiagnosticCheck {
NotRun {
code: DiagnosticCode,
message: String,
},
Passed,
Failed {
code: DiagnosticCode,
message: String,
},
}
impl DiagnosticCheck {
#[must_use]
pub const fn passed() -> Self {
Self::Passed
}
#[must_use]
pub fn failed(code: DiagnosticCode, message: impl Into<String>) -> Self {
Self::Failed {
code,
message: message.into(),
}
}
#[must_use]
pub fn not_run(code: DiagnosticCode, message: impl Into<String>) -> Self {
Self::NotRun {
code,
message: message.into(),
}
}
}
impl DiagnosticExport {
#[must_use]
pub fn from_ledger(ledger: &AllocationLedger, ledger_anchor: MemoryManagerSlot) -> Self {
Self::from_records(
ledger.current_generation,
ledger_anchor,
ledger.allocation_history.records.iter().cloned(),
ledger.allocation_history.generations.clone(),
)
}
pub(crate) fn from_owned_ledger(
ledger: AllocationLedger,
ledger_anchor: MemoryManagerSlot,
) -> Self {
Self::from_records(
ledger.current_generation,
ledger_anchor,
ledger.allocation_history.records.into_iter(),
ledger.allocation_history.generations,
)
}
fn from_records(
current_generation: u64,
ledger_anchor: MemoryManagerSlot,
records: impl Iterator<Item = AllocationRecord>,
generations: Vec<GenerationRecord>,
) -> Self {
Self {
current_generation,
ledger_anchor,
records: records
.map(|allocation| DiagnosticRecord {
allocation,
memory_size: None,
})
.collect(),
generations,
commit_recovery: None,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct DiagnosticRecord {
pub allocation: AllocationRecord,
#[serde(skip_serializing_if = "Option::is_none")]
pub memory_size: Option<DiagnosticMemorySize>,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct DiagnosticMemorySize {
pub wasm_pages: u64,
pub bytes: u64,
}
impl DiagnosticMemorySize {
#[must_use]
pub const fn from_wasm_pages(wasm_pages: u64) -> Self {
Self {
wasm_pages,
bytes: wasm_pages.saturating_mul(WASM_PAGE_SIZE_BYTES),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
declaration::AllocationDeclaration,
ledger::{AllocationHistory, AllocationRecord},
physical::{CommitRecoveryError, CommitSlotDiagnostic, CommitStoreDiagnostic},
schema::SchemaMetadata,
};
#[test]
fn diagnostic_export_copies_ledger_records() {
let declaration = AllocationDeclaration::new(
"app.users.v1",
MemoryManagerSlot::new(100).expect("usable slot"),
None,
SchemaMetadata::default(),
)
.expect("declaration");
let ledger = AllocationLedger {
current_generation: 3,
allocation_history: AllocationHistory::from_parts(
vec![AllocationRecord::active(3, &declaration)],
vec![GenerationRecord {
generation: 3,
parent_generation: 2,
runtime_fingerprint: Some("wasm:abc123".to_string()),
declaration_count: 1,
committed_at: None,
}],
),
};
let export =
DiagnosticExport::from_ledger(&ledger, MemoryManagerSlot::new(0).expect("usable slot"));
assert_eq!(export.current_generation, 3);
assert_eq!(export.records.len(), 1);
assert_eq!(export.records[0].memory_size, None);
assert_eq!(export.generations.len(), 1);
assert_eq!(
export.ledger_anchor,
MemoryManagerSlot::new(0).expect("usable slot")
);
assert_eq!(export.commit_recovery, None);
assert_eq!(
export.generations,
ledger.allocation_history().generations()
);
assert_eq!(
export,
DiagnosticExport::from_owned_ledger(ledger, export.ledger_anchor.clone())
);
let wire = serde_json::to_value(&export).expect("diagnostic JSON");
assert_eq!(
wire["generations"][0],
serde_json::json!({
"generation": 3,
"parent_generation": 2,
"runtime_fingerprint": "wasm:abc123",
"declaration_count": 1,
"committed_at": null,
})
);
let decoded: DiagnosticExport = serde_json::from_value(wire).expect("current JSON shape");
assert_eq!(decoded, export);
}
#[test]
fn diagnostic_export_rejects_unknown_top_level_fields() {
use crate::test_cbor::Value;
let export = DiagnosticExport {
current_generation: 0,
ledger_anchor: MemoryManagerSlot::new(0).expect("usable slot"),
records: Vec::new(),
generations: Vec::new(),
commit_recovery: None,
};
let Value::Map(mut map) = crate::test_cbor::to_value(export).expect("diagnostic value")
else {
panic!("diagnostic export encodes as a map");
};
map.push((Value::Text("future_field".to_string()), Value::Bool(true)));
let bytes = crate::test_cbor::to_vec(&Value::Map(map)).expect("diagnostic bytes");
let err = crate::test_cbor::from_slice::<DiagnosticExport>(&bytes)
.expect_err("unknown diagnostic field must fail closed");
assert!(err.to_string().contains("future_field"));
}
#[test]
fn diagnostic_outcome_states_round_trip() {
let stable_cell = DiagnosticStableCell::new(
DiagnosticStableCellStatus::Corrupt {
failure: DiagnosticFailure::new(
DiagnosticCode::StableCell,
"bad stable-cell record",
),
},
DiagnosticMemorySize::from_wasm_pages(1),
);
let range_authority =
DiagnosticRangeAuthority::new(Vec::new(), MemoryManagerRangeAuthority::default());
let check = DiagnosticCheck::failed(
DiagnosticCode::AllocationValidation,
"duplicate declaration",
);
for value in [DiagnosticCheck::passed(), check] {
let bytes = crate::test_cbor::to_vec(&value).expect("check bytes");
let decoded: DiagnosticCheck =
crate::test_cbor::from_slice(&bytes).expect("check round trip");
assert_eq!(decoded, value);
}
let bytes = crate::test_cbor::to_vec(&stable_cell).expect("stable-cell diagnostic bytes");
let decoded: DiagnosticStableCell =
crate::test_cbor::from_slice(&bytes).expect("stable-cell round trip");
assert_eq!(decoded, stable_cell);
let bytes = crate::test_cbor::to_vec(&range_authority).expect("range diagnostic bytes");
let decoded: DiagnosticRangeAuthority =
crate::test_cbor::from_slice(&bytes).expect("range round trip");
assert_eq!(decoded, range_authority);
}
#[test]
fn diagnostic_codes_have_stable_wire_names() {
let cases = [
(DiagnosticCode::StableCell, "stable_cell"),
(DiagnosticCode::UnsupportedFormat, "unsupported_format"),
(DiagnosticCode::LedgerRecovery, "ledger_recovery"),
(
DiagnosticCode::AllocationValidation,
"allocation_validation",
),
(DiagnosticCode::PolicyIdentity, "policy_identity"),
(DiagnosticCode::RuntimeBinding, "runtime_binding"),
];
for (code, expected) in cases {
assert_eq!(
crate::test_cbor::to_value(code).expect("diagnostic code value"),
crate::test_cbor::Value::Text(expected.to_string())
);
}
}
#[test]
fn diagnostic_export_can_include_commit_recovery_state() {
let ledger = AllocationLedger {
current_generation: 3,
allocation_history: AllocationHistory::default(),
};
let commit_recovery = CommitStoreDiagnostic {
slot0: CommitSlotDiagnostic::Valid { generation: 3 },
slot1: CommitSlotDiagnostic::Empty,
recovery: Ok(3),
};
let mut export =
DiagnosticExport::from_ledger(&ledger, MemoryManagerSlot::new(0).expect("usable slot"));
export.commit_recovery = Some(commit_recovery);
assert_eq!(export.commit_recovery, Some(commit_recovery));
}
#[test]
fn diagnostic_export_can_include_memory_sizes() {
let declaration = AllocationDeclaration::new(
"app.users.v1",
MemoryManagerSlot::new(100).expect("usable slot"),
None,
SchemaMetadata::default(),
)
.expect("declaration");
let ledger = AllocationLedger {
current_generation: 3,
allocation_history: AllocationHistory::from_parts(
vec![AllocationRecord::active(3, &declaration)],
Vec::new(),
),
};
let mut export =
DiagnosticExport::from_ledger(&ledger, MemoryManagerSlot::new(0).expect("usable slot"));
export.records[0].memory_size = Some(DiagnosticMemorySize::from_wasm_pages(2));
assert_eq!(
export.records[0].memory_size,
Some(DiagnosticMemorySize {
wasm_pages: 2,
bytes: 131_072,
})
);
let wire = serde_json::to_value(&export).expect("diagnostic JSON");
assert_eq!(
wire["records"][0]["memory_size"],
serde_json::json!({"wasm_pages": 2, "bytes": 131_072})
);
}
#[test]
fn diagnostic_export_can_report_recovery_failure() {
let ledger = AllocationLedger {
current_generation: 0,
allocation_history: AllocationHistory::default(),
};
let commit_recovery = CommitStoreDiagnostic {
slot0: CommitSlotDiagnostic::Empty,
slot1: CommitSlotDiagnostic::Empty,
recovery: Err(CommitRecoveryError::NoValidGeneration),
};
let mut export =
DiagnosticExport::from_ledger(&ledger, MemoryManagerSlot::new(0).expect("usable slot"));
export.commit_recovery = Some(commit_recovery);
assert_eq!(
export.commit_recovery.expect("commit recovery").recovery,
Err(CommitRecoveryError::NoValidGeneration)
);
}
}