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