1use super::*;
2use std::collections::{BTreeMap, BTreeSet};
3
4mod artifacts;
5mod canisters;
6mod config_digests;
7mod controllers;
8mod module_hashes;
9mod pools;
10mod receipt_resume;
11mod root_subnet;
12mod safety;
13mod verifier_readiness;
14
15use artifacts::compare_artifacts;
16#[cfg(test)]
17pub(super) use artifacts::{
18 ARTIFACT_DUPLICATE_DIFF_CATEGORY, ARTIFACT_FILE_DIGEST_MISMATCH_CODE,
19 ARTIFACT_FILE_SHA256_DIFF_CATEGORY, ARTIFACT_ROLE_CONFLICT_DIFF_CATEGORY,
20 DUPLICATE_ARTIFACT_OBSERVED_CODE, DUPLICATE_PLANNED_ARTIFACT_ROLE_CODE,
21 PLANNED_ARTIFACT_DUPLICATE_DIFF_CATEGORY, PLANNED_ARTIFACT_ROLE_CONFLICT_DIFF_CATEGORY,
22};
23pub(in crate::deployment_truth) use artifacts::{
24 ARTIFACT_MISSING_CODE, is_artifact_role_failure_code,
25};
26pub(super) use artifacts::{ARTIFACT_ROLE_CONFLICT_CODE, PLANNED_ARTIFACT_ROLE_CONFLICT_CODE};
27#[cfg(test)]
28pub(super) use canisters::{
29 CANISTER_DUPLICATE_DIFF_CATEGORY, CANISTER_EXTRA_DIFF_CATEGORY,
30 CANISTER_ID_ROLE_CONFLICT_DIFF_CATEGORY, CANISTER_ROLE_AMBIGUOUS_CODE,
31 CANISTER_ROLE_AMBIGUOUS_DIFF_CATEGORY, CANISTER_ROLE_MISMATCH_CODE, CANISTER_UNOBSERVED_CODE,
32 COMPONENT_REGISTRY_ROLE_MISSING_CODE, DUPLICATE_CANISTER_OBSERVED_CODE,
33 DUPLICATE_PLANNED_CANISTER_ROLE_CODE, EXTRA_CANISTER_OBSERVED_CODE,
34 PLANNED_CANISTER_DUPLICATE_DIFF_CATEGORY, PLANNED_CANISTER_ID_CONFLICT_DIFF_CATEGORY,
35 PLANNED_CANISTER_ROLE_CONFLICT_DIFF_CATEGORY, ROLE_MISMATCH_DIFF_CATEGORY,
36 UNSAFE_CONTROL_CLASS_CODE,
37};
38pub(super) use canisters::{
39 CANISTER_ID_ROLE_CONFLICT_CODE, PLANNED_CANISTER_ID_CONFLICT_CODE,
40 PLANNED_CANISTER_ROLE_CONFLICT_CODE,
41};
42use canisters::{compare_canisters, compare_observed_canister_id_conflicts};
43#[cfg(test)]
44pub(super) use config_digests::{RAW_CONFIG_DIGEST_MISMATCH_CODE, RAW_CONFIG_SHA256_DIFF_CATEGORY};
45use config_digests::{compare_embedded_config, compare_raw_config};
46use controllers::compare_authority_profile;
47#[cfg(test)]
48pub(super) use controllers::{
49 CONTROLLER_AUTHORITY_OVERLAP_CODE, CONTROLLER_EXTRA_DIFF_CATEGORY,
50 CONTROLLER_MISSING_DIFF_CATEGORY, CONTROLLERS_UNOBSERVED_CODE,
51 EXPECTED_CONTROLLER_MISSING_CODE, EXTRA_CONTROLLER_OBSERVED_CODE,
52};
53use module_hashes::compare_module_hashes;
54#[cfg(test)]
55pub(super) use module_hashes::{
56 INSTALLED_MODULE_HASH_AMBIGUOUS_CODE, INSTALLED_MODULE_HASH_AMBIGUOUS_DIFF_CATEGORY,
57 INSTALLED_MODULE_HASH_DIFF_CATEGORY, INSTALLED_MODULE_HASH_MISMATCH_CODE,
58};
59pub(super) use pools::{
60 CANISTER_POOL_ROLE_CONFLICT_CODE, PLANNED_POOL_CONFLICT_CODE, PLANNED_POOL_ID_CONFLICT_CODE,
61 POOL_CANISTER_ID_CONFLICT_CODE,
62};
63#[cfg(test)]
64pub(super) use pools::{
65 CANISTER_POOL_ROLE_CONFLICT_DIFF_CATEGORY, DUPLICATE_PLANNED_POOL_CODE,
66 DUPLICATE_POOL_CANISTER_OBSERVED_CODE, EXTRA_POOL_CANISTER_OBSERVED_CODE,
67 PLANNED_POOL_CONFLICT_DIFF_CATEGORY, PLANNED_POOL_DUPLICATE_DIFF_CATEGORY,
68 PLANNED_POOL_ID_CONFLICT_DIFF_CATEGORY, POOL_CANISTER_DIFF_CATEGORY,
69 POOL_CANISTER_DUPLICATE_DIFF_CATEGORY, POOL_CANISTER_ID_CONFLICT_DIFF_CATEGORY,
70 POOL_CANISTER_ID_DIFF_CATEGORY, POOL_CANISTER_ID_MISMATCH_CODE, POOL_CANISTER_MISSING_CODE,
71 POOL_CONTROL_CLASS_DIFF_CATEGORY, POOL_EXTRA_DIFF_CATEGORY, UNSAFE_POOL_CONTROL_CLASS_CODE,
72};
73use pools::{compare_observed_canister_pool_role_conflicts, compare_pools};
74pub use receipt_resume::compare_plan_inventory_and_receipt;
75#[cfg(test)]
76pub(super) use receipt_resume::{
77 DUPLICATE_RECEIPT_PHASE_CODE, DUPLICATE_RECEIPT_ROLE_PHASE_CODE,
78 RECEIPT_EXECUTION_STATUS_MISMATCH_CODE, RECEIPT_PLAN_MISMATCH_CODE,
79 RECEIPT_POSTCONDITION_UNVERIFIED_CODE,
80};
81pub(super) use receipt_resume::{RECEIPT_PHASE_CONFLICT_CODE, RECEIPT_ROLE_PHASE_CONFLICT_CODE};
82#[cfg(test)]
83pub(super) use root_subnet::ROOT_AUTH_CLOUD_ENGINE_SUBNET_CODE;
84pub(super) use root_subnet::apply_root_auth_signer_subnet_check;
85#[cfg(test)]
86pub(super) use root_subnet::{
87 RootSubnetEvidence, RootSubnetEvidenceSource, apply_root_auth_signer_subnet_check_with_source,
88};
89pub use safety::safety_report_from_diff;
90pub(in crate::deployment_truth::report) use safety::{resume_safety_reasons, safety_status};
91use verifier_readiness::compare_verifier_readiness;
92#[cfg(test)]
93pub(super) use verifier_readiness::{
94 DUPLICATE_PLANNED_VERIFIER_ROLE_EPOCH_CODE, DUPLICATE_VERIFIER_ROLE_EPOCH_OBSERVED_CODE,
95 PLANNED_VERIFIER_ROLE_EPOCH_CONFLICT_DIFF_CATEGORY,
96 PLANNED_VERIFIER_ROLE_EPOCH_DUPLICATE_DIFF_CATEGORY, VERIFIER_NOT_OBSERVED_LABEL,
97 VERIFIER_ROLE_EPOCH_CONFLICT_DIFF_CATEGORY, VERIFIER_ROLE_EPOCH_DIFF_CATEGORY,
98 VERIFIER_ROLE_EPOCH_DUPLICATE_DIFF_CATEGORY, VERIFIER_ROLE_EPOCH_STALE_CODE,
99 VERIFIER_ROLE_EPOCH_UNOBSERVED_CODE,
100};
101pub(super) use verifier_readiness::{
102 PLANNED_VERIFIER_ROLE_EPOCH_CONFLICT_CODE, VERIFIER_ROLE_EPOCH_CONFLICT_CODE,
103};
104
105pub(in crate::deployment_truth) const DEPLOYMENT_MANIFEST_MISMATCH_CODE: &str =
106 "deployment_manifest_mismatch";
107pub(in crate::deployment_truth) const OBSERVATION_GAP_CODE: &str = "observation_gap";
108pub(in crate::deployment_truth) const PLAN_ASSUMPTION_CODE: &str = "plan_assumption";
109const IDENTITY_UNOBSERVED_CODE: &str = "identity_unobserved";
110const ENVIRONMENT_MISMATCH_CODE: &str = "environment_mismatch";
111pub(in crate::deployment_truth) const FLEET_IDENTITY_MISMATCH_CODE: &str =
112 "fleet_identity_mismatch";
113const ROOT_TRUST_ANCHOR_MISMATCH_CODE: &str = "root_trust_anchor_mismatch";
114const DEPLOYMENT_MANIFEST_UNOBSERVED_CODE: &str = "deployment_manifest_unobserved";
115
116#[must_use]
117pub fn is_evidence_conflict_finding_code(code: &str) -> bool {
118 matches!(
119 code,
120 PLANNED_ARTIFACT_ROLE_CONFLICT_CODE
121 | ARTIFACT_ROLE_CONFLICT_CODE
122 | CANISTER_ID_ROLE_CONFLICT_CODE
123 | PLANNED_CANISTER_ROLE_CONFLICT_CODE
124 | PLANNED_CANISTER_ID_CONFLICT_CODE
125 | CANISTER_POOL_ROLE_CONFLICT_CODE
126 | PLANNED_POOL_CONFLICT_CODE
127 | PLANNED_POOL_ID_CONFLICT_CODE
128 | POOL_CANISTER_ID_CONFLICT_CODE
129 | RECEIPT_PHASE_CONFLICT_CODE
130 | RECEIPT_ROLE_PHASE_CONFLICT_CODE
131 | PLANNED_VERIFIER_ROLE_EPOCH_CONFLICT_CODE
132 | VERIFIER_ROLE_EPOCH_CONFLICT_CODE
133 )
134}
135
136struct DuplicateEvidenceGroup {
140 subject: String,
141 count: usize,
142 evidence_label: String,
143 is_conflict: bool,
144}
145
146#[derive(Clone, Debug, Eq, PartialEq)]
150pub struct LocalDeploymentCheckRequest {
151 pub fleet_name: String,
152 pub app: String,
153 pub environment: String,
154 pub artifact_environment: String,
155 pub workspace_root: std::path::PathBuf,
156 pub icp_root: std::path::PathBuf,
157 pub config_path: Option<std::path::PathBuf>,
158 pub observed_at: String,
159 pub runtime_variant: String,
160 pub build_profile: String,
161}
162
163pub fn check_local_deployment(
165 request: &LocalDeploymentCheckRequest,
166) -> Result<DeploymentCheckV1, DeploymentTruthError> {
167 let artifact_root =
168 crate::release_set::artifact_root_path(&request.icp_root, &request.artifact_environment);
169 check_local_deployment_at_root(request, &artifact_root)
170}
171
172pub fn check_local_deployment_at_root(
173 request: &LocalDeploymentCheckRequest,
174 artifact_root: &std::path::Path,
175) -> Result<DeploymentCheckV1, DeploymentTruthError> {
176 let plan = build_local_deployment_plan_at_root(
177 &LocalDeploymentPlanRequest {
178 fleet_name: request.fleet_name.clone(),
179 app: request.app.clone(),
180 environment: request.environment.clone(),
181 artifact_environment: request.artifact_environment.clone(),
182 workspace_root: request.workspace_root.clone(),
183 icp_root: request.icp_root.clone(),
184 config_path: request.config_path.clone(),
185 runtime_variant: request.runtime_variant.clone(),
186 build_profile: request.build_profile.clone(),
187 },
188 artifact_root,
189 );
190 let inventory = collect_local_deployment_inventory_at_root(
191 &LocalInventoryRequest {
192 fleet_name: request.fleet_name.clone(),
193 environment: request.environment.clone(),
194 artifact_environment: request.artifact_environment.clone(),
195 workspace_root: request.workspace_root.clone(),
196 icp_root: request.icp_root.clone(),
197 config_path: request.config_path.clone(),
198 observed_at: request.observed_at.clone(),
199 },
200 artifact_root,
201 )?;
202 let mut diff = compare_plan_to_inventory(&plan, &inventory);
203 apply_root_auth_signer_subnet_check(
204 &mut diff,
205 &inventory,
206 &request.environment,
207 &request.icp_root,
208 );
209 let report = safety_report_from_diff(
210 format!(
211 "local:{}:{}:report",
212 request.environment, request.fleet_name
213 ),
214 Some(format!(
215 "local:{}:{}:diff",
216 request.environment, request.fleet_name
217 )),
218 &diff,
219 );
220
221 Ok(DeploymentCheckV1 {
222 schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
223 check_id: format!("local:{}:{}:check", request.environment, request.fleet_name),
224 plan,
225 inventory,
226 diff,
227 report,
228 })
229}
230
231fn refresh_resume_safety(diff: &mut DeploymentDiffV1) {
232 diff.resume_safety.status = safety_status(&diff.hard_failures, &diff.warnings);
233 diff.resume_safety.reasons = resume_safety_reasons(&diff.hard_failures, &diff.warnings);
234}
235
236#[must_use]
238pub fn compare_plan_to_inventory(
239 plan: &DeploymentPlanV1,
240 inventory: &DeploymentInventoryV1,
241) -> DeploymentDiffV1 {
242 let mut artifact_diff = Vec::new();
243 let mut controller_diff = Vec::new();
244 let mut pool_diff = Vec::new();
245 let mut embedded_config_diff = Vec::new();
246 let mut module_hash_diff = Vec::new();
247 let mut verifier_readiness_diff = Vec::new();
248 let mut hard_failures = Vec::new();
249 let mut warnings = Vec::new();
250
251 compare_identity(plan, inventory, &mut hard_failures);
252 compare_authority_profile(plan, &mut controller_diff, &mut hard_failures);
253 compare_artifacts(
254 plan,
255 inventory,
256 &mut artifact_diff,
257 &mut hard_failures,
258 &mut warnings,
259 );
260 compare_observed_canister_id_conflicts(
261 inventory,
262 &mut controller_diff,
263 &mut hard_failures,
264 &mut warnings,
265 );
266 compare_observed_canister_pool_role_conflicts(inventory, &mut pool_diff, &mut hard_failures);
267 compare_canisters(
268 plan,
269 inventory,
270 &mut controller_diff,
271 &mut hard_failures,
272 &mut warnings,
273 );
274 compare_pools(
275 plan,
276 inventory,
277 &mut pool_diff,
278 &mut hard_failures,
279 &mut warnings,
280 );
281 compare_module_hashes(
282 plan,
283 inventory,
284 &mut module_hash_diff,
285 &mut hard_failures,
286 &mut warnings,
287 );
288 compare_raw_config(
289 plan,
290 inventory,
291 &mut embedded_config_diff,
292 &mut hard_failures,
293 );
294 compare_embedded_config(
295 plan,
296 inventory,
297 &mut embedded_config_diff,
298 &mut hard_failures,
299 &mut warnings,
300 );
301 compare_verifier_readiness(
302 plan,
303 inventory,
304 &mut verifier_readiness_diff,
305 &mut hard_failures,
306 &mut warnings,
307 );
308 record_plan_assumptions(plan, &mut warnings);
309 for gap in &inventory.unresolved_observations {
310 warnings.push(SafetyFindingV1 {
311 code: OBSERVATION_GAP_CODE.to_string(),
312 message: gap.description.clone(),
313 severity: SafetySeverityV1::Warning,
314 subject: Some(gap.key.clone()),
315 });
316 }
317
318 let status = safety_status(&hard_failures, &warnings);
319 DeploymentDiffV1 {
320 schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
321 plan_identity: plan.deployment_identity.clone(),
322 observed_identity: inventory.observed_identity.clone(),
323 artifact_diff,
324 controller_diff,
325 pool_diff,
326 embedded_config_diff,
327 module_hash_diff,
328 verifier_readiness_diff,
329 resume_safety: ResumeSafetyV1 {
330 status,
331 reasons: resume_safety_reasons(&hard_failures, &warnings),
332 },
333 hard_failures,
334 warnings,
335 resumable_phases: Vec::new(),
336 }
337}
338
339fn record_plan_assumptions(plan: &DeploymentPlanV1, warnings: &mut Vec<SafetyFindingV1>) {
340 for assumption in &plan.unresolved_assumptions {
341 warnings.push(SafetyFindingV1 {
342 code: PLAN_ASSUMPTION_CODE.to_string(),
343 message: assumption.description.clone(),
344 severity: SafetySeverityV1::Warning,
345 subject: Some(assumption.key.clone()),
346 });
347 }
348}
349
350fn compare_identity(
351 plan: &DeploymentPlanV1,
352 inventory: &DeploymentInventoryV1,
353 hard_failures: &mut Vec<SafetyFindingV1>,
354) {
355 let Some(observed) = &inventory.observed_identity else {
356 hard_failures.push(finding(
357 IDENTITY_UNOBSERVED_CODE,
358 "deployment identity was not observed",
359 SafetySeverityV1::HardFailure,
360 None,
361 ));
362 return;
363 };
364
365 if observed.environment != plan.deployment_identity.environment {
366 hard_failures.push(finding(
367 ENVIRONMENT_MISMATCH_CODE,
368 format!(
369 "plan environment {} differs from observed environment {}",
370 plan.deployment_identity.environment, observed.environment
371 ),
372 SafetySeverityV1::HardFailure,
373 Some("deployment_identity.environment".to_string()),
374 ));
375 }
376 for (field, expected, actual) in [
377 (
378 "fleet_name",
379 Some(plan.deployment_identity.fleet_name.clone()),
380 Some(observed.fleet_name.clone()),
381 ),
382 (
383 "app",
384 Some(plan.deployment_identity.app.clone()),
385 Some(observed.app.clone()),
386 ),
387 (
388 "canonical_network_id",
389 plan.deployment_identity
390 .canonical_network_id
391 .map(|value| value.to_string()),
392 observed.canonical_network_id.map(|value| value.to_string()),
393 ),
394 (
395 "fleet_id",
396 plan.deployment_identity
397 .fleet_id
398 .map(|value| value.to_string()),
399 observed.fleet_id.map(|value| value.to_string()),
400 ),
401 ] {
402 match (expected, actual) {
403 (Some(expected), Some(actual)) if expected != actual => {
404 hard_failures.push(finding(
405 FLEET_IDENTITY_MISMATCH_CODE,
406 format!("plan {field} {expected} differs from observed {field} {actual}"),
407 SafetySeverityV1::HardFailure,
408 Some(format!("deployment_identity.{field}")),
409 ));
410 }
411 (Some(expected), None) => {
412 hard_failures.push(finding(
413 FLEET_IDENTITY_MISMATCH_CODE,
414 format!("plan {field} {expected} was not observed"),
415 SafetySeverityV1::HardFailure,
416 Some(format!("deployment_identity.{field}")),
417 ));
418 }
419 _ => {}
420 }
421 }
422 if let (Some(expected), Some(actual)) = (
423 plan.deployment_identity.root_principal.as_ref(),
424 observed.root_principal.as_ref(),
425 ) && expected != actual
426 {
427 hard_failures.push(finding(
428 ROOT_TRUST_ANCHOR_MISMATCH_CODE,
429 format!("plan root {expected} differs from observed root {actual}"),
430 SafetySeverityV1::HardFailure,
431 Some("deployment_identity.root_principal".to_string()),
432 ));
433 }
434 match (
435 plan.deployment_identity.deployment_manifest_digest.as_ref(),
436 observed.deployment_manifest_digest.as_ref(),
437 ) {
438 (Some(expected), Some(actual)) if expected != actual => {
439 hard_failures.push(finding(
440 DEPLOYMENT_MANIFEST_MISMATCH_CODE,
441 "deployment manifest digest differs from the observed local config",
442 SafetySeverityV1::HardFailure,
443 Some("deployment_identity.deployment_manifest_digest".to_string()),
444 ));
445 }
446 (Some(_), None) => {
447 hard_failures.push(finding(
448 DEPLOYMENT_MANIFEST_UNOBSERVED_CODE,
449 "deployment manifest digest was not observed",
450 SafetySeverityV1::HardFailure,
451 Some("deployment_identity.deployment_manifest_digest".to_string()),
452 ));
453 }
454 _ => {}
455 }
456}
457
458fn finding(
459 code: impl Into<String>,
460 message: impl Into<String>,
461 severity: SafetySeverityV1,
462 subject: Option<String>,
463) -> SafetyFindingV1 {
464 SafetyFindingV1 {
465 code: code.into(),
466 message: message.into(),
467 severity,
468 subject,
469 }
470}
471
472fn diff_item(
473 category: impl Into<String>,
474 subject: impl Into<String>,
475 expected: Option<String>,
476 observed: Option<String>,
477 severity: SafetySeverityV1,
478) -> DiffItemV1 {
479 DiffItemV1 {
480 category: category.into(),
481 subject: subject.into(),
482 expected,
483 observed,
484 severity,
485 }
486}
487
488fn duplicate_evidence_groups<T>(
489 items: &[T],
490 subject: impl Fn(&T) -> String,
491 evidence: impl Fn(&T) -> String,
492 evidence_separator: &str,
493) -> Vec<DuplicateEvidenceGroup> {
494 let mut groups = Vec::new();
495 for (subject, entries) in group_by_subject(items, |item| Some(subject(item))) {
496 if entries.len() <= 1 {
497 continue;
498 }
499 let evidence_values = entries
500 .iter()
501 .map(|entry| evidence(entry))
502 .collect::<BTreeSet<_>>();
503 groups.push(DuplicateEvidenceGroup {
504 subject,
505 count: entries.len(),
506 evidence_label: evidence_values
507 .iter()
508 .cloned()
509 .collect::<Vec<_>>()
510 .join(evidence_separator),
511 is_conflict: evidence_values.len() > 1,
512 });
513 }
514 groups
515}
516
517fn duplicate_evidence_groups_by<T, K>(
518 items: &[T],
519 subject: impl Fn(&T) -> String,
520 evidence_key: impl Fn(&T) -> K,
521 evidence_label: impl Fn(&T) -> String,
522 evidence_separator: &str,
523) -> Vec<DuplicateEvidenceGroup>
524where
525 K: Ord,
526{
527 let mut groups = Vec::new();
528 for (subject, entries) in group_by_subject(items, |item| Some(subject(item))) {
529 if entries.len() <= 1 {
530 continue;
531 }
532 let evidence_values = entries
533 .iter()
534 .map(|entry| (evidence_key(entry), evidence_label(entry)))
535 .collect::<BTreeMap<_, _>>();
536 groups.push(DuplicateEvidenceGroup {
537 subject,
538 count: entries.len(),
539 evidence_label: evidence_values
540 .values()
541 .cloned()
542 .collect::<Vec<_>>()
543 .join(evidence_separator),
544 is_conflict: evidence_values.len() > 1,
545 });
546 }
547 groups
548}
549
550fn conflicting_assignment_groups<T>(
551 items: &[T],
552 subject: impl Fn(&T) -> Option<String>,
553 value: impl Fn(&T) -> String,
554 value_separator: &str,
555) -> Vec<DuplicateEvidenceGroup> {
556 let mut groups = Vec::new();
557 for (subject, entries) in group_by_subject(items, subject) {
558 if entries.len() <= 1 {
559 continue;
560 }
561 let values = entries
562 .iter()
563 .map(|entry| value(entry))
564 .collect::<BTreeSet<_>>();
565 if values.len() <= 1 {
566 continue;
567 }
568 groups.push(DuplicateEvidenceGroup {
569 subject,
570 count: entries.len(),
571 evidence_label: values
572 .iter()
573 .cloned()
574 .collect::<Vec<_>>()
575 .join(value_separator),
576 is_conflict: true,
577 });
578 }
579 groups
580}
581
582fn group_by_subject<T>(
583 items: &[T],
584 subject: impl Fn(&T) -> Option<String>,
585) -> BTreeMap<String, Vec<&T>> {
586 let mut by_subject = BTreeMap::<String, Vec<&T>>::new();
587 for item in items {
588 if let Some(subject) = subject(item) {
589 by_subject.entry(subject).or_default().push(item);
590 }
591 }
592 by_subject
593}
594
595#[cfg(test)]
596mod classification_tests {
597 use super::*;
598
599 #[test]
600 fn finding_classification_uses_exact_owner_codes() {
601 assert!(is_evidence_conflict_finding_code(
602 ARTIFACT_ROLE_CONFLICT_CODE
603 ));
604 assert!(is_evidence_conflict_finding_code(
605 RECEIPT_PHASE_CONFLICT_CODE
606 ));
607 assert!(!is_evidence_conflict_finding_code("artifact_conflict"));
608 assert!(!is_evidence_conflict_finding_code("conflict"));
609
610 assert!(is_artifact_role_failure_code(ARTIFACT_MISSING_CODE));
611 assert!(!is_artifact_role_failure_code(
612 PLANNED_ARTIFACT_ROLE_CONFLICT_CODE
613 ));
614 }
615}