use assert_cmd::Command;
use predicates::prelude::*;
fn cascade_viz() -> Command {
Command::cargo_bin("cascade-viz").unwrap()
}
#[test]
fn cascade_viz_live_tree_renders() {
cascade_viz()
.args(["live", "-n", "16", "-L", "2", "--no-color"])
.assert()
.success()
.stdout(predicate::str::contains("n0"))
.stdout(predicate::str::contains("├──"))
.stdout(predicate::str::contains("✔"));
}
#[test]
fn cascade_viz_live_json_is_valid() {
let output = cascade_viz()
.args(["--format", "json", "live", "-n", "8"])
.output()
.expect("failed to run cascade-viz");
assert!(output.status.success(), "exit status: {:?}", output.status);
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: serde_json::Value =
serde_json::from_str(&stdout).expect("stdout must be valid JSON");
assert!(parsed.is_array(), "expected JSON array of events");
let arr = parsed.as_array().unwrap();
assert!(
arr.len() >= 2,
"expected at least 2 events, got {}",
arr.len()
);
assert_eq!(arr[0]["kind"], "started");
assert_eq!(arr.last().unwrap()["kind"], "finished");
}
#[test]
fn cascade_viz_live_jsonl_each_line_is_valid_json() {
let output = cascade_viz()
.args(["--format", "jsonl", "live", "-n", "8"])
.output()
.expect("failed to run cascade-viz");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
for (i, line) in stdout.lines().enumerate() {
if line.trim().is_empty() {
continue;
}
let _: serde_json::Value = serde_json::from_str(line)
.unwrap_or_else(|e| panic!("line {i} invalid JSON: {e}\n line: {line}"));
}
}
#[test]
fn cascade_viz_live_max_bottleneck_strategy() {
cascade_viz()
.args(["live", "-n", "8", "-s", "max-bottleneck", "--no-color"])
.assert()
.success()
.stdout(predicate::str::contains("n0"));
}
#[test]
fn cascade_viz_live_steiner_strategy() {
cascade_viz()
.args(["live", "-n", "8", "-s", "steiner", "--no-color"])
.assert()
.success()
.stdout(predicate::str::contains("n0"));
}
#[test]
fn cascade_viz_replay_renders_tree() {
use std::io::Write;
let dir = tempfile::tempdir().unwrap();
let trace_path = dir.path().join("trace.jsonl");
let mut f = std::fs::File::create(&trace_path).unwrap();
writeln!(
f,
r#"{{"kind":"started","n_nodes":2,"seeded":[0],"strategy":"log2-fanout","at":0}}"#
)
.unwrap();
writeln!(
f,
r#"{{"kind":"edge_completed","round":0,"src":0,"tgt":1,"duration":100000000}}"#
)
.unwrap();
writeln!(
f,
r#"{{"kind":"finished","converged":2,"failed":0,"rounds":1}}"#
)
.unwrap();
drop(f);
cascade_viz()
.args(["replay", trace_path.to_str().unwrap(), "--no-color"])
.assert()
.success()
.stdout(predicate::str::contains("n0"))
.stdout(predicate::str::contains("✔"));
}
#[test]
fn cascade_viz_replay_bad_file_errors() {
cascade_viz()
.args(["replay", "/nonexistent/trace.jsonl", "--no-color"])
.assert()
.failure()
.stderr(predicate::str::contains("failed to open trace file"));
}
#[test]
fn cascade_viz_live_renders_all_requested_nodes() {
let output = cascade_viz()
.args(["live", "-n", "4", "--no-color"])
.output()
.expect("failed to run cascade-viz");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
for n in 0..4 {
assert!(
stdout.contains(&format!("n{n}")),
"expected n{n} in output: {stdout}"
);
}
}
#[test]
fn cascade_viz_max_depth_zero_shows_only_root() {
let output = cascade_viz()
.args(["live", "-n", "16", "-L", "0", "--no-color"])
.output()
.expect("failed to run cascade-viz");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("n0"), "missing seed root: {stdout}");
assert!(
stdout.contains("more)"),
"expected depth-limit truncation marker: {stdout}"
);
}