1use crate::on_error::ErrorPolicy;
2use serde::de::Error as _;
3use serde::Deserialize;
4
5use super::types::{Expected, TestCase, TestInput};
6
7const LEGACY_EXPECTED_KEYS: [&str; 4] = ["$ref", "must_contain", "sequence", "schema"];
13
14#[derive(Default)]
15enum RawExpected {
16 #[default]
17 Missing,
18 Present(serde_json::Value),
19}
20
21impl<'de> Deserialize<'de> for RawExpected {
22 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
23 where
24 D: serde::Deserializer<'de>,
25 {
26 serde_json::Value::deserialize(deserializer).map(Self::Present)
27 }
28}
29
30fn value_kind(v: &serde_json::Value) -> &'static str {
32 match v {
33 serde_json::Value::Null => "null",
34 serde_json::Value::Bool(_) => "a boolean",
35 serde_json::Value::Number(_) => "a number",
36 serde_json::Value::String(_) => "a string",
37 serde_json::Value::Array(_) => "a list",
38 serde_json::Value::Object(_) => "a mapping",
39 }
40}
41
42pub(crate) fn parse_expected_entry(item: &serde_json::Value) -> Result<Expected, String> {
58 let strict_err = match serde_json::from_value::<Expected>(item.clone()) {
60 Ok(exp) => return reject_for_parse(exp),
61 Err(e) => e,
62 };
63
64 let Some(obj) = item.as_object() else {
65 return Err(format!(
66 "`expected:` must be a mapping (or a list of one mapping), found {}",
67 value_kind(item)
68 ));
69 };
70
71 let matched_keys: Vec<&str> = LEGACY_EXPECTED_KEYS
72 .iter()
73 .copied()
74 .filter(|key| obj.contains_key(*key))
75 .collect();
76
77 if let Some(tag) = obj.get("type") {
81 let compatibility_key = match tag.as_str() {
82 Some("must_contain") => "must_contain",
83 Some("sequence") => "sequence",
84 _ => return Err(format!("invalid `expected:` block: {}", strict_err)),
85 };
86
87 if matched_keys.len() > 1 {
88 return Err(format!(
89 "ambiguous legacy `expected:` block contains multiple assertions {:?}; \
90 use one tagged assertion or move additional checks to `assertions:`",
91 matched_keys
92 ));
93 }
94
95 let only_compatibility_key = matched_keys.as_slice() == [compatibility_key];
96 let has_unknown_key = obj
97 .keys()
98 .any(|key| key != "type" && key != compatibility_key);
99 if !only_compatibility_key || has_unknown_key {
100 return Err(format!("invalid `expected:` block: {}", strict_err));
101 }
102 } else {
103 let unknown_keys: Vec<&str> = obj
104 .keys()
105 .map(String::as_str)
106 .filter(|key| !LEGACY_EXPECTED_KEYS.contains(key))
107 .collect();
108 if !unknown_keys.is_empty() {
109 return Err(format!(
110 "unrecognized legacy `expected:` key(s) {:?}; supported keys are {:?}",
111 unknown_keys, LEGACY_EXPECTED_KEYS
112 ));
113 }
114 }
115
116 let mut parsed = None;
123
124 if let Some(r) = obj.get("$ref") {
125 let path = r
126 .as_str()
127 .ok_or_else(|| format!("`$ref` must be a string, found {}", value_kind(r)))?;
128 parsed = Some(Expected::Reference {
129 path: path.to_string(),
130 });
131 }
132
133 if let Some(mc) = obj.get("must_contain") {
135 let val: Vec<String> = if let Some(s) = mc.as_str() {
139 vec![s.to_string()]
140 } else {
141 serde_json::from_value(mc.clone()).map_err(|e| {
142 format!(
143 "`must_contain` must be a string or a list of strings, found {}: {}",
144 value_kind(mc),
145 e
146 )
147 })?
148 };
149 if parsed.is_none() {
151 parsed = Some(Expected::MustContain { must_contain: val });
152 }
153 }
154
155 if let Some(seq) = obj.get("sequence") {
156 if parsed.is_none() {
157 let sequence: Vec<String> = serde_json::from_value(seq.clone()).map_err(|e| {
161 format!(
162 "`sequence` must be a list of strings, found {}: {}",
163 value_kind(seq),
164 e
165 )
166 })?;
167 parsed = Some(Expected::SequenceValid {
168 policy: None,
169 sequence: Some(sequence),
170 rules: None,
171 });
172 }
173 }
174
175 if obj.get("schema").is_some() && parsed.is_none() {
176 parsed = Some(Expected::ArgsValid {
177 policy: None,
178 schema: obj.get("schema").cloned(),
179 });
180 }
181
182 if matched_keys.len() > 1 {
183 return Err(format!(
184 "ambiguous legacy `expected:` block contains multiple assertions {:?}; \
185 use one tagged assertion or move additional checks to `assertions:`",
186 matched_keys
187 ));
188 }
189
190 if let Some(p) = parsed {
191 return reject_for_parse(p);
192 }
193
194 if obj.contains_key("type") {
198 return Err(format!("invalid `expected:` block: {}", strict_err));
199 }
200
201 let found: Vec<&str> = obj.keys().map(String::as_str).collect();
202 Err(format!(
203 "unrecognized `expected:` block, found key(s) {:?}. Use the tagged form \
204 (e.g. `type: must_contain` with `must_contain: [...]`) or one of the legacy \
205 keys {:?}",
206 found, LEGACY_EXPECTED_KEYS
207 ))
208}
209
210fn reject_vacuous(exp: Expected) -> Result<Expected, String> {
221 let Some(field) = super::validation::vacuous_expected_field(&exp) else {
222 return Ok(exp);
223 };
224
225 Err(format!(
226 "`{}` asserts nothing, so this test would pass for any response. \
227 Give it at least one entry, or remove the `expected:` block and put the \
228 test's checks in `assertions:`.",
229 field
230 ))
231}
232
233fn reject_for_parse(exp: Expected) -> Result<Expected, String> {
234 let exp = reject_vacuous(exp)?;
235 if let Some(reason) = super::validation::non_executable_expected_reason(&exp) {
236 return Err(format!("expected block is not executable: {reason}"));
237 }
238 if let Some(reason) = super::validation::ineffective_expected_reason(&exp) {
239 return Err(reason.to_string());
240 }
241 Ok(exp)
242}
243
244fn parse_expected_value(test_id: &str, val: &serde_json::Value) -> Result<Expected, String> {
254 let Some(arr) = val.as_array() else {
255 return parse_expected_entry(val).map_err(|e| format!("test '{}': {}", test_id, e));
256 };
257
258 match arr.len() {
259 0 => Err(format!(
260 "test '{}': `expected:` is an empty list, which asserts nothing. \
261 Remove the key or give it an assertion.",
262 test_id
263 )),
264 1 => parse_expected_entry(&arr[0])
265 .map_err(|e| format!("test '{}': `expected:` entry 0 is invalid: {}", test_id, e)),
266 n => Err(format!(
267 "test '{}': `expected:` has {} entries but only one is supported \
268 (earlier versions silently dropped all but the first). \
269 Split them into separate tests, or move the extra checks to `assertions:`.",
270 test_id, n
271 )),
272 }
273}
274
275impl<'de> Deserialize<'de> for TestCase {
276 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
277 where
278 D: serde::Deserializer<'de>,
279 {
280 #[derive(Deserialize)]
281 #[serde(deny_unknown_fields)]
282 struct RawTestCase {
283 id: String,
284 input: TestInput,
285 #[serde(default)]
286 expected: RawExpected,
287 assertions: Option<Vec<crate::agent_assertions::model::TraceAssertion>>,
288 #[serde(default)]
289 on_error: Option<ErrorPolicy>,
290 #[serde(default)]
291 tags: Vec<String>,
292 metadata: Option<serde_json::Value>,
293 }
294
295 let raw = RawTestCase::deserialize(deserializer)?;
296 let extra_assertions = raw.assertions.unwrap_or_default();
297
298 let expected_main = match &raw.expected {
304 RawExpected::Present(val) => {
305 parse_expected_value(&raw.id, val).map_err(D::Error::custom)?
306 }
307 RawExpected::Missing => Expected::default(),
308 };
309
310 Ok(TestCase {
311 id: raw.id,
312 input: raw.input,
313 expected: expected_main,
314 assertions: if extra_assertions.is_empty() {
315 None
316 } else {
317 Some(extra_assertions)
318 },
319 on_error: raw.on_error,
320 tags: raw.tags,
321 metadata: raw.metadata,
322 })
323 }
324}
325
326impl<'de> Deserialize<'de> for TestInput {
327 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
328 where
329 D: serde::Deserializer<'de>,
330 {
331 struct TestInputVisitor;
332
333 impl<'de> serde::de::Visitor<'de> for TestInputVisitor {
334 type Value = TestInput;
335
336 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
337 formatter.write_str("string or struct TestInput")
338 }
339
340 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
341 where
342 E: serde::de::Error,
343 {
344 Ok(TestInput {
345 prompt: value.to_owned(),
346 context: None,
347 })
348 }
349
350 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
351 where
352 A: serde::de::MapAccess<'de>,
353 {
354 #[derive(Deserialize)]
357 struct Helper {
358 prompt: String,
359 #[serde(default)]
360 context: Option<Vec<String>>,
361 }
362 let helper =
363 Helper::deserialize(serde::de::value::MapAccessDeserializer::new(map))?;
364 Ok(TestInput {
365 prompt: helper.prompt,
366 context: helper.context,
367 })
368 }
369 }
370
371 deserializer.deserialize_any(TestInputVisitor)
372 }
373}