greentic-x 0.4.13

Greentic-X CLI for catalog-driven composition, scaffolding, validation, and simulation.
Documentation
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

use jsonschema::validator_for;
use serde_json::Value;
use tempfile::TempDir;

fn repo_root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .expect("crates dir")
        .parent()
        .expect("repo root")
        .to_path_buf()
}

fn fixture_path(name: &str) -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("fixtures")
        .join(name)
}

fn binary_path() -> PathBuf {
    PathBuf::from(env!("CARGO_BIN_EXE_greentic-x"))
}

fn run_cli(args: &[&str], cwd: &Path) -> Result<String, String> {
    let output = Command::new(binary_path())
        .args(args)
        .current_dir(cwd)
        .output()
        .map_err(|err| format!("failed to run greentic-x: {err}"))?;
    if output.status.success() {
        String::from_utf8(output.stdout).map_err(|err| format!("stdout was not utf-8: {err}"))
    } else {
        let stderr = String::from_utf8_lossy(&output.stderr);
        let stdout = String::from_utf8_lossy(&output.stdout);
        Err(format!(
            "greentic-x failed with status {}.\nstdout:\n{}\nstderr:\n{}",
            output.status, stdout, stderr
        ))
    }
}

fn load_json(path: &Path) -> Value {
    serde_json::from_str(&fs::read_to_string(path).expect("read json")).expect("parse json")
}

fn assert_matches_schema(schema_path: &Path, value_path: &Path) {
    let schema = load_json(schema_path);
    let validator = validator_for(&schema).expect("validator");
    let value = load_json(value_path);
    validator.validate(&value).expect("schema validation");
}

fn copy_fixture_answers(cwd: &Path) -> PathBuf {
    let path = cwd.join("answers.json");
    fs::copy(fixture_path("network-assistant.answers.json"), &path).expect("copy fixture");
    path
}

fn copy_fixture_named(cwd: &Path, fixture_name: &str) -> PathBuf {
    let path = cwd.join("answers.json");
    fs::copy(fixture_path(fixture_name), &path).expect("copy fixture");
    path
}

fn copy_fixture_to(cwd: &Path, fixture_name: &str, output_name: &str) -> PathBuf {
    let path = cwd.join(output_name);
    fs::copy(fixture_path(fixture_name), &path).expect("copy fixture");
    path
}

#[test]
fn fixture_answers_validate_and_run_emit_compatible_outputs() {
    let temp = TempDir::new().expect("tempdir");
    let cwd = temp.path();
    copy_fixture_answers(cwd);

    let validate_output =
        run_cli(&["wizard", "validate", "--answers", "answers.json"], cwd).expect("validate");
    let validate_plan: Value = serde_json::from_str(&validate_output).expect("validate plan");
    assert_eq!(validate_plan["requested_action"], "validate");
    assert_eq!(validate_plan["metadata"]["execution"], "dry_run");

    let run_output = run_cli(&["wizard", "run", "--answers", "answers.json"], cwd).expect("run");
    let run_plan: Value = serde_json::from_str(&run_output).expect("run plan");
    assert_eq!(run_plan["requested_action"], "run");
    assert_eq!(run_plan["metadata"]["execution"], "execute");

    let root = repo_root();
    assert_matches_schema(
        &root.join("schemas/solution-intent.schema.json"),
        &cwd.join("dist/network-assistant.solution.json"),
    );
    assert_matches_schema(
        &root.join("schemas/toolchain-handoff.schema.json"),
        &cwd.join("dist/network-assistant.toolchain-handoff.json"),
    );
    assert_matches_schema(
        &root.join("schemas/pack-input.schema.json"),
        &cwd.join("dist/network-assistant.pack.input.json"),
    );

    let bundle_answers = load_json(&cwd.join("dist/network-assistant.bundle.answers.json"));
    assert_eq!(bundle_answers["wizard_id"], "greentic-bundle.wizard.run");
    assert_eq!(
        bundle_answers["schema_id"],
        "greentic-bundle.wizard.answers"
    );

    let launcher_answers = load_json(&cwd.join("dist/network-assistant.launcher.answers.json"));
    assert_eq!(
        launcher_answers["wizard_id"],
        "greentic-dev.wizard.launcher.main"
    );
    assert_eq!(launcher_answers["schema_id"], "greentic-dev.launcher.main");
    assert_eq!(launcher_answers["answers"]["selected_action"], "bundle");

    let pack_input = load_json(&cwd.join("dist/network-assistant.pack.input.json"));
    assert_eq!(pack_input["schema_id"], "gx.pack.input");
    assert_eq!(pack_input["solution_id"], "network-assistant");
    assert!(pack_input["unresolved_downstream_work"].is_array());

    let gtc_setup_handoff = load_json(&cwd.join("dist/network-assistant.gtc.setup.handoff.json"));
    assert_eq!(
        gtc_setup_handoff["schema_id"],
        "gtc.extension.setup.handoff"
    );
    assert_eq!(
        gtc_setup_handoff["bundle_ref"],
        "dist/dist/network-assistant.gtbundle"
    );
    assert_eq!(
        gtc_setup_handoff["answers_path"],
        "dist/network-assistant.setup.answers.json"
    );

    let gtc_start_handoff = load_json(&cwd.join("dist/network-assistant.gtc.start.handoff.json"));
    assert_eq!(
        gtc_start_handoff["schema_id"],
        "gtc.extension.start.handoff"
    );
    assert_eq!(
        gtc_start_handoff["bundle_ref"],
        "dist/dist/network-assistant.gtbundle"
    );
}

#[test]
fn telco_catalog_fixture_emits_generic_gtc_handoffs() {
    let temp = TempDir::new().expect("tempdir");
    let cwd = temp.path();
    copy_fixture_named(cwd, "telco-network-assistant.answers.json");
    copy_fixture_to(cwd, "telco-catalog.json", "catalog.json");
    let run_output = run_cli(
        &[
            "wizard",
            "run",
            "--answers",
            "answers.json",
            "--catalog",
            "catalog.json",
        ],
        cwd,
    )
    .expect("run");
    let run_plan: Value = serde_json::from_str(&run_output).expect("run plan");
    assert_eq!(run_plan["requested_action"], "run");
    assert_eq!(run_plan["metadata"]["execution"], "execute");

    let solution = load_json(&cwd.join("dist/telco-network-assistant.solution.json"));
    assert_eq!(solution["solution_id"], "telco-network-assistant");
    assert_eq!(
        solution["template"]["entry_id"],
        "tx.assistant-template.network-assistant.phase1"
    );

    let toolchain_handoff =
        load_json(&cwd.join("dist/telco-network-assistant.toolchain-handoff.json"));
    assert_eq!(toolchain_handoff["gtc_handoff"]["tool"], "gtc");
    assert_eq!(
        toolchain_handoff["gtc_handoff"]["setup_handoff_path"],
        "dist/telco-network-assistant.gtc.setup.handoff.json"
    );
    assert_eq!(
        toolchain_handoff["gtc_handoff"]["start_handoff_path"],
        "dist/telco-network-assistant.gtc.start.handoff.json"
    );

    let gtc_setup_handoff =
        load_json(&cwd.join("dist/telco-network-assistant.gtc.setup.handoff.json"));
    assert_eq!(
        gtc_setup_handoff["schema_id"],
        "gtc.extension.setup.handoff"
    );
    assert_eq!(
        gtc_setup_handoff["bundle_ref"],
        "dist/dist/telco-network-assistant.gtbundle"
    );
    assert_eq!(
        gtc_setup_handoff["answers_path"],
        "dist/telco-network-assistant.setup.answers.json"
    );

    let gtc_start_handoff =
        load_json(&cwd.join("dist/telco-network-assistant.gtc.start.handoff.json"));
    assert_eq!(
        gtc_start_handoff["schema_id"],
        "gtc.extension.start.handoff"
    );
    assert_eq!(
        gtc_start_handoff["bundle_ref"],
        "dist/dist/telco-network-assistant.gtbundle"
    );
}

#[test]
fn optional_external_toolchain_replay_checks() {
    if std::env::var_os("GX_TEST_EXTERNAL_TOOLCHAIN").is_none() {
        return;
    }

    let temp = TempDir::new().expect("tempdir");
    let cwd = temp.path();
    copy_fixture_answers(cwd);
    run_cli(&["wizard", "run", "--answers", "answers.json"], cwd).expect("run");

    if command_available("greentic-bundle") {
        let status = Command::new("greentic-bundle")
            .args([
                "wizard",
                "apply",
                "--answers",
                "dist/network-assistant.bundle.answers.json",
            ])
            .current_dir(cwd)
            .status()
            .expect("run greentic-bundle");
        assert!(status.success(), "greentic-bundle replay failed: {status}");
    }

    if command_available("greentic-dev") {
        let output = Command::new("greentic-dev")
            .args(["wizard", "--schema"])
            .current_dir(cwd)
            .output()
            .expect("run greentic-dev");
        assert!(
            output.status.success(),
            "greentic-dev --schema failed: {}",
            String::from_utf8_lossy(&output.stderr)
        );
        let schema: Value = serde_json::from_slice(&output.stdout).expect("launcher schema");
        let validator = validator_for(&schema).expect("launcher schema validator");
        let launcher_answers = load_json(&cwd.join("dist/network-assistant.launcher.answers.json"));
        validator
            .validate(&launcher_answers)
            .expect("launcher answers accepted by greentic-dev schema");
    }
}

fn command_available(name: &str) -> bool {
    Command::new(name)
        .arg("--version")
        .output()
        .map(|output| output.status.success())
        .unwrap_or(false)
}