#![allow(missing_docs, clippy::unwrap_used, clippy::expect_used)]
use serde_json::{Value, json};
use std::io::Write;
use std::process::{Command, Stdio};
fn cli(args: &[&str]) -> std::process::Output {
Command::new(env!("CARGO_BIN_EXE_saccade"))
.args(args)
.output()
.unwrap()
}
fn schema(value: &Value, file: &str) {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../saccade-core/schemas")
.join(file);
let doc: Value = serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
jsonschema::validator_for(&doc)
.unwrap()
.validate(value)
.unwrap();
}
#[test]
fn cli_artifacts_schemas_junit_and_root_confined_mcp() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let frames = root.join("frames");
std::fs::create_dir(&frames).unwrap();
for i in 0..64 {
image::RgbImage::from_pixel(
64,
32,
image::Rgb(if i % 8 < 4 { [255; 3] } else { [0; 3] }),
)
.save(frames.join(format!("frame_{i:03}.png")))
.unwrap();
}
let out = root.join("safety");
let xml = root.join("safety.xml");
let result = cli(&[
"experiment",
"safety",
frames.to_str().unwrap(),
"--fps",
"32",
"--out",
out.to_str().unwrap(),
"--json",
"--junit",
xml.to_str().unwrap(),
]);
assert_eq!(
result.status.code(),
Some(1),
"{}",
String::from_utf8_lossy(&result.stderr)
);
let value: Value =
serde_json::from_slice(&std::fs::read(out.join("saccade-safety.v1.json")).unwrap())
.unwrap();
schema(&value, "saccade-safety.v1.schema.json");
assert!(out.join("index.html").exists());
assert!(out.join("report.txt").exists());
assert!(std::fs::read_to_string(xml).unwrap().contains("<failure"));
let picture = frames.join("frame_000.png");
let aout = root.join("a11y");
let a = cli(&[
"experiment",
"a11y",
picture.to_str().unwrap(),
"--out",
aout.to_str().unwrap(),
"--json",
]);
assert_eq!(a.status.code(), Some(0));
schema(
&serde_json::from_slice(&std::fs::read(aout.join("saccade-a11y.v1.json")).unwrap())
.unwrap(),
"saccade-a11y.v1.schema.json",
);
let bad = cli(&[
"experiment",
"safety",
frames.to_str().unwrap(),
"--fps",
"0",
"--json",
"--out",
root.join("bad").to_str().unwrap(),
]);
assert_eq!(bad.status.code(), Some(2));
assert_eq!(
serde_json::from_slice::<Value>(&bad.stdout).unwrap()["schema"],
"saccade-result.v2"
);
let outputs = tempfile::tempdir().unwrap();
let mut child = Command::new(env!("CARGO_BIN_EXE_saccade"))
.args(["mcp", "--root", root.to_str().unwrap()])
.arg("--out-root")
.arg(outputs.path())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
let mut stdin = child.stdin.take().unwrap();
for msg in [
json!({"jsonrpc":"2.0","id":1,"method":"tools/list"}),
json!({"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"saccade_measure","arguments":{"operation":"safety","input":"frames","fps":32,"out":"mcp-safety"}}}),
json!({"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"saccade_measure","arguments":{"operation":"a11y","input":"frames/frame_000.png","out":"mcp-a11y"}}}),
json!({"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"saccade_measure","arguments":{"operation":"safety","input":"../escape","out":"mcp-unsafe"}}}),
] {
writeln!(stdin, "{msg}").unwrap();
}
drop(stdin);
let output = child.wait_with_output().unwrap();
assert!(output.status.success());
let replies: Vec<Value> = String::from_utf8(output.stdout)
.unwrap()
.lines()
.map(|s| serde_json::from_str(s).unwrap())
.collect();
assert!(
replies[0]["result"]["tools"]
.as_array()
.unwrap()
.iter()
.any(|t| t["name"] == "saccade_measure")
);
schema(
&replies[1]["result"]["structuredContent"],
"saccade-result.v2.schema.json",
);
schema(
&replies[2]["result"]["structuredContent"],
"saccade-result.v2.schema.json",
);
assert_eq!(replies[3]["result"]["isError"], true);
assert_eq!(
replies[3]["result"]["structuredContent"]["errors"][0]["code"],
"unsafe_path"
);
}
#[test]
fn video_metadata_and_missing_optional_ffmpeg() {
let tmp = tempfile::tempdir().unwrap();
let video = tmp.path().join("sample.mp4");
if Command::new("ffmpeg").arg("-version").output().is_ok()
&& Command::new("ffprobe").arg("-version").output().is_ok()
{
let generated = Command::new("ffmpeg")
.args([
"-v",
"error",
"-f",
"lavfi",
"-i",
"color=c=black:s=64x32:r=24:d=0.5",
"-c:v",
"mpeg4",
])
.arg(&video)
.output()
.unwrap();
assert!(
generated.status.success(),
"{}",
String::from_utf8_lossy(&generated.stderr)
);
let out = tmp.path().join("video-report");
let result = cli(&[
"experiment",
"safety",
video.to_str().unwrap(),
"--json",
"--out",
out.to_str().unwrap(),
]);
assert_eq!(
result.status.code(),
Some(0),
"{}",
String::from_utf8_lossy(&result.stderr)
);
let value: Value =
serde_json::from_slice(&std::fs::read(out.join("saccade-safety.v1.json")).unwrap())
.unwrap();
assert_eq!(value["fps"], 24.0);
schema(&value, "saccade-safety.v1.schema.json");
} else {
std::fs::write(&video, b"placeholder").unwrap();
}
let result = Command::new(env!("CARGO_BIN_EXE_saccade"))
.env("PATH", tmp.path())
.args([
"experiment",
"safety",
video.to_str().unwrap(),
"--fps",
"24",
"--json",
"--out",
tmp.path().join("missing").to_str().unwrap(),
])
.output()
.unwrap();
assert_eq!(result.status.code(), Some(2));
assert!(String::from_utf8_lossy(&result.stdout).contains("ffmpeg on PATH"));
}