Menu
AkurAI-Build
publicLatest 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")
}