1pub const STREAMING_VIRTUAL_FIELDS: &[&str] = &[
34 "chunks",
35 "chunks.length",
36 "stream_content",
37 "stream_complete",
38 "no_chunks_after_done",
39 "tool_calls",
40 "finish_reason",
41];
42
43const STREAMING_VIRTUAL_ROOTS: &[&str] = &["tool_calls", "finish_reason"];
53
54pub fn is_streaming_virtual_field(field: &str) -> bool {
62 if STREAMING_VIRTUAL_FIELDS.contains(&field) {
63 return true;
64 }
65 for root in STREAMING_VIRTUAL_ROOTS {
67 if field.len() > root.len() && field.starts_with(root) {
68 let rest = &field[root.len()..];
69 if rest.starts_with('[') || rest.starts_with('.') {
70 return true;
71 }
72 }
73 }
74 false
75}
76
77fn split_streaming_deep_path(field: &str) -> Option<(&str, &str)> {
83 for root in STREAMING_VIRTUAL_ROOTS {
84 if field.len() > root.len() && field.starts_with(root) {
85 let rest = &field[root.len()..];
86 if rest.starts_with('[') || rest.starts_with('.') {
87 return Some((root, rest));
88 }
89 }
90 }
91 None
92}
93
94const STREAMING_ONLY_AUTO_DETECT_FIELDS: &[&str] = &[
101 "chunks",
102 "chunks.length",
103 "stream_content",
104 "stream_complete",
105 "no_chunks_after_done",
106];
107
108pub fn resolve_is_streaming(fixture: &crate::fixture::Fixture, call_streaming: Option<bool>) -> bool {
120 if let Some(forced) = call_streaming {
121 return forced;
122 }
123 fixture.is_streaming_mock()
124 || fixture.assertions.iter().any(|a| {
125 a.field
126 .as_deref()
127 .is_some_and(|f| !f.is_empty() && STREAMING_ONLY_AUTO_DETECT_FIELDS.contains(&f))
128 })
129}
130
131pub struct StreamingFieldResolver;
133
134impl StreamingFieldResolver {
135 pub fn accessor(field: &str, lang: &str, chunks_var: &str) -> Option<String> {
141 match field {
142 "chunks" => Some(match lang {
143 "zig" => format!("{chunks_var}.items"),
145 "php" => format!("${chunks_var}"),
148 _ => chunks_var.to_string(),
149 }),
150
151 "chunks.length" => Some(match lang {
152 "rust" => format!("{chunks_var}.len()"),
153 "go" => format!("len({chunks_var})"),
154 "python" => format!("len({chunks_var})"),
155 "php" => format!("count(${chunks_var})"),
156 "elixir" => format!("length({chunks_var})"),
157 "kotlin" => format!("{chunks_var}.size"),
159 "zig" => format!("{chunks_var}.items.len"),
161 "swift" => format!("{chunks_var}.count"),
163 _ => format!("{chunks_var}.length"),
165 }),
166
167 "stream_content" => Some(match lang {
168 "rust" => {
169 format!(
170 "{chunks_var}.iter().map(|c| c.choices.first().and_then(|ch| ch.delta.content.as_deref()).unwrap_or(\"\")).collect::<String>()"
171 )
172 }
173 "go" => {
174 format!(
176 "func() string {{ var s string; for _, c := range {chunks_var} {{ if len(c.Choices) > 0 && c.Choices[0].Delta.Content != nil {{ s += *c.Choices[0].Delta.Content }} }}; return s }}()"
177 )
178 }
179 "java" => {
180 format!(
181 "{chunks_var}.stream().map(c -> c.choices().stream().findFirst().map(ch -> ch.delta().content() != null ? ch.delta().content() : \"\").orElse(\"\")).collect(java.util.stream.Collectors.joining())"
182 )
183 }
184 "php" => {
185 format!("implode('', array_map(fn($c) => $c->choices[0]->delta->content ?? '', ${chunks_var}))")
186 }
187 "kotlin" => {
188 format!(
191 "{chunks_var}.joinToString(\"\") {{ it.choices()?.firstOrNull()?.delta()?.content() ?: \"\" }}"
192 )
193 }
194 "kotlin_android" => {
195 format!("{chunks_var}.joinToString(\"\") {{ it.choices?.firstOrNull()?.delta?.content ?: \"\" }}")
197 }
198 "elixir" => {
199 format!(
203 "{chunks_var} |> Enum.map(fn c -> (Enum.at(c.choices, 0) || %{{}}) |> Map.get(:delta, %{{}}) |> Map.get(:content, \"\") end) |> Enum.join(\"\")"
204 )
205 }
206 "python" => {
207 format!("\"\".join(c.choices[0].delta.content or \"\" for c in {chunks_var} if c.choices)")
208 }
209 "zig" => {
210 format!("{chunks_var}_content.items")
213 }
214 "swift" => {
218 format!(
219 "{chunks_var}.map {{ c in c.choices().first.flatMap {{ ch in ch.delta().content()?.toString() }} ?? \"\" }}.joined()"
220 )
221 }
222 _ => {
224 format!("{chunks_var}.map((c: any) => c.choices?.[0]?.delta?.content ?? '').join('')")
225 }
226 }),
227
228 "stream_complete" => Some(match lang {
229 "rust" => {
230 format!(
231 "{chunks_var}.last().and_then(|c| c.choices.first()).and_then(|ch| ch.finish_reason.as_ref()).is_some()"
232 )
233 }
234 "go" => {
235 format!(
236 "func() bool {{ if len({chunks_var}) == 0 {{ return false }}; last := {chunks_var}[len({chunks_var})-1]; return len(last.Choices) > 0 && last.Choices[0].FinishReason != nil }}()"
237 )
238 }
239 "java" => {
240 format!(
241 "!{chunks_var}.isEmpty() && {chunks_var}.get({chunks_var}.size()-1).choices().stream().findFirst().flatMap(ch -> java.util.Optional.ofNullable(ch.finishReason())).isPresent()"
242 )
243 }
244 "php" => {
245 format!("!empty(${chunks_var}) && isset(end(${chunks_var})->choices[0]->finishReason)")
249 }
250 "kotlin" => {
251 format!(
253 "{chunks_var}.isNotEmpty() && {chunks_var}.last().choices()?.firstOrNull()?.finishReason() != null"
254 )
255 }
256 "kotlin_android" => {
257 format!(
259 "{chunks_var}.isNotEmpty() && {chunks_var}.last().choices?.firstOrNull()?.finishReason != null"
260 )
261 }
262 "python" => {
263 format!("bool({chunks_var}) and {chunks_var}[-1].choices[0].finish_reason is not None")
264 }
265 "elixir" => {
266 format!("Enum.at(List.last({chunks_var}).choices, 0).finish_reason != nil")
267 }
268 "zig" => {
271 format!("{chunks_var}.items.len > 0")
272 }
273 "swift" => {
277 format!("!{chunks_var}.isEmpty && {chunks_var}.last!.choices().first?.finish_reason() != nil")
278 }
279 _ => {
281 format!(
282 "{chunks_var}.length > 0 && {chunks_var}[{chunks_var}.length - 1].choices?.[0]?.finishReason != null"
283 )
284 }
285 }),
286
287 "no_chunks_after_done" => Some(match lang {
291 "rust" => "true".to_string(),
292 "go" => "true".to_string(),
293 "java" => "true".to_string(),
294 "php" => "true".to_string(),
295 _ => "true".to_string(),
296 }),
297
298 "tool_calls" => Some(match lang {
299 "rust" => {
300 format!(
301 "{chunks_var}.iter().flat_map(|c| c.choices.iter().flat_map(|ch| ch.delta.tool_calls.iter().flatten())).collect::<Vec<_>>()"
302 )
303 }
304 "go" => {
305 format!(
309 "func() []pkg.StreamToolCall {{ var tc []pkg.StreamToolCall; for _, c := range {chunks_var} {{ for _, ch := range c.Choices {{ tc = append(tc, ch.Delta.ToolCalls...) }} }}; return tc }}()"
310 )
311 }
312 "java" => {
313 format!(
314 "{chunks_var}.stream().flatMap(c -> c.choices().stream()).flatMap(ch -> ch.delta().toolCalls() != null ? ch.delta().toolCalls().stream() : java.util.stream.Stream.empty()).toList()"
315 )
316 }
317 "php" => {
318 format!(
321 "array_merge(...array_map(fn($c) => $c->choices[0]->delta->toolCalls ?? [], ${chunks_var}))"
322 )
323 }
324 "kotlin" => {
325 format!(
327 "{chunks_var}.flatMap {{ c -> c.choices()?.flatMap {{ ch -> ch.delta()?.toolCalls() ?: emptyList() }} ?: emptyList() }}"
328 )
329 }
330 "kotlin_android" => {
331 format!(
333 "{chunks_var}.flatMap {{ c -> c.choices?.flatMap {{ ch -> ch.delta?.toolCalls ?: emptyList() }} ?: emptyList() }}"
334 )
335 }
336 "python" => {
337 format!(
338 "[t for c in {chunks_var} for ch in (c.choices or []) for t in (ch.delta.tool_calls or [])]"
339 )
340 }
341 "elixir" => {
342 format!(
343 "{chunks_var} |> Enum.flat_map(fn c -> (List.first(c.choices) || %{{}}).delta |> Map.get(:tool_calls, []) end)"
344 )
345 }
346 "zig" => {
348 format!("{chunks_var}.items")
349 }
350 "swift" => {
357 format!(
358 "{chunks_var}.flatMap {{ c -> [StreamToolCallRef] in guard let ch = c.choices().first, let tcs = ch.delta().tool_calls() else {{ return [] }}; return Array(tcs) }}"
359 )
360 }
361 _ => {
362 format!("{chunks_var}.flatMap((c: any) => c.choices?.[0]?.delta?.toolCalls ?? [])")
363 }
364 }),
365
366 "finish_reason" => Some(match lang {
367 "rust" => {
368 format!(
371 "{chunks_var}.last().and_then(|c| c.choices.first()).and_then(|ch| ch.finish_reason.as_ref()).map(|v| v.to_string()).unwrap_or_default()"
372 )
373 }
374 "go" => {
375 format!(
378 "func() string {{ if len({chunks_var}) == 0 {{ return \"\" }}; last := {chunks_var}[len({chunks_var})-1]; if len(last.Choices) > 0 && last.Choices[0].FinishReason != nil {{ return string(*last.Choices[0].FinishReason) }}; return \"\" }}()"
379 )
380 }
381 "java" => {
382 format!(
386 "({chunks_var}.isEmpty() ? null : {chunks_var}.get({chunks_var}.size()-1).choices().stream().findFirst().map(ch -> ch.finishReason() == null ? null : ch.finishReason().getValue()).orElse(null))"
387 )
388 }
389 "php" => {
390 format!("(!empty(${chunks_var}) ? (end(${chunks_var})->choices[0]->finishReason ?? null) : null)")
393 }
394 "kotlin" => {
395 format!(
398 "(if ({chunks_var}.isEmpty()) null else {chunks_var}.last().choices()?.firstOrNull()?.finishReason()?.getValue())"
399 )
400 }
401 "kotlin_android" => {
402 format!(
404 "(if ({chunks_var}.isEmpty()) null else {chunks_var}.last().choices?.firstOrNull()?.finishReason?.name?.lowercase())"
405 )
406 }
407 "python" => {
408 format!(
412 "(str({chunks_var}[-1].choices[0].finish_reason) if {chunks_var} and {chunks_var}[-1].choices else None)"
413 )
414 }
415 "elixir" => {
416 format!("Enum.at(List.last({chunks_var}).choices, 0).finish_reason")
417 }
418 "zig" => {
421 format!(
422 "(blk: {{ if ({chunks_var}.items.len == 0) break :blk \"\"; var _lcp = std.json.parseFromSlice(std.json.Value, std.heap.c_allocator, {chunks_var}.items[{chunks_var}.items.len - 1], .{{}}) catch break :blk \"\"; defer _lcp.deinit(); if (_lcp.value.object.get(\"choices\")) |_lchs| if (_lchs.array.items.len > 0) if (_lchs.array.items[0].object.get(\"finish_reason\")) |_fr| if (_fr == .string) break :blk _fr.string; break :blk \"\"; }})"
423 )
424 }
425 "swift" => {
429 format!(
430 "({chunks_var}.isEmpty ? nil : {chunks_var}.last!.choices().first?.finish_reason()?.toString())"
431 )
432 }
433 _ => {
434 format!(
435 "{chunks_var}.length > 0 ? {chunks_var}[{chunks_var}.length - 1].choices?.[0]?.finishReason : undefined"
436 )
437 }
438 }),
439
440 "usage" => Some(match lang {
445 "python" => {
446 format!("({chunks_var}[-1].usage if {chunks_var} else None)")
450 }
451 "rust" => {
452 format!("{chunks_var}.last().and_then(|c| c.usage.as_ref())")
453 }
454 "go" => {
455 format!(
456 "func() interface{{}} {{ if len({chunks_var}) == 0 {{ return nil }}; return {chunks_var}[len({chunks_var})-1].Usage }}()"
457 )
458 }
459 "java" => {
460 format!("({chunks_var}.isEmpty() ? null : {chunks_var}.get({chunks_var}.size()-1).usage())")
461 }
462 "kotlin" => {
463 format!("(if ({chunks_var}.isEmpty()) null else {chunks_var}.last().usage())")
464 }
465 "kotlin_android" => {
466 format!("(if ({chunks_var}.isEmpty()) null else {chunks_var}.last().usage)")
468 }
469 "php" => {
470 format!("(!empty(${chunks_var}) ? end(${chunks_var})->usage ?? null : null)")
471 }
472 "elixir" => {
473 format!("(if length({chunks_var}) > 0, do: List.last({chunks_var}).usage, else: nil)")
474 }
475 "swift" => {
477 format!("({chunks_var}.isEmpty ? nil : {chunks_var}.last!.usage())")
478 }
479 _ => {
480 format!("({chunks_var}.length > 0 ? {chunks_var}[{chunks_var}.length - 1].usage : undefined)")
481 }
482 }),
483
484 _ => {
485 if let Some((root, tail)) = split_streaming_deep_path(field) {
489 if lang == "rust" && root == "tool_calls" {
493 return Some(render_rust_tool_calls_deep(chunks_var, tail));
494 }
495 if lang == "swift" && root == "tool_calls" {
499 let root_expr = Self::accessor(root, lang, chunks_var)?;
500 return Some(render_swift_tool_calls_deep(&root_expr, tail));
501 }
502 if lang == "zig" && root == "tool_calls" {
509 return None;
510 }
511 let root_expr = Self::accessor(root, lang, chunks_var)?;
512 Some(render_deep_tail(&root_expr, tail, lang))
513 } else {
514 None
515 }
516 }
517 }
518 }
519
520 pub fn collect_snippet(lang: &str, stream_var: &str, chunks_var: &str) -> Option<String> {
526 match lang {
527 "rust" => Some(format!(
528 "let {chunks_var}: Vec<_> = tokio_stream::StreamExt::collect::<Vec<_>>({stream_var}).await\n .into_iter()\n .map(|r| r.expect(\"stream item failed\"))\n .collect();"
529 )),
530 "go" => Some(format!(
531 "var {chunks_var} []pkg.ChatCompletionChunk\n\tfor chunk := range {stream_var} {{\n\t\t{chunks_var} = append({chunks_var}, chunk)\n\t}}"
532 )),
533 "java" => Some(format!(
534 "var {chunks_var} = new java.util.ArrayList<ChatCompletionChunk>();\n var _it = {stream_var}.iterator();\n while (_it.hasNext()) {{ {chunks_var}.add(_it.next()); }}"
535 )),
536 "php" => Some(format!(
543 "${chunks_var} = is_string(${stream_var}) ? (json_decode(${stream_var}) ?: []) : iterator_to_array(${stream_var});"
544 )),
545 "python" => Some(format!(
546 "{chunks_var} = []\n async for chunk in {stream_var}:\n {chunks_var}.append(chunk)"
547 )),
548 "kotlin" => {
549 Some(format!("val {chunks_var} = {stream_var}.asSequence().toList()"))
552 }
553 "kotlin_android" => {
554 Some(format!("val {chunks_var} = {stream_var}.toList()"))
557 }
558 "elixir" => Some(format!("{chunks_var} = Enum.to_list({stream_var})")),
559 "node" | "wasm" | "typescript" => Some(format!(
560 "const {chunks_var}: any[] = [];\n for await (const _chunk of {stream_var}) {{ {chunks_var}.push(_chunk); }}"
561 )),
562 "swift" => {
563 Some(format!(
568 "var {chunks_var}: [ChatCompletionChunk] = []\n for try await _chunk in {stream_var} {{ {chunks_var}.append(_chunk) }}"
569 ))
570 }
571 "zig" => Some(Self::collect_snippet_zig(stream_var, chunks_var, "module", "ffi")),
572 _ => None,
573 }
574 }
575
576 pub fn collect_snippet_zig(stream_var: &str, chunks_var: &str, module_name: &str, ffi_prefix: &str) -> String {
578 let stream_next = format!("{ffi_prefix}_default_client_chat_stream_next");
579 let chunk_to_json = format!("{ffi_prefix}_chat_completion_chunk_to_json");
580 let chunk_free = format!("{ffi_prefix}_chat_completion_chunk_free");
581 let free_string = format!("{ffi_prefix}_free_string");
582
583 format!(
590 concat!(
591 "var {chunks_var}: std.ArrayList([]u8) = .empty;
592",
593 " defer {{
594",
595 " for ({chunks_var}.items) |_cj| std.heap.c_allocator.free(_cj);
596",
597 " {chunks_var}.deinit(std.heap.c_allocator);
598",
599 " }}
600",
601 " var {chunks_var}_content: std.ArrayList(u8) = .empty;
602",
603 " defer {chunks_var}_content.deinit(std.heap.c_allocator);
604",
605 " while (true) {{
606",
607 " const _nc = {module_name}.c.{stream_next}({stream_var});
608",
609 " if (_nc == null) break;
610",
611 " const _np = {module_name}.c.{chunk_to_json}(_nc);
612",
613 " {module_name}.c.{chunk_free}(_nc);
614",
615 " if (_np == null) continue;
616",
617 " const _ns = std.mem.span(_np);
618",
619 " const _nj = try std.heap.c_allocator.dupe(u8, _ns);
620",
621 " {module_name}.c.{free_string}(_np);
622",
623 " if (std.json.parseFromSlice(std.json.Value, std.heap.c_allocator, _nj, .{{}})) |_cp| {{
624",
625 " defer _cp.deinit();
626",
627 " if (_cp.value.object.get(\"choices\")) |_chs|
628",
629 " if (_chs.array.items.len > 0)
630",
631 " if (_chs.array.items[0].object.get(\"delta\")) |_dl|
632",
633 " if (_dl.object.get(\"content\")) |_ct|
634",
635 " if (_ct == .string) try {chunks_var}_content.appendSlice(std.heap.c_allocator, _ct.string);
636",
637 " }} else |_| {{}}
638",
639 " try {chunks_var}.append(std.heap.c_allocator, _nj);
640",
641 " }}"
642 ),
643 chunks_var = chunks_var,
644 stream_var = stream_var,
645 module_name = module_name,
646 stream_next = stream_next,
647 chunk_to_json = chunk_to_json,
648 chunk_free = chunk_free,
649 free_string = free_string,
650 )
651 }
652}
653
654fn render_swift_tool_calls_deep(root_expr: &str, tail: &str) -> String {
666 use heck::ToLowerCamelCase;
667 let segs = parse_tail(tail);
668 let mut expr = root_expr.to_string();
669 let last_field_idx = segs.iter().rposition(|s| matches!(s, TailSeg::Field(_)));
670 let mut prev_was_index = false;
673
674 for (i, seg) in segs.iter().enumerate() {
675 match seg {
676 TailSeg::Index(n) => {
677 expr = format!("({expr})[{n}]");
678 prev_was_index = true;
679 }
680 TailSeg::Field(f) => {
681 let method = f.to_lower_camel_case();
682 let is_last = Some(i) == last_field_idx;
683 let chain = if prev_was_index { "." } else { "?." };
684 if is_last {
685 expr = format!("{expr}{chain}{method}()?.toString()");
687 } else {
688 expr = format!("{expr}{chain}{method}()");
690 }
691 prev_was_index = false;
692 }
693 }
694 }
695 expr
696}
697
698fn render_rust_tool_calls_deep(chunks_var: &str, tail: &str) -> String {
702 let segs = parse_tail(tail);
703 let idx = segs.iter().find_map(|s| match s {
705 TailSeg::Index(n) => Some(*n),
706 _ => None,
707 });
708 let field_segs: Vec<&str> = segs
709 .iter()
710 .filter_map(|s| match s {
711 TailSeg::Field(f) => Some(f.as_str()),
712 _ => None,
713 })
714 .collect();
715
716 let base = format!(
717 "{chunks_var}.iter().flat_map(|c| c.choices.iter().flat_map(|ch| ch.delta.tool_calls.iter().flatten()))"
718 );
719 let with_nth = match idx {
720 Some(n) => format!("{base}.nth({n})"),
721 None => base,
722 };
723
724 let mut expr = with_nth;
727 for (i, f) in field_segs.iter().enumerate() {
728 let is_leaf = i == field_segs.len() - 1;
729 if is_leaf {
730 expr = format!("{expr}.and_then(|x| x.{f}.as_deref())");
731 } else {
732 expr = format!("{expr}.and_then(|x| x.{f}.as_ref())");
733 }
734 }
735 format!("{expr}.unwrap_or(\"\")")
736}
737
738#[derive(Debug, PartialEq)]
743enum TailSeg {
744 Index(usize),
745 Field(String),
746}
747
748fn parse_tail(tail: &str) -> Vec<TailSeg> {
749 let mut segs = Vec::new();
750 let mut rest = tail;
751 while !rest.is_empty() {
752 if let Some(inner) = rest.strip_prefix('[') {
753 if let Some(close) = inner.find(']') {
755 let idx_str = &inner[..close];
756 if let Ok(idx) = idx_str.parse::<usize>() {
757 segs.push(TailSeg::Index(idx));
758 }
759 rest = &inner[close + 1..];
760 } else {
761 break;
762 }
763 } else if let Some(inner) = rest.strip_prefix('.') {
764 let end = inner.find(['.', '[']).unwrap_or(inner.len());
766 segs.push(TailSeg::Field(inner[..end].to_string()));
767 rest = &inner[end..];
768 } else {
769 break;
770 }
771 }
772 segs
773}
774
775fn render_deep_tail(root_expr: &str, tail: &str, lang: &str) -> String {
778 use heck::{ToLowerCamelCase, ToPascalCase};
779
780 let segs = parse_tail(tail);
781 let mut out = root_expr.to_string();
782
783 for seg in &segs {
784 match (seg, lang) {
785 (TailSeg::Index(n), "rust") => {
786 out = format!("({out})[{n}]");
787 }
788 (TailSeg::Index(n), "java") => {
789 out = format!("({out}).get({n})");
790 }
791 (TailSeg::Index(n), "kotlin") => {
792 if *n == 0 {
793 out = format!("({out}).first()");
794 } else {
795 out = format!("({out}).get({n})");
796 }
797 }
798 (TailSeg::Index(n), "kotlin_android") => {
799 if *n == 0 {
800 out = format!("({out}).first()");
801 } else {
802 out = format!("({out})[{n}]");
803 }
804 }
805 (TailSeg::Index(n), "elixir") => {
806 out = format!("Enum.at({out}, {n})");
807 }
808 (TailSeg::Index(n), "zig") => {
809 out = format!("({out}).items[{n}]");
810 }
811 (TailSeg::Index(n), "php") => {
812 out = format!("({out})[{n}]");
813 }
814 (TailSeg::Index(n), _) => {
815 out = format!("({out})[{n}]");
817 }
818 (TailSeg::Field(f), "rust") => {
819 use heck::ToSnakeCase;
820 out.push('.');
821 out.push_str(&f.to_snake_case());
822 }
823 (TailSeg::Field(f), "go") => {
824 use alef_codegen::naming::to_go_name;
825 out.push('.');
826 out.push_str(&to_go_name(f));
827 }
828 (TailSeg::Field(f), "java") => {
829 out.push('.');
830 out.push_str(&f.to_lower_camel_case());
831 out.push_str("()");
832 }
833 (TailSeg::Field(f), "kotlin") => {
834 out.push_str("?.");
840 out.push_str(&f.to_lower_camel_case());
841 out.push_str("()");
842 }
843 (TailSeg::Field(f), "kotlin_android") => {
844 out.push_str("?.");
846 out.push_str(&f.to_lower_camel_case());
847 }
848 (TailSeg::Field(f), "csharp") => {
849 out.push('.');
850 out.push_str(&f.to_pascal_case());
851 }
852 (TailSeg::Field(f), "php") => {
853 out.push_str("->");
858 out.push_str(f);
859 }
860 (TailSeg::Field(f), "elixir") => {
861 out.push('.');
862 out.push_str(f);
863 }
864 (TailSeg::Field(f), "zig") => {
865 out.push('.');
866 out.push_str(f);
867 }
868 (TailSeg::Field(f), "python") | (TailSeg::Field(f), "ruby") => {
869 out.push('.');
870 out.push_str(f);
871 }
872 (TailSeg::Field(f), _) => {
874 out.push('.');
875 out.push_str(&f.to_lower_camel_case());
876 }
877 }
878 }
879
880 out
881}
882
883#[cfg(test)]
884mod tests {
885 use super::*;
886
887 #[test]
888 fn is_streaming_virtual_field_recognizes_all_fields() {
889 for field in STREAMING_VIRTUAL_FIELDS {
890 assert!(
891 is_streaming_virtual_field(field),
892 "field '{field}' not recognized as streaming virtual"
893 );
894 }
895 }
896
897 #[test]
898 fn is_streaming_virtual_field_rejects_real_fields() {
899 assert!(!is_streaming_virtual_field("content"));
900 assert!(!is_streaming_virtual_field("choices"));
901 assert!(!is_streaming_virtual_field("model"));
902 assert!(!is_streaming_virtual_field(""));
903 }
904
905 #[test]
906 fn is_streaming_virtual_field_rejects_non_root_paths_with_matching_tail() {
907 assert!(!is_streaming_virtual_field("choices[0].finish_reason"));
912 assert!(!is_streaming_virtual_field("choices[0].message.content"));
913 assert!(!is_streaming_virtual_field("data[0].embedding"));
914 }
915
916 #[test]
917 fn is_streaming_virtual_field_does_not_match_usage() {
918 assert!(!is_streaming_virtual_field("usage"));
922 assert!(!is_streaming_virtual_field("usage.total_tokens"));
923 assert!(!is_streaming_virtual_field("usage.prompt_tokens"));
924 }
925
926 #[test]
927 fn accessor_chunks_returns_var_name() {
928 assert_eq!(
929 StreamingFieldResolver::accessor("chunks", "rust", "chunks"),
930 Some("chunks".to_string())
931 );
932 assert_eq!(
933 StreamingFieldResolver::accessor("chunks", "node", "chunks"),
934 Some("chunks".to_string())
935 );
936 }
937
938 #[test]
939 fn accessor_chunks_length_uses_language_idiom() {
940 let rust = StreamingFieldResolver::accessor("chunks.length", "rust", "chunks").unwrap();
941 assert!(rust.contains(".len()"), "rust: {rust}");
942
943 let go = StreamingFieldResolver::accessor("chunks.length", "go", "chunks").unwrap();
944 assert!(go.starts_with("len("), "go: {go}");
945
946 let node = StreamingFieldResolver::accessor("chunks.length", "node", "chunks").unwrap();
947 assert!(node.contains(".length"), "node: {node}");
948
949 let php = StreamingFieldResolver::accessor("chunks.length", "php", "chunks").unwrap();
950 assert!(php.starts_with("count("), "php: {php}");
951 }
952
953 #[test]
954 fn accessor_chunks_length_zig_uses_items_len() {
955 let zig = StreamingFieldResolver::accessor("chunks.length", "zig", "chunks").unwrap();
956 assert_eq!(zig, "chunks.items.len", "zig chunks.length: {zig}");
957 }
958
959 #[test]
960 fn accessor_stream_content_zig_uses_content_items() {
961 let zig = StreamingFieldResolver::accessor("stream_content", "zig", "chunks").unwrap();
962 assert_eq!(zig, "chunks_content.items", "zig stream_content: {zig}");
963 }
964
965 #[test]
966 fn collect_snippet_zig_drains_via_ffi() {
967 let snip = StreamingFieldResolver::collect_snippet("zig", "_stream_handle", "chunks").unwrap();
968 assert!(snip.contains("std.ArrayList([]u8)"), "zig collect: {snip}");
969 assert!(snip.contains("chat_stream_next(_stream_handle)"), "zig collect: {snip}");
970 assert!(snip.contains("chunks_content"), "zig collect: {snip}");
971 assert!(
972 snip.contains("chunks.append(std.heap.c_allocator"),
973 "zig collect: {snip}"
974 );
975 assert!(snip.contains(".empty;"), "zig collect (Zig 0.16 unmanaged): {snip}");
976 }
977
978 #[test]
979 fn accessor_stream_content_rust_uses_iterator() {
980 let expr = StreamingFieldResolver::accessor("stream_content", "rust", "chunks").unwrap();
981 assert!(expr.contains(".collect::<String>()"), "rust stream_content: {expr}");
982 }
983
984 #[test]
985 fn accessor_no_chunks_after_done_returns_true() {
986 for lang in ["rust", "go", "java", "php", "node", "wasm", "elixir"] {
987 let expr = StreamingFieldResolver::accessor("no_chunks_after_done", lang, "chunks").unwrap();
988 assert_eq!(expr, "true", "lang {lang}: expected 'true', got '{expr}'");
989 }
990 }
991
992 #[test]
993 fn accessor_elixir_chunks_length_uses_length_function() {
994 let expr = StreamingFieldResolver::accessor("chunks.length", "elixir", "chunks").unwrap();
995 assert_eq!(expr, "length(chunks)", "elixir chunks.length: {expr}");
996 }
997
998 #[test]
999 fn accessor_elixir_stream_content_uses_pipe() {
1000 let expr = StreamingFieldResolver::accessor("stream_content", "elixir", "chunks").unwrap();
1001 assert!(expr.contains("|> Enum.join"), "elixir stream_content: {expr}");
1002 assert!(expr.contains("|> Enum.map"), "elixir stream_content: {expr}");
1003 assert!(
1005 !expr.contains("choices[0]"),
1006 "elixir stream_content must not use bracket access on list: {expr}"
1007 );
1008 assert!(
1009 expr.contains("Enum.at("),
1010 "elixir stream_content must use Enum.at for list index: {expr}"
1011 );
1012 }
1013
1014 #[test]
1015 fn accessor_elixir_stream_complete_uses_list_last() {
1016 let expr = StreamingFieldResolver::accessor("stream_complete", "elixir", "chunks").unwrap();
1017 assert!(expr.contains("List.last(chunks)"), "elixir stream_complete: {expr}");
1018 assert!(expr.contains("finish_reason != nil"), "elixir stream_complete: {expr}");
1019 assert!(
1021 !expr.contains("choices[0]"),
1022 "elixir stream_complete must not use bracket access on list: {expr}"
1023 );
1024 assert!(
1025 expr.contains("Enum.at("),
1026 "elixir stream_complete must use Enum.at for list index: {expr}"
1027 );
1028 }
1029
1030 #[test]
1031 fn accessor_elixir_finish_reason_uses_list_last() {
1032 let expr = StreamingFieldResolver::accessor("finish_reason", "elixir", "chunks").unwrap();
1033 assert!(expr.contains("List.last(chunks)"), "elixir finish_reason: {expr}");
1034 assert!(expr.contains("finish_reason"), "elixir finish_reason: {expr}");
1035 assert!(
1037 !expr.contains("choices[0]"),
1038 "elixir finish_reason must not use bracket access on list: {expr}"
1039 );
1040 assert!(
1041 expr.contains("Enum.at("),
1042 "elixir finish_reason must use Enum.at for list index: {expr}"
1043 );
1044 }
1045
1046 #[test]
1047 fn collect_snippet_elixir_uses_enum_to_list() {
1048 let snip = StreamingFieldResolver::collect_snippet("elixir", "result", "chunks").unwrap();
1049 assert!(snip.contains("Enum.to_list(result)"), "elixir: {snip}");
1050 assert!(snip.contains("chunks ="), "elixir: {snip}");
1051 }
1052
1053 #[test]
1054 fn collect_snippet_rust_uses_tokio_stream() {
1055 let snip = StreamingFieldResolver::collect_snippet("rust", "result", "chunks").unwrap();
1056 assert!(snip.contains("tokio_stream::StreamExt::collect"), "rust: {snip}");
1057 assert!(snip.contains("let chunks"), "rust: {snip}");
1058 assert!(snip.contains(".expect("), "rust must unwrap Result items: {snip}");
1060 }
1061
1062 #[test]
1063 fn collect_snippet_go_drains_channel() {
1064 let snip = StreamingFieldResolver::collect_snippet("go", "stream", "chunks").unwrap();
1065 assert!(snip.contains("for chunk := range stream"), "go: {snip}");
1066 }
1067
1068 #[test]
1069 fn collect_snippet_java_uses_iterator() {
1070 let snip = StreamingFieldResolver::collect_snippet("java", "result", "chunks").unwrap();
1071 assert!(
1074 snip.contains(".iterator()"),
1075 "java snippet must call .iterator() on stream: {snip}"
1076 );
1077 assert!(snip.contains("hasNext()"), "java: {snip}");
1078 assert!(snip.contains(".next()"), "java: {snip}");
1079 }
1080
1081 #[test]
1082 fn collect_snippet_php_decodes_json_or_iterates() {
1083 let snip = StreamingFieldResolver::collect_snippet("php", "result", "chunks").unwrap();
1084 assert!(snip.contains("json_decode"), "php must decode JSON: {snip}");
1089 assert!(
1090 snip.contains("iterator_to_array"),
1091 "php must keep iterator_to_array fallback: {snip}"
1092 );
1093 assert!(snip.contains("$chunks ="), "php must bind $chunks: {snip}");
1094 }
1095
1096 #[test]
1097 fn collect_snippet_node_uses_for_await() {
1098 let snip = StreamingFieldResolver::collect_snippet("node", "result", "chunks").unwrap();
1099 assert!(snip.contains("for await"), "node: {snip}");
1100 }
1101
1102 #[test]
1103 fn collect_snippet_python_uses_async_for() {
1104 let snip = StreamingFieldResolver::collect_snippet("python", "result", "chunks").unwrap();
1105 assert!(snip.contains("async for chunk in result"), "python: {snip}");
1106 assert!(snip.contains("chunks.append(chunk)"), "python: {snip}");
1107 }
1108
1109 #[test]
1110 fn accessor_stream_content_python_uses_join() {
1111 let expr = StreamingFieldResolver::accessor("stream_content", "python", "chunks").unwrap();
1112 assert!(expr.contains("\"\".join("), "python stream_content: {expr}");
1113 assert!(expr.contains("c.choices"), "python stream_content: {expr}");
1114 }
1115
1116 #[test]
1117 fn accessor_stream_complete_python_uses_finish_reason() {
1118 let expr = StreamingFieldResolver::accessor("stream_complete", "python", "chunks").unwrap();
1119 assert!(
1120 expr.contains("finish_reason is not None"),
1121 "python stream_complete: {expr}"
1122 );
1123 }
1124
1125 #[test]
1126 fn accessor_finish_reason_python_uses_last_chunk() {
1127 let expr = StreamingFieldResolver::accessor("finish_reason", "python", "chunks").unwrap();
1128 assert!(expr.contains("chunks[-1]"), "python finish_reason: {expr}");
1129 assert!(
1131 expr.starts_with("(str(") || expr.contains("str(chunks"),
1132 "python finish_reason must wrap in str(): {expr}"
1133 );
1134 }
1135
1136 #[test]
1137 fn accessor_tool_calls_python_uses_list_comprehension() {
1138 let expr = StreamingFieldResolver::accessor("tool_calls", "python", "chunks").unwrap();
1139 assert!(expr.contains("for c in chunks"), "python tool_calls: {expr}");
1140 assert!(expr.contains("tool_calls"), "python tool_calls: {expr}");
1141 }
1142
1143 #[test]
1144 fn accessor_usage_python_uses_last_chunk() {
1145 let expr = StreamingFieldResolver::accessor("usage", "python", "chunks").unwrap();
1146 assert!(
1147 expr.contains("chunks[-1].usage"),
1148 "python usage: expected chunks[-1].usage, got: {expr}"
1149 );
1150 }
1151
1152 #[test]
1153 fn accessor_usage_total_tokens_does_not_route_via_chunks() {
1154 assert!(StreamingFieldResolver::accessor("usage.total_tokens", "python", "chunks").is_none());
1158 }
1159
1160 #[test]
1161 fn accessor_unknown_field_returns_none() {
1162 assert_eq!(
1163 StreamingFieldResolver::accessor("nonexistent_field", "rust", "chunks"),
1164 None
1165 );
1166 }
1167
1168 #[test]
1173 fn is_streaming_virtual_field_recognizes_deep_tool_calls_paths() {
1174 assert!(
1175 is_streaming_virtual_field("tool_calls[0].function.name"),
1176 "tool_calls[0].function.name should be recognized"
1177 );
1178 assert!(
1179 is_streaming_virtual_field("tool_calls[0].id"),
1180 "tool_calls[0].id should be recognized"
1181 );
1182 assert!(
1183 is_streaming_virtual_field("tool_calls[1].function.arguments"),
1184 "tool_calls[1].function.arguments should be recognized"
1185 );
1186 assert!(is_streaming_virtual_field("tool_calls"));
1188 assert!(!is_streaming_virtual_field("tool_calls_extra.name"));
1190 assert!(!is_streaming_virtual_field("nonexistent[0].field"));
1191 }
1192
1193 #[test]
1200 fn deep_tool_calls_function_name_snapshot_rust_kotlin_ts() {
1201 let field = "tool_calls[0].function.name";
1202
1203 let rust = StreamingFieldResolver::accessor(field, "rust", "chunks").unwrap();
1204 assert!(
1208 rust.contains(".nth(0)"),
1209 "rust deep tool_calls: expected .nth(0) iterator index, got: {rust}"
1210 );
1211 assert!(
1212 rust.contains("x.function.as_ref()"),
1213 "rust deep tool_calls: expected Option-aware function access, got: {rust}"
1214 );
1215 assert!(
1216 rust.contains("x.name.as_deref()"),
1217 "rust deep tool_calls: expected Option-aware name leaf, got: {rust}"
1218 );
1219 assert!(
1220 !rust.contains("// skipped"),
1221 "rust deep tool_calls: must not emit skip comment, got: {rust}"
1222 );
1223
1224 let kotlin = StreamingFieldResolver::accessor(field, "kotlin", "chunks").unwrap();
1225 assert!(
1227 kotlin.contains(".first()"),
1228 "kotlin deep tool_calls: expected .first() for index 0, got: {kotlin}"
1229 );
1230 assert!(
1231 kotlin.contains(".function()"),
1232 "kotlin deep tool_calls: expected .function() method call, got: {kotlin}"
1233 );
1234 assert!(
1235 kotlin.contains(".name()"),
1236 "kotlin deep tool_calls: expected .name() method call, got: {kotlin}"
1237 );
1238
1239 let ts = StreamingFieldResolver::accessor(field, "node", "chunks").unwrap();
1240 assert!(
1242 ts.contains("[0]"),
1243 "ts/node deep tool_calls: expected [0] index, got: {ts}"
1244 );
1245 assert!(
1246 ts.contains(".function"),
1247 "ts/node deep tool_calls: expected .function segment, got: {ts}"
1248 );
1249 assert!(
1250 ts.contains(".name"),
1251 "ts/node deep tool_calls: expected .name segment, got: {ts}"
1252 );
1253 }
1254
1255 #[test]
1256 fn deep_tool_calls_id_snapshot_all_langs() {
1257 let field = "tool_calls[0].id";
1258
1259 let rust = StreamingFieldResolver::accessor(field, "rust", "chunks").unwrap();
1260 assert!(rust.contains(".nth(0)"), "rust: {rust}");
1261 assert!(rust.contains("x.id.as_deref()"), "rust: {rust}");
1262
1263 let go = StreamingFieldResolver::accessor(field, "go", "chunks").unwrap();
1264 assert!(go.contains("[0]"), "go: {go}");
1265 assert!(go.contains(".ID"), "go: expected .ID initialism, got: {go}");
1267
1268 let python = StreamingFieldResolver::accessor(field, "python", "chunks").unwrap();
1269 assert!(python.contains("[0]"), "python: {python}");
1270 assert!(python.contains(".id"), "python: {python}");
1271
1272 let php = StreamingFieldResolver::accessor(field, "php", "chunks").unwrap();
1273 assert!(php.contains("[0]"), "php: {php}");
1274 assert!(php.contains("->id"), "php: expected ->id, got: {php}");
1275
1276 let java = StreamingFieldResolver::accessor(field, "java", "chunks").unwrap();
1277 assert!(java.contains(".get(0)"), "java: expected .get(0), got: {java}");
1278 assert!(java.contains(".id()"), "java: expected .id() method call, got: {java}");
1279
1280 let csharp = StreamingFieldResolver::accessor(field, "csharp", "chunks").unwrap();
1281 assert!(csharp.contains("[0]"), "csharp: {csharp}");
1282 assert!(
1283 csharp.contains(".Id"),
1284 "csharp: expected .Id (PascalCase), got: {csharp}"
1285 );
1286
1287 let elixir = StreamingFieldResolver::accessor(field, "elixir", "chunks").unwrap();
1288 assert!(elixir.contains("Enum.at("), "elixir: expected Enum.at(, got: {elixir}");
1289 assert!(elixir.contains(".id"), "elixir: {elixir}");
1290 }
1291
1292 #[test]
1293 fn deep_tool_calls_function_name_snapshot_python_elixir_zig() {
1294 let field = "tool_calls[0].function.name";
1295
1296 let python = StreamingFieldResolver::accessor(field, "python", "chunks").unwrap();
1297 assert!(python.contains("[0]"), "python: {python}");
1298 assert!(python.contains(".function"), "python: {python}");
1299 assert!(python.contains(".name"), "python: {python}");
1300
1301 let elixir = StreamingFieldResolver::accessor(field, "elixir", "chunks").unwrap();
1302 assert!(elixir.contains("Enum.at("), "elixir: {elixir}");
1304 assert!(elixir.contains(".function"), "elixir: {elixir}");
1305 assert!(elixir.contains(".name"), "elixir: {elixir}");
1306
1307 assert!(
1311 StreamingFieldResolver::accessor(field, "zig", "chunks").is_none(),
1312 "zig: expected None for deep tool_calls path"
1313 );
1314 }
1315
1316 #[test]
1317 fn parse_tail_parses_index_then_field_segments() {
1318 let segs = parse_tail("[0].function.name");
1319 assert_eq!(segs.len(), 3, "expected 3 segments, got: {segs:?}");
1320 assert_eq!(segs[0], TailSeg::Index(0));
1321 assert_eq!(segs[1], TailSeg::Field("function".to_string()));
1322 assert_eq!(segs[2], TailSeg::Field("name".to_string()));
1323 }
1324
1325 #[test]
1326 fn parse_tail_parses_simple_index_field() {
1327 let segs = parse_tail("[0].id");
1328 assert_eq!(segs.len(), 2, "expected 2 segments, got: {segs:?}");
1329 assert_eq!(segs[0], TailSeg::Index(0));
1330 assert_eq!(segs[1], TailSeg::Field("id".to_string()));
1331 }
1332
1333 #[test]
1334 fn parse_tail_handles_nonzero_index() {
1335 let segs = parse_tail("[2].function.arguments");
1336 assert_eq!(segs[0], TailSeg::Index(2));
1337 assert_eq!(segs[1], TailSeg::Field("function".to_string()));
1338 assert_eq!(segs[2], TailSeg::Field("arguments".to_string()));
1339 }
1340
1341 #[test]
1346 fn accessor_chunks_length_swift_uses_count() {
1347 let swift = StreamingFieldResolver::accessor("chunks.length", "swift", "chunks").unwrap();
1348 assert_eq!(swift, "chunks.count", "swift chunks.length: {swift}");
1349 }
1350
1351 #[test]
1352 fn accessor_stream_content_swift_uses_swift_closures() {
1353 let expr = StreamingFieldResolver::accessor("stream_content", "swift", "chunks").unwrap();
1354 assert!(
1356 expr.contains("{ c in"),
1357 "swift stream_content must use Swift closure syntax, got: {expr}"
1358 );
1359 assert!(
1360 !expr.contains("=>"),
1361 "swift stream_content must not contain JS arrow `=>`, got: {expr}"
1362 );
1363 assert!(
1365 expr.contains("choices()"),
1366 "swift stream_content must use .choices() method call, got: {expr}"
1367 );
1368 assert!(
1369 expr.contains("delta()"),
1370 "swift stream_content must use .delta() method call, got: {expr}"
1371 );
1372 assert!(
1373 expr.contains("content()"),
1374 "swift stream_content must use .content() method call, got: {expr}"
1375 );
1376 assert!(
1377 expr.contains(".toString()"),
1378 "swift stream_content must convert RustString via .toString(), got: {expr}"
1379 );
1380 assert!(
1381 expr.contains(".joined()"),
1382 "swift stream_content must join with .joined(), got: {expr}"
1383 );
1384 assert!(
1386 !expr.contains(".length"),
1387 "swift stream_content must not use JS .length, got: {expr}"
1388 );
1389 assert!(
1390 !expr.contains(".join("),
1391 "swift stream_content must not use JS .join(, got: {expr}"
1392 );
1393 }
1394
1395 #[test]
1396 fn accessor_stream_complete_swift_uses_swift_syntax() {
1397 let expr = StreamingFieldResolver::accessor("stream_complete", "swift", "chunks").unwrap();
1398 assert!(
1400 expr.contains("isEmpty"),
1401 "swift stream_complete must use .isEmpty, got: {expr}"
1402 );
1403 assert!(
1404 expr.contains(".last!"),
1405 "swift stream_complete must use .last!, got: {expr}"
1406 );
1407 assert!(
1408 expr.contains("choices()"),
1409 "swift stream_complete must use .choices() method call, got: {expr}"
1410 );
1411 assert!(
1412 expr.contains("finish_reason()"),
1413 "swift stream_complete must use .finish_reason(), got: {expr}"
1414 );
1415 assert!(
1416 !expr.contains(".length"),
1417 "swift stream_complete must not use JS .length, got: {expr}"
1418 );
1419 assert!(
1420 !expr.contains("!= null"),
1421 "swift stream_complete must not use JS `!= null`, got: {expr}"
1422 );
1423 }
1424
1425 #[test]
1426 fn accessor_tool_calls_swift_uses_swift_flatmap() {
1427 let expr = StreamingFieldResolver::accessor("tool_calls", "swift", "chunks").unwrap();
1428 assert!(
1430 !expr.contains("=>"),
1431 "swift tool_calls must not contain JS arrow `=>`, got: {expr}"
1432 );
1433 assert!(
1434 expr.contains("flatMap"),
1435 "swift tool_calls must use flatMap, got: {expr}"
1436 );
1437 assert!(
1438 expr.contains("choices()"),
1439 "swift tool_calls must use .choices() method call, got: {expr}"
1440 );
1441 assert!(
1442 expr.contains("delta()"),
1443 "swift tool_calls must use .delta() method call, got: {expr}"
1444 );
1445 assert!(
1446 expr.contains("tool_calls()"),
1447 "swift tool_calls must use .tool_calls() method call, got: {expr}"
1448 );
1449 }
1450
1451 #[test]
1452 fn accessor_tool_calls_deep_path_swift_uses_method_calls_with_optional_chain() {
1453 let expr = StreamingFieldResolver::accessor("tool_calls[0].function.name", "swift", "chunks").unwrap();
1457 assert!(
1458 expr.contains("function()"),
1459 "swift deep tool_calls must use .function() method call, got: {expr}"
1460 );
1461 assert!(
1462 expr.contains("name()"),
1463 "swift deep tool_calls must use .name() method call, got: {expr}"
1464 );
1465 assert!(
1466 expr.contains(".toString()"),
1467 "swift deep tool_calls must convert RustString via .toString(), got: {expr}"
1468 );
1469 assert!(
1470 !expr.contains("=>"),
1471 "swift deep tool_calls must not use JS arrow syntax, got: {expr}"
1472 );
1473 }
1474
1475 #[test]
1476 fn accessor_finish_reason_swift_uses_swift_syntax() {
1477 let expr = StreamingFieldResolver::accessor("finish_reason", "swift", "chunks").unwrap();
1478 assert!(
1480 expr.contains("isEmpty"),
1481 "swift finish_reason must use .isEmpty, got: {expr}"
1482 );
1483 assert!(
1484 expr.contains(".last!"),
1485 "swift finish_reason must use .last!, got: {expr}"
1486 );
1487 assert!(
1488 expr.contains("finish_reason()"),
1489 "swift finish_reason must use .finish_reason() method call, got: {expr}"
1490 );
1491 assert!(
1492 expr.contains(".toString()"),
1493 "swift finish_reason must convert RustString via .toString(), got: {expr}"
1494 );
1495 assert!(
1496 !expr.contains("undefined"),
1497 "swift finish_reason must not use JS `undefined`, got: {expr}"
1498 );
1499 assert!(
1500 !expr.contains(".length"),
1501 "swift finish_reason must not use JS .length, got: {expr}"
1502 );
1503 }
1504
1505 #[test]
1506 fn accessor_usage_swift_uses_swift_syntax() {
1507 let expr = StreamingFieldResolver::accessor("usage", "swift", "chunks").unwrap();
1508 assert!(expr.contains("isEmpty"), "swift usage must use .isEmpty, got: {expr}");
1510 assert!(expr.contains(".last!"), "swift usage must use .last!, got: {expr}");
1511 assert!(
1512 expr.contains("usage()"),
1513 "swift usage must use .usage() method call, got: {expr}"
1514 );
1515 assert!(
1516 !expr.contains("undefined"),
1517 "swift usage must not use JS `undefined`, got: {expr}"
1518 );
1519 assert!(
1520 !expr.contains(".length"),
1521 "swift usage must not use JS .length, got: {expr}"
1522 );
1523 }
1524
1525 #[test]
1530 fn kotlin_android_collect_snippet_uses_flow_to_list() {
1531 let snip = StreamingFieldResolver::collect_snippet("kotlin_android", "result", "chunks").unwrap();
1532 assert!(
1534 snip.contains("result.toList()"),
1535 "kotlin_android collect must use Flow.toList(), got: {snip}"
1536 );
1537 assert!(
1538 !snip.contains("asSequence()"),
1539 "kotlin_android collect must NOT use asSequence(), got: {snip}"
1540 );
1541 }
1542
1543 #[test]
1544 fn kotlin_android_stream_content_uses_property_access() {
1545 let expr = StreamingFieldResolver::accessor("stream_content", "kotlin_android", "chunks").unwrap();
1546 assert!(
1547 expr.contains(".choices"),
1548 "kotlin_android stream_content must use .choices property, got: {expr}"
1549 );
1550 assert!(
1551 !expr.contains(".choices()"),
1552 "kotlin_android stream_content must NOT use .choices() getter, got: {expr}"
1553 );
1554 assert!(
1555 expr.contains(".delta"),
1556 "kotlin_android stream_content must use .delta property, got: {expr}"
1557 );
1558 assert!(
1559 !expr.contains(".delta()"),
1560 "kotlin_android stream_content must NOT use .delta() getter, got: {expr}"
1561 );
1562 assert!(
1563 expr.contains(".content"),
1564 "kotlin_android stream_content must use .content property, got: {expr}"
1565 );
1566 assert!(
1567 !expr.contains(".content()"),
1568 "kotlin_android stream_content must NOT use .content() getter, got: {expr}"
1569 );
1570 }
1571
1572 #[test]
1573 fn kotlin_android_finish_reason_uses_name_lowercase_not_get_value() {
1574 let expr = StreamingFieldResolver::accessor("finish_reason", "kotlin_android", "chunks").unwrap();
1575 assert!(
1576 expr.contains(".finishReason"),
1577 "kotlin_android finish_reason must use .finishReason property, got: {expr}"
1578 );
1579 assert!(
1580 !expr.contains(".finishReason()"),
1581 "kotlin_android finish_reason must NOT use .finishReason() getter, got: {expr}"
1582 );
1583 assert!(
1584 expr.contains(".name"),
1585 "kotlin_android finish_reason must use .name for enum wire value, got: {expr}"
1586 );
1587 assert!(
1588 expr.contains(".lowercase()"),
1589 "kotlin_android finish_reason must use .lowercase(), got: {expr}"
1590 );
1591 assert!(
1592 !expr.contains(".getValue()"),
1593 "kotlin_android finish_reason must NOT use .getValue(), got: {expr}"
1594 );
1595 }
1596
1597 #[test]
1598 fn kotlin_android_usage_uses_property_access() {
1599 let expr = StreamingFieldResolver::accessor("usage", "kotlin_android", "chunks").unwrap();
1600 assert!(
1601 expr.contains(".usage"),
1602 "kotlin_android usage must use .usage property, got: {expr}"
1603 );
1604 assert!(
1605 !expr.contains(".usage()"),
1606 "kotlin_android usage must NOT use .usage() getter, got: {expr}"
1607 );
1608 }
1609
1610 #[test]
1611 fn kotlin_android_deep_tool_calls_uses_property_access() {
1612 let expr = StreamingFieldResolver::accessor("tool_calls[0].function.name", "kotlin_android", "chunks").unwrap();
1613 assert!(
1614 expr.contains(".function"),
1615 "kotlin_android deep tool_calls must use .function property, got: {expr}"
1616 );
1617 assert!(
1618 !expr.contains(".function()"),
1619 "kotlin_android deep tool_calls must NOT use .function() getter, got: {expr}"
1620 );
1621 assert!(
1622 expr.contains(".name"),
1623 "kotlin_android deep tool_calls must use .name property, got: {expr}"
1624 );
1625 assert!(
1626 !expr.contains(".name()"),
1627 "kotlin_android deep tool_calls must NOT use .name() getter, got: {expr}"
1628 );
1629 }
1630}