icydb-cli 0.179.3

Developer CLI tools for IcyDB
//! Module: CLI config tests.
//! Responsibility: exercise config command inputs, reports, and endpoint surface gates.
//! Does not own: ICP process command construction or observability decoding.
//! Boundary: test-only assertions over config helpers and generated endpoint constants.

use clap::Parser;

use crate::{
    cli::{CliArgs, CliCommand, ConfigCommand},
    config::{
        ConfigSurface, FIXTURES_LOAD_ENDPOINT, METRICS_ENDPOINT, METRICS_RESET_ENDPOINT,
        SCHEMA_CHECK_ENDPOINT, SCHEMA_ENDPOINT, SNAPSHOT_ENDPOINT, SQL_DDL_ENDPOINT,
        SQL_QUERY_ENDPOINT, init_config,
        test_support::{
            config_surface_enabled_for_resolved, config_sync_issues,
            configured_endpoint_enabled_for_resolved, disabled_config_surface_message,
            render_config_report,
        },
    },
};

#[test]
fn config_init_writes_default_config_at_workspace_root() {
    let root =
        std::env::temp_dir().join(format!("icydb-cli-config-init-test-{}", std::process::id()));
    let workspace = root.join("workspace");
    let canister = workspace.join("canisters").join("demo").join("rpg");
    std::fs::create_dir_all(canister.as_path()).expect("test directory should be created");
    std::fs::write(workspace.join("Cargo.toml"), "[workspace]\n")
        .expect("workspace manifest should be written");

    let args = CliArgs::try_parse_from([
        "icydb",
        "config",
        "init",
        "--start-dir",
        canister.to_str().expect("test path should be UTF-8"),
        "--canister",
        "demo_rpg",
        "--ddl",
        "--fixtures",
        "--metrics",
        "--metrics-reset",
        "--snapshot",
        "--schema",
    ])
    .expect("config init args should parse");
    let CliCommand::Config(ConfigCommand::Init(args)) = args.into_command() else {
        panic!("expected config init command");
    };

    init_config(args).expect("config init should succeed");

    let config = std::fs::read_to_string(workspace.join("icydb.toml"))
        .expect("config file should be written");
    assert_eq!(
        config,
        "[canisters.demo_rpg.sql]\nreadonly = true\nddl = true\nfixtures = true\n\n[canisters.demo_rpg.metrics]\nenabled = true\nreset = true\n\n[canisters.demo_rpg.snapshot]\nenabled = true\n\n[canisters.demo_rpg.schema]\nenabled = true\n"
    );

    std::fs::remove_dir_all(root).expect("test directory should be removed");
}

#[test]
fn config_report_marks_canister_settings_against_icp_environment() {
    let root = std::env::temp_dir().join(format!(
        "icydb-cli-config-report-test-{}",
        std::process::id()
    ));
    let canister = root.join("canisters").join("demo").join("rpg");
    std::fs::create_dir_all(canister.as_path()).expect("test directory should be created");
    let config_path = root.join("canisters").join("demo").join("icydb.toml");
    std::fs::write(
        config_path.as_path(),
        r"
            [canisters.demo_rpg.sql]
            readonly = true
            ddl = true
            fixtures = true

            [canisters.demo_rpg.metrics]
            enabled = true
            reset = true

            [canisters.demo_rpg.snapshot]
            enabled = true

            [canisters.demo_rpg.schema]
            enabled = true
        ",
    )
    .expect("config should be written");
    let resolved = icydb_config_build::load_resolved_icydb_toml(canister.as_path(), &["demo_rpg"])
        .expect("config should resolve");

    let report = render_config_report(
        canister.as_path(),
        Some("demo"),
        &[String::from("demo_rpg")],
        &resolved,
    );

    assert!(report.contains(
        "  canister   SQL surfaces              metrics          snapshot   schema    ICP environment\n"
    ));
    assert!(
        report
            .lines()
            .any(|line| line.starts_with("  --------   -----------------------"))
    );
    assert!(report.contains(
        "  demo_rpg   readonly, ddl, fixtures   enabled, reset   enabled    enabled   ok\n"
    ));
    std::fs::remove_dir_all(root).expect("test directory should be removed");
}

#[test]
fn config_check_reports_mismatched_canister_settings() {
    let root = std::env::temp_dir().join(format!(
        "icydb-cli-config-check-test-{}",
        std::process::id()
    ));
    let canister = root.join("canisters").join("demo").join("rpg");
    std::fs::create_dir_all(canister.as_path()).expect("test directory should be created");
    std::fs::write(
        root.join("canisters").join("demo").join("icydb.toml"),
        r"
            [canisters.missing_rpg.sql]
            readonly = true
        ",
    )
    .expect("config should be written");
    let resolved = icydb_config_build::load_resolved_icydb_toml(canister.as_path(), &[])
        .expect("config should resolve without known canister validation");

    let issues = config_sync_issues(Some("test"), &[String::from("demo_rpg")], &resolved);

    assert!(
        issues
            .iter()
            .any(|issue| issue.contains("canisters.missing_rpg")),
        "missing configured canister should be reported: {issues:?}",
    );
    std::fs::remove_dir_all(root).expect("test directory should be removed");
}

#[test]
fn config_surface_helper_tracks_generated_endpoint_switches() {
    let root = std::env::temp_dir().join(format!(
        "icydb-cli-config-surface-test-{}",
        std::process::id()
    ));
    let canister = root.join("canisters").join("demo").join("rpg");
    std::fs::create_dir_all(canister.as_path()).expect("test directory should be created");
    std::fs::write(
        root.join("icydb.toml"),
        r"
            [canisters.demo_rpg.sql]
            readonly = true
            ddl = false
            fixtures = true

            [canisters.demo_rpg.metrics]
            enabled = true
            reset = false

            [canisters.demo_rpg.snapshot]
            enabled = true

            [canisters.demo_rpg.schema]
            enabled = true
        ",
    )
    .expect("config should be written");
    let resolved = icydb_config_build::load_resolved_icydb_toml(canister.as_path(), &[])
        .expect("config should resolve");

    assert!(config_surface_enabled_for_resolved(
        &resolved,
        "demo_rpg",
        ConfigSurface::SqlReadonly,
    ));
    assert!(!config_surface_enabled_for_resolved(
        &resolved,
        "demo_rpg",
        ConfigSurface::SqlDdl,
    ));
    assert!(config_surface_enabled_for_resolved(
        &resolved,
        "demo_rpg",
        ConfigSurface::SqlFixtures,
    ));
    assert!(config_surface_enabled_for_resolved(
        &resolved,
        "demo_rpg",
        ConfigSurface::Metrics,
    ));
    assert!(!config_surface_enabled_for_resolved(
        &resolved,
        "demo_rpg",
        ConfigSurface::MetricsReset,
    ));
    assert!(config_surface_enabled_for_resolved(
        &resolved,
        "demo_rpg",
        ConfigSurface::Snapshot,
    ));
    assert!(config_surface_enabled_for_resolved(
        &resolved,
        "demo_rpg",
        ConfigSurface::Schema,
    ));
    assert!(!config_surface_enabled_for_resolved(
        &resolved,
        "missing_rpg",
        ConfigSurface::Snapshot,
    ));
    std::fs::remove_dir_all(root).expect("test directory should be removed");
}

#[test]
fn configured_endpoint_helper_tracks_endpoint_surface_pairs() {
    let root = std::env::temp_dir().join(format!(
        "icydb-cli-configured-endpoint-test-{}",
        std::process::id()
    ));
    let canister = root.join("canisters").join("demo").join("rpg");
    std::fs::create_dir_all(canister.as_path()).expect("test directory should be created");
    std::fs::write(
        root.join("icydb.toml"),
        r"
            [canisters.demo_rpg.sql]
            readonly = true
            ddl = false
            fixtures = true

            [canisters.demo_rpg.metrics]
            enabled = true
            reset = false

            [canisters.demo_rpg.snapshot]
            enabled = true

            [canisters.demo_rpg.schema]
            enabled = true
        ",
    )
    .expect("config should be written");
    let resolved = icydb_config_build::load_resolved_icydb_toml(canister.as_path(), &[])
        .expect("config should resolve");

    assert!(configured_endpoint_enabled_for_resolved(
        &resolved,
        "demo_rpg",
        SQL_QUERY_ENDPOINT,
    ));
    assert!(!configured_endpoint_enabled_for_resolved(
        &resolved,
        "demo_rpg",
        SQL_DDL_ENDPOINT,
    ));
    assert!(configured_endpoint_enabled_for_resolved(
        &resolved,
        "demo_rpg",
        FIXTURES_LOAD_ENDPOINT,
    ));
    assert!(configured_endpoint_enabled_for_resolved(
        &resolved,
        "demo_rpg",
        METRICS_ENDPOINT,
    ));
    assert!(!configured_endpoint_enabled_for_resolved(
        &resolved,
        "demo_rpg",
        METRICS_RESET_ENDPOINT,
    ));
    assert!(configured_endpoint_enabled_for_resolved(
        &resolved,
        "demo_rpg",
        SNAPSHOT_ENDPOINT,
    ));
    assert!(configured_endpoint_enabled_for_resolved(
        &resolved,
        "demo_rpg",
        SCHEMA_ENDPOINT,
    ));
    std::fs::remove_dir_all(root).expect("test directory should be removed");
}

#[test]
fn disabled_config_surface_message_names_surface_key_and_rebuild_step() {
    let root = std::env::temp_dir().join(format!(
        "icydb-cli-config-diagnostic-test-{}",
        std::process::id()
    ));
    let canister = root.join("canisters").join("demo").join("rpg");
    std::fs::create_dir_all(canister.as_path()).expect("test directory should be created");
    let config_path = root.join("icydb.toml");
    std::fs::write(
        config_path.as_path(),
        r"
            [canisters.demo_rpg.sql]
            readonly = true
        ",
    )
    .expect("config should be written");
    let resolved = icydb_config_build::load_resolved_icydb_toml(canister.as_path(), &[])
        .expect("config should resolve");

    let message =
        disabled_config_surface_message(&resolved, "demo_rpg", ConfigSurface::MetricsReset);

    assert!(message.contains("metrics reset"));
    assert!(message.contains("canisters.<name>.metrics.reset"));
    assert!(message.contains(config_path.to_string_lossy().as_ref()));
    assert!(message.contains("rebuild and deploy"));
    std::fs::remove_dir_all(root).expect("test directory should be removed");
}

#[test]
fn configured_endpoint_methods_match_generated_endpoint_names() {
    assert_eq!(SNAPSHOT_ENDPOINT.method(), "__icydb_snapshot");
    assert_eq!(SCHEMA_ENDPOINT.method(), "__icydb_schema");
    assert_eq!(SCHEMA_CHECK_ENDPOINT.method(), "__icydb_schema_check");
    assert_eq!(METRICS_ENDPOINT.method(), "__icydb_metrics");
    assert_eq!(METRICS_RESET_ENDPOINT.method(), "__icydb_metrics_reset");
    assert_eq!(FIXTURES_LOAD_ENDPOINT.method(), "__icydb_fixtures_load");
    assert_eq!(SQL_QUERY_ENDPOINT.method(), "__icydb_query");
    assert_eq!(SQL_DDL_ENDPOINT.method(), "__icydb_ddl");
}