use std::fs;
use std::process::Command;
fn allium() -> Command {
Command::new(env!("CARGO_BIN_EXE_allium"))
}
struct SpecFile {
path: std::path::PathBuf,
}
impl SpecFile {
fn new(tag: &str, content: &str) -> Self {
let path = std::env::temp_dir().join(format!(
"allium-smoke-{tag}-{}.allium",
std::process::id()
));
fs::write(&path, content).unwrap();
Self { path }
}
fn arg(&self) -> String {
self.path.to_string_lossy().into_owned()
}
}
impl Drop for SpecFile {
fn drop(&mut self) {
let _ = fs::remove_file(&self.path);
}
}
const VALID: &str = "-- allium: 3\n\n\
entity Job {\n status: pending | done\n transitions status { pending -> done terminal: done }\n}\n\n\
rule CreateJob {\n when: JobRequested()\n ensures: Job.created(status: pending)\n}\n\n\
rule Finish {\n when: j: Job.status becomes pending\n ensures: j.status = done\n}\n\n\
surface JobIntake {\n provides:\n JobRequested()\n}\n";
const BROKEN: &str = "-- allium: 3\n\n\
rule R {\n when: Go()\n ensures: Ghost.created(status: pending)\n}\n";
const UNPARSEABLE: &str = "-- allium: 3\n\nentity Broken {\n";
#[test]
fn check_valid_spec_exits_zero_with_empty_reports() {
let spec = SpecFile::new("valid", VALID);
let out = allium().arg("check").arg(spec.arg()).output().expect("spawn allium");
assert!(out.status.success(), "expected exit 0, got {:?}", out.status);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("\"command\": \"check\""), "missing command envelope: {stdout}");
assert!(stdout.contains("\"diagnostics\": []"), "expected no diagnostics: {stdout}");
assert!(stdout.contains("\"findings\": []"), "expected no findings: {stdout}");
}
#[test]
fn check_spec_with_error_exits_one_and_names_the_offender() {
let spec = SpecFile::new("broken", BROKEN);
let out = allium().arg("check").arg(spec.arg()).output().expect("spawn allium");
assert_eq!(out.status.code(), Some(1), "an error-severity diagnostic should exit 1");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("allium.rule.undefinedTypeReference"), "expected the type-ref error: {stdout}");
assert!(stdout.contains("\"severity\": \"error\""), "expected an error severity: {stdout}");
assert!(stdout.contains("Ghost"), "diagnostic should name the offending reference: {stdout}");
}
#[test]
fn analyse_valid_spec_emits_the_json_envelope() {
let spec = SpecFile::new("analyse", VALID);
let out = allium().arg("analyse").arg(spec.arg()).output().expect("spawn allium");
assert!(out.status.success(), "expected exit 0, got {:?}", out.status);
let stdout = String::from_utf8_lossy(&out.stdout);
for key in ["\"command\"", "\"diagnostics\"", "\"findings\""] {
assert!(stdout.contains(key), "analyse output missing {key}: {stdout}");
}
serde_json::from_str::<serde_json::Value>(stdout.trim())
.expect("analyse stdout should be valid JSON");
}
#[test]
fn parse_valid_spec_exits_zero() {
let spec = SpecFile::new("parse", VALID);
let out = allium().arg("parse").arg(spec.arg()).output().expect("spawn allium");
assert!(out.status.success(), "expected exit 0 on a well-formed spec, got {:?}", out.status);
}
#[test]
fn single_file_commands_exit_nonzero_on_unparseable_spec() {
for command in ["plan", "model", "parse"] {
let spec = SpecFile::new(&format!("unparseable-{command}"), UNPARSEABLE);
let out = allium().arg(command).arg(spec.arg()).output().expect("spawn allium");
assert_eq!(
out.status.code(),
Some(1),
"{command} on an unparseable spec should exit 1, got {:?}",
out.status
);
}
}
#[test]
fn plan_surfaces_parse_diagnostics_in_its_json() {
let spec = SpecFile::new("plan-unparseable", UNPARSEABLE);
let out = allium().arg("plan").arg(spec.arg()).output().expect("spawn allium");
let stdout = String::from_utf8_lossy(&out.stdout);
let json: serde_json::Value =
serde_json::from_str(stdout.trim()).expect("plan stdout should be valid JSON");
let diags = json["diagnostics"].as_array().expect("plan output should carry a diagnostics array");
assert!(
diags.iter().any(|d| d["severity"] == "error"),
"plan should surface the parse error in its diagnostics: {stdout}"
);
assert_eq!(json["obligations"].as_array().map(Vec::len), Some(0));
}
#[test]
fn plan_valid_spec_exits_zero_with_empty_diagnostics() {
let spec = SpecFile::new("plan-valid", VALID);
let out = allium().arg("plan").arg(spec.arg()).output().expect("spawn allium");
assert!(out.status.success(), "a valid spec should exit 0, got {:?}", out.status);
let stdout = String::from_utf8_lossy(&out.stdout);
let json: serde_json::Value =
serde_json::from_str(stdout.trim()).expect("plan stdout should be valid JSON");
assert_eq!(
json["diagnostics"].as_array().map(Vec::len),
Some(0),
"a valid spec should carry an empty diagnostics array: {stdout}"
);
}