use super::test_support::line_containing;
use super::*;
use crate::e2e::config::CallConfig;
fn streaming_owner_adapter(function_name: &str, owner_type: &str) -> crate::core::config::extras::AdapterConfig {
crate::core::config::extras::AdapterConfig {
name: function_name.to_string(),
pattern: crate::core::config::extras::AdapterPattern::Streaming,
core_path: format!("test_core::{function_name}"),
params: vec![crate::core::config::extras::AdapterParam {
name: "request".to_string(),
ty: "sample::StreamRequest".to_string(),
optional: false,
}],
returns: None,
error_type: None,
owner_type: Some(owner_type.to_string()),
item_type: Some("Item".to_string()),
gil_release: false,
trait_name: None,
trait_method: None,
detect_async: false,
request_type: None,
skip_languages: Vec::new(),
}
}
fn streaming_owner_call() -> CallConfig {
CallConfig {
function: "stream_items".into(),
result_var: "result".into(),
args: vec![
crate::e2e::config::ArgMapping {
name: "handle".into(),
field: "input.handle".into(),
arg_type: "handle".into(),
optional: false,
owned: false,
element_type: None,
go_type: None,
vec_inner_is_ref: false,
trait_name: None,
},
crate::e2e::config::ArgMapping {
name: "url".into(),
field: "input.url".into(),
arg_type: "string".into(),
optional: false,
owned: false,
element_type: None,
go_type: None,
vec_inner_is_ref: false,
trait_name: None,
},
],
..CallConfig::default()
}
}
fn streaming_owner_fixture() -> Fixture {
Fixture {
id: "stream_basic".into(),
description: "Stream items".into(),
input: serde_json::json!({ "handle": {}, "url": "https://example.com" }),
..Fixture::default()
}
}
#[test]
fn kotlin_snippet_binds_the_declared_request_before_the_call() {
let config = ResolvedCrateConfig {
name: "sample".into(),
adapters: vec![streaming_owner_adapter("stream_items", "Engine")],
..ResolvedCrateConfig::default()
};
let body = render_snippet_body(
&streaming_owner_fixture(),
&E2eConfig {
call: streaming_owner_call(),
..E2eConfig::default()
},
&config,
&[],
&[],
false,
)
.expect("snippet renders");
assert_eq!(
line_containing(&body, "val request ="),
r#"val request = mapper.readValue("{\"url\":\"https://example.com\"}", StreamRequest::class.java)"#
);
assert_eq!(
line_containing(&body, "val result ="),
"val result = handle.streamItems(request)"
);
assert!(
!body.contains("\\\"handle\\\""),
"owner handle config must not leak into the request JSON:\n{body}"
);
assert!(
!body.contains("streamItems(\"https://example.com\")"),
"the raw url must not be passed positionally in place of the declared request:\n{body}"
);
}
#[test]
fn kotlin_android_snippet_binds_the_declared_request_before_the_call() {
let config = ResolvedCrateConfig {
name: "sample".into(),
adapters: vec![streaming_owner_adapter("stream_items", "Engine")],
..ResolvedCrateConfig::default()
};
let body = render_snippet_body(
&streaming_owner_fixture(),
&E2eConfig {
call: streaming_owner_call(),
..E2eConfig::default()
},
&config,
&[],
&[],
true,
)
.expect("snippet renders");
assert_eq!(
line_containing(&body, "val request ="),
r#"val request = mapper.readValue("{\"url\":\"https://example.com\"}", StreamRequest::class.java)"#
);
assert_eq!(
line_containing(&body, "val result ="),
"val result = handle.streamItems(request)"
);
assert!(
!body.contains("\\\"handle\\\""),
"owner handle config must not leak into the request JSON:\n{body}"
);
}
fn cold_flow_snippet(presentation: bool, expects_error: bool) -> String {
let mut fixture: Fixture = serde_json::from_value(serde_json::json!({
"id": "cold_flow", "description": "Cold flow", "input": null,
"docs": {"topic": "streaming", "presentation": {"operations": [
{"op": "show", "path": "usage.total_tokens", "display": true}
]}}
}))
.expect("fixture");
if !presentation {
fixture.docs = None;
}
if expects_error {
fixture.assertions = serde_json::from_value(serde_json::json!([{"type": "error"}])).expect("assertions");
}
let config: E2eConfig = serde_json::from_value(serde_json::json!({
"call": {"function": "chat_stream", "streaming": true, "overrides": {
"java": {"client_factory": "create_client"}
}},
"result_fields": ["usage"], "fields_optional": ["usage"]
}))
.expect("config");
render_snippet_body(
&fixture,
&config,
&ResolvedCrateConfig {
name: "sample".into(),
..ResolvedCrateConfig::default()
},
&[],
&[],
true,
)
.expect("snippet")
}
#[test]
fn android_cold_flow_collects_once_inside_client_lifetime() {
for presentation in [false, true] {
let body = cold_flow_snippet(presentation, false);
assert!(body.contains(".use { client ->\n"), "{body}");
assert!(body.contains("client.chatStream().collect { resultChunk ->"), "{body}");
assert_eq!(body.matches(".collect {").count(), 1, "{body}");
assert!(!body.contains("toList("), "{body}");
assert!(!body.contains("result.usage"), "{body}");
assert!(
body.contains(if presentation {
"println(resultChunk.usage?.totalTokens)"
} else {
"println(resultChunk)"
}),
"{body}"
);
}
}
#[test]
fn android_deferred_flow_error_is_collected_inside_the_catch_scope() {
let body = cold_flow_snippet(false, true);
let attempt = body.find("try {").expect("error handler");
let lifetime = body.find(".use { client ->").expect("client lifetime");
let collect = body.find(".collect {").expect("cold flow collected");
let catch = body.find("catch (error: Exception)").expect("error catch");
assert!(attempt < lifetime && lifetime < collect && collect < catch, "{body}");
assert_eq!(body.matches(".collect {").count(), 1, "{body}");
}