canic-host 0.68.23

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

use crate::{
    canister_build::{CanisterArtifactBuildOutput, CanisterBuildProfile},
    evidence_envelope::{CommandProvenanceV1, EvidenceTargetKindV1, PayloadSchemaRefV1},
    test_support::temp_dir,
};

#[test]
fn build_provenance_schema_is_stable() {
    assert_eq!(
        build_provenance_schema(),
        PayloadSchemaRefV1::stable("canic.build_provenance.v1", "1")
    );
}

#[test]
fn unknown_source_provenance_is_explicit() {
    let root = temp_dir("canic-build-provenance-no-git");
    fs::create_dir_all(&root).expect("create root");

    let provenance = source_provenance(&root);

    fs::remove_dir_all(&root).expect("remove root");
    assert_eq!(provenance.vcs, SourceVcsV1::Unknown);
    assert_eq!(provenance.dirty_policy, SourceDirtyPolicyV1::Unknown);
}

#[test]
fn source_provenance_requires_selected_git_worktree_root() {
    let temp = temp_dir("canic-build-provenance-parent-git");
    let root = canic_repo_root()
        .join("target")
        .join(temp.file_name().expect("temp path has file name"));
    fs::create_dir_all(&root).expect("create root");

    let provenance = source_provenance(&root);

    fs::remove_dir_all(&root).expect("remove root");
    assert_eq!(provenance.vcs, SourceVcsV1::Unknown);
    assert_eq!(provenance.dirty_policy, SourceDirtyPolicyV1::Unknown);
}

#[test]
fn artifact_provenance_records_wasm_and_gzip_separately() {
    let root = temp_dir("canic-build-provenance-artifacts");
    let artifact_root = root.join(".icp/local/canisters/app");
    fs::create_dir_all(&artifact_root).expect("create artifacts");
    let wasm_path = artifact_root.join("app.wasm");
    let wasm_gz_path = artifact_root.join("app.wasm.gz");
    let did_path = artifact_root.join("app.did");
    fs::write(&wasm_path, b"wasm").expect("write wasm");
    fs::write(&wasm_gz_path, b"gzip").expect("write gzip");

    let request = sample_request(
        &root,
        CanisterArtifactBuildOutput {
            artifact_root,
            wasm_path,
            wasm_gz_path,
            did_path,
            manifest_path: None,
        },
    );
    let artifacts = artifact_provenance(&request).expect("artifact provenance");

    fs::remove_dir_all(&root).expect("remove root");
    assert_eq!(artifacts.len(), 2);
    assert_eq!(artifacts[0].artifact_kind, ArtifactProvenanceKindV1::Wasm);
    assert_eq!(
        artifacts[1].artifact_kind,
        ArtifactProvenanceKindV1::WasmGzip
    );
    assert_ne!(artifacts[0].sha256, artifacts[1].sha256);
}

#[test]
fn build_provenance_envelope_wraps_stable_payload() {
    let root = temp_dir("canic-build-provenance-envelope");
    write_sample_workspace(&root, "demo", "app");
    let output = write_sample_artifacts(&root, "app");
    let request = BuildProvenanceRequest {
        fleet: "demo".to_string(),
        role: "app".to_string(),
        network: "local".to_string(),
        profile: CanisterBuildProfile::Fast,
        workspace_root: root.clone(),
        config_path: root.join("fleets/demo/canic.toml"),
        output,
        command: sample_command(),
        generated_at: "unix:1".to_string(),
        canic_version: "0.0.0-test".to_string(),
    };

    let envelope = build_provenance_envelope(&request).expect("build envelope");
    let payload = serde_json::from_value::<BuildProvenanceV1>(envelope.payload.clone())
        .expect("decode payload");

    fs::remove_dir_all(&root).expect("remove root");
    assert_eq!(envelope.target.kind, EvidenceTargetKindV1::Artifact);
    assert_eq!(envelope.target.fleet.as_deref(), Some("demo"));
    assert_eq!(envelope.target.role.as_deref(), Some("app"));
    assert_eq!(envelope.payload_schema, build_provenance_schema());
    assert_eq!(payload.cargo.package_metadata_fleet, "demo");
    assert_eq!(payload.cargo.package_metadata_role, "app");
    assert!(payload.cargo.cargo_lock_sha256.is_some());
    assert_eq!(payload.artifacts.len(), 2);
}

fn sample_request(root: &Path, output: CanisterArtifactBuildOutput) -> BuildProvenanceRequest {
    BuildProvenanceRequest {
        fleet: "demo".to_string(),
        role: "app".to_string(),
        network: "local".to_string(),
        profile: CanisterBuildProfile::Fast,
        workspace_root: root.to_path_buf(),
        config_path: root.join("fleets/demo/canic.toml"),
        output,
        command: sample_command(),
        generated_at: "unix:1".to_string(),
        canic_version: "0.0.0-test".to_string(),
    }
}

fn sample_command() -> CommandProvenanceV1 {
    CommandProvenanceV1 {
        name: "canic build".to_string(),
        argv_normalized: vec!["canic".to_string(), "build".to_string()],
        argv_redactions: Vec::new(),
        format: "provenance".to_string(),
    }
}

fn write_sample_workspace(root: &Path, fleet: &str, role: &str) {
    let package_dir = root.join("fleets").join(fleet).join(role);
    fs::create_dir_all(package_dir.join("src")).expect("create package");
    fs::write(
        root.join("Cargo.toml"),
        format!(
            r#"[workspace]
members = ["fleets/{fleet}/{role}"]
resolver = "3"
"#
        ),
    )
    .expect("write workspace manifest");
    fs::write(root.join("Cargo.lock"), "# lock\n").expect("write lock");
    fs::write(
        root.join("fleets").join(fleet).join("canic.toml"),
        format!(
            r#"[fleet]
name = "{fleet}"

[roles.{role}]
kind = "canister"
package = "{role}"

[subnets.prime.canisters.{role}]
kind = "service"
"#
        ),
    )
    .expect("write canic config");
    fs::write(
        package_dir.join("Cargo.toml"),
        format!(
            r#"[package]
name = "canister_{fleet}_{role}"
version = "0.0.0"
edition = "2024"

[package.metadata.canic]
fleet = "{fleet}"
role = "{role}"
"#
        ),
    )
    .expect("write package manifest");
    fs::write(package_dir.join("src/lib.rs"), "").expect("write lib");
}

fn write_sample_artifacts(root: &Path, role: &str) -> CanisterArtifactBuildOutput {
    let artifact_root = root.join(".icp/local/canisters").join(role);
    fs::create_dir_all(&artifact_root).expect("create artifacts");
    let wasm_path = artifact_root.join(format!("{role}.wasm"));
    let wasm_gz_path = artifact_root.join(format!("{role}.wasm.gz"));
    let did_path = artifact_root.join(format!("{role}.did"));
    fs::write(&wasm_path, b"wasm").expect("write wasm");
    fs::write(&wasm_gz_path, b"gzip").expect("write gzip");

    CanisterArtifactBuildOutput {
        artifact_root,
        wasm_path,
        wasm_gz_path,
        did_path,
        manifest_path: None,
    }
}

fn canic_repo_root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .ancestors()
        .find(|path| path.join(".git").exists())
        .expect("Canic repository root has .git")
        .to_path_buf()
}