canic-host 0.67.41

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

#[test]
fn root_verification_receipt_validation_accepts_state_transition() {
    let receipt = sample_root_verification_receipt();

    assert!(validate_deployment_root_verification_receipt(&receipt).is_ok());
}

#[test]
fn root_verification_receipt_round_trips_through_json() {
    let receipt = sample_root_verification_receipt();

    assert_json_round_trip(&receipt);
}

#[test]
fn root_verification_receipt_json_shape_is_stable() {
    let receipt = sample_root_verification_receipt();
    let value = serde_json::to_value(&receipt).expect("encode root verification receipt");

    assert_object_keys(
        &value,
        &[
            "schema_version",
            "receipt_id",
            "receipt_digest",
            "deployment_name",
            "network",
            "fleet_template",
            "root_principal",
            "previous_root_verification",
            "new_root_verification",
            "state_transition",
            "source_report_id",
            "source_report_digest",
            "source_report_requested_at",
            "source_report_source",
            "source_report_evidence_status",
            "source_report_current_root_verification",
            "source_report_state_transition",
            "source_root_observation_source",
            "source_observed_root_canister_id",
            "source_check_id",
            "source_check_digest",
            "source_deployment_plan_id",
            "source_deployment_plan_digest",
            "source_inventory_id",
            "source_inventory_digest",
            "verified_at_unix_secs",
            "local_state_path",
            "local_state_digest_before",
            "local_state_digest_after",
            "warnings",
        ],
    );
    assert_eq!(value["schema_version"], DEPLOYMENT_TRUTH_SCHEMA_VERSION);
    assert_eq!(value["deployment_name"], "demo");
    assert_eq!(value["network"], "local");
    assert_eq!(value["fleet_template"], "root");
    assert_eq!(value["root_principal"], "aaaaa-aa");
    assert_eq!(value["previous_root_verification"], "NotVerified");
    assert_eq!(value["new_root_verification"], "Verified");
    assert_eq!(value["state_transition"], "PromotedNotVerifiedToVerified");
    assert_eq!(value["source_report_requested_at"], "2026-05-27T00:00:00Z");
    assert_eq!(value["source_report_source"], "DeploymentTruthCheck");
    assert_eq!(value["source_report_evidence_status"], "EvidenceSatisfied");
    assert_eq!(
        value["source_report_current_root_verification"],
        "NotVerified"
    );
    assert_eq!(
        value["source_report_state_transition"],
        "WouldPromoteNotVerifiedToVerified"
    );
    assert_eq!(value["source_root_observation_source"], "IcpCanisterStatus");
    assert_eq!(value["source_observed_root_canister_id"], "aaaaa-aa");
    assert_eq!(value["source_check_id"], "check-1");
    assert_eq!(value["source_deployment_plan_id"], "plan-local-root");
    assert_eq!(value["source_inventory_id"], "inventory-1");
    assert_eq!(value["receipt_digest"].as_str().expect("digest").len(), 64);
}

#[test]
fn root_verification_receipt_text_distinguishes_local_state_write_from_canister_execution() {
    let receipt = sample_root_verification_receipt();
    let text = deployment_root_verification_receipt_text(&receipt);

    assert!(text.contains("mode: local-state-write"));
    assert!(text.contains("canister_execution: none"));
    assert!(text.contains("local_state_write: recorded"));
    assert!(text.contains("source_report_requested_at: 2026-05-27T00:00:00Z"));
    assert!(text.contains("source_report_source: DeploymentTruthCheck"));
    assert!(text.contains("source_report_evidence_status: EvidenceSatisfied"));
    assert!(text.contains("source_report_current_root_verification: NotVerified"));
    assert!(text.contains("source_report_state_transition: WouldPromoteNotVerifiedToVerified"));
    assert!(text.contains("source_root_observation_source: IcpCanisterStatus"));
    assert!(text.contains("source_observed_root_canister_id: aaaaa-aa"));
    assert!(!text.lines().any(|line| line == "execution: none"));
}

#[test]
fn root_verification_receipt_validation_rejects_digest_drift() {
    let mut receipt = sample_root_verification_receipt();
    receipt.network = "other-network".to_string();

    let err = validate_deployment_root_verification_receipt(&receipt)
        .expect_err("receipt digest drift should fail");

    assert_eq!(
        err,
        DeploymentRootVerificationReceiptError::DigestMismatch {
            field: "receipt_digest"
        }
    );
}

#[test]
fn root_verification_receipt_validation_rejects_bad_digest_shape() {
    let mut receipt = sample_root_verification_receipt();
    receipt.source_check_digest = "NOT-A-DIGEST".to_string();

    let err = validate_deployment_root_verification_receipt(&receipt)
        .expect_err("malformed source check digest should fail");

    assert_eq!(
        err,
        DeploymentRootVerificationReceiptError::InvalidSha256Digest {
            field: "source_check_digest"
        }
    );
}

#[test]
fn root_verification_receipt_validation_rejects_unsatisfied_source_report_status() {
    let mut receipt = sample_root_verification_receipt();
    receipt.source_report_evidence_status =
        DeploymentRootVerificationEvidenceStatusV1::VerificationFailed;
    receipt.receipt_digest = deployment_root_verification_receipt_digest(&receipt);

    let err = validate_deployment_root_verification_receipt(&receipt)
        .expect_err("receipt source report status drift should fail");

    assert_eq!(
        err,
        DeploymentRootVerificationReceiptError::SourceEvidenceMismatch
    );
}

#[test]
fn root_verification_receipt_validation_rejects_missing_source_report_timestamp() {
    let mut receipt = sample_root_verification_receipt();
    receipt.source_report_requested_at.clear();
    receipt.receipt_digest = deployment_root_verification_receipt_digest(&receipt);

    let err = validate_deployment_root_verification_receipt(&receipt)
        .expect_err("missing source report timestamp should fail");

    assert_eq!(
        err,
        DeploymentRootVerificationReceiptError::MissingRequiredField {
            field: "source_report_requested_at"
        }
    );
}

#[test]
fn root_verification_receipt_validation_rejects_bad_source_report_timestamp() {
    let mut receipt = sample_root_verification_receipt();
    receipt.source_report_requested_at = "not-a-timestamp".to_string();
    receipt.receipt_digest = deployment_root_verification_receipt_digest(&receipt);

    let err = validate_deployment_root_verification_receipt(&receipt)
        .expect_err("bad source report timestamp should fail");

    assert_eq!(
        err,
        DeploymentRootVerificationReceiptError::InvalidTimestampLabel {
            field: "source_report_requested_at"
        }
    );
}

#[test]
fn root_verification_receipt_validation_accepts_unix_source_report_timestamp() {
    let mut receipt = sample_root_verification_receipt();
    receipt.source_report_requested_at = "unix:100".to_string();
    receipt.receipt_digest = deployment_root_verification_receipt_digest(&receipt);

    assert!(validate_deployment_root_verification_receipt(&receipt).is_ok());
}

#[test]
fn root_verification_receipt_validation_rejects_unix_source_timestamp_mismatch() {
    let mut receipt = sample_root_verification_receipt();
    receipt.source_report_requested_at = "unix:101".to_string();
    receipt.receipt_digest = deployment_root_verification_receipt_digest(&receipt);

    let err = validate_deployment_root_verification_receipt(&receipt)
        .expect_err("unix source report timestamp mismatch should fail");

    assert_eq!(
        err,
        DeploymentRootVerificationReceiptError::SourceEvidenceMismatch
    );
}

#[test]
fn root_verification_receipt_validation_rejects_wrong_source_report_transition() {
    let mut receipt = sample_root_verification_receipt();
    receipt.source_report_state_transition =
        DeploymentRootVerificationStateTransitionV1::NoStateChange;
    receipt.receipt_digest = deployment_root_verification_receipt_digest(&receipt);

    let err = validate_deployment_root_verification_receipt(&receipt)
        .expect_err("receipt source report transition mismatch should fail");

    assert_eq!(
        err,
        DeploymentRootVerificationReceiptError::SourceEvidenceMismatch
    );
}

#[test]
fn root_verification_receipt_validation_rejects_source_report_current_state_mismatch() {
    let mut receipt = sample_root_verification_receipt();
    receipt.source_report_current_root_verification = DeploymentRootVerificationStateV1::Verified;
    receipt.receipt_digest = deployment_root_verification_receipt_digest(&receipt);

    let err = validate_deployment_root_verification_receipt(&receipt)
        .expect_err("receipt source report current state mismatch should fail");

    assert_eq!(
        err,
        DeploymentRootVerificationReceiptError::SourceEvidenceMismatch
    );
}

#[test]
fn root_verification_receipt_validation_rejects_local_state_root_source() {
    let mut receipt = sample_root_verification_receipt();
    receipt.source_root_observation_source =
        DeploymentRootObservationSourceV1::LocalDeploymentState;
    receipt.receipt_digest = deployment_root_verification_receipt_digest(&receipt);

    let err = validate_deployment_root_verification_receipt(&receipt)
        .expect_err("receipt source root observation source drift should fail");

    assert_eq!(
        err,
        DeploymentRootVerificationReceiptError::SourceEvidenceMismatch
    );
}

#[test]
fn root_verification_receipt_validation_rejects_observed_root_canister_id_mismatch() {
    let mut receipt = sample_root_verification_receipt();
    receipt.source_observed_root_canister_id = "other-root".to_string();
    receipt.receipt_digest = deployment_root_verification_receipt_digest(&receipt);

    let err = validate_deployment_root_verification_receipt(&receipt)
        .expect_err("receipt source observed root canister id mismatch should fail");

    assert_eq!(
        err,
        DeploymentRootVerificationReceiptError::SourceEvidenceMismatch
    );
}

#[test]
fn root_verification_receipt_validation_rejects_bad_transition() {
    let mut receipt = sample_root_verification_receipt();
    receipt.state_transition = DeploymentRootVerificationStateTransitionV1::NoStateChange;
    receipt.receipt_digest = deployment_root_verification_receipt_digest(&receipt);

    let err = validate_deployment_root_verification_receipt(&receipt)
        .expect_err("invalid transition should fail");

    assert_eq!(
        err,
        DeploymentRootVerificationReceiptError::StateTransitionMismatch
    );
}

#[test]
fn root_verification_receipt_validation_rejects_noop_digest_change() {
    let mut receipt = sample_root_verification_receipt();
    receipt.previous_root_verification = DeploymentRootVerificationStateV1::Verified;
    receipt.state_transition = DeploymentRootVerificationStateTransitionV1::NoStateChange;
    receipt.source_report_current_root_verification = DeploymentRootVerificationStateV1::Verified;
    receipt.source_report_state_transition =
        DeploymentRootVerificationStateTransitionV1::NoStateChange;
    receipt.receipt_digest = deployment_root_verification_receipt_digest(&receipt);

    let err = validate_deployment_root_verification_receipt(&receipt)
        .expect_err("no-op receipt with changed state digest should fail");

    assert_eq!(
        err,
        DeploymentRootVerificationReceiptError::LocalStateDigestMismatch
    );
}

#[test]
fn root_verification_receipt_validation_rejects_promotion_without_digest_change() {
    let mut receipt = sample_root_verification_receipt();
    receipt.local_state_digest_after = receipt.local_state_digest_before.clone();
    receipt.receipt_digest = deployment_root_verification_receipt_digest(&receipt);

    let err = validate_deployment_root_verification_receipt(&receipt)
        .expect_err("promotion receipt without state digest change should fail");

    assert_eq!(
        err,
        DeploymentRootVerificationReceiptError::LocalStateDigestMismatch
    );
}