1use crate::{
2 constants::WASM_PAGE_SIZE_BYTES,
3 ledger::{AllocationLedger, AllocationRecord},
4 physical::CommitSnapshotDiagnostic,
5 policy::PolicyIdentity,
6 requests::SealedDeclarationFingerprint,
7 slot::MemoryManagerSlot,
8};
9use serde::{Deserialize, Serialize};
10
11#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
16#[serde(deny_unknown_fields)]
17pub struct DiagnosticExport {
18 pub current_generation: u64,
20 pub ledger_anchor: MemoryManagerSlot,
22 pub records: Vec<DiagnosticRecord>,
24 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
26 pub commit_recovery: Option<CommitSnapshotDiagnostic>,
27}
28
29#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
37#[serde(deny_unknown_fields)]
38pub struct DiagnosticRuntimeBinding {
39 pub policy_identity: PolicyIdentity,
41 pub declaration_fingerprint: SealedDeclarationFingerprint,
43 pub allocation_pool: crate::MemoryAllocationPool,
45}
46
47impl DiagnosticRuntimeBinding {
48 #[must_use]
50 pub const fn new(
51 policy_identity: PolicyIdentity,
52 declaration_fingerprint: SealedDeclarationFingerprint,
53 allocation_pool: crate::MemoryAllocationPool,
54 ) -> Self {
55 Self {
56 policy_identity,
57 declaration_fingerprint,
58 allocation_pool,
59 }
60 }
61}
62
63#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
75#[serde(deny_unknown_fields)]
76pub struct MemoryRuntimeDoctorReport {
77 pub bootstrapped: bool,
79 pub tested_policy_identity: Result<PolicyIdentity, DiagnosticFailure>,
81 pub tested_declaration_fingerprint: SealedDeclarationFingerprint,
83 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
85 pub established_bootstrap_binding: Option<DiagnosticRuntimeBinding>,
86 pub bootstrap_binding: DiagnosticCheck,
89 pub ledger_anchor: MemoryManagerSlot,
91 pub stable_cell: DiagnosticStableCell,
93 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
95 pub commit_recovery: Option<CommitSnapshotDiagnostic>,
96 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
98 pub ledger: Option<DiagnosticExport>,
99 pub requests: Vec<crate::MemoryRequest>,
101 pub allocation_pool: crate::MemoryAllocationPool,
103 pub validation: DiagnosticCheck,
105}
106
107#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
114pub enum DiagnosticCode {
115 #[serde(rename = "stable_cell")]
117 StableCell,
118 #[serde(rename = "unsupported_format")]
120 UnsupportedFormat,
121 #[serde(rename = "ledger_recovery")]
123 LedgerRecovery,
124 #[serde(rename = "allocation_validation")]
126 AllocationValidation,
127 #[serde(rename = "policy_identity")]
129 PolicyIdentity,
130 #[serde(rename = "runtime_binding")]
132 RuntimeBinding,
133}
134
135#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
142#[serde(deny_unknown_fields)]
143pub struct DiagnosticFailure {
144 pub code: DiagnosticCode,
146 pub message: String,
148}
149
150impl DiagnosticFailure {
151 #[must_use]
153 pub fn new(code: DiagnosticCode, message: impl Into<String>) -> Self {
154 Self {
155 code,
156 message: message.into(),
157 }
158 }
159}
160
161#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
168#[serde(deny_unknown_fields)]
169pub struct DiagnosticStableCell {
170 pub status: DiagnosticStableCellStatus,
172 pub memory_size: DiagnosticMemorySize,
174}
175
176impl DiagnosticStableCell {
177 #[must_use]
179 pub const fn new(
180 status: DiagnosticStableCellStatus,
181 memory_size: DiagnosticMemorySize,
182 ) -> Self {
183 Self {
184 status,
185 memory_size,
186 }
187 }
188}
189
190#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
197#[serde(deny_unknown_fields)]
198pub enum DiagnosticStableCellStatus {
199 Empty,
201 Readable,
203 Corrupt {
206 failure: DiagnosticFailure,
208 },
209}
210
211#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
218#[serde(deny_unknown_fields)]
219pub enum DiagnosticCheck {
220 NotRun {
222 code: DiagnosticCode,
224 message: String,
226 },
227 Passed,
229 Failed {
231 code: DiagnosticCode,
233 message: String,
235 },
236}
237
238impl DiagnosticCheck {
239 #[must_use]
241 pub const fn passed() -> Self {
242 Self::Passed
243 }
244
245 #[must_use]
247 pub fn failed(code: DiagnosticCode, message: impl Into<String>) -> Self {
248 Self::Failed {
249 code,
250 message: message.into(),
251 }
252 }
253
254 #[must_use]
256 pub fn not_run(code: DiagnosticCode, message: impl Into<String>) -> Self {
257 Self::NotRun {
258 code,
259 message: message.into(),
260 }
261 }
262}
263
264impl DiagnosticExport {
265 #[must_use]
271 pub fn from_ledger(ledger: &AllocationLedger, ledger_anchor: MemoryManagerSlot) -> Self {
272 Self::from_records(
273 ledger.current_generation,
274 ledger_anchor,
275 ledger.records.iter().cloned(),
276 )
277 }
278
279 pub(crate) fn from_owned_ledger(
282 ledger: AllocationLedger,
283 ledger_anchor: MemoryManagerSlot,
284 ) -> Self {
285 Self::from_records(
286 ledger.current_generation,
287 ledger_anchor,
288 ledger.records.into_iter(),
289 )
290 }
291
292 fn from_records(
293 current_generation: u64,
294 ledger_anchor: MemoryManagerSlot,
295 records: impl Iterator<Item = AllocationRecord>,
296 ) -> Self {
297 Self {
298 current_generation,
299 ledger_anchor,
300 records: records
301 .map(|allocation| DiagnosticRecord {
302 allocation,
303 memory_size: None,
304 })
305 .collect(),
306 commit_recovery: None,
307 }
308 }
309}
310
311#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
316#[serde(deny_unknown_fields)]
317pub struct DiagnosticRecord {
318 pub allocation: AllocationRecord,
320 #[serde(skip_serializing_if = "Option::is_none")]
325 pub memory_size: Option<DiagnosticMemorySize>,
326}
327
328#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
335#[serde(deny_unknown_fields)]
336pub struct DiagnosticMemorySize {
337 pub wasm_pages: u64,
339 pub bytes: u64,
341}
342
343impl DiagnosticMemorySize {
344 #[must_use]
346 pub const fn from_wasm_pages(wasm_pages: u64) -> Self {
347 Self {
348 wasm_pages,
349 bytes: wasm_pages.saturating_mul(WASM_PAGE_SIZE_BYTES),
350 }
351 }
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357 use crate::AllocationDeclaration;
358 use crate::{
359 ledger::AllocationRecord, physical::CommitSnapshotDiagnostic, schema::SchemaMetadata,
360 };
361
362 #[test]
363 fn diagnostic_export_copies_ledger_records() {
364 let declaration = AllocationDeclaration::new(
365 "app.users.v1",
366 MemoryManagerSlot::new(100).expect("usable slot"),
367 SchemaMetadata::default(),
368 )
369 .expect("declaration");
370 let ledger = AllocationLedger {
371 current_generation: 3,
372 records: vec![AllocationRecord::active(&declaration)],
373 };
374
375 let export =
376 DiagnosticExport::from_ledger(&ledger, MemoryManagerSlot::new(0).expect("usable slot"));
377
378 assert_eq!(export.current_generation, 3);
379 assert_eq!(export.records.len(), 1);
380 assert_eq!(export.records[0].memory_size, None);
381
382 assert_eq!(
383 export.ledger_anchor,
384 MemoryManagerSlot::new(0).expect("usable slot")
385 );
386 assert_eq!(export.commit_recovery, None);
387
388 assert_eq!(
389 export,
390 DiagnosticExport::from_owned_ledger(ledger, export.ledger_anchor.clone())
391 );
392 let wire = serde_json::to_value(&export).expect("diagnostic JSON");
393 assert_eq!(
394 wire["records"][0]["allocation"]["schema"],
395 serde_json::json!({"schema_version": null})
396 );
397
398 let decoded: DiagnosticExport = serde_json::from_value(wire).expect("current JSON shape");
399 assert_eq!(decoded, export);
400 }
401
402 #[test]
403 fn diagnostic_export_rejects_unknown_top_level_fields() {
404 use crate::test_cbor::Value;
405
406 let export = DiagnosticExport {
407 current_generation: 0,
408 ledger_anchor: MemoryManagerSlot::new(0).expect("usable slot"),
409 records: Vec::new(),
410 commit_recovery: None,
411 };
412 let Value::Map(mut map) = crate::test_cbor::to_value(export).expect("diagnostic value")
413 else {
414 panic!("diagnostic export encodes as a map");
415 };
416 map.push((Value::Text("future_field".to_string()), Value::Bool(true)));
417 let bytes = crate::test_cbor::to_vec(&Value::Map(map)).expect("diagnostic bytes");
418
419 let err = crate::test_cbor::from_slice::<DiagnosticExport>(&bytes)
420 .expect_err("unknown diagnostic field must fail closed");
421
422 assert!(err.to_string().contains("future_field"));
423 }
424
425 #[test]
426 fn diagnostic_outcome_states_round_trip() {
427 let stable_cell = DiagnosticStableCell::new(
428 DiagnosticStableCellStatus::Corrupt {
429 failure: DiagnosticFailure::new(
430 DiagnosticCode::StableCell,
431 "bad stable-cell record",
432 ),
433 },
434 DiagnosticMemorySize::from_wasm_pages(1),
435 );
436 let allocation_pool = crate::MemoryAllocationPool::new(Vec::new(), Vec::new()).unwrap();
437 let check = DiagnosticCheck::failed(
438 DiagnosticCode::AllocationValidation,
439 "duplicate declaration",
440 );
441
442 for value in [DiagnosticCheck::passed(), check] {
443 let bytes = crate::test_cbor::to_vec(&value).expect("check bytes");
444 let decoded: DiagnosticCheck =
445 crate::test_cbor::from_slice(&bytes).expect("check round trip");
446 assert_eq!(decoded, value);
447 }
448
449 let bytes = crate::test_cbor::to_vec(&stable_cell).expect("stable-cell diagnostic bytes");
450 let decoded: DiagnosticStableCell =
451 crate::test_cbor::from_slice(&bytes).expect("stable-cell round trip");
452 assert_eq!(decoded, stable_cell);
453
454 let bytes = crate::test_cbor::to_vec(&allocation_pool).expect("pool diagnostic bytes");
455 let decoded: crate::MemoryAllocationPool =
456 crate::test_cbor::from_slice(&bytes).expect("pool round trip");
457 assert_eq!(decoded, allocation_pool);
458 }
459
460 #[test]
461 fn diagnostic_codes_have_stable_wire_names() {
462 let cases = [
463 (DiagnosticCode::StableCell, "stable_cell"),
464 (DiagnosticCode::UnsupportedFormat, "unsupported_format"),
465 (DiagnosticCode::LedgerRecovery, "ledger_recovery"),
466 (
467 DiagnosticCode::AllocationValidation,
468 "allocation_validation",
469 ),
470 (DiagnosticCode::PolicyIdentity, "policy_identity"),
471 (DiagnosticCode::RuntimeBinding, "runtime_binding"),
472 ];
473
474 for (code, expected) in cases {
475 assert_eq!(
476 crate::test_cbor::to_value(code).expect("diagnostic code value"),
477 crate::test_cbor::Value::Text(expected.to_string())
478 );
479 }
480 }
481
482 #[test]
483 fn diagnostic_export_can_include_commit_recovery_state() {
484 let ledger = AllocationLedger {
485 current_generation: 3,
486 records: Vec::new(),
487 };
488 let commit_recovery = CommitSnapshotDiagnostic::Valid { generation: 3 };
489
490 let mut export =
491 DiagnosticExport::from_ledger(&ledger, MemoryManagerSlot::new(0).expect("usable slot"));
492 export.commit_recovery = Some(commit_recovery);
493
494 assert_eq!(export.commit_recovery, Some(commit_recovery));
495 }
496
497 #[test]
498 fn diagnostic_export_can_include_memory_sizes() {
499 let declaration = AllocationDeclaration::new(
500 "app.users.v1",
501 MemoryManagerSlot::new(100).expect("usable slot"),
502 SchemaMetadata::default(),
503 )
504 .expect("declaration");
505 let ledger = AllocationLedger {
506 current_generation: 3,
507 records: vec![AllocationRecord::active(&declaration)],
508 };
509
510 let mut export =
511 DiagnosticExport::from_ledger(&ledger, MemoryManagerSlot::new(0).expect("usable slot"));
512 export.records[0].memory_size = Some(DiagnosticMemorySize::from_wasm_pages(2));
513
514 assert_eq!(
515 export.records[0].memory_size,
516 Some(DiagnosticMemorySize {
517 wasm_pages: 2,
518 bytes: 131_072,
519 })
520 );
521 let wire = serde_json::to_value(&export).expect("diagnostic JSON");
522 assert_eq!(
523 wire["records"][0]["memory_size"],
524 serde_json::json!({"wasm_pages": 2, "bytes": 131_072})
525 );
526 }
527
528 #[test]
529 fn diagnostic_export_can_report_recovery_failure() {
530 let ledger = AllocationLedger {
531 current_generation: 0,
532 records: Vec::new(),
533 };
534 let commit_recovery = CommitSnapshotDiagnostic::Empty;
535
536 let mut export =
537 DiagnosticExport::from_ledger(&ledger, MemoryManagerSlot::new(0).expect("usable slot"));
538 export.commit_recovery = Some(commit_recovery);
539
540 assert_eq!(
541 export.commit_recovery.expect("commit recovery"),
542 CommitSnapshotDiagnostic::Empty
543 );
544 }
545}