use std::path::PathBuf;
use std::process::Command;
fn fixture() -> String {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join(".claude/skills/author-fsmp-workflow/fsmp-definition.yaml")
.to_string_lossy()
.into_owned()
}
struct Env {
home: PathBuf,
}
impl Env {
fn new(name: &str) -> Env {
let home = std::env::temp_dir().join(format!("fsmp-author-it-{name}"));
let _ = std::fs::remove_dir_all(&home);
std::fs::create_dir_all(&home).unwrap();
Env { home }
}
fn run(&self, args: &[&str]) -> Out {
let out = Command::new(env!("CARGO_BIN_EXE_fsmp"))
.env("FSMP_HOME", &self.home)
.args(args)
.output()
.expect("failed to run fsmp");
Out {
code: out.status.code().unwrap_or(-1),
text: format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
),
}
}
}
struct Out {
code: i32,
text: String,
}
impl Out {
fn ok(self) -> Out {
assert_eq!(
self.code, 0,
"expected success, got {}:\n{}",
self.code, self.text
);
self
}
fn fail(self) -> Out {
assert_ne!(
self.code, 0,
"expected failure, got success:\n{}",
self.text
);
self
}
fn has(self, needle: &str) -> Out {
assert!(
self.text.contains(needle),
"missing {needle:?} in:\n{}",
self.text
);
self
}
}
fn to_drafting(name: &str) -> Env {
let e = Env::new(name);
let f = fixture();
e.run(&["new", "--def", &f, "--id", "m"])
.ok()
.has("state: graph_review");
e.run(&[
"do",
"graph_approved",
"--id",
"m",
"--data",
"def_path=/tmp/demo.yaml",
])
.ok()
.has("state: drafting")
.has("/tmp/demo.yaml");
e
}
#[test]
fn happy_path_reaches_done_through_every_gate() {
let e = to_drafting("happy");
e.run(&["do", "draft_written", "--id", "m"])
.ok()
.has("state: linting");
e.run(&["do", "lint_clean", "--id", "m"])
.ok()
.has("state: dry_run");
e.run(&["do", "dryrun_passed", "--id", "m"])
.ok()
.has("state: user_signoff");
e.run(&["do", "accepted", "--id", "m"])
.ok()
.has("state: done")
.has("terminal");
e.run(&["do", "accepted", "--id", "m"])
.fail()
.has("terminal");
}
#[test]
fn no_yaml_before_the_graph_is_signed_off() {
let e = Env::new("gate_graph");
let f = fixture();
e.run(&["new", "--def", &f, "--id", "m"]).ok();
e.run(&["do", "draft_written", "--id", "m"])
.fail()
.has("not a valid transition")
.has("graph_approved"); e.run(&["do", "graph_approved", "--id", "m"])
.fail()
.has("requires data: def_path")
.has("--data def_path=");
}
#[test]
fn no_dry_run_before_lint_is_clean() {
let e = to_drafting("gate_lint");
e.run(&["do", "dryrun_passed", "--id", "m"])
.fail()
.has("not a valid transition");
e.run(&["do", "draft_written", "--id", "m"])
.ok()
.has("state: linting");
e.run(&["do", "dryrun_passed", "--id", "m"])
.fail()
.has("not a valid transition");
}
#[test]
fn no_sign_off_before_a_dry_run() {
let e = to_drafting("gate_signoff");
e.run(&["do", "draft_written", "--id", "m"]).ok();
e.run(&["do", "lint_clean", "--id", "m"])
.ok()
.has("state: dry_run");
e.run(&["do", "accepted", "--id", "m"])
.fail()
.has("not a valid transition");
e.run(&["do", "dryrun_passed", "--id", "m"])
.ok()
.has("state: user_signoff");
}
#[test]
fn a_failed_gate_loops_back_to_drafting() {
let e = to_drafting("retry");
e.run(&["do", "draft_written", "--id", "m"]).ok();
e.run(&["do", "lint_failed", "--id", "m"])
.ok()
.has("state: drafting");
e.run(&["do", "draft_written", "--id", "m"]).ok();
e.run(&["do", "lint_clean", "--id", "m"]).ok();
e.run(&["do", "dryrun_failed", "--id", "m"])
.ok()
.has("state: drafting");
e.run(&["do", "draft_written", "--id", "m"]).ok();
e.run(&["do", "lint_clean", "--id", "m"]).ok();
e.run(&["do", "dryrun_passed", "--id", "m"]).ok();
e.run(&["do", "changes_requested", "--id", "m"])
.ok()
.has("state: drafting");
}
#[test]
fn escalate_reaches_a_terminal_state() {
let e = Env::new("escalate");
let f = fixture();
e.run(&["new", "--def", &f, "--id", "m"]).ok();
e.run(&["do", "escalate", "--id", "m"])
.ok()
.has("state: escalated")
.has("terminal");
e.run(&["do", "escalate", "--id", "m"])
.fail()
.has("terminal");
}