#![cfg(unix)]
use std::path::Path;
use std::process::Command;
const CUTTLEFISH: &str = env!("CARGO_BIN_EXE_cuttlefish");
const FAN_IN_SPEC: &str = r#"
spec fan_in_spec = {
description = "A spec whose graph fans in, used to test cuttlefish build's refusal.";
model = Path "./models/stub.gguf";
data_policy = Any;
capabilities = [ ];
nodes = {
a = { block = "blocks/a.wasm" };
b = { block = "blocks/b.wasm" };
c = { block = "blocks/c.wasm"; in = { x = a.out; y = b.out; }; };
};
}
"#;
fn run_build(spec_path: &Path) -> std::process::Output {
Command::new(CUTTLEFISH)
.args(["build"])
.arg(spec_path)
.output()
.expect("the cuttlefish binary under test must be spawnable")
}
#[test]
fn build_refuses_a_fan_in_graph_before_touching_the_catalog() {
let dir = tempfile::tempdir().unwrap();
let spec_path = dir.path().join("fan_in.cuttlefish");
std::fs::write(&spec_path, FAN_IN_SPEC).unwrap();
let out = run_build(&spec_path);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success(),
"build must fail on a fan-in graph: stdout={}\nstderr={stderr}",
String::from_utf8_lossy(&out.stdout)
);
assert!(
stderr.contains("isn't a simple linear chain"),
"the refusal must name the real reason (fan-in/loop/branches), not some \
downstream resolution failure: {stderr}"
);
assert!(
!stderr.contains("resolving the pipeline"),
"the refusal must happen before block resolution is ever attempted: {stderr}"
);
assert!(!spec_path.with_extension("cfbundle").exists());
}