AkurAI Build
Menu

AkurAI-Build

public

Latest change ab672e9cd796b2ae3b9efc364a14d20dec03a31d - Add bounded agent wait and readiness recovery by Ólafur Búi Ólafsson

use std::{fs, path::Path, process::Command};

use anyhow::{Context, Result, ensure};
use serde_json::Value;

#[test]
fn git_to_artifact_to_approved_deployment() -> Result<()> {
    let temporary = tempfile::tempdir()?;
    let repository = temporary.path().join("repository");
    let data = temporary.path().join("data");
    let key = temporary.path().join("database.key");
    fs::create_dir(&repository)?;
    git(&repository, &["init", "-b", "main"])?;
    git(&repository, &["config", "user.name", "AkurAI Test"])?;
    git(
        &repository,
        &["config", "user.email", "test@example.invalid"],
    )?;
    fs::write(
        repository.join(".akurai.yml"),
        r#"version: 1
jobs:
  - name: build
    executor: native
    run: mkdir -p out && printf hello > out/artifact.txt
    artifacts: [out/artifact.txt]
  - name: deploy
    needs: [build]
    executor: native
    environment: test
    approval: true
    run: test "$(cat out/artifact.txt)" = hello
"#,
    )?;
    git(&repository, &["add", ".akurai.yml"])?;
    git(&repository, &["commit", "-m", "pipeline"])?;

    cli(&data, &key, &["keygen", "--out", path(&key)?], false)?;
    cli(&data, &key, &["migrate"], false)?;
    cli(
        &data,
        &key,
        &[
            "repo",
            "add",
            "fixture",
            path(&repository)?,
            "--branch",
            "main",
        ],
        false,
    )?;
    let first = cli(&data, &key, &["run", "fixture", "--wait"], true)?;
    ensure!(
        first["data"]["status"] == "waiting",
        "run should wait for approval: {first}"
    );
    let id = first["data"]["id"].as_i64().context("run has no id")?;
    let promoted = cli(
        &data,
        &key,
        &["promote", &id.to_string(), "test", "--wait"],
        true,
    )?;
    ensure!(
        promoted["data"]["status"] == "succeeded",
        "promotion failed: {promoted}"
    );
    ensure!(
        promoted["data"]["deployments"][0]["status"] == "succeeded",
        "deployment was not persisted"
    );
    let waited = cli(
        &data,
        &key,
        &["wait", &id.to_string(), "--timeout", "1"],
        false,
    )?;
    ensure!(
        waited["data"]["status"] == "succeeded",
        "wait returned early"
    );
    let artifact = promoted["data"]["artifacts"][0].clone();
    let artifact_id = artifact["id"].as_i64().context("artifact has no id")?;
    let output = temporary.path().join("download.txt");
    cli(
        &data,
        &key,
        &["artifact", "get", &artifact_id.to_string(), path(&output)?],
        false,
    )?;
    ensure!(
        fs::read_to_string(output)? == "hello",
        "artifact bytes differ"
    );
    let stored = data.join(artifact["path"].as_str().context("artifact has no path")?);
    fs::write(stored, "tampered")?;
    let rejected = cli_failure(
        &data,
        &key,
        &[
            "artifact",
            "get",
            &artifact_id.to_string(),
            path(&temporary.path().join("tampered.txt"))?,
        ],
    )?;
    ensure!(
        rejected["error"]["message"]
            .as_str()
            .is_some_and(|message| message.contains("integrity")),
        "tampered artifact was not rejected: {rejected}"
    );

    let denied = cli(&data, &key, &["run", "fixture", "--wait"], false)?;
    ensure!(
        denied["data"]["status"] == "failed",
        "native execution did not fail closed"
    );
    Ok(())
}

fn cli(data: &Path, key: &Path, arguments: &[&str], native: bool) -> Result<Value> {
    let mut command = Command::new(env!("CARGO_BIN_EXE_akurai"));
    command
        .args(["--data", path(data)?, "--key-file", path(key)?])
        .args(arguments);
    if native {
        command.env("AKURAI_ALLOW_NATIVE", "1");
    } else {
        command.env_remove("AKURAI_ALLOW_NATIVE");
    }
    let output = command.output()?;
    let value: Value = serde_json::from_slice(&output.stdout)
        .with_context(|| format!("invalid JSON: {}", String::from_utf8_lossy(&output.stdout)))?;
    ensure!(
        output.status.success(),
        "CLI failed: {value}\n{}",
        String::from_utf8_lossy(&output.stderr)
    );
    ensure!(value["ok"] == true, "CLI envelope failed: {value}");
    Ok(value)
}

fn cli_failure(data: &Path, key: &Path, arguments: &[&str]) -> Result<Value> {
    let output = Command::new(env!("CARGO_BIN_EXE_akurai"))
        .args(["--data", path(data)?, "--key-file", path(key)?])
        .args(arguments)
        .output()?;
    ensure!(!output.status.success(), "CLI unexpectedly succeeded");
    let value: Value = serde_json::from_slice(&output.stdout)?;
    ensure!(value["ok"] == false, "failure envelope was not returned");
    Ok(value)
}

fn git(directory: &Path, arguments: &[&str]) -> Result<()> {
    let output = Command::new("git")
        .current_dir(directory)
        .args(arguments)
        .output()?;
    ensure!(
        output.status.success(),
        "git failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    Ok(())
}

fn path(path: &Path) -> Result<&str> {
    path.to_str().context("test path is not UTF-8")
}