use std::fs;
use std::path::Path;
use loopflow::engine::flow::{ConcreteItem, FlowItem, Step};
use loopflow::engine::{expand_flow, load_flow};
use tempfile::TempDir;
fn write_step(repo: &Path, name: &str, content: &str) {
let steps_dir = repo.join(".lf/steps");
fs::create_dir_all(&steps_dir).unwrap();
fs::write(steps_dir.join(format!("{name}.md")), content).unwrap();
}
fn write_flow(repo: &Path, name: &str, content: &str) {
let flows_dir = repo.join(".lf/flows");
fs::create_dir_all(&flows_dir).unwrap();
fs::write(flows_dir.join(format!("{name}.yaml")), content).unwrap();
}
fn expand_named_flow(repo: &Path, name: &str) -> Vec<ConcreteItem> {
let flow = load_flow(name, repo).unwrap();
expand_flow(&flow, repo).unwrap()
}
fn assert_step_name(item: &ConcreteItem, expected: &str) {
match item {
ConcreteItem::Step(step) => assert_eq!(step.step.name, expected),
other => panic!("expected step {expected}, got {other:?}"),
}
}
fn assert_step_sequence(items: &[ConcreteItem], expected: &[&str]) {
assert_eq!(items.len(), expected.len());
for (item, step_name) in items.iter().zip(expected) {
assert_step_name(item, step_name);
}
}
#[test]
fn flow_parsing_parity() {
let temp = TempDir::new().unwrap();
let repo = temp.path();
write_flow(
repo,
"sample",
r#"
- implement
- step:
name: review
interactive: true
direction: [ux, security]
"#,
);
let flow = load_flow("sample", repo).unwrap();
assert_eq!(flow.name, "sample");
assert_eq!(flow.items.len(), 2);
assert_eq!(
flow.items[0],
FlowItem::Step(Step {
name: "implement".to_string(),
agent: None,
default_agent: None,
directions: vec![],
action_style: None,
interactive: None,
content: None,
fast_path: None,
})
);
assert_eq!(
flow.items[1],
FlowItem::Step(Step {
name: "review".to_string(),
agent: None,
default_agent: None,
directions: vec!["ux".to_string(), "security".to_string()],
action_style: None,
interactive: Some(true),
content: None,
fast_path: None,
})
);
}
#[test]
fn golden_flows() {
let temp = TempDir::new().unwrap();
let repo = temp.path();
write_flow(
repo,
"forked",
r#"
- and:
branches:
- step: { name: implement }
- step: { name: polish }
- and:
branches:
- step: { name: quick }
- step: { name: deep }
- flow: nested
"#,
);
let flow = load_flow("forked", repo).unwrap();
assert_eq!(flow.items.len(), 3);
match &flow.items[0] {
FlowItem::And { branches, .. } => {
assert_eq!(branches.len(), 2);
}
_ => panic!("expected and"),
}
match &flow.items[1] {
FlowItem::And { branches, .. } => {
assert_eq!(branches.len(), 2);
}
_ => panic!("expected and"),
}
match &flow.items[2] {
FlowItem::FlowRef(name) => {
assert_eq!(name, "nested");
}
_ => panic!("expected flow ref"),
}
}
#[test]
fn and_select_is_rejected() {
let temp = TempDir::new().unwrap();
let repo = temp.path();
write_flow(
repo,
"forked",
r#"
- and:
branches:
- step: { name: implement }
select: all
"#,
);
let err = load_flow("forked", repo).expect_err("and select should fail");
let message = err.to_string();
assert!(message.contains("and select modes are not supported"));
}
#[test]
fn flow_ref_parses_into_items() {
let temp = TempDir::new().unwrap();
let repo = temp.path();
write_flow(
repo,
"child",
r#"
- implement
"#,
);
write_flow(
repo,
"parent",
r#"
- flow: child
- review
"#,
);
let flow = load_flow("parent", repo).unwrap();
assert_eq!(flow.items.len(), 2);
assert!(matches!(flow.items[0], FlowItem::FlowRef(_)));
assert!(matches!(flow.items[1], FlowItem::Step(_)));
}
#[test]
fn ops_item_parses_and_expands() {
let temp = TempDir::new().unwrap();
let repo = temp.path();
write_flow(
repo,
"ship-ish",
r#"
- implement
- op: land --create-pr
"#,
);
let flow = load_flow("ship-ish", repo).unwrap();
assert_eq!(flow.items.len(), 2);
match &flow.items[1] {
FlowItem::Op(item) => {
assert_eq!(item.command, "land");
assert_eq!(item.args, vec!["--create-pr"]);
}
other => panic!("expected ops item, got {other:?}"),
}
let expanded = expand_flow(&flow, repo).unwrap();
assert!(matches!(&expanded[1], ConcreteItem::Op(_)));
}
#[test]
fn expand_flow_tracks_parents() {
let temp = TempDir::new().unwrap();
let repo = temp.path();
write_flow(
repo,
"child",
r#"
- implement
"#,
);
write_flow(
repo,
"parent",
r#"
- flow: child
- review
"#,
);
let flow = load_flow("parent", repo).unwrap();
let items = expand_flow(&flow, repo).unwrap();
match &items[0] {
ConcreteItem::Step(step) => {
assert_eq!(step.step.name, "implement");
assert_eq!(step.flow_parents, vec!["parent", "child"]);
}
_ => panic!("expected expanded step"),
}
}
#[test]
fn expand_flow_resolves_plain_string_as_subflow() {
let temp = TempDir::new().unwrap();
let repo = temp.path();
write_flow(repo, "publish", "- step-a\n- step-b");
write_flow(repo, "parent", "- review\n- publish");
let items = expand_named_flow(repo, "parent");
assert_eq!(items.len(), 3, "publish should expand into its sub-steps");
assert_step_name(&items[0], "review");
match &items[1] {
ConcreteItem::Step(s) => {
assert_eq!(s.step.name, "step-a");
assert_eq!(s.flow_parents, vec!["parent", "publish"]);
}
_ => panic!("expected step from publish sub-flow"),
}
match &items[2] {
ConcreteItem::Step(s) => {
assert_eq!(s.step.name, "step-b");
assert_eq!(s.flow_parents, vec!["parent", "publish"]);
}
_ => panic!("expected step from publish sub-flow"),
}
}
#[test]
fn expand_flow_prefers_step_over_single_step_flow() {
let temp = TempDir::new().unwrap();
let repo = temp.path();
write_step(repo, "review", "Review the code.");
write_flow(repo, "parent", "- review");
let items = expand_named_flow(repo, "parent");
assert_eq!(items.len(), 1);
match &items[0] {
ConcreteItem::Step(s) => {
assert_eq!(s.step.name, "review");
assert_eq!(s.flow_parents, vec!["parent"]);
}
_ => panic!("expected step"),
}
}
#[test]
fn builtin_deploy_uses_ops_land_item() {
let temp = TempDir::new().unwrap();
let repo = temp.path();
let items = expand_named_flow(repo, "deploy");
assert!(!items.is_empty());
assert!(matches!(&items[1], ConcreteItem::Op(_)));
}
#[test]
fn builtin_garden_flow_structure() {
let temp = TempDir::new().unwrap();
let repo = temp.path();
let items = expand_named_flow(repo, "garden");
assert_eq!(items.len(), 3);
assert_step_name(&items[0], "scan");
assert_step_name(&items[1], "assess");
match &items[2] {
ConcreteItem::Xor(xor_def) => {
assert_eq!(xor_def.paths.len(), 2);
assert!(xor_def.paths.contains_key("act"));
assert!(xor_def.paths.contains_key("silence"));
}
other => panic!("expected Xor, got {other:?}"),
}
}
#[test]
fn builtin_governance_flows_structure() {
let temp = TempDir::new().unwrap();
let repo = temp.path();
let cases = [
("govern-identity", ["s5-scan", "s5-assess", "mutate"]),
("govern-intelligence", ["s4-scan", "s4-assess", "mutate"]),
("govern-control", ["s3-scan", "s3-assess", "mutate"]),
("govern-coordination", ["s2-scan", "s2-assess", "mutate"]),
];
for (flow_name, expected) in cases {
let items = expand_named_flow(repo, flow_name);
assert_step_sequence(&items, &expected);
}
}
#[test]
fn builtin_build_or_silent_has_xor_branch() {
let temp = TempDir::new().unwrap();
let repo = temp.path();
let items = expand_named_flow(repo, "build-or-silent");
assert_eq!(items.len(), 3);
assert!(
matches!(&items[0], ConcreteItem::Op(_)),
"expected op:pm pull at index 0"
);
assert_step_name(&items[1], "ingest");
match &items[2] {
ConcreteItem::Xor(xor_def) => {
assert!(xor_def.paths.contains_key("build"));
assert!(xor_def.paths.contains_key("silence"));
}
other => panic!("expected Xor in build-or-silent, got {other:?}"),
};
}