canic-cli 0.111.0

Operator CLI for Canic fleet setup, builds, evidence, catalog, backup, and restore workflows
Documentation
use super::*;
use crate::fleet::{FleetCommandError, json_error, tests::cycle_quantity_report};
use canic_host::fleet_ensure::{
    model::FleetEnsureSuccessorReviewReason, workflow::EnsureWorkflowError,
};
use std::{ffi::OsString, path::PathBuf};

fn options() -> EnsureOptions {
    let mut options =
        EnsureOptions::parse(["ensure", "demo", "--json"].map(OsString::from)).unwrap();
    options.desired = PathBuf::from("fleets/operator's desired.toml");
    options.source = PathBuf::from(".tools/operator policy.toml");
    options.seed = PathBuf::from(".tools/operator estate.toml");
    options.identity = Some("operator with spaces".into());
    options
}

#[test]
fn next_actions_preserve_exact_arguments_and_distinguish_new_approval_from_resume() {
    let mut report = cycle_quantity_report("rrkah-fqaaa-aaaaa-aaaaq-cai");
    report.plan.plan_sha256 = "ab".repeat(32);
    report.terminal = false;
    let mut options = options();
    let result = ensure(&report, &options, false);
    let next = result.next_action.unwrap();
    assert_eq!(next.kind, ActionKind::Apply);
    assert!(next.requires_approval);
    assert!(
        next.arguments
            .contains(&"fleets/operator's desired.toml".into())
    );
    assert!(next.arguments.contains(&"operator with spaces".into()));
    assert_eq!(next.arguments.last(), Some(&report.plan.plan_sha256));
    assert_valid_ensure_arguments(&next.arguments);
    options.apply = Some(report.plan.plan_sha256.clone());
    let next = ensure(&report, &options, false).next_action.unwrap();
    assert_eq!(next.kind, ActionKind::Resume);
    assert!(!next.requires_approval);
    assert_valid_ensure_arguments(&next.arguments);
}

fn assert_valid_ensure_arguments(arguments: &[String]) -> EnsureOptions {
    use crate::cli::globals::{DISPATCH_ARGS, apply_global_environment, apply_global_icp};
    let matches = crate::cli::clap::parse_matches(
        crate::cli::top_level_command(),
        arguments.iter().map(OsString::from),
    )
    .unwrap();
    let (command, subcommand) = matches.subcommand().unwrap();
    let mut tail = subcommand
        .get_many::<OsString>(DISPATCH_ARGS)
        .unwrap()
        .cloned()
        .collect::<Vec<_>>();
    apply_global_icp(
        command,
        &mut tail,
        matches.get_one::<String>("icp").cloned(),
    );
    apply_global_environment(
        command,
        &mut tail,
        matches.get_one::<String>("environment").cloned(),
    );
    let parsed = EnsureOptions::parse(tail).unwrap();
    assert!(parsed.json);
    assert_eq!(
        parsed.desired,
        PathBuf::from("fleets/operator's desired.toml")
    );
    assert_eq!(parsed.identity.as_deref(), Some("operator with spaces"));
    assert_eq!(parsed.source, options().source);
    assert_eq!(parsed.seed, options().seed);
    parsed
}

#[test]
fn intermediate_completion_requests_review_and_only_full_terminal_completes_fleet() {
    let mut report = cycle_quantity_report("rrkah-fqaaa-aaaaa-aaaaq-cai");
    report.terminal = true;
    let options = options();
    for scope in [
        FleetEnsurePlanScope::InfrastructureBootstrap,
        FleetEnsurePlanScope::RootReinstallPrerequisite,
        FleetEnsurePlanScope::RootStartPrerequisite,
    ] {
        report.plan.scope = scope;
        let result = ensure(&report, &options, true);
        assert!(result.phase_completed);
        assert!(!result.fleet_completed);
        let next = result.next_action.unwrap();
        assert_eq!(next.kind, ActionKind::Review);
        assert!(!next.requires_approval);
        assert!(next.arguments.contains(&"--reinstall".into()));
        assert_valid_ensure_arguments(&next.arguments);
    }
    report.plan.scope = FleetEnsurePlanScope::Full;
    let result = ensure(&report, &options, true);
    assert!(result.fleet_completed);
    assert!(result.next_action.is_none());
}

#[test]
fn completed_import_advances_to_review_without_reusing_its_approval() {
    let options = options();
    let digest = "ab".repeat(32);
    let result = import(
        digest.clone(),
        true,
        true,
        reinstall_review_command(&options, "local"),
        true,
    );
    assert_eq!(result.review_sha256, Some(digest));
    assert!(result.phase_completed);
    assert!(!result.fleet_completed);
    let next = result.next_action.unwrap();
    assert_eq!(next.kind, ActionKind::Review);
    assert_valid_ensure_arguments(&next.arguments);
    assert!(!next.arguments.contains(&"--apply".into()));
}

#[test]
fn typed_successor_pause_reports_review_without_reading_private_journals() {
    let error =
        FleetCommandError::Workflow(Box::new(EnsureWorkflowError::SuccessorReviewRequired {
            reason: FleetEnsureSuccessorReviewReason::BudgetExceeded,
            review: None,
        }));
    let mut options = options();
    options.reinstall = true;
    options.apply = Some("ab".repeat(32));
    let FleetCommandError::JsonReported { report, .. } = json_error(error, Some(&options)) else {
        panic!("JSON error")
    };
    let value: serde_json::Value = serde_json::from_str(&report).unwrap();
    assert_eq!(value["code"], "successor_review_required");
    assert_eq!(value["next_action"]["kind"], "review");
    assert_eq!(value["next_action"]["requires_approval"], false);
    let arguments = value["next_action"]["arguments"]
        .as_array()
        .unwrap()
        .iter()
        .map(|value| value.as_str().unwrap().to_owned())
        .collect::<Vec<_>>();
    assert!(assert_valid_ensure_arguments(&arguments).reinstall);
    assert!(
        value["next_action"]["arguments"]
            .as_array()
            .unwrap()
            .iter()
            .all(|arg| arg != "--apply")
    );
}

#[test]
fn import_apply_and_resume_keep_nondefault_selection() {
    let options = options();
    for applying in [false, true] {
        let result = import(
            "ab".repeat(32),
            false,
            applying,
            ensure_command(&options, "local"),
            true,
        );
        let next = result.next_action.unwrap();
        assert_eq!(
            next.kind,
            if applying {
                ActionKind::Resume
            } else {
                ActionKind::Apply
            }
        );
        assert_valid_ensure_arguments(&next.arguments);
    }
}

#[test]
fn reinstall_roundtrip_keeps_inputs_through_infrastructure_import_and_successor() {
    let mut report = cycle_quantity_report("rrkah-fqaaa-aaaaa-aaaaq-cai");
    report.plan.scope = FleetEnsurePlanScope::InfrastructureBootstrap;
    report.plan.plan_sha256 = "ab".repeat(32);
    report.terminal = false;
    let mut selected = options();
    selected.reinstall = true;
    let next = ensure(&report, &selected, true).next_action.unwrap();
    let applying = assert_valid_ensure_arguments(&next.arguments);
    assert!(!applying.reinstall);
    assert_eq!(applying.apply.as_ref(), Some(&report.plan.plan_sha256));
    let retry = ensure(&report, &applying, true).next_action.unwrap();
    assert_eq!(retry.kind, ActionKind::Resume);
    let resumed = assert_valid_ensure_arguments(&retry.arguments);
    report.terminal = true;
    let next = ensure(&report, &resumed, true).next_action.unwrap();
    let reviewing = assert_valid_ensure_arguments(&next.arguments);
    assert!(reviewing.reinstall);
    assert!(reviewing.apply.is_none());
    let next = import(
        "cd".repeat(32),
        false,
        false,
        ensure_command(&reviewing, "local"),
        true,
    )
    .next_action
    .unwrap();
    let importing = assert_valid_ensure_arguments(&next.arguments);
    let next = import(
        "cd".repeat(32),
        true,
        true,
        reinstall_review_command(&importing, "local"),
        true,
    )
    .next_action
    .unwrap();
    let successor = assert_valid_ensure_arguments(&next.arguments);
    assert!(successor.reinstall);
    report.plan.scope = FleetEnsurePlanScope::Full;
    assert!(ensure(&report, &successor, true).next_action.is_none());
}