fundaia 0.52.1

Command line for the Fundaia deployment platform: projects, services, variables, deployments, logs and metrics
//! Budgets on the compiled binary, spawned the way an agent spawns it.
//!
//! Startup is the one cost every invocation pays: an AI agent driving this tool
//! runs it dozens of times per task, and a CLI that takes half a second to say
//! `--version` taxes every one of them. These tests measure the real
//! executable — `CARGO_BIN_EXE_fundaia` is the binary this same profile just
//! built — so a dependency that drags startup down fails the pipeline instead
//! of being discovered by somebody's stopwatch.
//!
//! The budget comes from `FUNDAIA_PERF_BUDGET_MS` so the pipeline can hold the
//! release build to a tighter number than a debug build on a laptop; the
//! default is far above what the binary does today. The median of several runs
//! rather than the worst: one descheduled process on a busy runner must not
//! paint the build red.

use std::process::{Command, Output};
use std::time::{Duration, Instant};

fn budget() -> Duration {
    let millis = std::env::var("FUNDAIA_PERF_BUDGET_MS")
        .ok()
        .and_then(|value| value.parse().ok())
        .unwrap_or(500);
    Duration::from_millis(millis)
}

/// The binary with nothing configured: no profile on disk, no token in the
/// environment, and the update check off so no run touches the network.
fn fundaia(arguments: &[&str]) -> Command {
    let mut command = Command::new(env!("CARGO_BIN_EXE_fundaia"));
    command
        .args(arguments)
        .env("FUNDAIA_CONFIG", "/nonexistent/fundaia-perf/config.toml")
        .env("FUNDAIA_NO_UPDATE_CHECK", "1")
        .env_remove("FUNDAIA_URL")
        .env_remove("FUNDAIA_TOKEN")
        .env_remove("FUNDAIA_PROFILE")
        .env_remove("FUNDAIA_JSON");
    command
}

fn run(arguments: &[&str]) -> Output {
    fundaia(arguments)
        .output()
        .expect("the binary this test suite was built alongside runs")
}

fn median_run_millis(arguments: &[&str]) -> u128 {
    // One unmeasured run first, so the page cache is warm for every sample the
    // way it is for every real invocation but the machine's first.
    run(arguments);

    let mut samples: Vec<u128> = (0..9)
        .map(|_| {
            let started = Instant::now();
            run(arguments);
            started.elapsed().as_millis()
        })
        .collect();
    samples.sort_unstable();

    samples[samples.len() / 2]
}

#[test]
fn it_should_print_the_version_within_the_startup_budget() {
    assert!(median_run_millis(&["--version"]) < budget().as_millis());
}

#[test]
fn it_should_print_the_help_within_the_startup_budget() {
    assert!(median_run_millis(&["--help"]) < budget().as_millis());
}

/// The failure path is a startup path too: an unconfigured `projects` must be
/// refused before any network is touched, so an agent with a bad environment
/// learns it immediately rather than after a connect timeout.
#[test]
fn it_should_refuse_an_unconfigured_command_within_the_startup_budget() {
    assert!(median_run_millis(&["projects"]) < budget().as_millis());
}

#[test]
fn it_should_exit_nonzero_when_nothing_is_configured() {
    assert!(!run(&["projects"]).status.success());
}

#[test]
fn it_should_ask_for_a_login_when_nothing_is_configured() {
    let stderr = String::from_utf8_lossy(&run(&["projects"]).stderr).to_string();
    assert!(stderr.contains("login"));
}

/// The agent contract: under `--json` a failure is one JSON object on stderr,
/// so the reader branches on `error.code` instead of parsing Spanish.
///
/// `unauthorized` and not a `cli_error` of its own: nothing is configured, the
/// fix is `fundaia login`, and that is the same fact the server states with the
/// same word when a token it once knew has gone. Which side worked it out is
/// not something a caller should have to branch on.
#[test]
fn it_should_answer_an_error_as_json_in_json_mode() {
    let output = run(&["--json", "projects"]);
    let stderr = String::from_utf8_lossy(&output.stderr);

    let parsed: serde_json::Value =
        serde_json::from_str(stderr.trim()).expect("stderr is exactly one JSON object");

    assert_eq!(parsed["error"]["code"], "unauthorized");
}

/// The number a shell reads before anything is parsed. Five is "no token", and
/// it is the contract `fundaia schema` publishes.
#[test]
fn it_should_exit_five_when_nothing_is_configured() {
    assert_eq!(run(&["projects"]).status.code(), Some(5));
}

/// Both halves of the same failure, agreeing. They are written by different
/// code paths — one builds the object, the other stops the process — and a
/// caller that read one should never need the other to check it.
#[test]
fn it_should_carry_the_exit_code_it_is_about_to_leave_in_the_json() {
    let output = run(&["--json", "projects"]);
    let parsed: serde_json::Value =
        serde_json::from_str(String::from_utf8_lossy(&output.stderr).trim())
            .expect("stderr is exactly one JSON object");

    assert_eq!(
        parsed["error"]["exit"].as_i64(),
        output.status.code().map(i64::from)
    );
}

/// Nothing is configured and nothing will be: repeating it is not the answer.
#[test]
fn it_should_not_invite_a_retry_when_nothing_is_configured() {
    let output = run(&["--json", "projects"]);
    let parsed: serde_json::Value =
        serde_json::from_str(String::from_utf8_lossy(&output.stderr).trim())
            .expect("stderr is exactly one JSON object");

    assert_eq!(parsed["error"]["retryable"], false);
}

/// `schema` reaches nothing, so it answers on a machine with no profile, no
/// token and no network — which is the machine an agent works out what it may
/// do on.
#[test]
fn it_should_describe_itself_with_nothing_configured() {
    let stdout = String::from_utf8_lossy(&run(&["schema", "--json"]).stdout).to_string();
    let parsed: serde_json::Value =
        serde_json::from_str(stdout.trim()).expect("stdout is exactly one JSON object");

    assert_eq!(parsed["tool"], "fundaia");
}

#[test]
fn it_should_describe_itself_within_the_startup_budget() {
    assert!(median_run_millis(&["schema", "--json"]) < budget().as_millis());
}

/// The guide travels in the binary, so it answers with nothing configured too.
#[test]
fn it_should_hand_over_the_guide_with_nothing_configured() {
    assert!(run(&["guide"]).status.success());
}

#[test]
fn it_should_keep_stdout_empty_when_a_json_command_fails() {
    assert!(run(&["--json", "projects"]).stdout.is_empty());
}

#[test]
fn it_should_write_a_completion_script_on_stdout() {
    let stdout = String::from_utf8_lossy(&run(&["completions", "bash"]).stdout).to_string();
    assert!(stdout.contains("fundaia"));
}