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, ReservedMemoryManifest, StateDomainManifest, StateMigrationManifest,
245 StateRoleManifest, 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_passes_when_every_active_memory_id_is_modeled() {
342 let report = build_state_audit_report(Some("root"));
343
344 assert_eq!(report.status, StateAuditStatus::Pass);
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 .all(|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, 18, 20, 29, 30, 34, 35, 39, 40, 41, 42, 43, 44, 45, 46, 47, 80,
523 81, 82, 83, 85,
524 ]
525 );
526 assert_eq!(
527 manifest.roles[0]
528 .reserved_memory
529 .iter()
530 .map(|entry| entry.memory_id)
531 .collect::<Vec<_>>(),
532 Vec::<u8>::new()
533 );
534 }
535
536 #[test]
537 fn unknown_role_resolution_returns_no_manifest() {
538 let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
539 let config = workspace.join("canisters/audit/root_probe/canic.toml");
540 let resolution = resolve_project_state_manifest(&workspace, &[config], Some("missing"));
541
542 assert!(matches!(
543 resolution,
544 StateManifestResolution::Rejected { errors }
545 if errors.iter().any(|finding| matches!(finding, RoleContractFinding::RoleUnknown { .. }))
546 ));
547 }
548
549 #[test]
550 fn unsupported_manifest_schema_version_fails() {
551 let mut manifest = test_state_manifest(Some("root"));
552 manifest.schema_version = STATE_MANIFEST_SCHEMA_VERSION + 1;
553
554 let checks = audit_checks(&manifest, Some("root"));
555
556 assert!(checks.iter().any(|check| {
557 check.code == "state_manifest_schema_version_unsupported"
558 && check.status == StateAuditStatus::Fail
559 }));
560 }
561
562 #[test]
563 fn duplicate_state_role_fails() {
564 let mut manifest = test_state_manifest(Some("root"));
565 let duplicate = manifest.roles[0].clone();
566 manifest.roles.push(duplicate);
567
568 let checks = audit_checks(&manifest, None);
569
570 assert!(checks.iter().any(|check| {
571 check.code == "state_role_duplicate" && check.status == StateAuditStatus::Fail
572 }));
573 }
574
575 #[test]
576 fn duplicate_memory_id_fails_within_role() {
577 let mut manifest = test_state_manifest(Some("root"));
578 let role = manifest.roles.first_mut().expect("root role");
579 role.state[1].memory_id = role.state[0].memory_id;
580
581 let checks = audit_checks(&manifest, Some("root"));
582 assert!(
583 checks
584 .iter()
585 .any(|check| check.code == "memory_id_duplicate"
586 && check.status == StateAuditStatus::Fail)
587 );
588 }
589
590 #[test]
591 fn duplicate_state_domain_fails_within_role() {
592 let mut manifest = test_state_manifest(Some("root"));
593 let role = manifest.roles.first_mut().expect("root role");
594 let mut duplicate = role.state[0].clone();
595 duplicate.memory_id = Some(250);
596 role.state.push(duplicate);
597
598 let checks = audit_checks(&manifest, Some("root"));
599
600 assert!(
601 checks
602 .iter()
603 .any(|check| check.code == "state_domain_duplicate"
604 && check.status == StateAuditStatus::Fail)
605 );
606 }
607
608 #[test]
609 fn active_cycle_tracker_memory_is_not_reported_as_reserved() {
610 let report = build_state_audit_report(Some("root"));
611
612 assert!(
613 report
614 .checks
615 .iter()
616 .all(|check| check.code != "reserved_memory_id_declared")
617 );
618 }
619
620 #[test]
621 fn active_domain_reclaiming_reserved_memory_id_fails() {
622 let mut manifest = test_state_manifest(Some("root"));
623 let role = manifest.roles.first_mut().expect("root role");
624 let reserved_id = 250;
625 role.reserved_memory.push(ReservedMemoryManifest {
626 label: "future_state".to_string(),
627 memory_id: reserved_id,
628 owner: "canic-core".to_string(),
629 reason: "synthetic collision fixture".to_string(),
630 });
631 role.state[0].memory_id = Some(reserved_id);
632
633 let checks = audit_checks(&manifest, Some("root"));
634
635 assert!(
636 checks
637 .iter()
638 .any(|check| check.code == "reserved_memory_id_collision"
639 && check.status == StateAuditStatus::Fail)
640 );
641 }
642
643 #[test]
644 fn storage_not_applicable_is_explicit_metadata() {
645 let manifest = StateManifest {
646 schema_version: STATE_MANIFEST_SCHEMA_VERSION,
647 roles: vec![StateRoleManifest {
648 canister_role: "root".to_string(),
649 state: vec![StateDomainManifest {
650 domain: "external_authority".to_string(),
651 version: 1,
652 storage: StateStorage::NotApplicable,
653 memory_id: None,
654 owner: "canic-core".to_string(),
655 record: "ExternalAuthorityRecord".to_string(),
656 snapshot: "ExternalAuthorityData".to_string(),
657 min_supported_version: 1,
658 migration_policy: MigrationPolicy::NotApplicable,
659 restore_order: Some(10),
660 post_upgrade_invariant: Some("external_authority_invariants".to_string()),
661 migrations: Vec::new(),
662 }],
663 reserved_memory: Vec::new(),
664 }],
665 };
666
667 let checks = audit_checks(&manifest, None);
668
669 assert!(checks.iter().any(|check| {
670 check.code == "state_domain_storage_not_applicable"
671 && check.status == StateAuditStatus::Pass
672 }));
673 assert!(
674 checks
675 .iter()
676 .all(|check| check.code != "state_domain_missing_memory_id")
677 );
678 }
679
680 #[test]
681 fn invalid_support_window_fails() {
682 let manifest = StateManifest {
683 schema_version: STATE_MANIFEST_SCHEMA_VERSION,
684 roles: vec![StateRoleManifest {
685 canister_role: "root".to_string(),
686 state: vec![StateDomainManifest {
687 domain: "auth_sessions".to_string(),
688 version: 2,
689 storage: StateStorage::StableMemory,
690 memory_id: Some(19),
691 owner: "canic-core".to_string(),
692 record: "AuthSessionRecord".to_string(),
693 snapshot: "AuthSessionsData".to_string(),
694 min_supported_version: 3,
695 migration_policy: MigrationPolicy::NewDomain,
696 restore_order: Some(10),
697 post_upgrade_invariant: Some("auth_sessions_invariants".to_string()),
698 migrations: Vec::new(),
699 }],
700 reserved_memory: Vec::new(),
701 }],
702 };
703
704 let checks = audit_checks(&manifest, None);
705
706 assert!(checks.iter().any(|check| {
707 check.code == "state_domain_invalid_support_window"
708 && check.status == StateAuditStatus::Fail
709 }));
710 assert!(
711 checks
712 .iter()
713 .all(|check| check.code != "migration_available")
714 );
715 }
716
717 #[test]
718 fn duplicate_migration_declaration_fails() {
719 let manifest = StateManifest {
720 schema_version: STATE_MANIFEST_SCHEMA_VERSION,
721 roles: vec![StateRoleManifest {
722 canister_role: "root".to_string(),
723 state: vec![StateDomainManifest {
724 domain: "auth_sessions".to_string(),
725 version: 3,
726 storage: StateStorage::StableMemory,
727 memory_id: Some(19),
728 owner: "canic-core".to_string(),
729 record: "AuthSessionRecord".to_string(),
730 snapshot: "AuthSessionsData".to_string(),
731 min_supported_version: 2,
732 migration_policy: MigrationPolicy::Migrate,
733 restore_order: Some(10),
734 post_upgrade_invariant: Some("auth_sessions_invariants".to_string()),
735 migrations: vec![
736 StateMigrationManifest {
737 from: 2,
738 to: 3,
739 kind: "function".to_string(),
740 name: Some("migrate_auth_sessions_v2_to_v3".to_string()),
741 test: Some(
742 "auth_sessions_v2_to_v3_upgrade_preserves_sessions".to_string(),
743 ),
744 },
745 StateMigrationManifest {
746 from: 2,
747 to: 3,
748 kind: "function".to_string(),
749 name: Some("migrate_auth_sessions_v2_to_v3_again".to_string()),
750 test: Some(
751 "auth_sessions_v2_to_v3_upgrade_preserves_sessions".to_string(),
752 ),
753 },
754 ],
755 }],
756 reserved_memory: Vec::new(),
757 }],
758 };
759
760 let checks = audit_checks(&manifest, None);
761
762 assert!(checks.iter().any(|check| {
763 check.code == "migration_declaration_duplicate"
764 && check.status == StateAuditStatus::Fail
765 }));
766 }
767
768 #[test]
769 fn invalid_migration_declaration_fails() {
770 let manifest = StateManifest {
771 schema_version: STATE_MANIFEST_SCHEMA_VERSION,
772 roles: vec![StateRoleManifest {
773 canister_role: "root".to_string(),
774 state: vec![StateDomainManifest {
775 domain: "auth_sessions".to_string(),
776 version: 4,
777 storage: StateStorage::StableMemory,
778 memory_id: Some(19),
779 owner: "canic-core".to_string(),
780 record: "AuthSessionRecord".to_string(),
781 snapshot: "AuthSessionsData".to_string(),
782 min_supported_version: 2,
783 migration_policy: MigrationPolicy::Migrate,
784 restore_order: Some(10),
785 post_upgrade_invariant: Some("auth_sessions_invariants".to_string()),
786 migrations: vec![StateMigrationManifest {
787 from: 2,
788 to: 4,
789 kind: "function".to_string(),
790 name: Some("migrate_auth_sessions_v2_to_v4".to_string()),
791 test: Some("auth_sessions_v2_to_v4_upgrade_preserves_sessions".to_string()),
792 }],
793 }],
794 reserved_memory: Vec::new(),
795 }],
796 };
797
798 let checks = audit_checks(&manifest, None);
799
800 assert!(checks.iter().any(|check| {
801 check.code == "migration_declaration_invalid" && check.status == StateAuditStatus::Fail
802 }));
803 }
804
805 #[test]
806 fn missing_migration_test_warns_separately_from_missing_migration() {
807 let manifest = StateManifest {
808 schema_version: STATE_MANIFEST_SCHEMA_VERSION,
809 roles: vec![StateRoleManifest {
810 canister_role: "root".to_string(),
811 state: vec![StateDomainManifest {
812 domain: "auth_sessions".to_string(),
813 version: 3,
814 storage: StateStorage::StableMemory,
815 memory_id: Some(19),
816 owner: "canic-core".to_string(),
817 record: "AuthSessionRecord".to_string(),
818 snapshot: "AuthSessionsData".to_string(),
819 min_supported_version: 2,
820 migration_policy: MigrationPolicy::Migrate,
821 restore_order: Some(10),
822 post_upgrade_invariant: Some("auth_sessions_invariants".to_string()),
823 migrations: vec![StateMigrationManifest {
824 from: 2,
825 to: 3,
826 kind: "function".to_string(),
827 name: Some("migrate_auth_sessions_v2_to_v3".to_string()),
828 test: None,
829 }],
830 }],
831 reserved_memory: Vec::new(),
832 }],
833 };
834 let checks = audit_checks(&manifest, None);
835
836 assert!(
837 checks
838 .iter()
839 .any(|check| check.code == "upgrade_test_missing"
840 && check.status == StateAuditStatus::Warn)
841 );
842 assert!(checks.iter().all(|check| check.code != "migration_missing"));
843 }
844
845 #[test]
846 fn unknown_filtered_role_fails() {
847 let report = build_state_audit_report(Some("missing"));
848
849 assert_eq!(report.status, StateAuditStatus::Fail);
850 assert_eq!(report.checks[0].code, "state_role_missing");
851 }
852}