#[path = "test_support/gleam.rs"]
mod gleam_test_support;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
type TestError = Box<dyn std::error::Error>;
const GENERATED: &[&str] = &[
"src/aion_order_saga_codecs.gleam",
"src/aion_order_saga_activity_wrappers.gleam",
"worker/worker.py",
"test/aion_order_saga_wire_compat_test.gleam",
"schemas/cancel_shipment_input.json",
"schemas/compensation_output.json",
"schemas/inventory_reservation.json",
"schemas/order_input.json",
"schemas/payment_receipt.json",
"schemas/refund_payment_input.json",
"schemas/release_inventory_input.json",
"schemas/shipment.json",
];
fn repo_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..")
}
fn stage_order_saga() -> Result<(tempfile::TempDir, PathBuf), TestError> {
let root = repo_root().canonicalize()?;
let source = root.join("examples/order-saga");
let temp = tempfile::tempdir()?;
let project = temp.path().join("order-saga");
copy_tree(&source, &project)?;
let aion_flow = root.join("gleam/aion_flow");
let absolute = format!("\"{}\"", aion_flow.display());
for descriptor in ["gleam.toml", "manifest.toml"] {
let path = project.join(descriptor);
let contents = fs::read_to_string(&path)?.replace("\"../../gleam/aion_flow\"", &absolute);
fs::write(&path, contents)?;
}
Ok((temp, project))
}
fn copy_tree(from: &Path, to: &Path) -> Result<(), TestError> {
fs::create_dir_all(to)?;
for entry in fs::read_dir(from)? {
let entry = entry?;
let name = entry.file_name();
if name == "build" {
continue;
}
let source = entry.path();
let target = to.join(&name);
if source.is_dir() {
copy_tree(&source, &target)?;
} else if source.extension().is_none_or(|ext| ext != "aion") {
fs::copy(&source, &target)?;
}
}
Ok(())
}
fn run_generate(project: &Path, check: bool) -> Result<(bool, String), TestError> {
let mut command = Command::new(env!("CARGO_BIN_EXE_aion"));
command.arg("generate").arg(project);
if check {
command.arg("--check");
}
let output = command
.output()
.map_err(|error| format!("failed to spawn the `aion` binary for `generate`: {error}"))?;
let combined = format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
if combined.contains("failed to run `gleam run") || combined.contains("Command not found") {
return Err(format!(
"`aion generate` could not drive the Gleam toolchain (is `gleam` on PATH?). \
This gate fails loudly by design rather than skipping.\n{combined}"
)
.into());
}
Ok((output.status.success(), combined))
}
#[test]
fn generate_round_trips_check_gates_and_invents_no_defaults() -> Result<(), TestError> {
if crate::gleam_test_support::skip_if_unavailable() {
return Ok(());
}
let (_temp, project) = stage_order_saga()?;
let types_module = project.join("src/aion_order_saga_io.gleam");
let authored_before = fs::read(&types_module)?;
let (ok, output) = run_generate(&project, false)?;
assert!(ok, "first `aion generate` failed:\n{output}");
assert_eq!(
fs::read(&types_module)?,
authored_before,
"generation must never touch the authored types module"
);
let mut snapshot = Vec::new();
for relative in GENERATED {
let path = project.join(relative);
let contents = fs::read(&path)
.map_err(|error| format!("expected {relative} to be generated: {error}"))?;
snapshot.push((path, contents));
}
for (path, _) in &snapshot {
fs::remove_file(path)?;
}
let (ok, output) = run_generate(&project, false)?;
assert!(ok, "regeneration after delete failed:\n{output}");
for (path, original) in &snapshot {
let regenerated = fs::read(path)?;
assert!(
®enerated == original,
"round-trip drift in {}: regeneration is not byte-identical",
path.display()
);
}
let (ok, output) = run_generate(&project, true)?;
assert!(ok, "`--check` failed on a clean tree:\n{output}");
let wrappers = project.join("src/aion_order_saga_activity_wrappers.gleam");
let mut tampered = fs::read_to_string(&wrappers)?;
tampered.push_str("\n//// hand edit\n");
fs::write(&wrappers, &tampered)?;
let (ok, output) = run_generate(&project, true)?;
assert!(
!ok,
"`--check` passed despite a hand-edited generated file:\n{output}"
);
assert!(
output.contains("aion_order_saga_activity_wrappers.gleam"),
"the --check failure must name the drifted file:\n{output}"
);
run_generate(&project, false)?;
let wrappers = fs::read_to_string(&wrappers)?;
for forbidden in ["retry", "timeout", "backoff", "heartbeat", "RetryPolicy"] {
assert!(
!wrappers.contains(forbidden),
"generated wrappers must not invent activity policy, found `{forbidden}`"
);
}
Ok(())
}
#[test]
fn generate_check_gates_workflow_toml_drift() -> Result<(), TestError> {
if crate::gleam_test_support::skip_if_unavailable() {
return Ok(());
}
let (_temp, project) = stage_order_saga()?;
let (ok, output) = run_generate(&project, false)?;
assert!(ok, "first `aion generate` failed:\n{output}");
let (ok, output) = run_generate(&project, true)?;
assert!(ok, "`--check` failed on a clean tree:\n{output}");
let toml_path = project.join("workflow.toml");
let mut toml = fs::read_to_string(&toml_path)?;
toml = toml.replace(
" \"cancel_shipment\",\n",
" \"cancel_shipment\",\n \"not_a_real_activity\",\n",
);
assert!(
toml.contains("not_a_real_activity"),
"the desync edit must alter the activities list"
);
fs::write(&toml_path, &toml)?;
let (ok, output) = run_generate(&project, true)?;
assert!(
!ok,
"`--check` passed despite a desynced workflow.toml activities list:\n{output}"
);
assert!(
output.contains("workflow.toml") && output.contains("out of date"),
"the --check failure must reference workflow.toml and say it is out of date:\n{output}"
);
Ok(())
}