use std::process::Command;
fn atman_bin() -> &'static str {
env!("CARGO_BIN_EXE_atman")
}
fn run(cwd: &std::path::Path, args: &[&str]) -> (String, String, i32) {
let out = Command::new(atman_bin())
.args(args)
.current_dir(cwd)
.output()
.expect("spawn atman");
let code = out.status.code().unwrap_or(-1);
(
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
code,
)
}
#[test]
fn flow_snapshot_versions_rollback_end_to_end() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let flow_path = root.join("greet.at");
std::fs::write(&flow_path, "flow greet() { return 1 }\n").unwrap();
let (out, err, code) = run(root, &["flow", "snapshot", "greet.at"]);
assert_eq!(code, 0, "snapshot exit: stderr={err}");
assert!(out.contains("snapshot ok"), "stdout: {out}");
assert!(root.join(".atman/flow-registry.db").exists());
std::fs::write(&flow_path, "flow greet() { return 2 }\n").unwrap();
let (_, err, code) = run(root, &["flow", "snapshot", "greet.at"]);
assert_eq!(code, 0, "second snapshot exit: stderr={err}");
let (out, err, code) = run(root, &["flow", "versions", "greet"]);
assert_eq!(code, 0, "versions exit: stderr={err}");
let hash_rows = out.lines().filter(|l| l.contains("hash:")).count();
assert_eq!(hash_rows, 2, "should list 2 revisions, got:\n{out}");
let hash_first_short = {
let src_v1 = "flow greet() { return 1 }\n";
atman_runtime::flow_meta::FlowMeta::short_hash(src_v1)
};
let target = root.join("restored.at");
let (out, err, code) = run(
root,
&[
"flow",
"rollback",
"greet",
&hash_first_short,
"--to",
target.to_str().unwrap(),
"--yes",
],
);
assert_eq!(code, 0, "rollback exit: stderr={err}\nstdout={out}");
let restored = std::fs::read_to_string(&target).unwrap();
assert_eq!(restored, "flow greet() { return 1 }\n");
}
#[test]
fn flow_versions_on_unknown_flow_is_ok_and_prints_hint() {
let tmp = tempfile::tempdir().unwrap();
let (out, err, code) = run(tmp.path(), &["flow", "versions", "nobody"]);
assert_eq!(code, 0, "unknown-name should exit 0: stderr={err}");
assert!(out.contains("no revisions"), "stdout: {out}");
}
#[test]
fn atman_run_auto_snapshots_when_env_flag_is_set() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let flow_path = root.join("hello.at");
std::fs::write(&flow_path, "flow hello() { return 1 }\n").unwrap();
let out = Command::new(atman_bin())
.args(["run", "hello.at", "--ephemeral"])
.current_dir(root)
.env("ATMAN_AUTO_SNAPSHOT", "1")
.env("ATMAN_DISABLE_MIGRATION", "1")
.output()
.expect("spawn atman run");
assert!(
out.status.success(),
"atman run exit: stderr={}",
String::from_utf8_lossy(&out.stderr)
);
let (versions_out, err, code) = run(root, &["flow", "versions", "hello"]);
assert_eq!(code, 0, "versions exit: stderr={err}");
assert!(
versions_out.contains("hash:"),
"auto_snapshot should leave a revision, got:\n{versions_out}"
);
}
#[test]
fn atman_run_does_not_auto_snapshot_without_env_flag() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let flow_path = root.join("hello.at");
std::fs::write(&flow_path, "flow hello() { return 1 }\n").unwrap();
let out = Command::new(atman_bin())
.args(["run", "hello.at", "--ephemeral"])
.current_dir(root)
.env_remove("ATMAN_AUTO_SNAPSHOT")
.env("ATMAN_DISABLE_MIGRATION", "1")
.output()
.expect("spawn atman run");
assert!(
out.status.success(),
"atman run exit: stderr={}",
String::from_utf8_lossy(&out.stderr)
);
let (versions_out, err, code) = run(root, &["flow", "versions", "hello"]);
assert_eq!(code, 0, "versions exit: stderr={err}");
assert!(
versions_out.contains("no revisions"),
"no snapshot expected, got:\n{versions_out}"
);
}
fn have_git() -> bool {
Command::new("git")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn git_init(dir: &std::path::Path) {
let out = Command::new("git")
.args(["init", "--initial-branch=main"])
.current_dir(dir)
.output()
.expect("git init");
assert!(
out.status.success(),
"git init: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn flow_rollback_inside_git_repo_warns_and_aborts_without_yes() {
if !have_git() {
eprintln!("skip: git binary not on PATH");
return;
}
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let flow_path = root.join("g.at");
std::fs::write(&flow_path, "flow g() { return 1 }\n").unwrap();
let (_, err, code) = run(root, &["flow", "snapshot", "g.at"]);
assert_eq!(code, 0, "snapshot: {err}");
git_init(root);
let hash = atman_runtime::flow_meta::FlowMeta::short_hash("flow g() { return 1 }\n");
let target = root.join("restored.at");
let (out, err, code) = run(
root,
&[
"flow",
"rollback",
"g",
&hash,
"--to",
target.to_str().unwrap(),
],
);
assert_ne!(code, 0, "expected abort inside git repo without --yes");
assert!(
err.contains("git checkout") || out.contains("git checkout"),
"want git-checkout hint, stdout={out} stderr={err}"
);
assert!(
!target.exists(),
"target should not have been written on abort"
);
}
#[test]
fn flow_rollback_inside_git_repo_with_yes_prints_hint_and_writes() {
if !have_git() {
eprintln!("skip: git binary not on PATH");
return;
}
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let flow_path = root.join("g.at");
std::fs::write(&flow_path, "flow g() { return 1 }\n").unwrap();
let (_, err, code) = run(root, &["flow", "snapshot", "g.at"]);
assert_eq!(code, 0, "snapshot: {err}");
git_init(root);
let hash = atman_runtime::flow_meta::FlowMeta::short_hash("flow g() { return 1 }\n");
let target = root.join("restored.at");
let (out, err, code) = run(
root,
&[
"flow",
"rollback",
"g",
&hash,
"--to",
target.to_str().unwrap(),
"--yes",
],
);
assert_eq!(code, 0, "rollback --yes exit: stderr={err}\nstdout={out}");
assert!(
err.contains("git checkout") || out.contains("git checkout"),
"hint should still print with --yes, stdout={out} stderr={err}"
);
let restored = std::fs::read_to_string(&target).unwrap();
assert_eq!(restored, "flow g() { return 1 }\n");
}
#[test]
fn flow_rollback_without_to_uses_stored_origin_path() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let flow_path = root.join("greet.at");
std::fs::write(&flow_path, "flow greet() { return 1 }\n").unwrap();
let (_, err, code) = run(root, &["flow", "snapshot", "greet.at"]);
assert_eq!(code, 0, "snapshot v1: {err}");
std::fs::write(&flow_path, "flow greet() { return 2 }\n").unwrap();
let (_, err, code) = run(root, &["flow", "snapshot", "greet.at"]);
assert_eq!(code, 0, "snapshot v2: {err}");
let hash_v1 = atman_runtime::flow_meta::FlowMeta::short_hash("flow greet() { return 1 }\n");
let (out, err, code) = run(root, &["flow", "rollback", "greet", &hash_v1, "--yes"]);
assert_eq!(code, 0, "rollback exit: stderr={err}\nstdout={out}");
assert!(
out.contains("using stored origin"),
"want origin hint on stdout, got: {out}"
);
let restored = std::fs::read_to_string(&flow_path).unwrap();
assert_eq!(restored, "flow greet() { return 1 }\n");
}
#[test]
fn flow_rollback_explicit_to_overrides_stored_origin() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let flow_path = root.join("greet.at");
std::fs::write(&flow_path, "flow greet() { return 1 }\n").unwrap();
let (_, err, code) = run(root, &["flow", "snapshot", "greet.at"]);
assert_eq!(code, 0, "snapshot: {err}");
let hash_v1 = atman_runtime::flow_meta::FlowMeta::short_hash("flow greet() { return 1 }\n");
let alt = root.join("elsewhere.at");
let (out, err, code) = run(
root,
&[
"flow",
"rollback",
"greet",
&hash_v1,
"--to",
alt.to_str().unwrap(),
"--yes",
],
);
assert_eq!(code, 0, "rollback exit: stderr={err}");
assert!(
!out.contains("using stored origin"),
"explicit --to should not fall through to origin hint: {out}"
);
assert!(alt.exists(), "explicit target should be written");
}
#[test]
fn flow_diff_between_two_revisions() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let flow_path = root.join("g.at");
std::fs::write(&flow_path, "flow g() { return 1 }\n").unwrap();
let (_, err, code) = run(root, &["flow", "snapshot", "g.at"]);
assert_eq!(code, 0, "snap v1: {err}");
std::fs::write(&flow_path, "flow g() { return 2 }\n").unwrap();
let (_, err, code) = run(root, &["flow", "snapshot", "g.at"]);
assert_eq!(code, 0, "snap v2: {err}");
let hash_v1 = atman_runtime::flow_meta::FlowMeta::short_hash("flow g() { return 1 }\n");
let hash_v2 = atman_runtime::flow_meta::FlowMeta::short_hash("flow g() { return 2 }\n");
let (out, err, code) = run(root, &["flow", "diff", "g", &hash_v1, &hash_v2]);
assert_eq!(code, 0, "diff exit: stderr={err}");
assert!(
out.contains("-flow g() { return 1 }"),
"want deletion, got:\n{out}"
);
assert!(
out.contains("+flow g() { return 2 }"),
"want insertion, got:\n{out}"
);
}