canic-host 0.70.5

Host-side build, install, deployment, and fleet-template library for Canic workspaces
Documentation
use super::*;

#[test]
fn artifact_promotion_plan_round_trips_through_json() {
    let plan = sample_artifact_promotion_plan();

    assert_json_round_trip(&plan);
    let encoded = serde_json::to_value(&plan).expect("promotion plan should encode");
    assert_object_keys(
        &encoded,
        &[
            "schema_version",
            "plan_id",
            "artifact_promotion_plan_digest",
            "generated_at",
            "status",
            "target_plan_id",
            "promoted_plan_id",
            "promotion_plan_lineage_digest",
            "readiness",
            "artifact_identity_report",
            "transform",
            "target_execution_lineage",
            "blockers",
        ],
    );
    assert_eq!(encoded["plan_id"], "artifact-promotion-plan-1");
    assert_eq!(encoded["status"], "Ready");
    assert!(
        encoded["artifact_promotion_plan_digest"]
            .as_str()
            .is_some_and(|digest| digest.len() == 64)
    );
}

#[test]
fn artifact_promotion_plan_validation_accepts_generated_plan() {
    let plan = sample_artifact_promotion_plan();

    validate_artifact_promotion_plan(&plan).expect("generated promotion plan should validate");
}

#[test]
fn artifact_promotion_plan_validation_rejects_status_blocker_mismatch() {
    let mut plan = sample_artifact_promotion_plan();
    plan.blockers.push(SafetyFindingV1 {
        code: "promotion_blocker".to_string(),
        message: "blocked".to_string(),
        severity: SafetySeverityV1::HardFailure,
        subject: Some("root".to_string()),
    });

    let err =
        validate_artifact_promotion_plan(&plan).expect_err("ready plan with blockers should fail");

    std::assert_matches!(
        err,
        ArtifactPromotionPlanError::StatusBlockerMismatch { .. }
    );
}

#[test]
fn artifact_promotion_plan_validation_rejects_stale_lineage_copy() {
    let mut plan = sample_artifact_promotion_plan();
    plan.promotion_plan_lineage_digest = sample_sha256("9");

    let err =
        validate_artifact_promotion_plan(&plan).expect_err("stale plan lineage copy should fail");

    std::assert_matches!(
        err,
        ArtifactPromotionPlanError::LinkageMismatch {
            field: "promotion_plan_lineage_digest"
        }
    );
}

#[test]
fn artifact_promotion_plan_validation_rejects_stale_digest() {
    let mut plan = sample_artifact_promotion_plan();
    plan.artifact_promotion_plan_digest = sample_sha256("9");

    let err = validate_artifact_promotion_plan(&plan).expect_err("stale plan digest should fail");

    std::assert_matches!(
        err,
        ArtifactPromotionPlanError::LinkageMismatch {
            field: "artifact_promotion_plan_digest"
        }
    );
}

#[test]
fn artifact_promotion_plan_validation_rejects_mismatched_target_lineage() {
    let mut plan = sample_artifact_promotion_plan();
    let mut lineage = plan
        .target_execution_lineage
        .clone()
        .expect("sample plan should carry target lineage");
    lineage.transform.transform_id = "different-transform".to_string();
    plan.target_execution_lineage = Some(lineage);

    let err = validate_artifact_promotion_plan(&plan)
        .expect_err("target lineage with different transform should fail");

    std::assert_matches!(
        err,
        ArtifactPromotionPlanError::LinkageMismatch {
            field: "target_execution_lineage.transform"
        }
    );
}

#[test]
fn artifact_promotion_plan_for_check_accepts_matching_promoted_plan_check() {
    let plan = sample_artifact_promotion_plan();
    let check = sample_check(
        plan.transform.promoted_plan.clone(),
        sample_matching_inventory(),
    );

    validate_artifact_promotion_plan_for_check(&plan, &check)
        .expect("promotion plan should validate against target check");
}

#[test]
fn artifact_promotion_plan_for_check_rejects_other_target_plan() {
    let plan = sample_artifact_promotion_plan();
    let mut other_plan = sample_promotion_target_plan();
    other_plan.plan_id = "other-target-plan".to_string();
    let check = sample_check(other_plan, sample_matching_inventory());

    let err = validate_artifact_promotion_plan_for_check(&plan, &check)
        .expect_err("target check for another plan should fail");

    std::assert_matches!(
        err,
        ArtifactPromotionPlanError::LinkageMismatch {
            field: "target_check.plan"
        }
    );
}

#[test]
fn artifact_promotion_plan_for_check_rejects_missing_target_execution_lineage() {
    let sample = sample_artifact_promotion_plan();
    let promoted_plan = sample.transform.promoted_plan.clone();
    let plan = artifact_promotion_plan(ArtifactPromotionPlanRequest {
        plan_id: sample.plan_id,
        generated_at: sample.generated_at,
        readiness: sample.readiness,
        artifact_identity_report: sample.artifact_identity_report,
        transform: sample.transform,
        target_execution_lineage: None,
    })
    .expect("sample plan without lineage should still validate");
    let check = sample_check(promoted_plan, sample_matching_inventory());

    let err = validate_artifact_promotion_plan_for_check(&plan, &check)
        .expect_err("target check validation should require execution lineage");

    std::assert_matches!(
        err,
        ArtifactPromotionPlanError::MissingTargetExecutionLineage
    );
}

#[test]
fn artifact_promotion_plan_for_check_rejects_preflight_check_mismatch() {
    let plan = sample_artifact_promotion_plan();
    let mut check = sample_check(
        plan.transform.promoted_plan.clone(),
        sample_matching_inventory(),
    );
    check.report.report_id = "other-report".to_string();

    let err = validate_artifact_promotion_plan_for_check(&plan, &check)
        .expect_err("preflight mismatch should fail");

    std::assert_matches!(
        err,
        ArtifactPromotionPlanError::TargetCheck(
            DeploymentExecutionPreflightError::SourceCheckMismatch {
                field: "safety_report_id",
                ..
            }
        )
    );
}

#[test]
fn artifact_promotion_plan_text_reports_passive_summary() {
    let plan = sample_artifact_promotion_plan();

    let text = artifact_promotion_plan_text(&plan);

    assert!(text.contains("Artifact promotion plan"));
    assert!(text.contains("mode: passive"));
    assert!(text.contains("execution: none"));
    assert!(text.contains("plan_id: artifact-promotion-plan-1"));
    assert!(text.contains("artifact_promotion_plan_digest:"));
    assert!(text.contains("target_execution_lineage: target-execution-lineage-1"));
    assert!(text.contains("readiness_roles: 1"));
    assert!(text.contains("artifact_identity_roles: 1"));
    assert!(text.contains("transform_roles: 1"));
    assert!(text.contains("  Promotion readiness report"));
    assert!(text.contains("  Promotion artifact identity report"));
    assert!(text.contains("  Promotion plan transform"));
}