use crate::e2e::fixture::Assertion;
use super::super::escape_dart;
fn choices(result_var: &str) -> String {
format!("{result_var}.expand((c) => c.choices)")
}
fn tool_calls(result_var: &str) -> String {
format!("{}.expand((ch) => ch.delta.toolCalls ?? [])", choices(result_var))
}
fn tool_call_leaf(field: &str) -> Option<(usize, &'static str)> {
let rest = field.strip_prefix("tool_calls[")?;
let (index, rest) = rest.split_once(']')?;
let leaf = match rest {
".function.name" => "function?.name",
".function.arguments" => "function?.arguments",
".id" => "id",
_ => return None,
};
Some((index.parse().ok()?, leaf))
}
pub(super) fn try_render(out: &mut String, assertion: &Assertion, result_var: &str) -> bool {
let Some(field) = assertion.field.as_deref() else {
return false;
};
let kind = assertion.assertion_type.as_str();
let (template, context) = match (field, kind) {
("stream_complete", "is_true" | "is_false") => (
"dart/stream_complete.jinja",
minijinja::context! {
choices => choices(result_var),
matcher => if kind == "is_true" { "isTrue" } else { "isFalse" },
},
),
("no_chunks_after_done", "is_true") => ("dart/no_chunks_after_done.jinja", minijinja::context! { result_var }),
("tool_calls", "not_empty") => (
"dart/stream_tool_calls.jinja",
minijinja::context! { tool_calls => tool_calls(result_var) },
),
(_, "equals") => {
let Some(rendered) = equality_template(assertion, field, result_var) else {
return false;
};
rendered
}
_ => return false,
};
out.push_str(&crate::e2e::template_env::render(template, context));
true
}
fn equality_template(assertion: &Assertion, field: &str, result_var: &str) -> Option<(&'static str, minijinja::Value)> {
let serde_json::Value::String(expected) = assertion.value.as_ref()? else {
return None;
};
if field == "finish_reason" {
return Some((
"dart/stream_finish_reason.jinja",
minijinja::context! { choices => choices(result_var), expected => escape_dart(expected) },
));
}
let (index, leaf) = tool_call_leaf(field)?;
Some((
"dart/stream_tool_call_leaf.jinja",
minijinja::context! {
tool_calls => tool_calls(result_var), index, leaf, expected => escape_dart(expected),
},
))
}
#[cfg(test)]
mod tests {
use super::*;
fn render(kind: &str, field: &str, value: Option<serde_json::Value>) -> Option<String> {
let assertion = Assertion {
assertion_type: kind.to_string(),
field: Some(field.to_string()),
value,
..Assertion::default()
};
let mut out = String::new();
try_render(&mut out, &assertion, "result").then_some(out)
}
#[test]
fn stream_complete_asks_for_a_chunk_with_a_finish_reason() {
assert_eq!(
render("is_true", "stream_complete", None).as_deref(),
Some(" expect(result.expand((c) => c.choices).any((ch) => ch.finishReason != null), isTrue);\n")
);
}
#[test]
fn no_chunks_after_done_requires_the_done_chunk_to_be_last() {
let out = render("is_true", "no_chunks_after_done", None).expect("rendered");
assert!(out.contains("final _doneAt = result.indexWhere"), "got:\n{out}");
assert!(
out.contains("_doneAt >= 0 && _doneAt == result.length - 1"),
"got:\n{out}"
);
}
#[test]
fn finish_reason_compares_the_wire_value() {
let out = render("equals", "finish_reason", Some(serde_json::json!("tool_calls"))).expect("rendered");
assert!(
out.contains("ch.finishReason?.wireValue).nonNulls.lastOrNull, equals('tool_calls')"),
"got:\n{out}"
);
}
#[test]
fn tool_calls_are_flattened_across_chunks() {
let out = render("not_empty", "tool_calls", None).expect("rendered");
assert!(out.contains("expand((ch) => ch.delta.toolCalls ?? [])"), "got:\n{out}");
let named = render(
"equals",
"tool_calls[0].function.name",
Some(serde_json::json!("get_weather")),
)
.expect("rendered");
assert!(
named.contains(".elementAtOrNull(0)?.function?.name, equals('get_weather')"),
"got:\n{named}"
);
}
#[test]
fn the_streaming_entry_point_asserts_the_stream_state() {
let cases = [
("is_true", "stream_complete", None, "finishReason != null"),
(
"is_true",
"no_chunks_after_done",
None,
"final _doneAt = result.indexWhere",
),
(
"equals",
"finish_reason",
Some(serde_json::json!("stop")),
"ch.finishReason?.wireValue",
),
(
"not_empty",
"tool_calls",
None,
"expand((ch) => ch.delta.toolCalls ?? [])",
),
];
for (kind, field, value, expected) in cases {
let assertion = Assertion {
assertion_type: kind.to_string(),
field: Some(field.to_string()),
value,
..Assertion::default()
};
let mut out = String::new();
super::super::render_streaming_assertion_dart(&mut out, &assertion, "result");
assert!(
out.contains(expected),
"{field} must render {expected}. Emitted:\n{out}"
);
assert!(!out.contains("skipped"), "{field} must not be skipped. Emitted:\n{out}");
}
}
#[test]
fn an_unrelated_assertion_is_left_to_the_caller() {
assert_eq!(render("equals", "finish_reason", Some(serde_json::json!(3))), None);
assert_eq!(render("contains", "tool_calls", None), None);
}
}