1use crate::{
2 constants::WASM_PAGE_SIZE_BYTES,
3 ledger::{AllocationLedger, AllocationRecord},
4 physical::CommitSnapshotDiagnostic,
5 policy::PolicyIdentity,
6 slot::MemoryManagerSlot,
7};
8use serde::{Deserialize, Serialize};
9
10#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
15#[serde(deny_unknown_fields)]
16pub struct DiagnosticExport {
17 pub current_generation: u64,
19 pub ledger_anchor: MemoryManagerSlot,
21 pub records: Vec<DiagnosticRecord>,
23 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
25 pub commit_recovery: Option<CommitSnapshotDiagnostic>,
26}
27
28#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
36#[serde(deny_unknown_fields)]
37pub struct DiagnosticRuntimeBinding {
38 pub policy_identity: PolicyIdentity,
40 pub allocation_pool: crate::MemoryAllocationPool,
42}
43
44impl DiagnosticRuntimeBinding {
45 #[must_use]
47 pub const fn new(
48 policy_identity: PolicyIdentity,
49 allocation_pool: crate::MemoryAllocationPool,
50 ) -> Self {
51 Self {
52 policy_identity,
53 allocation_pool,
54 }
55 }
56}
57
58#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
70#[serde(deny_unknown_fields)]
71pub struct MemoryRuntimeDoctorReport {
72 pub bootstrapped: bool,
74 pub tested_policy_identity: Result<PolicyIdentity, DiagnosticFailure>,
76 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
78 pub established_bootstrap_binding: Option<DiagnosticRuntimeBinding>,
79 pub bootstrap_binding: DiagnosticCheck,
82 pub ledger_anchor: MemoryManagerSlot,
84 pub stable_cell: DiagnosticStableCell,
86 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
88 pub commit_recovery: Option<CommitSnapshotDiagnostic>,
89 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
91 pub ledger: Option<DiagnosticExport>,
92 pub requests: Vec<crate::MemoryRequest>,
94 pub allocation_pool: crate::MemoryAllocationPool,
96 pub validation: DiagnosticCheck,
98}
99
100#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
107pub enum DiagnosticCode {
108 #[serde(rename = "stable_cell")]
110 StableCell,
111 #[serde(rename = "unsupported_format")]
113 UnsupportedFormat,
114 #[serde(rename = "ledger_recovery")]
116 LedgerRecovery,
117 #[serde(rename = "allocation_validation")]
119 AllocationValidation,
120 #[serde(rename = "policy_identity")]
122 PolicyIdentity,
123 #[serde(rename = "runtime_binding")]
125 RuntimeBinding,
126}
127
128#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
135#[serde(deny_unknown_fields)]
136pub struct DiagnosticFailure {
137 pub code: DiagnosticCode,
139 pub message: String,
141}
142
143impl DiagnosticFailure {
144 #[must_use]
146 pub fn new(code: DiagnosticCode, message: impl Into<String>) -> Self {
147 Self {
148 code,
149 message: message.into(),
150 }
151 }
152}
153
154#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
161#[serde(deny_unknown_fields)]
162pub struct DiagnosticStableCell {
163 pub status: DiagnosticStableCellStatus,
165 pub memory_size: DiagnosticMemorySize,
167}
168
169impl DiagnosticStableCell {
170 #[must_use]
172 pub const fn new(
173 status: DiagnosticStableCellStatus,
174 memory_size: DiagnosticMemorySize,
175 ) -> Self {
176 Self {
177 status,
178 memory_size,
179 }
180 }
181}
182
183#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
190#[serde(deny_unknown_fields)]
191pub enum DiagnosticStableCellStatus {
192 Empty,
194 Readable,
196 Corrupt {
199 failure: DiagnosticFailure,
201 },
202}
203
204#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
211#[serde(deny_unknown_fields)]
212pub enum DiagnosticCheck {
213 NotRun {
215 code: DiagnosticCode,
217 message: String,
219 },
220 Passed,
222 Failed {
224 code: DiagnosticCode,
226 message: String,
228 },
229}
230
231impl DiagnosticCheck {
232 #[must_use]
234 pub const fn passed() -> Self {
235 Self::Passed
236 }
237
238 #[must_use]
240 pub fn failed(code: DiagnosticCode, message: impl Into<String>) -> Self {
241 Self::Failed {
242 code,
243 message: message.into(),
244 }
245 }
246
247 #[must_use]
249 pub fn not_run(code: DiagnosticCode, message: impl Into<String>) -> Self {
250 Self::NotRun {
251 code,
252 message: message.into(),
253 }
254 }
255}
256
257impl DiagnosticExport {
258 #[must_use]
264 pub fn from_ledger(ledger: &AllocationLedger, ledger_anchor: MemoryManagerSlot) -> Self {
265 Self::from_records(
266 ledger.current_generation,
267 ledger_anchor,
268 ledger.records.iter().cloned(),
269 )
270 }
271
272 pub(crate) fn from_owned_ledger(
275 ledger: AllocationLedger,
276 ledger_anchor: MemoryManagerSlot,
277 ) -> Self {
278 Self::from_records(
279 ledger.current_generation,
280 ledger_anchor,
281 ledger.records.into_iter(),
282 )
283 }
284
285 fn from_records(
286 current_generation: u64,
287 ledger_anchor: MemoryManagerSlot,
288 records: impl Iterator<Item = AllocationRecord>,
289 ) -> Self {
290 Self {
291 current_generation,
292 ledger_anchor,
293 records: records
294 .map(|allocation| DiagnosticRecord {
295 allocation,
296 memory_size: None,
297 })
298 .collect(),
299 commit_recovery: None,
300 }
301 }
302}
303
304#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
309#[serde(deny_unknown_fields)]
310pub struct DiagnosticRecord {
311 pub allocation: AllocationRecord,
313 #[serde(skip_serializing_if = "Option::is_none")]
318 pub memory_size: Option<DiagnosticMemorySize>,
319}
320
321#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
328#[serde(deny_unknown_fields)]
329pub struct DiagnosticMemorySize {
330 pub wasm_pages: u64,
332}
333
334impl DiagnosticMemorySize {
335 #[must_use]
337 pub const fn from_wasm_pages(wasm_pages: u64) -> Self {
338 Self { wasm_pages }
339 }
340
341 #[must_use]
343 pub const fn bytes(self) -> u64 {
344 self.wasm_pages.saturating_mul(WASM_PAGE_SIZE_BYTES)
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351 use crate::AllocationDeclaration;
352 use crate::{
353 ledger::AllocationRecord, physical::CommitSnapshotDiagnostic, schema::SchemaMetadata,
354 };
355
356 #[test]
357 fn diagnostic_export_copies_ledger_records() {
358 let declaration = AllocationDeclaration::new(
359 "app.users.v1",
360 MemoryManagerSlot::new(100).expect("usable slot"),
361 SchemaMetadata::default(),
362 )
363 .expect("declaration");
364 let ledger = AllocationLedger {
365 current_generation: 3,
366 records: vec![AllocationRecord::active(&declaration)],
367 };
368
369 let export =
370 DiagnosticExport::from_ledger(&ledger, MemoryManagerSlot::new(0).expect("usable slot"));
371
372 assert_eq!(export.current_generation, 3);
373 assert_eq!(export.records.len(), 1);
374 assert_eq!(export.records[0].memory_size, None);
375
376 assert_eq!(
377 export.ledger_anchor,
378 MemoryManagerSlot::new(0).expect("usable slot")
379 );
380 assert_eq!(export.commit_recovery, None);
381
382 assert_eq!(
383 export,
384 DiagnosticExport::from_owned_ledger(ledger, export.ledger_anchor.clone())
385 );
386 let wire = serde_json::to_value(&export).expect("diagnostic JSON");
387 assert_eq!(
388 wire["records"][0]["allocation"]["schema"],
389 serde_json::json!({"schema_version": null})
390 );
391
392 let decoded: DiagnosticExport = serde_json::from_value(wire).expect("current JSON shape");
393 assert_eq!(decoded, export);
394 }
395
396 #[test]
397 fn diagnostic_export_rejects_unknown_top_level_fields() {
398 use crate::test_cbor::Value;
399
400 let export = DiagnosticExport {
401 current_generation: 0,
402 ledger_anchor: MemoryManagerSlot::new(0).expect("usable slot"),
403 records: Vec::new(),
404 commit_recovery: None,
405 };
406 let Value::Map(mut map) = crate::test_cbor::to_value(export).expect("diagnostic value")
407 else {
408 panic!("diagnostic export encodes as a map");
409 };
410 map.push((Value::Text("future_field".to_string()), Value::Bool(true)));
411 let bytes = crate::test_cbor::to_vec(&Value::Map(map)).expect("diagnostic bytes");
412
413 let err = crate::test_cbor::from_slice::<DiagnosticExport>(&bytes)
414 .expect_err("unknown diagnostic field must fail closed");
415
416 assert!(err.to_string().contains("future_field"));
417 }
418
419 #[test]
420 fn diagnostic_outcome_states_round_trip() {
421 let stable_cell = DiagnosticStableCell::new(
422 DiagnosticStableCellStatus::Corrupt {
423 failure: DiagnosticFailure::new(
424 DiagnosticCode::StableCell,
425 "bad stable-cell record",
426 ),
427 },
428 DiagnosticMemorySize::from_wasm_pages(1),
429 );
430 let allocation_pool = crate::MemoryAllocationPool::new(Vec::new(), Vec::new()).unwrap();
431 let check = DiagnosticCheck::failed(
432 DiagnosticCode::AllocationValidation,
433 "duplicate declaration",
434 );
435
436 for value in [DiagnosticCheck::passed(), check] {
437 let bytes = crate::test_cbor::to_vec(&value).expect("check bytes");
438 let decoded: DiagnosticCheck =
439 crate::test_cbor::from_slice(&bytes).expect("check round trip");
440 assert_eq!(decoded, value);
441 }
442
443 let bytes = crate::test_cbor::to_vec(&stable_cell).expect("stable-cell diagnostic bytes");
444 let decoded: DiagnosticStableCell =
445 crate::test_cbor::from_slice(&bytes).expect("stable-cell round trip");
446 assert_eq!(decoded, stable_cell);
447
448 let bytes = crate::test_cbor::to_vec(&allocation_pool).expect("pool diagnostic bytes");
449 let decoded: crate::MemoryAllocationPool =
450 crate::test_cbor::from_slice(&bytes).expect("pool round trip");
451 assert_eq!(decoded, allocation_pool);
452 }
453
454 #[test]
455 fn diagnostic_codes_have_stable_wire_names() {
456 let cases = [
457 (DiagnosticCode::StableCell, "stable_cell"),
458 (DiagnosticCode::UnsupportedFormat, "unsupported_format"),
459 (DiagnosticCode::LedgerRecovery, "ledger_recovery"),
460 (
461 DiagnosticCode::AllocationValidation,
462 "allocation_validation",
463 ),
464 (DiagnosticCode::PolicyIdentity, "policy_identity"),
465 (DiagnosticCode::RuntimeBinding, "runtime_binding"),
466 ];
467
468 for (code, expected) in cases {
469 assert_eq!(
470 crate::test_cbor::to_value(code).expect("diagnostic code value"),
471 crate::test_cbor::Value::Text(expected.to_string())
472 );
473 }
474 }
475
476 #[test]
477 fn diagnostic_export_can_include_commit_recovery_state() {
478 let ledger = AllocationLedger {
479 current_generation: 3,
480 records: Vec::new(),
481 };
482 let commit_recovery = CommitSnapshotDiagnostic::Valid { generation: 3 };
483
484 let mut export =
485 DiagnosticExport::from_ledger(&ledger, MemoryManagerSlot::new(0).expect("usable slot"));
486 export.commit_recovery = Some(commit_recovery);
487
488 assert_eq!(export.commit_recovery, Some(commit_recovery));
489 }
490
491 #[test]
492 fn diagnostic_export_can_include_memory_sizes() {
493 let declaration = AllocationDeclaration::new(
494 "app.users.v1",
495 MemoryManagerSlot::new(100).expect("usable slot"),
496 SchemaMetadata::default(),
497 )
498 .expect("declaration");
499 let ledger = AllocationLedger {
500 current_generation: 3,
501 records: vec![AllocationRecord::active(&declaration)],
502 };
503
504 let mut export =
505 DiagnosticExport::from_ledger(&ledger, MemoryManagerSlot::new(0).expect("usable slot"));
506 export.records[0].memory_size = Some(DiagnosticMemorySize::from_wasm_pages(2));
507
508 assert_eq!(
509 export.records[0].memory_size,
510 Some(DiagnosticMemorySize { wasm_pages: 2 })
511 );
512 let wire = serde_json::to_value(&export).expect("diagnostic JSON");
513 assert_eq!(
514 wire["records"][0]["memory_size"],
515 serde_json::json!({"wasm_pages": 2})
516 );
517 }
518
519 #[test]
520 fn diagnostic_export_can_report_recovery_failure() {
521 let ledger = AllocationLedger {
522 current_generation: 0,
523 records: Vec::new(),
524 };
525 let commit_recovery = CommitSnapshotDiagnostic::Empty;
526
527 let mut export =
528 DiagnosticExport::from_ledger(&ledger, MemoryManagerSlot::new(0).expect("usable slot"));
529 export.commit_recovery = Some(commit_recovery);
530
531 assert_eq!(
532 export.commit_recovery.expect("commit recovery"),
533 CommitSnapshotDiagnostic::Empty
534 );
535 }
536}