//! Every JSON object in every fixture must reject an unknown key. Do not edit. //! //! The other test files check unknown fields in a few hand-picked places. This one checks all of //! them: it walks each fixture, adds one unknown key to one object at a time, at every depth, and //! requires that the result no longer decodes. use proto::{AuditRecord, Envelope, Grant, LogRecord}; use serde::de::DeserializeOwned; use serde_json::Value; /// Every copy of `value` that has exactly one extra key in exactly one object. fn with_one_unknown_key(value: &Value) -> Vec { let mut out = Vec::new(); match value { Value::Object(map) => { let mut extended = map.clone(); extended.insert("zz_unknown".to_string(), Value::Bool(true)); out.push(Value::Object(extended)); for (key, child) in map { for changed in with_one_unknown_key(child) { let mut copy = map.clone(); copy.insert(key.clone(), changed); out.push(Value::Object(copy)); } } } Value::Array(items) => { for (i, child) in items.iter().enumerate() { for changed in with_one_unknown_key(child) { let mut copy = items.clone(); copy[i] = changed; out.push(Value::Array(copy)); } } } _ => {} } out } /// Returns how many variations were tried, so callers can check the walk reached nested objects. fn check(what: &str, text: &str) -> usize { let value: Value = serde_json::from_str(text).unwrap_or_else(|e| panic!("{what}: {e}")); assert!( serde_json::from_value::(value.clone()).is_ok(), "{what}: fixture must decode" ); let variations = with_one_unknown_key(&value); for changed in &variations { assert!( serde_json::from_value::(changed.clone()).is_err(), "{what}: accepted an unknown key: {changed}" ); } variations.len() } fn fixture(path: &str) -> String { let full = format!("{}/tests/fixtures/{path}", env!("CARGO_MANIFEST_DIR")); std::fs::read_to_string(&full).unwrap_or_else(|e| panic!("{full}: {e}")) } #[test] fn envelopes_reject_unknown_keys_at_every_depth() { for name in [ "tool_request.json", "tool_response_pending.json", "tool_response_result.json", "tool_response_failed.json", "tool_response_denied.json", "error.json", "turn.json", "turn_event_tool_result.json", "turn_event_content.json", "turn_event_retrying.json", "turn_done.json", "error_session_full.json", ] { // Envelope, msg and body: three objects; turn_done also has a usage object. let want = if name == "turn_done.json" { 4 } else { 3 }; assert_eq!( check::(name, &fixture(&format!("wire/{name}"))), want, "{name}" ); } } #[test] fn audit_records_reject_unknown_keys_at_every_depth() { for (i, line) in fixture("records/audit.jsonl").lines().enumerate() { // The record and its decision: two objects. assert_eq!( check::(&format!("audit.jsonl:{}", i + 1), line), 2 ); } } #[test] fn log_records_reject_unknown_keys_at_every_depth() { let mut tried = 0; for (i, line) in fixture("records/session.jsonl").lines().enumerate() { tried += check::(&format!("session.jsonl:{}", i + 1), line); } // Seven records, plus the one tool call inside the first assistant record. assert_eq!(tried, 8); } #[test] fn usage_log_records_reject_unknown_keys_at_every_depth() { let mut tried = 0; for (i, line) in fixture("records/session_usage.jsonl").lines().enumerate() { tried += check::(&format!("session_usage.jsonl:{}", i + 1), line); } // Seven records, plus the one tool call inside the first assistant record. assert_eq!(tried, 8); } #[test] fn grants_reject_unknown_keys_at_every_depth() { let grant: Grant = toml::from_str(&fixture("grant/full.toml")).unwrap(); let text = serde_json::to_string(&grant).unwrap(); // The grant and its constraints: two objects. assert_eq!(check::("full.toml as JSON", &text), 2); }