1use crate::{
2 constants::WASM_PAGE_SIZE_BYTES,
3 ledger::{AllocationLedger, AllocationRecord},
4 physical::CommitStoreDiagnostic,
5 policy::PolicyIdentity,
6 registry::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<CommitStoreDiagnostic>,
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<CommitStoreDiagnostic>,
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,
360 physical::{CommitRecoveryError, CommitSlotDiagnostic, CommitStoreDiagnostic},
361 schema::SchemaMetadata,
362 };
363
364 #[test]
365 fn diagnostic_export_copies_ledger_records() {
366 let declaration = AllocationDeclaration::new(
367 "app.users.v1",
368 MemoryManagerSlot::new(100).expect("usable slot"),
369 None,
370 SchemaMetadata::default(),
371 )
372 .expect("declaration");
373 let ledger = AllocationLedger {
374 current_generation: 3,
375 records: vec![AllocationRecord::active(&declaration)],
376 };
377
378 let export =
379 DiagnosticExport::from_ledger(&ledger, MemoryManagerSlot::new(0).expect("usable slot"));
380
381 assert_eq!(export.current_generation, 3);
382 assert_eq!(export.records.len(), 1);
383 assert_eq!(export.records[0].memory_size, None);
384
385 assert_eq!(
386 export.ledger_anchor,
387 MemoryManagerSlot::new(0).expect("usable slot")
388 );
389 assert_eq!(export.commit_recovery, None);
390
391 assert_eq!(
392 export,
393 DiagnosticExport::from_owned_ledger(ledger, export.ledger_anchor.clone())
394 );
395 let wire = serde_json::to_value(&export).expect("diagnostic JSON");
396 assert_eq!(
397 wire["records"][0]["allocation"]["schema"],
398 serde_json::json!({"schema_version": null})
399 );
400
401 let decoded: DiagnosticExport = serde_json::from_value(wire).expect("current JSON shape");
402 assert_eq!(decoded, export);
403 }
404
405 #[test]
406 fn diagnostic_export_rejects_unknown_top_level_fields() {
407 use crate::test_cbor::Value;
408
409 let export = DiagnosticExport {
410 current_generation: 0,
411 ledger_anchor: MemoryManagerSlot::new(0).expect("usable slot"),
412 records: Vec::new(),
413 commit_recovery: None,
414 };
415 let Value::Map(mut map) = crate::test_cbor::to_value(export).expect("diagnostic value")
416 else {
417 panic!("diagnostic export encodes as a map");
418 };
419 map.push((Value::Text("future_field".to_string()), Value::Bool(true)));
420 let bytes = crate::test_cbor::to_vec(&Value::Map(map)).expect("diagnostic bytes");
421
422 let err = crate::test_cbor::from_slice::<DiagnosticExport>(&bytes)
423 .expect_err("unknown diagnostic field must fail closed");
424
425 assert!(err.to_string().contains("future_field"));
426 }
427
428 #[test]
429 fn diagnostic_outcome_states_round_trip() {
430 let stable_cell = DiagnosticStableCell::new(
431 DiagnosticStableCellStatus::Corrupt {
432 failure: DiagnosticFailure::new(
433 DiagnosticCode::StableCell,
434 "bad stable-cell record",
435 ),
436 },
437 DiagnosticMemorySize::from_wasm_pages(1),
438 );
439 let allocation_pool = crate::MemoryAllocationPool::new(Vec::new(), Vec::new()).unwrap();
440 let check = DiagnosticCheck::failed(
441 DiagnosticCode::AllocationValidation,
442 "duplicate declaration",
443 );
444
445 for value in [DiagnosticCheck::passed(), check] {
446 let bytes = crate::test_cbor::to_vec(&value).expect("check bytes");
447 let decoded: DiagnosticCheck =
448 crate::test_cbor::from_slice(&bytes).expect("check round trip");
449 assert_eq!(decoded, value);
450 }
451
452 let bytes = crate::test_cbor::to_vec(&stable_cell).expect("stable-cell diagnostic bytes");
453 let decoded: DiagnosticStableCell =
454 crate::test_cbor::from_slice(&bytes).expect("stable-cell round trip");
455 assert_eq!(decoded, stable_cell);
456
457 let bytes = crate::test_cbor::to_vec(&allocation_pool).expect("pool diagnostic bytes");
458 let decoded: crate::MemoryAllocationPool =
459 crate::test_cbor::from_slice(&bytes).expect("pool round trip");
460 assert_eq!(decoded, allocation_pool);
461 }
462
463 #[test]
464 fn diagnostic_codes_have_stable_wire_names() {
465 let cases = [
466 (DiagnosticCode::StableCell, "stable_cell"),
467 (DiagnosticCode::UnsupportedFormat, "unsupported_format"),
468 (DiagnosticCode::LedgerRecovery, "ledger_recovery"),
469 (
470 DiagnosticCode::AllocationValidation,
471 "allocation_validation",
472 ),
473 (DiagnosticCode::PolicyIdentity, "policy_identity"),
474 (DiagnosticCode::RuntimeBinding, "runtime_binding"),
475 ];
476
477 for (code, expected) in cases {
478 assert_eq!(
479 crate::test_cbor::to_value(code).expect("diagnostic code value"),
480 crate::test_cbor::Value::Text(expected.to_string())
481 );
482 }
483 }
484
485 #[test]
486 fn diagnostic_export_can_include_commit_recovery_state() {
487 let ledger = AllocationLedger {
488 current_generation: 3,
489 records: Vec::new(),
490 };
491 let commit_recovery = CommitStoreDiagnostic {
492 slot0: CommitSlotDiagnostic::Valid { generation: 3 },
493 slot1: CommitSlotDiagnostic::Empty,
494 recovery: Ok(3),
495 };
496
497 let mut export =
498 DiagnosticExport::from_ledger(&ledger, MemoryManagerSlot::new(0).expect("usable slot"));
499 export.commit_recovery = Some(commit_recovery);
500
501 assert_eq!(export.commit_recovery, Some(commit_recovery));
502 }
503
504 #[test]
505 fn diagnostic_export_can_include_memory_sizes() {
506 let declaration = AllocationDeclaration::new(
507 "app.users.v1",
508 MemoryManagerSlot::new(100).expect("usable slot"),
509 None,
510 SchemaMetadata::default(),
511 )
512 .expect("declaration");
513 let ledger = AllocationLedger {
514 current_generation: 3,
515 records: vec![AllocationRecord::active(&declaration)],
516 };
517
518 let mut export =
519 DiagnosticExport::from_ledger(&ledger, MemoryManagerSlot::new(0).expect("usable slot"));
520 export.records[0].memory_size = Some(DiagnosticMemorySize::from_wasm_pages(2));
521
522 assert_eq!(
523 export.records[0].memory_size,
524 Some(DiagnosticMemorySize {
525 wasm_pages: 2,
526 bytes: 131_072,
527 })
528 );
529 let wire = serde_json::to_value(&export).expect("diagnostic JSON");
530 assert_eq!(
531 wire["records"][0]["memory_size"],
532 serde_json::json!({"wasm_pages": 2, "bytes": 131_072})
533 );
534 }
535
536 #[test]
537 fn diagnostic_export_can_report_recovery_failure() {
538 let ledger = AllocationLedger {
539 current_generation: 0,
540 records: Vec::new(),
541 };
542 let commit_recovery = CommitStoreDiagnostic {
543 slot0: CommitSlotDiagnostic::Empty,
544 slot1: CommitSlotDiagnostic::Empty,
545 recovery: Err(CommitRecoveryError::NoValidGeneration),
546 };
547
548 let mut export =
549 DiagnosticExport::from_ledger(&ledger, MemoryManagerSlot::new(0).expect("usable slot"));
550 export.commit_recovery = Some(commit_recovery);
551
552 assert_eq!(
553 export.commit_recovery.expect("commit recovery").recovery,
554 Err(CommitRecoveryError::NoValidGeneration)
555 );
556 }
557}