canic-host 0.69.4

Host-side build, install, deployment, and fleet-template library for Canic workspaces
Documentation
use super::*;
use crate::test_support::temp_dir;
use std::{collections::BTreeMap, fmt::Write as _, fs, path::Path};

#[test]
fn defaults_local_gateway_port_without_network_config() {
    let source = "canisters: []\n";

    assert_eq!(
        local_gateway_port_from_yaml(source),
        DEFAULT_LOCAL_GATEWAY_PORT
    );
}

#[test]
fn reads_local_gateway_port_from_network_config() {
    let source = "networks:\n  - name: local\n    mode: managed\n    gateway:\n      bind: 127.0.0.1\n      port: 8001\n";

    assert_eq!(local_gateway_port_from_yaml(source), 8001);
}

#[test]
fn ignores_nested_networks_keys_when_reading_local_gateway_port() {
    let source = "canisters:\n  - name: root\n    metadata:\n      networks:\n        - local\n\nnetworks:\n  - name: local\n    mode: managed\n    gateway:\n      bind: 127.0.0.1\n      port: 8010\n";

    assert_eq!(local_gateway_port_from_yaml(source), 8010);
}

#[test]
fn inspects_icp_yaml_without_mutating_it() {
    let root = temp_dir("canic-icp-read-only");
    let config = root.join("fleets/toko/canic.toml");
    fs::create_dir_all(config.parent().expect("config parent")).expect("create config parent");
    fs::write(
        &config,
        r#"
[fleet]
name = "toko"

[roles.root]
kind = "root"
package = "root"

[roles.app]
kind = "canister"
package = "app"

[subnets.prime.canisters.root]
kind = "root"

[subnets.prime.canisters.app]
kind = "service"
"#,
    )
    .expect("write config");
    let source = r"
canisters:
  - name: root

networks:
  - name: local
    mode: managed
    gateway:
      port: 8010

environments:
  - name: toko
    network: local
    canisters: [root]
";
    fs::write(root.join("icp.yaml"), source).expect("write icp yaml");

    let report = inspect_canic_icp_yaml_from_root(&root, Some("toko")).expect("inspect");

    assert_eq!(report.canisters, vec!["root", "app"]);
    assert_eq!(report.environments, vec!["toko"]);
    assert_eq!(report.missing_canisters, vec!["app"]);
    assert!(report.missing_environments.is_empty());
    assert!(report.local_network_present);
    assert!(!report.is_ready());
    assert_eq!(
        fs::read_to_string(root.join("icp.yaml")).expect("read icp yaml"),
        source
    );
    fs::remove_dir_all(root).expect("clean temp dir");
}

#[test]
fn reports_missing_icp_yaml_as_incomplete() {
    let root = temp_dir("canic-icp-missing-yaml");
    let config = root.join("fleets/toko/canic.toml");
    fs::create_dir_all(config.parent().expect("config parent")).expect("create config parent");
    fs::write(
        &config,
        r#"
[fleet]
name = "toko"

[roles.root]
kind = "root"
package = "root"

[subnets.prime.canisters.root]
kind = "root"
"#,
    )
    .expect("write config");

    let report = inspect_canic_icp_yaml_from_root(&root, Some("toko")).expect("inspect");

    assert!(!report.icp_yaml_present);
    assert_eq!(report.missing_canisters, vec!["root"]);
    assert_eq!(report.missing_environments, vec!["toko"]);
    assert!(!report.local_network_present);
    assert!(!report.is_ready());
    fs::remove_dir_all(root).expect("clean temp dir");
}

#[test]
fn discovers_root_fleet_configs_for_icp_inspection() {
    let root = temp_dir("canic-icp-inspect-root-fleets");
    let config = root.join("fleets/toko/canic.toml");
    fs::create_dir_all(config.parent().expect("config parent")).expect("create config parent");
    fs::write(
        &config,
        r#"
[fleet]
name = "toko"

[roles.root]
kind = "root"
package = "root"

[roles.app]
kind = "canister"
package = "app"

[subnets.prime.canisters.root]
kind = "root"

[subnets.prime.canisters.app]
kind = "service"
"#,
    )
    .expect("write config");

    let spec = discover_project_spec(&root, Some("toko")).expect("discover spec");

    assert_eq!(spec.canisters, vec!["root", "app"]);
    assert_eq!(
        spec.environments,
        BTreeMap::from([(
            "toko".to_string(),
            vec!["root".to_string(), "app".to_string()]
        )])
    );
    fs::remove_dir_all(root).expect("clean temp dir");
}

#[test]
fn fleet_filter_limits_inspected_project_spec() {
    let root = temp_dir("canic-icp-inspect-fleet-filter");
    write_test_config(
        &root.join("fleets/demo/canic.toml"),
        "demo",
        &["root", "app"],
    );
    write_test_config(
        &root.join("fleets/test/canic.toml"),
        "test",
        &["root", "scale"],
    );

    let spec = discover_project_spec(&root, Some("test")).expect("discover spec");

    assert_eq!(spec.canisters, vec!["root", "scale"]);
    assert_eq!(
        spec.environments,
        BTreeMap::from([(
            "test".to_string(),
            vec!["root".to_string(), "scale".to_string()]
        )])
    );
    fs::remove_dir_all(root).expect("clean temp dir");
}

#[test]
fn nested_commands_discover_outer_project_root_with_fleets() {
    let root = temp_dir("canic-icp-root-nested");
    let config = root.join("fleets/toko/canic.toml");
    let nested = root.join("backend/src");
    fs::create_dir_all(&nested).expect("create nested dir");
    fs::create_dir_all(config.parent().expect("config parent")).expect("create config parent");
    fs::write(root.join("icp.yaml"), "").expect("write icp config");
    fs::write(&config, "[fleet]\nname = \"toko\"\n").expect("write config");

    let icp_root = crate::install_root::discover_canic_project_root_from(&nested)
        .expect("discover project root")
        .expect("project root is present");

    assert_eq!(icp_root, root.canonicalize().expect("canonical root"));
    fs::remove_dir_all(root).expect("clean temp dir");
}

#[test]
fn outer_project_root_wins_over_nested_fleets() {
    let root = temp_dir("canic-icp-root-outer-wins");
    let outer_config = root.join("fleets/toko/canic.toml");
    let nested_config = root.join("services/fleets/toko/canic.toml");
    let nested = root.join("services/src");
    fs::create_dir_all(outer_config.parent().expect("outer config parent"))
        .expect("create outer config parent");
    fs::create_dir_all(nested_config.parent().expect("nested config parent"))
        .expect("create nested config parent");
    fs::create_dir_all(&nested).expect("create nested dir");
    fs::write(root.join("icp.yaml"), "").expect("write icp config");
    fs::write(&outer_config, "[fleet]\nname = \"toko\"\n").expect("write outer config");
    fs::write(&nested_config, "[fleet]\nname = \"toko\"\n").expect("write nested config");

    let icp_root = crate::install_root::discover_canic_project_root_from(&nested)
        .expect("discover project root")
        .expect("project root is present");

    assert_eq!(icp_root, root.canonicalize().expect("canonical root"));
    fs::remove_dir_all(root).expect("clean temp dir");
}

#[test]
fn icp_inspection_rejects_missing_fleet_configs() {
    let root = temp_dir("canic-icp-inspect-missing");
    fs::create_dir_all(&root).expect("create root");

    let err = discover_project_spec(&root, None).expect_err("missing configs should fail");
    let message = err.to_string();

    assert!(message.contains("no Canic fleet configs found under"));
    assert!(message.contains("fleets/<fleet>/canic.toml"));
    fs::remove_dir_all(root).expect("clean temp dir");
}

fn write_test_config(path: &Path, fleet: &str, roles: &[&str]) {
    fs::create_dir_all(path.parent().expect("config parent")).expect("create config parent");
    let mut source = format!("[fleet]\nname = \"{fleet}\"\n");
    for role in roles {
        let kind = if *role == "root" { "root" } else { "canister" };
        write!(
            source,
            "\n[roles.{role}]\nkind = \"{kind}\"\npackage = \"{role}\"\n"
        )
        .expect("write role declaration");
    }
    for role in roles {
        let kind = if *role == "root" { "root" } else { "service" };
        write!(
            source,
            "\n[subnets.prime.canisters.{role}]\nkind = \"{kind}\"\n"
        )
        .expect("write config source");
    }
    fs::write(path, source).expect("write config");
}