mod common;
use std::process::Command;
use common::{cli_fixtures_dir, sonda_bin};
#[test]
fn run_v1_scenario_is_rejected_with_migration_hint() {
let fixture = cli_fixtures_dir().join("inline-v1.yaml");
let output = Command::new(sonda_bin())
.args(["--quiet", "run", "--scenario"])
.arg(&fixture)
.output()
.expect("must spawn sonda");
assert!(
!output.status.success(),
"v1 multi-scenario must not succeed; stderr:\n{}",
String::from_utf8_lossy(&output.stderr),
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("v2"),
"rejection must mention v2 requirement, got:\n{stderr}"
);
}
#[test]
fn run_v2_scenario_succeeds() {
let fixture = cli_fixtures_dir().join("inline.v2.yaml");
let output = Command::new(sonda_bin())
.args(["--quiet", "run", "--scenario"])
.arg(&fixture)
.output()
.expect("must spawn sonda");
assert!(
output.status.success(),
"v2 run failed: {:?}\nstderr:\n{}",
output.status.code(),
String::from_utf8_lossy(&output.stderr),
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("v2_inline_metric"),
"expected v2 metric name in stdout, got:\n{stdout}"
);
}
#[test]
fn run_v2_dry_run_emits_spec_pretty_output() {
let fixture = cli_fixtures_dir().join("multi-after-chain.v2.yaml");
let output = Command::new(sonda_bin())
.args(["run", "--scenario"])
.arg(&fixture)
.arg("--dry-run")
.output()
.expect("must spawn sonda");
assert!(
output.status.success(),
"v2 dry-run failed: {:?}\nstderr:\n{}",
output.status.code(),
String::from_utf8_lossy(&output.stderr),
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("[config] file:") && stderr.contains("version: 2"),
"missing v2 header in stderr:\n{stderr}"
);
assert!(
stderr.contains("Validation: OK"),
"missing validation footer:\n{stderr}"
);
assert!(
stderr.contains("phase_offset:"),
"missing phase_offset annotation:\n{stderr}"
);
assert!(
stderr.contains("clock_group:") && stderr.contains("(auto)"),
"missing auto clock_group line:\n{stderr}"
);
assert!(
output.stdout.is_empty(),
"dry-run must not write to stdout, got:\n{}",
String::from_utf8_lossy(&output.stdout)
);
}
#[test]
fn run_v2_dry_run_json_format_emits_stable_dto() {
let fixture = cli_fixtures_dir().join("inline.v2.yaml");
let output = Command::new(sonda_bin())
.args(["run", "--scenario"])
.arg(&fixture)
.args(["--dry-run", "--format=json"])
.output()
.expect("must spawn sonda");
assert!(
output.status.success(),
"v2 dry-run --format=json failed: {:?}\nstderr:\n{}",
output.status.code(),
String::from_utf8_lossy(&output.stderr),
);
let stdout = String::from_utf8_lossy(&output.stdout);
let json: serde_json::Value = serde_json::from_str(&stdout).expect("json output must parse");
assert_eq!(json["version"], 2);
assert_eq!(json["scenarios"][0]["name"], "v2_inline_metric");
assert_eq!(json["scenarios"][0]["signal"], "metrics");
}
#[test]
fn run_flat_v1_single_scenario_is_rejected_with_migration_hint() {
let fixture = cli_fixtures_dir().join("flat-v1-metrics.yaml");
let output = Command::new(sonda_bin())
.args(["--quiet", "run", "--scenario"])
.arg(&fixture)
.output()
.expect("must spawn sonda");
assert!(
!output.status.success(),
"flat v1 file must not succeed; stderr:\n{}",
String::from_utf8_lossy(&output.stderr),
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("v2"),
"rejection must mention v2 requirement, got:\n{stderr}"
);
assert!(
stderr.contains("v2-scenarios.md") || stderr.contains("Migrate"),
"rejection must point at migration guide, got:\n{stderr}"
);
}
#[test]
fn catalog_run_cpu_spike_builtin_succeeds() {
let repo_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("sonda crate has a parent")
.to_path_buf();
let scenarios_dir = repo_root.join("scenarios");
if !scenarios_dir.exists() {
eprintln!("skipping: {} missing", scenarios_dir.display());
return;
}
let output = Command::new(sonda_bin())
.args(["--quiet", "--scenario-path"])
.arg(&scenarios_dir)
.args([
"catalog",
"run",
"cpu-spike",
"--duration",
"300ms",
"--rate",
"1",
])
.output()
.expect("must spawn sonda");
assert!(
output.status.success(),
"catalog run cpu-spike failed: {:?}\nstderr:\n{}",
output.status.code(),
String::from_utf8_lossy(&output.stderr),
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("node_cpu_usage_percent"),
"expected cpu-spike metric name in stdout, got:\n{stdout}"
);
}
#[test]
fn v2_compile_error_surfaces_with_context() {
let fixture = cli_fixtures_dir().join("broken-self-ref.v2.yaml");
let output = Command::new(sonda_bin())
.args(["run", "--scenario"])
.arg(&fixture)
.arg("--dry-run")
.output()
.expect("must spawn sonda");
assert!(
!output.status.success(),
"self-ref must produce non-zero exit"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("broken-self-ref.v2.yaml") || stderr.to_lowercase().contains("self"),
"error must identify the source file or the self-reference, got:\n{stderr}"
);
}