canic-host 0.95.5

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

#[test]
fn local_inventory_reports_missing_config_as_observation_gap() {
    let temp = TempWorkspace::new("canic-host-local-inventory-missing-config");

    let inventory = collect_local_deployment_inventory(&LocalInventoryRequest {
        deployment_name: "demo".to_string(),
        environment: "local".to_string(),
        artifact_environment: "local".to_string(),
        workspace_root: temp.path().join("workspace"),
        icp_root: temp.path().join("icp"),
        config_path: None,
        observed_at: "2026-05-21T00:00:00Z".to_string(),
    })
    .expect("collect inventory");

    assert_eq!(inventory.inventory_id, "local:local:demo");
    assert!(
        inventory
            .unresolved_observations
            .iter()
            .any(|gap| gap.key == "local_config.fleet_name")
    );
    assert!(
        inventory
            .unresolved_observations
            .iter()
            .any(|gap| gap.key == "local_config.roles")
    );
}

#[test]
fn local_artifact_manifest_collects_roles_and_release_set_hashes() {
    let temp = TempWorkspace::new("canic-host-local-artifact-manifest");
    let workspace_root = temp.path().join("workspace");
    let icp_root = temp.path().join("icp");
    let config_dir = workspace_root.join("fleets");
    fs::create_dir_all(&config_dir).expect("create config dir");
    fs::write(config_dir.join("canic.toml"), SAMPLE_CONFIG).expect("write config");
    write_artifact(&icp_root, "root", b"root-artifact");
    write_artifact(&icp_root, "wasm_store", b"wasm-store-artifact");
    write_artifact(&icp_root, "user_hub", b"user-hub-artifact");
    write_release_set_manifest(&icp_root);

    let manifest = collect_local_role_artifact_manifest(&LocalArtifactManifestRequest {
        environment: "local".to_string(),
        artifact_environment: "local".to_string(),
        workspace_root,
        icp_root,
        config_path: None,
    });

    assert_eq!(manifest.manifest_id, "local:local:demo:artifacts");
    assert_eq!(manifest.role_artifacts.len(), 3);
    assert!(
        manifest
            .role_artifacts
            .iter()
            .all(|artifact| artifact.role != "store")
    );
    let wasm_store = manifest
        .role_artifacts
        .iter()
        .find(|artifact| artifact.role == "wasm_store")
        .expect("wasm_store artifact");
    assert_eq!(wasm_store.source, ArtifactSourceV1::WasmStore);
    assert_eq!(
        wasm_store.observed_wasm_gz_file_sha256_source,
        Some(ArtifactDigestSourceV1::ObservedFileDigest)
    );
    let user_hub = manifest
        .role_artifacts
        .iter()
        .find(|artifact| artifact.role == "user_hub")
        .expect("user_hub artifact");
    assert_eq!(
        user_hub.wasm_gz_sha256.as_deref(),
        Some(RELEASE_SET_USER_HUB_SHA256)
    );
    assert_eq!(
        user_hub.wasm_gz_sha256_source,
        Some(ArtifactDigestSourceV1::ReleaseSetManifest)
    );
    assert_eq!(user_hub.wasm_gz_size_bytes, Some(17));
    assert_eq!(
        user_hub.observed_wasm_gz_file_sha256_source,
        Some(ArtifactDigestSourceV1::ObservedFileDigest)
    );
    assert_eq!(
        user_hub
            .observed_wasm_gz_file_sha256
            .as_ref()
            .map(String::len),
        Some(64)
    );
    let root = manifest
        .role_artifacts
        .iter()
        .find(|artifact| artifact.role == "root")
        .expect("root artifact");
    assert_eq!(root.wasm_gz_sha256, None);
    assert_eq!(root.wasm_gz_sha256_source, None);
    assert!(manifest.unresolved_artifacts.is_empty());
}

#[test]
fn local_artifact_manifest_requires_selected_environment_artifact_root() {
    let temp = TempWorkspace::new("canic-host-local-artifact-manifest-environment-root");
    let workspace_root = temp.path().join("workspace");
    let icp_root = temp.path().join("icp");
    let config_dir = workspace_root.join("fleets");
    fs::create_dir_all(&config_dir).expect("create config dir");
    fs::write(config_dir.join("canic.toml"), SAMPLE_CONFIG).expect("write config");
    write_artifact(&icp_root, "root", b"root-artifact");

    let manifest = collect_local_role_artifact_manifest(&LocalArtifactManifestRequest {
        environment: "ic".to_string(),
        artifact_environment: "ic".to_string(),
        workspace_root,
        icp_root,
        config_path: None,
    });

    assert_eq!(manifest.artifact_root, None);
    assert_eq!(manifest.role_artifacts.len(), 3);
    assert!(manifest.role_artifacts.iter().all(|artifact| {
        artifact
            .wasm_gz_path
            .as_deref()
            .is_some_and(|path| path.contains(".icp/ic/canisters"))
    }));
    assert!(
        manifest
            .unresolved_artifacts
            .iter()
            .any(|gap| gap.key == "local_artifacts.root"
                && gap.description.contains(".icp/ic/canisters"))
    );
    assert!(
        manifest
            .unresolved_artifacts
            .iter()
            .all(|gap| gap.key != "local_artifacts.environment_fallback")
    );
}

#[test]
fn local_deployment_check_rejects_missing_exact_artifact_root() {
    let temp = TempWorkspace::new("canic-host-local-check-missing-artifact-root");
    let workspace_root = temp.path().join("workspace");
    let icp_root = temp.path().join("icp");
    let config_dir = workspace_root.join("fleets");
    fs::create_dir_all(&config_dir).expect("create config dir");
    fs::write(config_dir.join("canic.toml"), SAMPLE_CONFIG).expect("write config");

    let check = check_local_deployment(&LocalDeploymentCheckRequest {
        deployment_name: "demo".to_string(),
        environment: "ic".to_string(),
        artifact_environment: "ic".to_string(),
        workspace_root,
        icp_root,
        config_path: None,
        observed_at: "2026-07-18T00:00:00Z".to_string(),
        runtime_variant: "ic".to_string(),
        build_profile: "release".to_string(),
    })
    .expect("check local deployment");

    assert_eq!(check.plan.role_artifacts.len(), 3);
    assert!(check.inventory.observed_artifacts.is_empty());
    assert!(check.report.hard_failures.iter().any(|finding| {
        finding.code == "artifact_missing" && finding.subject.as_deref() == Some("root")
    }));
}

#[test]
fn local_deployment_check_separates_target_environment_from_artifact_environment() {
    let temp = TempWorkspace::new("canic-host-local-check-explicit-artifact-environment");
    let workspace_root = temp.path().join("workspace");
    let icp_root = temp.path().join("icp");
    let config_dir = workspace_root.join("fleets");
    fs::create_dir_all(&config_dir).expect("create config dir");
    fs::write(config_dir.join("canic.toml"), SAMPLE_CONFIG).expect("write config");
    write_artifact(&icp_root, "root", b"root-artifact");
    write_artifact(&icp_root, "wasm_store", b"wasm-store-artifact");
    write_artifact(&icp_root, "user_hub", b"user-hub-artifact");
    write_release_set_manifest(&icp_root);

    let check = check_local_deployment(&LocalDeploymentCheckRequest {
        deployment_name: "demo".to_string(),
        environment: "staging".to_string(),
        artifact_environment: "local".to_string(),
        workspace_root,
        icp_root,
        config_path: None,
        observed_at: "2026-07-18T00:00:00Z".to_string(),
        runtime_variant: "staging".to_string(),
        build_profile: "release".to_string(),
    })
    .expect("check local deployment");

    assert_eq!(check.plan.deployment_identity.environment, "staging");
    assert_eq!(check.plan.role_artifacts.len(), 3);
    assert_eq!(check.inventory.observed_artifacts.len(), 3);
    assert!(check.report.hard_failures.iter().all(|finding| {
        !matches!(
            finding.code.as_str(),
            "artifact_missing" | "artifact_file_digest_mismatch"
        )
    }));
}

#[test]
fn local_artifact_manifest_records_missing_artifacts_as_gaps() {
    let temp = TempWorkspace::new("canic-host-local-artifact-manifest-missing");
    let workspace_root = temp.path().join("workspace");
    let icp_root = temp.path().join("icp");
    let config_dir = workspace_root.join("fleets");
    fs::create_dir_all(&config_dir).expect("create config dir");
    fs::write(config_dir.join("canic.toml"), SAMPLE_CONFIG).expect("write config");
    write_artifact(&icp_root, "root", b"root-artifact");

    let manifest = collect_local_role_artifact_manifest(&LocalArtifactManifestRequest {
        environment: "local".to_string(),
        artifact_environment: "local".to_string(),
        workspace_root,
        icp_root,
        config_path: None,
    });

    assert!(
        manifest
            .unresolved_artifacts
            .iter()
            .any(|gap| gap.key == "local_artifacts.release_set_manifest")
    );
    assert!(
        manifest
            .unresolved_artifacts
            .iter()
            .any(|gap| gap.key == "local_artifacts.user_hub")
    );
    assert!(
        manifest
            .unresolved_artifacts
            .iter()
            .any(|gap| gap.key == "local_artifacts.wasm_store")
    );
    assert!(
        manifest
            .unresolved_artifacts
            .iter()
            .all(|gap| gap.key != "local_artifacts.store")
    );
}