#![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());
}
const SCRIPT_TEXT: &str = "//! signature: {n: json} -> {n: json}\ninput\n";
#[test]
fn build_refuses_a_spec_with_a_script_node() {
let dir = tempfile::tempdir().unwrap();
let script_path = dir.path().join("echo.rhai");
std::fs::write(&script_path, SCRIPT_TEXT).unwrap();
let home = dir.path().join("home");
std::fs::create_dir_all(&home).unwrap();
let add = Command::new(CUTTLEFISH)
.args(["catalog", "add", "echo@1"])
.arg(&script_path)
.env("CUTTLEFISH_HOME", &home)
.output()
.expect("the cuttlefish binary under test must be spawnable");
assert!(
add.status.success(),
"cataloging the fixture script failed: {}",
String::from_utf8_lossy(&add.stderr)
);
let spec_path = dir.path().join("script_node.cuttlefish");
std::fs::write(
&spec_path,
r#"
spec script_node_spec = {
description = "A spec with a Script node, used to test cuttlefish build's refusal.";
model = Path "./models/stub.gguf";
data_policy = Any;
capabilities = [ ];
nodes = {
echo = { block = "echo@1" };
};
}
"#,
)
.unwrap();
let out = Command::new(CUTTLEFISH)
.args(["build"])
.arg(&spec_path)
.env("CUTTLEFISH_HOME", &home)
.output()
.expect("the cuttlefish binary under test must be spawnable");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success(),
"build must refuse a Script node: stdout={}\nstderr={stderr}",
String::from_utf8_lossy(&out.stdout)
);
assert!(
stderr.contains("echo") && stderr.to_lowercase().contains("script"),
"the refusal must name the node and mention it's a script: {stderr}"
);
assert!(!spec_path.with_extension("cfbundle").exists());
}
#[test]
fn build_s_script_refusal_names_the_node_key_not_the_block_name() {
let dir = tempfile::tempdir().unwrap();
let script_path = dir.path().join("echo.rhai");
std::fs::write(&script_path, SCRIPT_TEXT).unwrap();
let home = dir.path().join("home");
std::fs::create_dir_all(&home).unwrap();
let add = Command::new(CUTTLEFISH)
.args(["catalog", "add", "echo@1"])
.arg(&script_path)
.env("CUTTLEFISH_HOME", &home)
.output()
.expect("the cuttlefish binary under test must be spawnable");
assert!(
add.status.success(),
"cataloging the fixture script failed: {}",
String::from_utf8_lossy(&add.stderr)
);
let spec_path = dir.path().join("renamed_node.cuttlefish");
std::fs::write(
&spec_path,
r#"
spec renamed_node_spec = {
description = "A Script node whose key differs from its block name.";
model = Path "./models/stub.gguf";
data_policy = Any;
capabilities = [ ];
nodes = {
sum = { block = "echo@1" };
};
}
"#,
)
.unwrap();
let out = Command::new(CUTTLEFISH)
.args(["build"])
.arg(&spec_path)
.env("CUTTLEFISH_HOME", &home)
.output()
.expect("the cuttlefish binary under test must be spawnable");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(!out.status.success(), "build must refuse a Script node");
assert!(
stderr.contains("`sum`"),
"the refusal must name the node key `sum`, not the block name `echo`: {stderr}"
);
}