mod common;
use std::process::Command;
use common::{cli_fixtures_dir, sonda_bin};
fn scenarios_dir() -> std::path::PathBuf {
cli_fixtures_dir().join("catalog-scenarios")
}
fn packs_dir() -> std::path::PathBuf {
cli_fixtures_dir().join("catalog-packs")
}
#[test]
fn catalog_list_shows_scenarios_and_packs() {
let output = Command::new(sonda_bin())
.args(["--scenario-path"])
.arg(scenarios_dir())
.args(["--pack-path"])
.arg(packs_dir())
.args(["catalog", "list"])
.output()
.expect("must spawn sonda");
assert!(
output.status.success(),
"list failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("scn-a"), "missing scenario A:\n{stdout}");
assert!(stdout.contains("scn-b"), "missing scenario B:\n{stdout}");
assert!(stdout.contains("tiny_pack"), "missing pack:\n{stdout}");
}
#[test]
fn catalog_list_type_filter_scenarios() {
let output = Command::new(sonda_bin())
.args(["--scenario-path"])
.arg(scenarios_dir())
.args(["--pack-path"])
.arg(packs_dir())
.args(["catalog", "list", "--type", "scenario"])
.output()
.expect("must spawn sonda");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("scn-a"));
assert!(
!stdout.contains("tiny_pack"),
"pack must not appear:\n{stdout}"
);
}
#[test]
fn catalog_list_type_filter_packs() {
let output = Command::new(sonda_bin())
.args(["--scenario-path"])
.arg(scenarios_dir())
.args(["--pack-path"])
.arg(packs_dir())
.args(["catalog", "list", "--type", "pack"])
.output()
.expect("must spawn sonda");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("tiny_pack"));
assert!(
!stdout.contains("scn-a"),
"scenario must not appear:\n{stdout}"
);
}
#[test]
fn catalog_list_category_filter() {
let output = Command::new(sonda_bin())
.args(["--scenario-path"])
.arg(scenarios_dir())
.args(["--pack-path"])
.arg(packs_dir())
.args(["catalog", "list", "--category", "network"])
.output()
.expect("must spawn sonda");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("scn-a"));
assert!(stdout.contains("tiny_pack"));
assert!(
!stdout.contains("scn-b"),
"infrastructure entry must be filtered out:\n{stdout}"
);
}
#[test]
fn catalog_list_json_emits_stable_dto() {
let output = Command::new(sonda_bin())
.args(["--scenario-path"])
.arg(scenarios_dir())
.args(["--pack-path"])
.arg(packs_dir())
.args(["catalog", "list", "--json"])
.output()
.expect("must spawn sonda");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let entries: Vec<serde_json::Value> = serde_json::from_str(&stdout).expect("json must parse");
let scn = entries
.iter()
.find(|e| e["name"] == "scn-a")
.expect("scn-a must appear");
assert_eq!(scn["type"], "scenario");
assert_eq!(scn["category"], "network");
assert_eq!(scn["signal"], "metrics");
assert_eq!(scn["runnable"], true);
let pack = entries
.iter()
.find(|e| e["name"] == "tiny_pack")
.expect("tiny_pack must appear");
assert_eq!(pack["type"], "pack");
assert_eq!(pack["runnable"], false);
}
#[test]
fn catalog_show_scenario_prints_yaml() {
let output = Command::new(sonda_bin())
.args(["--scenario-path"])
.arg(scenarios_dir())
.args(["--pack-path"])
.arg(packs_dir())
.args(["catalog", "show", "scn-a"])
.output()
.expect("must spawn sonda");
assert!(
output.status.success(),
"show failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("scn_a_metric"),
"must dump YAML content on stdout:\n{stdout}"
);
}
#[test]
fn catalog_show_pack_prints_yaml() {
let output = Command::new(sonda_bin())
.args(["--scenario-path"])
.arg(scenarios_dir())
.args(["--pack-path"])
.arg(packs_dir())
.args(["catalog", "show", "tiny_pack"])
.output()
.expect("must spawn sonda");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("pack_metric_a") && stdout.contains("pack_metric_b"),
"must dump pack YAML content on stdout:\n{stdout}"
);
}
#[test]
fn catalog_run_scenario_succeeds() {
let output = Command::new(sonda_bin())
.args(["--quiet", "--scenario-path"])
.arg(scenarios_dir())
.args(["--pack-path"])
.arg(packs_dir())
.args(["catalog", "run", "scn-a"])
.output()
.expect("must spawn sonda");
assert!(
output.status.success(),
"scenario run failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("scn_a_metric"),
"expected metric output, got:\n{stdout}"
);
}
#[test]
fn catalog_run_pack_with_labels_succeeds() {
let output = Command::new(sonda_bin())
.args(["--quiet", "--scenario-path"])
.arg(scenarios_dir())
.args(["--pack-path"])
.arg(packs_dir())
.args([
"catalog",
"run",
"tiny_pack",
"--rate",
"1",
"--duration",
"300ms",
"--label",
"device=rtr-test-01",
])
.output()
.expect("must spawn sonda");
assert!(
output.status.success(),
"pack run failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("pack_metric_a"),
"expected pack metric output, got:\n{stdout}"
);
assert!(
stdout.contains("rtr-test-01"),
"expected label value in output, got:\n{stdout}"
);
}
#[test]
fn catalog_run_pack_honors_output_flag() {
let tmp = tempfile::tempdir().expect("must create temp dir");
let out_path = tmp.path().join("pack-out.prom");
let output = Command::new(sonda_bin())
.args(["--quiet", "--scenario-path"])
.arg(scenarios_dir())
.args(["--pack-path"])
.arg(packs_dir())
.args(["catalog", "run", "tiny_pack", "-o"])
.arg(&out_path)
.args([
"--rate",
"1",
"--duration",
"300ms",
"--label",
"device=rtr-test-01",
])
.output()
.expect("must spawn sonda");
assert!(
output.status.success(),
"pack run -o failed: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(
out_path.exists(),
"output file must be created at {}",
out_path.display()
);
let contents = std::fs::read_to_string(&out_path).expect("must read output file");
assert!(
!contents.is_empty(),
"output file must have metric lines, got empty file"
);
assert!(
contents.contains("pack_metric_a") || contents.contains("pack_metric_b"),
"output file must contain a pack metric, got:\n{contents}"
);
assert!(
contents.contains("device=\"rtr-test-01\""),
"output file must carry the --label override, got:\n{contents}"
);
assert!(
output.stdout.is_empty(),
"stdout must be empty when -o redirects to file, got:\n{}",
String::from_utf8_lossy(&output.stdout)
);
}
#[test]
fn catalog_run_unknown_name_errors() {
let output = Command::new(sonda_bin())
.args(["--scenario-path"])
.arg(scenarios_dir())
.args(["--pack-path"])
.arg(packs_dir())
.args(["catalog", "run", "does-not-exist"])
.output()
.expect("must spawn sonda");
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("does-not-exist") || stderr.contains("unknown"),
"error must mention the bad name:\n{stderr}"
);
}
#[test]
fn legacy_scenarios_subcommand_still_works() {
let output = Command::new(sonda_bin())
.args(["--scenario-path"])
.arg(scenarios_dir())
.args(["scenarios", "list"])
.output()
.expect("must spawn sonda");
assert!(
output.status.success(),
"scenarios list must still work: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("scn-a"));
}
#[test]
fn legacy_packs_subcommand_still_works() {
let output = Command::new(sonda_bin())
.args(["--pack-path"])
.arg(packs_dir())
.args(["packs", "list"])
.output()
.expect("must spawn sonda");
assert!(
output.status.success(),
"packs list must still work: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("tiny_pack"));
}
#[test]
fn hidden_subcommands_are_absent_from_top_level_help() {
let output = Command::new(sonda_bin())
.arg("--help")
.output()
.expect("must spawn sonda");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("catalog"), "catalog must appear in help");
assert!(
!stdout.contains("\n scenarios "),
"scenarios should be hidden, got:\n{stdout}"
);
assert!(
!stdout.contains("\n packs "),
"packs should be hidden, got:\n{stdout}"
);
assert!(
!stdout.contains("\n story "),
"story should be hidden, got:\n{stdout}"
);
}