canic-host 0.70.4

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

#[test]
fn exit_class_serializes_to_snake_case() {
    let encoded = serde_json::to_string(&ExitClassV1::SuccessWithWarnings).expect("serialize");

    assert_eq!(encoded, "\"success_with_warnings\"");
}

#[test]
fn exit_class_precedence_prefers_policy_relevant_failures() {
    assert_eq!(
        combine_exit_classes([
            ExitClassV1::SuccessWithWarnings,
            ExitClassV1::BlockedByPolicy,
            ExitClassV1::EvidenceConflict,
        ]),
        ExitClassV1::EvidenceConflict
    );
    assert!(ExitClassV1::InvalidInput.dominates(ExitClassV1::EvidenceConflict));
    assert!(ExitClassV1::InternalError.dominates(ExitClassV1::ExecutionFailed));
}

#[test]
fn evidence_summary_exit_class_uses_stable_precedence() {
    let mut summary = EvidenceSummaryV1 {
        warnings: vec![EvidenceMessageV1::new(
            "test.warning",
            "warning",
            EvidenceMessageSeverityV1::Warning,
        )],
        blocked_actions: Vec::new(),
        missing_or_stale_evidence: Vec::new(),
        evidence_conflicts: Vec::new(),
    };

    assert_eq!(
        evidence_summary_exit_class(&summary, false),
        ExitClassV1::SuccessWithWarnings
    );

    summary.blocked_actions.push(EvidenceMessageV1::new(
        "test.blocked",
        "blocked",
        EvidenceMessageSeverityV1::Error,
    ));
    assert_eq!(
        evidence_summary_exit_class(&summary, false),
        ExitClassV1::BlockedByPolicy
    );
    assert_eq!(
        evidence_summary_exit_class(&summary, true),
        ExitClassV1::MissingRequiredEvidence
    );

    summary.evidence_conflicts.push(EvidenceMessageV1::new(
        "test.conflict",
        "conflict",
        EvidenceMessageSeverityV1::Error,
    ));
    assert_eq!(
        evidence_summary_exit_class(&summary, true),
        ExitClassV1::EvidenceConflict
    );
}

#[test]
fn schema_refs_record_stability() {
    assert_eq!(
        evidence_envelope_schema(),
        PayloadSchemaRefV1 {
            id: "canic.evidence_envelope.v1".to_string(),
            version: "1".to_string(),
            stability: PayloadSchemaStabilityV1::Stable,
        }
    );
    assert_eq!(
        adoption_report_schema().stability,
        PayloadSchemaStabilityV1::Experimental
    );
    assert_eq!(
        deployment_check_schema().stability,
        PayloadSchemaStabilityV1::Internal
    );
}

#[test]
fn file_input_fingerprint_uses_relative_path_under_root() {
    let root = temp_dir("canic-envelope-relative");
    let input = root.join("evidence").join("input.json");
    fs::create_dir_all(input.parent().expect("input parent")).expect("create parent");
    fs::write(&input, b"{\"ok\":true}").expect("write input");

    let fingerprint =
        file_input_fingerprint("input", &input, &root, None, None).expect("fingerprint");

    fs::remove_dir_all(&root).expect("clean temp dir");
    assert_eq!(fingerprint.path.as_deref(), Some("evidence/input.json"));
    assert_eq!(fingerprint.path_display, InputPathDisplayV1::Relative);
    assert_eq!(fingerprint.size_bytes, Some(11));
    assert!(
        fingerprint
            .sha256
            .as_deref()
            .is_some_and(|hash| hash.len() == 64)
    );
}

#[test]
fn file_input_fingerprint_redacts_absolute_path_outside_root() {
    let root = temp_dir("canic-envelope-root");
    let outside = temp_dir("canic-envelope-outside");
    fs::create_dir_all(&root).expect("create root");
    fs::create_dir_all(&outside).expect("create outside");
    let input = outside.join("secret.json");
    fs::write(&input, b"secret").expect("write input");

    let fingerprint =
        file_input_fingerprint("input", &input, &root, None, None).expect("fingerprint");
    let command_path = command_path_for_root(&input, &root);

    fs::remove_dir_all(&root).expect("clean root");
    fs::remove_dir_all(&outside).expect("clean outside");
    assert_eq!(fingerprint.path, None);
    assert_eq!(
        fingerprint.path_display,
        InputPathDisplayV1::AbsoluteRedacted
    );
    assert_eq!(command_path, "<redacted:absolute-outside-root>");
}

fn temp_dir(name: &str) -> PathBuf {
    let suffix = std::time::SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("system clock before unix epoch")
        .as_nanos();
    std::env::temp_dir().join(format!("{name}-{suffix}"))
}