diff --git a/crates/engine/tests/hostile_params.rs b/crates/engine/tests/hostile_params.rs new file mode 100644 index 00000000..b7769239 --- /dev/null +++ b/crates/engine/tests/hostile_params.rs @@ -0,0 +1,104 @@ +//! Every command survives hostile parameters (what an agent or a buggy script might send). + +use proptest::prelude::*; +use serde_json::{Value, json}; + +const KEYS: &[&str] = &[ + "track", + "tracks", + "start", + "end", + "at", + "length", + "clip", + "clips", + "db", + "delta_db", + "value", + "pan", + "count", + "ratio", + "bpm", + "format", + "slot", + "param", + "plugin", + "mode", + "tool", + "kind", + "name", + "index", + "number", + "semitones", + "by", + "to", + "grid", + "height", + "view", + "color", + "params", + "process", + "group", + "bus", + "input", + "output", + "numerator", + "denominator", + "factor", + "sensitivity", + "rating", + "direction", + "seconds", + "frames", + "samples", + "notes", + "pitch", + "velocity", + "flag", +]; + +fn hostile_value() -> impl Strategy { + prop_oneof![ + Just(json!(null)), + Just(json!(-1)), + Just(json!(0)), + Just(json!(i64::MAX)), + Just(json!(i64::MIN)), + Just(json!(1.0e308)), + Just(json!(-1.0e308)), + Just(json!(0.5)), + Just(json!("")), + Just(json!("Kick")), + Just(json!("bars_beats:9999999|99|9999")), + Just(json!("\u{0}\u{fffd}")), + Just(json!([])), + Just(json!([1, "Kick", null, -5])), + Just(json!({})), + Just(json!({"seconds": -1e12})), + Just(json!({"a": 1})), + Just(json!(true)), + ] +} + +proptest! { + #![proptest_config(ProptestConfig { cases: 8, .. ProptestConfig::default() })] + #[test] + fn no_command_panics_on_hostile_params(fields in proptest::collection::vec((0usize..KEYS.len(), hostile_value()), 0..6)) { + let mut params = serde_json::Map::new(); + for (k, v) in fields { + params.insert(KEYS[k].to_string(), v); + } + let params = Value::Object(params); + for spec in soundcraft_engine::command_specs() { + // Commands that write files take paths; keep the test hermetic. + if spec.params.contains("path") || spec.params.contains("dir") || spec.id == "app.quit" { + continue; + } + let mut e = soundcraft_engine::demo::demo_engine(); + let _ = e.execute("edit.select", &json!({"tracks": ["Kick", "Keys"], "start": 100_000, "end": 600_000})); + if let Err(soundcraft_engine::EngineError::Internal(id, msg)) = e.execute(spec.id, ¶ms) { + prop_assert!(false, "{id} panicked with {params}: {msg}"); + } + } + } +}