1mod aggregation;
11mod audit;
12mod resolution;
13
14pub use resolution::{StateManifestResolution, resolve_project_state_manifest};
15
16use canic_core::state_contract::{STATE_MANIFEST_SCHEMA_VERSION, StateManifest};
17use serde::Serialize;
18
19pub const STATE_AUDIT_COMMAND: &str = "canic state audit";
20pub const STATE_MANIFEST_COMMAND: &str = "canic state manifest";
21pub const STATE_AUDIT_SCHEMA_VERSION: u16 = 2;
22
23const SCOPE_PROJECT: StateAuditScope = StateAuditScope::Project;
24const SCOPE_ROLE: StateAuditScope = StateAuditScope::Role;
25
26#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
33pub struct StateAuditReport {
34 pub schema_version: u16,
35 pub command: &'static str,
36 pub scope: StateAuditScope,
37 pub role: Option<String>,
38 pub status: StateAuditStatus,
39 pub manifest: StateManifest,
40 pub checks: Vec<StateAuditCheck>,
41 pub next_actions: Vec<String>,
42}
43
44#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
51pub struct StateAuditCheck {
52 pub category: StateAuditCategory,
53 pub code: &'static str,
54 pub status: StateAuditStatus,
55 pub severity: StateAuditSeverity,
56 pub subject: String,
57 pub detail: String,
58 pub next: Option<String>,
59 pub source: StateAuditSource,
60}
61
62#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
69#[serde(rename_all = "snake_case")]
70pub enum StateAuditScope {
71 Project,
72 Role,
73}
74
75impl StateAuditScope {
76 #[must_use]
77 pub const fn label(self) -> &'static str {
78 match self {
79 Self::Project => "project",
80 Self::Role => "role",
81 }
82 }
83}
84
85#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
92#[serde(rename_all = "snake_case")]
93pub enum StateAuditCategory {
94 Invariant,
95 Lifecycle,
96 Manifest,
97 MemoryId,
98 Migration,
99 Naming,
100 SchemaVersion,
101 Snapshot,
102 TestCoverage,
103}
104
105impl StateAuditCategory {
106 #[must_use]
107 pub const fn label(self) -> &'static str {
108 match self {
109 Self::Invariant => "invariant",
110 Self::Lifecycle => "lifecycle",
111 Self::Manifest => "manifest",
112 Self::MemoryId => "memory_id",
113 Self::Migration => "migration",
114 Self::Naming => "naming",
115 Self::SchemaVersion => "schema_version",
116 Self::Snapshot => "snapshot",
117 Self::TestCoverage => "test_coverage",
118 }
119 }
120}
121
122#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
129#[serde(rename_all = "snake_case")]
130pub enum StateAuditSource {
131 StateManifest,
132}
133
134impl StateAuditSource {
135 #[must_use]
136 pub const fn label(self) -> &'static str {
137 match self {
138 Self::StateManifest => "state_manifest",
139 }
140 }
141}
142
143#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
150#[serde(rename_all = "snake_case")]
151pub enum StateAuditStatus {
152 Pass,
153 Warn,
154 Fail,
155 NotEvaluated,
156}
157
158impl StateAuditStatus {
159 #[must_use]
160 pub const fn label(self) -> &'static str {
161 match self {
162 Self::Pass => "pass",
163 Self::Warn => "warn",
164 Self::Fail => "fail",
165 Self::NotEvaluated => "not_evaluated",
166 }
167 }
168}
169
170#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
177#[serde(rename_all = "snake_case")]
178pub enum StateAuditSeverity {
179 Info,
180 Warning,
181 Blocked,
182 Unsupported,
183}
184
185#[must_use]
186pub fn build_state_audit_report(
187 resolution: &StateManifestResolution,
188 role: Option<&str>,
189) -> StateAuditReport {
190 let (manifest, contract_errors) = match resolution {
191 StateManifestResolution::Resolved { manifest, .. } => (manifest.clone(), Vec::new()),
192 StateManifestResolution::Rejected { errors } => (
193 StateManifest {
194 schema_version: STATE_MANIFEST_SCHEMA_VERSION,
195 roles: Vec::new(),
196 },
197 errors.clone(),
198 ),
199 };
200 let mut checks = audit::audit_checks(
201 &manifest,
202 if contract_errors.is_empty() {
203 role
204 } else {
205 None
206 },
207 );
208 checks.extend(contract_errors.iter().map(audit::role_contract_check));
209 aggregation::sort_checks(&mut checks);
210
211 let status = aggregation::aggregate_status(&checks);
212 let mut next_actions = aggregation::next_actions(status, role);
213 next_actions.sort();
214 next_actions.dedup();
215
216 StateAuditReport {
217 schema_version: STATE_AUDIT_SCHEMA_VERSION,
218 command: STATE_AUDIT_COMMAND,
219 scope: if role.is_some() {
220 SCOPE_ROLE
221 } else {
222 SCOPE_PROJECT
223 },
224 role: role.map(ToString::to_string),
225 status,
226 manifest,
227 checks,
228 next_actions,
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use crate::role_contract::materialize_state_manifest;
236 use canic_core::{
237 ids::CanisterRole,
238 role_contract::{
239 AllocationOwner, BuiltInRoleKind, CanicFeatureKey, ResolvedRoleContract,
240 ResolvedStateAllocation, RoleContractFinding, SelectionProvenance, StateAllocationKey,
241 allocation::allocation_definition,
242 },
243 state_contract::{
244 MigrationPolicy, StateDomainManifest, StateMigrationManifest, StateRoleManifest,
245 StateStorage,
246 },
247 };
248 use std::{collections::BTreeSet, path::PathBuf};
249
250 use super::audit::audit_checks;
251
252 fn test_state_manifest(role: Option<&str>) -> StateManifest {
253 let contracts = match role {
254 Some("root") | None => vec![test_contract(
255 "root",
256 None,
257 &[
258 StateAllocationKey::CoreRuntimeTopology,
259 StateAllocationKey::CoreRootAppRegistry,
260 StateAllocationKey::CoreRuntimeEnvironment,
261 StateAllocationKey::CoreAuthState,
262 StateAllocationKey::CoreReplayReceipts,
263 StateAllocationKey::CoreRuntimeObservability,
264 StateAllocationKey::CoreRuntimeIntent,
265 StateAllocationKey::CanisterPool,
266 StateAllocationKey::TemplateManifests,
267 StateAllocationKey::TemplateChunkSets,
268 StateAllocationKey::TemplateChunkRefs,
269 StateAllocationKey::TemplateChunkPayloads,
270 StateAllocationKey::ControlPlaneSubnetState,
271 ],
272 )],
273 Some("wasm_store") => vec![test_contract(
274 "wasm_store",
275 Some(BuiltInRoleKind::WasmStore),
276 &[
277 StateAllocationKey::TemplateManifests,
278 StateAllocationKey::TemplateChunkSets,
279 StateAllocationKey::TemplateChunkRefs,
280 StateAllocationKey::TemplateChunkPayloads,
281 StateAllocationKey::WasmStoreGcState,
282 ],
283 )],
284 Some(_) => Vec::new(),
285 };
286 materialize_state_manifest(&contracts).expect("test manifest")
287 }
288
289 fn test_contract(
290 role: &str,
291 built_in: Option<BuiltInRoleKind>,
292 keys: &[StateAllocationKey],
293 ) -> ResolvedRoleContract {
294 let allocations = keys
295 .iter()
296 .map(|key| {
297 let definition = allocation_definition(*key).expect("allocation definition");
298 ResolvedStateAllocation {
299 key: *key,
300 owner: definition.owner,
301 memory_ids: definition.memory_ids.to_vec(),
302 selected_by: BTreeSet::from([if let Some(built_in) = built_in {
303 SelectionProvenance::BuiltInRole(built_in)
304 } else if definition.owner == AllocationOwner::CanicControlPlane {
305 SelectionProvenance::EffectiveFeature(CanicFeatureKey::ControlPlane)
306 } else {
307 SelectionProvenance::Capability(
308 canic_core::role_contract::RoleCapabilityKey::Root,
309 )
310 }]),
311 }
312 })
313 .collect();
314 ResolvedRoleContract {
315 role: CanisterRole::owned(role.to_string()),
316 built_in,
317 capabilities: BTreeSet::new(),
318 required_features: BTreeSet::new(),
319 effective_features: BTreeSet::new(),
320 allocations,
321 }
322 }
323
324 fn build_state_audit_report(role: Option<&str>) -> StateAuditReport {
325 let resolution = StateManifestResolution::Resolved {
326 manifest: test_state_manifest(role),
327 contracts: Vec::new(),
328 };
329 super::build_state_audit_report(&resolution, role)
330 }
331
332 #[test]
333 fn state_audit_status_owns_serialized_labels() {
334 assert_eq!(StateAuditStatus::Pass.label(), "pass");
335 assert_eq!(StateAuditStatus::Warn.label(), "warn");
336 assert_eq!(StateAuditStatus::Fail.label(), "fail");
337 assert_eq!(StateAuditStatus::NotEvaluated.label(), "not_evaluated");
338 }
339
340 #[test]
341 fn builtin_report_is_warning_only_for_reserved_memory() {
342 let report = build_state_audit_report(Some("root"));
343
344 assert_eq!(report.status, StateAuditStatus::Warn);
345 assert!(report.checks.iter().any(|check| {
346 check.code == "state_manifest_schema_version_supported"
347 && check.status == StateAuditStatus::Pass
348 }));
349 assert!(
350 report
351 .checks
352 .iter()
353 .any(|check| check.code == "reserved_memory_id_declared")
354 );
355 assert!(
356 report
357 .checks
358 .iter()
359 .all(|check| check.code != "snapshot_name_invalid")
360 );
361 assert!(
362 report
363 .checks
364 .iter()
365 .any(|check| check.code == "reserved_export_import_ok")
366 );
367 assert!(
368 report
369 .checks
370 .iter()
371 .all(|check| check.status != StateAuditStatus::Fail)
372 );
373 }
374
375 #[test]
376 fn builtin_manifest_merges_control_plane_state_by_role() {
377 let manifest = test_state_manifest(Some("root"));
378 let role = manifest.roles.first().expect("root role");
379
380 assert!(role.state.iter().any(|domain| {
381 domain.domain == "template_manifests"
382 && domain.owner == "canic-control-plane"
383 && domain.memory_id == Some(80)
384 }));
385 assert!(
386 role.state
387 .iter()
388 .any(|domain| { domain.domain == "auth_state" && domain.owner == "canic-core" })
389 );
390 }
391
392 #[test]
393 fn wasm_store_role_audits_every_owned_memory_domain_cleanly() {
394 let report = build_state_audit_report(Some("wasm_store"));
395
396 assert_eq!(report.status, StateAuditStatus::Pass);
397 let role = report
398 .manifest
399 .roles
400 .iter()
401 .find(|role| role.canister_role == "wasm_store")
402 .expect("wasm_store role");
403 let domains = role
404 .state
405 .iter()
406 .map(|domain| domain.domain.as_str())
407 .collect::<Vec<_>>();
408
409 for expected in [
410 "template_manifests",
411 "template_chunk_sets",
412 "template_chunk_refs",
413 "template_chunk_payloads",
414 "wasm_store_gc_state",
415 ] {
416 assert!(domains.contains(&expected));
417 }
418 assert_eq!(domains.len(), 5);
419 }
420
421 #[test]
422 fn complete_descriptor_registry_satisfies_state_audit_metadata_contract() {
423 let keys = canic_core::role_contract::allocation::allocation_definitions()
424 .iter()
425 .map(|definition| definition.key)
426 .collect::<Vec<_>>();
427 let manifest = materialize_state_manifest(&[test_contract("catalog", None, &keys)])
428 .expect("complete descriptor manifest");
429 let checks = audit_checks(&manifest, Some("catalog"));
430
431 assert!(
432 checks
433 .iter()
434 .all(|check| check.status != StateAuditStatus::Fail),
435 "complete descriptor registry must satisfy audit metadata: {checks:#?}"
436 );
437 }
438
439 #[test]
440 fn exact_blob_role_resolution_materializes_blob_allocations() {
441 let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
442 let config = workspace.join("canisters/test/blob_storage_probe/canic.toml");
443 let resolution = resolve_project_state_manifest(&workspace, &[config], Some("test"));
444 let StateManifestResolution::Resolved {
445 manifest,
446 contracts,
447 } = resolution
448 else {
449 panic!("blob role contract should resolve")
450 };
451
452 assert_eq!(contracts.len(), 1);
453 let role = manifest.roles.first().expect("blob role manifest");
454 assert_eq!(role.canister_role, "test");
455 assert_eq!(
456 role.state
457 .iter()
458 .filter_map(|domain| domain.memory_id)
459 .filter(|memory_id| (62..=65).contains(memory_id))
460 .collect::<Vec<_>>(),
461 vec![63, 65, 64, 62]
462 );
463 assert!(role.state.iter().all(|domain| domain.owner == "canic-core"));
464 }
465
466 #[test]
467 fn placement_roles_materialize_exact_placement_state() {
468 let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
469
470 for (config_path, role, expected_ids) in [
471 ("fleets/test/canic.toml", "user_hub", vec![49, 53, 54, 56]),
472 (
473 "canisters/audit/scaling_probe/canic.toml",
474 "scale_hub",
475 vec![49, 52],
476 ),
477 (
478 "canisters/test/project_hub_stub/canic.toml",
479 "project_hub",
480 vec![49, 55],
481 ),
482 ] {
483 let config = workspace.join(config_path);
484 let resolution = resolve_project_state_manifest(&workspace, &[config], Some(role));
485 let StateManifestResolution::Resolved { manifest, .. } = resolution else {
486 panic!("{role} role contract should resolve");
487 };
488 let mut actual_ids = manifest.roles[0]
489 .state
490 .iter()
491 .filter_map(|domain| domain.memory_id)
492 .filter(|memory_id| (49..=56).contains(memory_id))
493 .collect::<Vec<_>>();
494 actual_ids.sort_unstable();
495
496 assert_eq!(actual_ids, expected_ids, "unexpected state for {role}");
497 }
498 }
499
500 #[test]
501 fn exact_built_in_resolution_materializes_runtime_template_and_gc_allocations() {
502 let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
503 let resolution = resolve_project_state_manifest(&workspace, &[], Some("wasm_store"));
504 let StateManifestResolution::Resolved {
505 manifest,
506 contracts,
507 } = resolution
508 else {
509 panic!("built-in wasm_store contract should resolve")
510 };
511
512 assert_eq!(contracts.len(), 1);
513 let mut ids = manifest.roles[0]
514 .state
515 .iter()
516 .filter_map(|domain| domain.memory_id)
517 .collect::<Vec<_>>();
518 ids.sort_unstable();
519 assert_eq!(
520 ids,
521 vec![
522 11, 12, 13, 15, 16, 17, 18, 20, 30, 34, 39, 40, 41, 42, 43, 80, 81, 82, 83, 85,
523 ]
524 );
525 assert_eq!(
526 manifest.roles[0]
527 .reserved_memory
528 .iter()
529 .map(|entry| entry.memory_id)
530 .collect::<Vec<_>>(),
531 vec![29, 31, 32]
532 );
533 }
534
535 #[test]
536 fn unknown_role_resolution_returns_no_manifest() {
537 let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
538 let config = workspace.join("canisters/audit/root_probe/canic.toml");
539 let resolution = resolve_project_state_manifest(&workspace, &[config], Some("missing"));
540
541 assert!(matches!(
542 resolution,
543 StateManifestResolution::Rejected { errors }
544 if errors.iter().any(|finding| matches!(finding, RoleContractFinding::RoleUnknown { .. }))
545 ));
546 }
547
548 #[test]
549 fn unsupported_manifest_schema_version_fails() {
550 let mut manifest = test_state_manifest(Some("root"));
551 manifest.schema_version = STATE_MANIFEST_SCHEMA_VERSION + 1;
552
553 let checks = audit_checks(&manifest, Some("root"));
554
555 assert!(checks.iter().any(|check| {
556 check.code == "state_manifest_schema_version_unsupported"
557 && check.status == StateAuditStatus::Fail
558 }));
559 }
560
561 #[test]
562 fn duplicate_state_role_fails() {
563 let mut manifest = test_state_manifest(Some("root"));
564 let duplicate = manifest.roles[0].clone();
565 manifest.roles.push(duplicate);
566
567 let checks = audit_checks(&manifest, None);
568
569 assert!(checks.iter().any(|check| {
570 check.code == "state_role_duplicate" && check.status == StateAuditStatus::Fail
571 }));
572 }
573
574 #[test]
575 fn duplicate_memory_id_fails_within_role() {
576 let mut manifest = test_state_manifest(Some("root"));
577 let role = manifest.roles.first_mut().expect("root role");
578 role.state[1].memory_id = role.state[0].memory_id;
579
580 let checks = audit_checks(&manifest, Some("root"));
581 assert!(
582 checks
583 .iter()
584 .any(|check| check.code == "memory_id_duplicate"
585 && check.status == StateAuditStatus::Fail)
586 );
587 }
588
589 #[test]
590 fn duplicate_state_domain_fails_within_role() {
591 let mut manifest = test_state_manifest(Some("root"));
592 let role = manifest.roles.first_mut().expect("root role");
593 let mut duplicate = role.state[0].clone();
594 duplicate.memory_id = Some(250);
595 role.state.push(duplicate);
596
597 let checks = audit_checks(&manifest, Some("root"));
598
599 assert!(
600 checks
601 .iter()
602 .any(|check| check.code == "state_domain_duplicate"
603 && check.status == StateAuditStatus::Fail)
604 );
605 }
606
607 #[test]
608 fn reserved_memory_ids_warn_until_modeled() {
609 let report = build_state_audit_report(Some("root"));
610
611 assert!(report.checks.iter().any(|check| {
612 check.code == "reserved_memory_id_declared" && check.status == StateAuditStatus::Warn
613 }));
614 }
615
616 #[test]
617 fn active_domain_reclaiming_reserved_memory_id_fails() {
618 let mut manifest = test_state_manifest(Some("root"));
619 let role = manifest.roles.first_mut().expect("root role");
620 let reserved_id = role
621 .reserved_memory
622 .first()
623 .map(|entry| entry.memory_id)
624 .expect("reserved memory id");
625 role.state[0].memory_id = Some(reserved_id);
626
627 let checks = audit_checks(&manifest, Some("root"));
628
629 assert!(
630 checks
631 .iter()
632 .any(|check| check.code == "reserved_memory_id_collision"
633 && check.status == StateAuditStatus::Fail)
634 );
635 }
636
637 #[test]
638 fn storage_not_applicable_is_explicit_metadata() {
639 let manifest = StateManifest {
640 schema_version: STATE_MANIFEST_SCHEMA_VERSION,
641 roles: vec![StateRoleManifest {
642 canister_role: "root".to_string(),
643 state: vec![StateDomainManifest {
644 domain: "external_authority".to_string(),
645 version: 1,
646 storage: StateStorage::NotApplicable,
647 memory_id: None,
648 owner: "canic-core".to_string(),
649 record: "ExternalAuthorityRecord".to_string(),
650 snapshot: "ExternalAuthorityData".to_string(),
651 min_supported_version: 1,
652 migration_policy: MigrationPolicy::NotApplicable,
653 restore_order: Some(10),
654 post_upgrade_invariant: Some("external_authority_invariants".to_string()),
655 migrations: Vec::new(),
656 }],
657 reserved_memory: Vec::new(),
658 }],
659 };
660
661 let checks = audit_checks(&manifest, None);
662
663 assert!(checks.iter().any(|check| {
664 check.code == "state_domain_storage_not_applicable"
665 && check.status == StateAuditStatus::Pass
666 }));
667 assert!(
668 checks
669 .iter()
670 .all(|check| check.code != "state_domain_missing_memory_id")
671 );
672 }
673
674 #[test]
675 fn invalid_support_window_fails() {
676 let manifest = StateManifest {
677 schema_version: STATE_MANIFEST_SCHEMA_VERSION,
678 roles: vec![StateRoleManifest {
679 canister_role: "root".to_string(),
680 state: vec![StateDomainManifest {
681 domain: "auth_sessions".to_string(),
682 version: 2,
683 storage: StateStorage::StableMemory,
684 memory_id: Some(19),
685 owner: "canic-core".to_string(),
686 record: "AuthSessionRecord".to_string(),
687 snapshot: "AuthSessionsData".to_string(),
688 min_supported_version: 3,
689 migration_policy: MigrationPolicy::NewDomain,
690 restore_order: Some(10),
691 post_upgrade_invariant: Some("auth_sessions_invariants".to_string()),
692 migrations: Vec::new(),
693 }],
694 reserved_memory: Vec::new(),
695 }],
696 };
697
698 let checks = audit_checks(&manifest, None);
699
700 assert!(checks.iter().any(|check| {
701 check.code == "state_domain_invalid_support_window"
702 && check.status == StateAuditStatus::Fail
703 }));
704 assert!(
705 checks
706 .iter()
707 .all(|check| check.code != "migration_available")
708 );
709 }
710
711 #[test]
712 fn duplicate_migration_declaration_fails() {
713 let manifest = StateManifest {
714 schema_version: STATE_MANIFEST_SCHEMA_VERSION,
715 roles: vec![StateRoleManifest {
716 canister_role: "root".to_string(),
717 state: vec![StateDomainManifest {
718 domain: "auth_sessions".to_string(),
719 version: 3,
720 storage: StateStorage::StableMemory,
721 memory_id: Some(19),
722 owner: "canic-core".to_string(),
723 record: "AuthSessionRecord".to_string(),
724 snapshot: "AuthSessionsData".to_string(),
725 min_supported_version: 2,
726 migration_policy: MigrationPolicy::Migrate,
727 restore_order: Some(10),
728 post_upgrade_invariant: Some("auth_sessions_invariants".to_string()),
729 migrations: vec![
730 StateMigrationManifest {
731 from: 2,
732 to: 3,
733 kind: "function".to_string(),
734 name: Some("migrate_auth_sessions_v2_to_v3".to_string()),
735 test: Some(
736 "auth_sessions_v2_to_v3_upgrade_preserves_sessions".to_string(),
737 ),
738 },
739 StateMigrationManifest {
740 from: 2,
741 to: 3,
742 kind: "function".to_string(),
743 name: Some("migrate_auth_sessions_v2_to_v3_again".to_string()),
744 test: Some(
745 "auth_sessions_v2_to_v3_upgrade_preserves_sessions".to_string(),
746 ),
747 },
748 ],
749 }],
750 reserved_memory: Vec::new(),
751 }],
752 };
753
754 let checks = audit_checks(&manifest, None);
755
756 assert!(checks.iter().any(|check| {
757 check.code == "migration_declaration_duplicate"
758 && check.status == StateAuditStatus::Fail
759 }));
760 }
761
762 #[test]
763 fn invalid_migration_declaration_fails() {
764 let manifest = StateManifest {
765 schema_version: STATE_MANIFEST_SCHEMA_VERSION,
766 roles: vec![StateRoleManifest {
767 canister_role: "root".to_string(),
768 state: vec![StateDomainManifest {
769 domain: "auth_sessions".to_string(),
770 version: 4,
771 storage: StateStorage::StableMemory,
772 memory_id: Some(19),
773 owner: "canic-core".to_string(),
774 record: "AuthSessionRecord".to_string(),
775 snapshot: "AuthSessionsData".to_string(),
776 min_supported_version: 2,
777 migration_policy: MigrationPolicy::Migrate,
778 restore_order: Some(10),
779 post_upgrade_invariant: Some("auth_sessions_invariants".to_string()),
780 migrations: vec![StateMigrationManifest {
781 from: 2,
782 to: 4,
783 kind: "function".to_string(),
784 name: Some("migrate_auth_sessions_v2_to_v4".to_string()),
785 test: Some("auth_sessions_v2_to_v4_upgrade_preserves_sessions".to_string()),
786 }],
787 }],
788 reserved_memory: Vec::new(),
789 }],
790 };
791
792 let checks = audit_checks(&manifest, None);
793
794 assert!(checks.iter().any(|check| {
795 check.code == "migration_declaration_invalid" && check.status == StateAuditStatus::Fail
796 }));
797 }
798
799 #[test]
800 fn missing_migration_test_warns_separately_from_missing_migration() {
801 let manifest = StateManifest {
802 schema_version: STATE_MANIFEST_SCHEMA_VERSION,
803 roles: vec![StateRoleManifest {
804 canister_role: "root".to_string(),
805 state: vec![StateDomainManifest {
806 domain: "auth_sessions".to_string(),
807 version: 3,
808 storage: StateStorage::StableMemory,
809 memory_id: Some(19),
810 owner: "canic-core".to_string(),
811 record: "AuthSessionRecord".to_string(),
812 snapshot: "AuthSessionsData".to_string(),
813 min_supported_version: 2,
814 migration_policy: MigrationPolicy::Migrate,
815 restore_order: Some(10),
816 post_upgrade_invariant: Some("auth_sessions_invariants".to_string()),
817 migrations: vec![StateMigrationManifest {
818 from: 2,
819 to: 3,
820 kind: "function".to_string(),
821 name: Some("migrate_auth_sessions_v2_to_v3".to_string()),
822 test: None,
823 }],
824 }],
825 reserved_memory: Vec::new(),
826 }],
827 };
828 let checks = audit_checks(&manifest, None);
829
830 assert!(
831 checks
832 .iter()
833 .any(|check| check.code == "upgrade_test_missing"
834 && check.status == StateAuditStatus::Warn)
835 );
836 assert!(checks.iter().all(|check| check.code != "migration_missing"));
837 }
838
839 #[test]
840 fn unknown_filtered_role_fails() {
841 let report = build_state_audit_report(Some("missing"));
842
843 assert_eq!(report.status, StateAuditStatus::Fail);
844 assert_eq!(report.checks[0].code, "state_role_missing");
845 }
846}