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 "elixir" => {
195 format!(
199 "{chunks_var} |> Enum.map(fn c -> (Enum.at(c.choices, 0) || %{{}}) |> Map.get(:delta, %{{}}) |> Map.get(:content, \"\") end) |> Enum.join(\"\")"
200 )
201 }
202 "python" => {
203 format!("\"\".join(c.choices[0].delta.content or \"\" for c in {chunks_var} if c.choices)")
204 }
205 "zig" => {
206 format!("{chunks_var}_content.items")
209 }
210 "swift" => {
214 format!(
215 "{chunks_var}.map {{ c in c.choices().first.flatMap {{ ch in ch.delta().content()?.toString() }} ?? \"\" }}.joined()"
216 )
217 }
218 _ => {
220 format!("{chunks_var}.map((c: any) => c.choices?.[0]?.delta?.content ?? '').join('')")
221 }
222 }),
223
224 "stream_complete" => Some(match lang {
225 "rust" => {
226 format!(
227 "{chunks_var}.last().and_then(|c| c.choices.first()).and_then(|ch| ch.finish_reason.as_ref()).is_some()"
228 )
229 }
230 "go" => {
231 format!(
232 "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 }}()"
233 )
234 }
235 "java" => {
236 format!(
237 "!{chunks_var}.isEmpty() && {chunks_var}.get({chunks_var}.size()-1).choices().stream().findFirst().flatMap(ch -> java.util.Optional.ofNullable(ch.finishReason())).isPresent()"
238 )
239 }
240 "php" => {
241 format!("!empty(${chunks_var}) && isset(end(${chunks_var})->choices[0]->finishReason)")
245 }
246 "kotlin" => {
247 format!(
249 "{chunks_var}.isNotEmpty() && {chunks_var}.last().choices()?.firstOrNull()?.finishReason() != null"
250 )
251 }
252 "python" => {
253 format!("bool({chunks_var}) and {chunks_var}[-1].choices[0].finish_reason is not None")
254 }
255 "elixir" => {
256 format!("Enum.at(List.last({chunks_var}).choices, 0).finish_reason != nil")
257 }
258 "zig" => {
261 format!("{chunks_var}.items.len > 0")
262 }
263 "swift" => {
267 format!("!{chunks_var}.isEmpty && {chunks_var}.last!.choices().first?.finish_reason() != nil")
268 }
269 _ => {
271 format!(
272 "{chunks_var}.length > 0 && {chunks_var}[{chunks_var}.length - 1].choices?.[0]?.finishReason != null"
273 )
274 }
275 }),
276
277 "no_chunks_after_done" => Some(match lang {
281 "rust" => "true".to_string(),
282 "go" => "true".to_string(),
283 "java" => "true".to_string(),
284 "php" => "true".to_string(),
285 _ => "true".to_string(),
286 }),
287
288 "tool_calls" => Some(match lang {
289 "rust" => {
290 format!(
291 "{chunks_var}.iter().flat_map(|c| c.choices.iter().flat_map(|ch| ch.delta.tool_calls.iter().flatten())).collect::<Vec<_>>()"
292 )
293 }
294 "go" => {
295 format!(
299 "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 }}()"
300 )
301 }
302 "java" => {
303 format!(
304 "{chunks_var}.stream().flatMap(c -> c.choices().stream()).flatMap(ch -> ch.delta().toolCalls() != null ? ch.delta().toolCalls().stream() : java.util.stream.Stream.empty()).toList()"
305 )
306 }
307 "php" => {
308 format!(
311 "array_merge(...array_map(fn($c) => $c->choices[0]->delta->toolCalls ?? [], ${chunks_var}))"
312 )
313 }
314 "kotlin" => {
315 format!(
317 "{chunks_var}.flatMap {{ c -> c.choices()?.flatMap {{ ch -> ch.delta()?.toolCalls() ?: emptyList() }} ?: emptyList() }}"
318 )
319 }
320 "python" => {
321 format!(
322 "[t for c in {chunks_var} for ch in (c.choices or []) for t in (ch.delta.tool_calls or [])]"
323 )
324 }
325 "elixir" => {
326 format!(
327 "{chunks_var} |> Enum.flat_map(fn c -> (List.first(c.choices) || %{{}}).delta |> Map.get(:tool_calls, []) end)"
328 )
329 }
330 "zig" => {
332 format!("{chunks_var}.items")
333 }
334 "swift" => {
338 format!(
339 "{chunks_var}.flatMap {{ c in c.choices().first.map {{ ch in ch.delta().tool_calls().map {{ Array($0) }} ?? [] }} ?? [] }}"
340 )
341 }
342 _ => {
343 format!("{chunks_var}.flatMap((c: any) => c.choices?.[0]?.delta?.toolCalls ?? [])")
344 }
345 }),
346
347 "finish_reason" => Some(match lang {
348 "rust" => {
349 format!(
352 "{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()"
353 )
354 }
355 "go" => {
356 format!(
359 "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 \"\" }}()"
360 )
361 }
362 "java" => {
363 format!(
367 "({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))"
368 )
369 }
370 "php" => {
371 format!("(!empty(${chunks_var}) ? (end(${chunks_var})->choices[0]->finishReason ?? null) : null)")
374 }
375 "kotlin" => {
376 format!(
379 "(if ({chunks_var}.isEmpty()) null else {chunks_var}.last().choices()?.firstOrNull()?.finishReason()?.getValue())"
380 )
381 }
382 "python" => {
383 format!(
387 "(str({chunks_var}[-1].choices[0].finish_reason) if {chunks_var} and {chunks_var}[-1].choices else None)"
388 )
389 }
390 "elixir" => {
391 format!("Enum.at(List.last({chunks_var}).choices, 0).finish_reason")
392 }
393 "zig" => {
396 format!(
397 "(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 \"\"; }})"
398 )
399 }
400 "swift" => {
404 format!(
405 "({chunks_var}.isEmpty ? nil : {chunks_var}.last!.choices().first?.finish_reason()?.toString())"
406 )
407 }
408 _ => {
409 format!(
410 "{chunks_var}.length > 0 ? {chunks_var}[{chunks_var}.length - 1].choices?.[0]?.finishReason : undefined"
411 )
412 }
413 }),
414
415 "usage" => Some(match lang {
420 "python" => {
421 format!("({chunks_var}[-1].usage if {chunks_var} else None)")
425 }
426 "rust" => {
427 format!("{chunks_var}.last().and_then(|c| c.usage.as_ref())")
428 }
429 "go" => {
430 format!(
431 "func() interface{{}} {{ if len({chunks_var}) == 0 {{ return nil }}; return {chunks_var}[len({chunks_var})-1].Usage }}()"
432 )
433 }
434 "java" => {
435 format!("({chunks_var}.isEmpty() ? null : {chunks_var}.get({chunks_var}.size()-1).usage())")
436 }
437 "kotlin" => {
438 format!("(if ({chunks_var}.isEmpty()) null else {chunks_var}.last().usage())")
439 }
440 "php" => {
441 format!("(!empty(${chunks_var}) ? end(${chunks_var})->usage ?? null : null)")
442 }
443 "elixir" => {
444 format!("(if length({chunks_var}) > 0, do: List.last({chunks_var}).usage, else: nil)")
445 }
446 "swift" => {
448 format!("({chunks_var}.isEmpty ? nil : {chunks_var}.last!.usage())")
449 }
450 _ => {
451 format!("({chunks_var}.length > 0 ? {chunks_var}[{chunks_var}.length - 1].usage : undefined)")
452 }
453 }),
454
455 _ => {
456 if let Some((root, tail)) = split_streaming_deep_path(field) {
460 if lang == "rust" && root == "tool_calls" {
464 return Some(render_rust_tool_calls_deep(chunks_var, tail));
465 }
466 if lang == "zig" && root == "tool_calls" {
473 return None;
474 }
475 let root_expr = Self::accessor(root, lang, chunks_var)?;
476 Some(render_deep_tail(&root_expr, tail, lang))
477 } else {
478 None
479 }
480 }
481 }
482 }
483
484 pub fn collect_snippet(lang: &str, stream_var: &str, chunks_var: &str) -> Option<String> {
490 match lang {
491 "rust" => Some(format!(
492 "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();"
493 )),
494 "go" => Some(format!(
495 "var {chunks_var} []pkg.ChatCompletionChunk\n\tfor chunk := range {stream_var} {{\n\t\t{chunks_var} = append({chunks_var}, chunk)\n\t}}"
496 )),
497 "java" => Some(format!(
498 "var {chunks_var} = new java.util.ArrayList<ChatCompletionChunk>();\n var _it = {stream_var};\n while (_it.hasNext()) {{ {chunks_var}.add(_it.next()); }}"
499 )),
500 "php" => Some(format!(
507 "${chunks_var} = is_string(${stream_var}) ? (json_decode(${stream_var}) ?: []) : iterator_to_array(${stream_var});"
508 )),
509 "python" => Some(format!(
510 "{chunks_var} = []\n async for chunk in {stream_var}:\n {chunks_var}.append(chunk)"
511 )),
512 "kotlin" => {
513 Some(format!("val {chunks_var} = {stream_var}.asSequence().toList()"))
516 }
517 "elixir" => Some(format!("{chunks_var} = Enum.to_list({stream_var})")),
518 "node" | "wasm" | "typescript" => Some(format!(
519 "const {chunks_var}: any[] = [];\n for await (const _chunk of {stream_var}) {{ {chunks_var}.push(_chunk); }}"
520 )),
521 "swift" => {
522 Some(format!(
527 "var {chunks_var}: [ChatCompletionChunk] = []\n for try await _chunk in {stream_var} {{ {chunks_var}.append(_chunk) }}"
528 ))
529 }
530 "zig" => Some(Self::collect_snippet_zig(stream_var, chunks_var, "module", "ffi")),
531 _ => None,
532 }
533 }
534
535 pub fn collect_snippet_zig(stream_var: &str, chunks_var: &str, module_name: &str, ffi_prefix: &str) -> String {
537 let stream_next = format!("{ffi_prefix}_default_client_chat_stream_next");
538 let chunk_to_json = format!("{ffi_prefix}_chat_completion_chunk_to_json");
539 let chunk_free = format!("{ffi_prefix}_chat_completion_chunk_free");
540 let free_string = format!("{ffi_prefix}_free_string");
541
542 format!(
549 concat!(
550 "var {chunks_var}: std.ArrayList([]u8) = .empty;
551",
552 " defer {{
553",
554 " for ({chunks_var}.items) |_cj| std.heap.c_allocator.free(_cj);
555",
556 " {chunks_var}.deinit(std.heap.c_allocator);
557",
558 " }}
559",
560 " var {chunks_var}_content: std.ArrayList(u8) = .empty;
561",
562 " defer {chunks_var}_content.deinit(std.heap.c_allocator);
563",
564 " while (true) {{
565",
566 " const _nc = {module_name}.c.{stream_next}({stream_var});
567",
568 " if (_nc == null) break;
569",
570 " const _np = {module_name}.c.{chunk_to_json}(_nc);
571",
572 " {module_name}.c.{chunk_free}(_nc);
573",
574 " if (_np == null) continue;
575",
576 " const _ns = std.mem.span(_np);
577",
578 " const _nj = try std.heap.c_allocator.dupe(u8, _ns);
579",
580 " {module_name}.c.{free_string}(_np);
581",
582 " if (std.json.parseFromSlice(std.json.Value, std.heap.c_allocator, _nj, .{{}})) |_cp| {{
583",
584 " defer _cp.deinit();
585",
586 " if (_cp.value.object.get(\"choices\")) |_chs|
587",
588 " if (_chs.array.items.len > 0)
589",
590 " if (_chs.array.items[0].object.get(\"delta\")) |_dl|
591",
592 " if (_dl.object.get(\"content\")) |_ct|
593",
594 " if (_ct == .string) try {chunks_var}_content.appendSlice(std.heap.c_allocator, _ct.string);
595",
596 " }} else |_| {{}}
597",
598 " try {chunks_var}.append(std.heap.c_allocator, _nj);
599",
600 " }}"
601 ),
602 chunks_var = chunks_var,
603 stream_var = stream_var,
604 module_name = module_name,
605 stream_next = stream_next,
606 chunk_to_json = chunk_to_json,
607 chunk_free = chunk_free,
608 free_string = free_string,
609 )
610 }
611}
612
613fn render_rust_tool_calls_deep(chunks_var: &str, tail: &str) -> String {
617 let segs = parse_tail(tail);
618 let idx = segs.iter().find_map(|s| match s {
620 TailSeg::Index(n) => Some(*n),
621 _ => None,
622 });
623 let field_segs: Vec<&str> = segs
624 .iter()
625 .filter_map(|s| match s {
626 TailSeg::Field(f) => Some(f.as_str()),
627 _ => None,
628 })
629 .collect();
630
631 let base = format!(
632 "{chunks_var}.iter().flat_map(|c| c.choices.iter().flat_map(|ch| ch.delta.tool_calls.iter().flatten()))"
633 );
634 let with_nth = match idx {
635 Some(n) => format!("{base}.nth({n})"),
636 None => base,
637 };
638
639 let mut expr = with_nth;
642 for (i, f) in field_segs.iter().enumerate() {
643 let is_leaf = i == field_segs.len() - 1;
644 if is_leaf {
645 expr = format!("{expr}.and_then(|x| x.{f}.as_deref())");
646 } else {
647 expr = format!("{expr}.and_then(|x| x.{f}.as_ref())");
648 }
649 }
650 format!("{expr}.unwrap_or(\"\")")
651}
652
653#[derive(Debug, PartialEq)]
658enum TailSeg {
659 Index(usize),
660 Field(String),
661}
662
663fn parse_tail(tail: &str) -> Vec<TailSeg> {
664 let mut segs = Vec::new();
665 let mut rest = tail;
666 while !rest.is_empty() {
667 if let Some(inner) = rest.strip_prefix('[') {
668 if let Some(close) = inner.find(']') {
670 let idx_str = &inner[..close];
671 if let Ok(idx) = idx_str.parse::<usize>() {
672 segs.push(TailSeg::Index(idx));
673 }
674 rest = &inner[close + 1..];
675 } else {
676 break;
677 }
678 } else if let Some(inner) = rest.strip_prefix('.') {
679 let end = inner.find(['.', '[']).unwrap_or(inner.len());
681 segs.push(TailSeg::Field(inner[..end].to_string()));
682 rest = &inner[end..];
683 } else {
684 break;
685 }
686 }
687 segs
688}
689
690fn render_deep_tail(root_expr: &str, tail: &str, lang: &str) -> String {
693 use heck::{ToLowerCamelCase, ToPascalCase};
694
695 let segs = parse_tail(tail);
696 let mut out = root_expr.to_string();
697
698 for seg in &segs {
699 match (seg, lang) {
700 (TailSeg::Index(n), "rust") => {
701 out = format!("({out})[{n}]");
702 }
703 (TailSeg::Index(n), "java") => {
704 out = format!("({out}).get({n})");
705 }
706 (TailSeg::Index(n), "kotlin") => {
707 if *n == 0 {
708 out = format!("({out}).first()");
709 } else {
710 out = format!("({out}).get({n})");
711 }
712 }
713 (TailSeg::Index(n), "elixir") => {
714 out = format!("Enum.at({out}, {n})");
715 }
716 (TailSeg::Index(n), "zig") => {
717 out = format!("({out}).items[{n}]");
718 }
719 (TailSeg::Index(n), "php") => {
720 out = format!("({out})[{n}]");
721 }
722 (TailSeg::Index(n), _) => {
723 out = format!("({out})[{n}]");
725 }
726 (TailSeg::Field(f), "rust") => {
727 use heck::ToSnakeCase;
728 out.push('.');
729 out.push_str(&f.to_snake_case());
730 }
731 (TailSeg::Field(f), "go") => {
732 use alef_codegen::naming::to_go_name;
733 out.push('.');
734 out.push_str(&to_go_name(f));
735 }
736 (TailSeg::Field(f), "java") => {
737 out.push('.');
738 out.push_str(&f.to_lower_camel_case());
739 out.push_str("()");
740 }
741 (TailSeg::Field(f), "kotlin") => {
742 out.push_str("?.");
748 out.push_str(&f.to_lower_camel_case());
749 out.push_str("()");
750 }
751 (TailSeg::Field(f), "csharp") => {
752 out.push('.');
753 out.push_str(&f.to_pascal_case());
754 }
755 (TailSeg::Field(f), "php") => {
756 out.push_str("->");
761 out.push_str(f);
762 }
763 (TailSeg::Field(f), "elixir") => {
764 out.push('.');
765 out.push_str(f);
766 }
767 (TailSeg::Field(f), "zig") => {
768 out.push('.');
769 out.push_str(f);
770 }
771 (TailSeg::Field(f), "python") | (TailSeg::Field(f), "ruby") => {
772 out.push('.');
773 out.push_str(f);
774 }
775 (TailSeg::Field(f), _) => {
777 out.push('.');
778 out.push_str(&f.to_lower_camel_case());
779 }
780 }
781 }
782
783 out
784}
785
786#[cfg(test)]
787mod tests {
788 use super::*;
789
790 #[test]
791 fn is_streaming_virtual_field_recognizes_all_fields() {
792 for field in STREAMING_VIRTUAL_FIELDS {
793 assert!(
794 is_streaming_virtual_field(field),
795 "field '{field}' not recognized as streaming virtual"
796 );
797 }
798 }
799
800 #[test]
801 fn is_streaming_virtual_field_rejects_real_fields() {
802 assert!(!is_streaming_virtual_field("content"));
803 assert!(!is_streaming_virtual_field("choices"));
804 assert!(!is_streaming_virtual_field("model"));
805 assert!(!is_streaming_virtual_field(""));
806 }
807
808 #[test]
809 fn is_streaming_virtual_field_rejects_non_root_paths_with_matching_tail() {
810 assert!(!is_streaming_virtual_field("choices[0].finish_reason"));
815 assert!(!is_streaming_virtual_field("choices[0].message.content"));
816 assert!(!is_streaming_virtual_field("data[0].embedding"));
817 }
818
819 #[test]
820 fn is_streaming_virtual_field_does_not_match_usage() {
821 assert!(!is_streaming_virtual_field("usage"));
825 assert!(!is_streaming_virtual_field("usage.total_tokens"));
826 assert!(!is_streaming_virtual_field("usage.prompt_tokens"));
827 }
828
829 #[test]
830 fn accessor_chunks_returns_var_name() {
831 assert_eq!(
832 StreamingFieldResolver::accessor("chunks", "rust", "chunks"),
833 Some("chunks".to_string())
834 );
835 assert_eq!(
836 StreamingFieldResolver::accessor("chunks", "node", "chunks"),
837 Some("chunks".to_string())
838 );
839 }
840
841 #[test]
842 fn accessor_chunks_length_uses_language_idiom() {
843 let rust = StreamingFieldResolver::accessor("chunks.length", "rust", "chunks").unwrap();
844 assert!(rust.contains(".len()"), "rust: {rust}");
845
846 let go = StreamingFieldResolver::accessor("chunks.length", "go", "chunks").unwrap();
847 assert!(go.starts_with("len("), "go: {go}");
848
849 let node = StreamingFieldResolver::accessor("chunks.length", "node", "chunks").unwrap();
850 assert!(node.contains(".length"), "node: {node}");
851
852 let php = StreamingFieldResolver::accessor("chunks.length", "php", "chunks").unwrap();
853 assert!(php.starts_with("count("), "php: {php}");
854 }
855
856 #[test]
857 fn accessor_chunks_length_zig_uses_items_len() {
858 let zig = StreamingFieldResolver::accessor("chunks.length", "zig", "chunks").unwrap();
859 assert_eq!(zig, "chunks.items.len", "zig chunks.length: {zig}");
860 }
861
862 #[test]
863 fn accessor_stream_content_zig_uses_content_items() {
864 let zig = StreamingFieldResolver::accessor("stream_content", "zig", "chunks").unwrap();
865 assert_eq!(zig, "chunks_content.items", "zig stream_content: {zig}");
866 }
867
868 #[test]
869 fn collect_snippet_zig_drains_via_ffi() {
870 let snip = StreamingFieldResolver::collect_snippet("zig", "_stream_handle", "chunks").unwrap();
871 assert!(snip.contains("std.ArrayList([]u8)"), "zig collect: {snip}");
872 assert!(snip.contains("chat_stream_next(_stream_handle)"), "zig collect: {snip}");
873 assert!(snip.contains("chunks_content"), "zig collect: {snip}");
874 assert!(
875 snip.contains("chunks.append(std.heap.c_allocator"),
876 "zig collect: {snip}"
877 );
878 assert!(snip.contains(".empty;"), "zig collect (Zig 0.16 unmanaged): {snip}");
879 }
880
881 #[test]
882 fn accessor_stream_content_rust_uses_iterator() {
883 let expr = StreamingFieldResolver::accessor("stream_content", "rust", "chunks").unwrap();
884 assert!(expr.contains(".collect::<String>()"), "rust stream_content: {expr}");
885 }
886
887 #[test]
888 fn accessor_no_chunks_after_done_returns_true() {
889 for lang in ["rust", "go", "java", "php", "node", "wasm", "elixir"] {
890 let expr = StreamingFieldResolver::accessor("no_chunks_after_done", lang, "chunks").unwrap();
891 assert_eq!(expr, "true", "lang {lang}: expected 'true', got '{expr}'");
892 }
893 }
894
895 #[test]
896 fn accessor_elixir_chunks_length_uses_length_function() {
897 let expr = StreamingFieldResolver::accessor("chunks.length", "elixir", "chunks").unwrap();
898 assert_eq!(expr, "length(chunks)", "elixir chunks.length: {expr}");
899 }
900
901 #[test]
902 fn accessor_elixir_stream_content_uses_pipe() {
903 let expr = StreamingFieldResolver::accessor("stream_content", "elixir", "chunks").unwrap();
904 assert!(expr.contains("|> Enum.join"), "elixir stream_content: {expr}");
905 assert!(expr.contains("|> Enum.map"), "elixir stream_content: {expr}");
906 assert!(
908 !expr.contains("choices[0]"),
909 "elixir stream_content must not use bracket access on list: {expr}"
910 );
911 assert!(
912 expr.contains("Enum.at("),
913 "elixir stream_content must use Enum.at for list index: {expr}"
914 );
915 }
916
917 #[test]
918 fn accessor_elixir_stream_complete_uses_list_last() {
919 let expr = StreamingFieldResolver::accessor("stream_complete", "elixir", "chunks").unwrap();
920 assert!(expr.contains("List.last(chunks)"), "elixir stream_complete: {expr}");
921 assert!(expr.contains("finish_reason != nil"), "elixir stream_complete: {expr}");
922 assert!(
924 !expr.contains("choices[0]"),
925 "elixir stream_complete must not use bracket access on list: {expr}"
926 );
927 assert!(
928 expr.contains("Enum.at("),
929 "elixir stream_complete must use Enum.at for list index: {expr}"
930 );
931 }
932
933 #[test]
934 fn accessor_elixir_finish_reason_uses_list_last() {
935 let expr = StreamingFieldResolver::accessor("finish_reason", "elixir", "chunks").unwrap();
936 assert!(expr.contains("List.last(chunks)"), "elixir finish_reason: {expr}");
937 assert!(expr.contains("finish_reason"), "elixir finish_reason: {expr}");
938 assert!(
940 !expr.contains("choices[0]"),
941 "elixir finish_reason must not use bracket access on list: {expr}"
942 );
943 assert!(
944 expr.contains("Enum.at("),
945 "elixir finish_reason must use Enum.at for list index: {expr}"
946 );
947 }
948
949 #[test]
950 fn collect_snippet_elixir_uses_enum_to_list() {
951 let snip = StreamingFieldResolver::collect_snippet("elixir", "result", "chunks").unwrap();
952 assert!(snip.contains("Enum.to_list(result)"), "elixir: {snip}");
953 assert!(snip.contains("chunks ="), "elixir: {snip}");
954 }
955
956 #[test]
957 fn collect_snippet_rust_uses_tokio_stream() {
958 let snip = StreamingFieldResolver::collect_snippet("rust", "result", "chunks").unwrap();
959 assert!(snip.contains("tokio_stream::StreamExt::collect"), "rust: {snip}");
960 assert!(snip.contains("let chunks"), "rust: {snip}");
961 assert!(snip.contains(".expect("), "rust must unwrap Result items: {snip}");
963 }
964
965 #[test]
966 fn collect_snippet_go_drains_channel() {
967 let snip = StreamingFieldResolver::collect_snippet("go", "stream", "chunks").unwrap();
968 assert!(snip.contains("for chunk := range stream"), "go: {snip}");
969 }
970
971 #[test]
972 fn collect_snippet_java_uses_iterator() {
973 let snip = StreamingFieldResolver::collect_snippet("java", "result", "chunks").unwrap();
974 assert!(snip.contains("hasNext()"), "java: {snip}");
975 }
976
977 #[test]
978 fn collect_snippet_php_decodes_json_or_iterates() {
979 let snip = StreamingFieldResolver::collect_snippet("php", "result", "chunks").unwrap();
980 assert!(snip.contains("json_decode"), "php must decode JSON: {snip}");
985 assert!(
986 snip.contains("iterator_to_array"),
987 "php must keep iterator_to_array fallback: {snip}"
988 );
989 assert!(snip.contains("$chunks ="), "php must bind $chunks: {snip}");
990 }
991
992 #[test]
993 fn collect_snippet_node_uses_for_await() {
994 let snip = StreamingFieldResolver::collect_snippet("node", "result", "chunks").unwrap();
995 assert!(snip.contains("for await"), "node: {snip}");
996 }
997
998 #[test]
999 fn collect_snippet_python_uses_async_for() {
1000 let snip = StreamingFieldResolver::collect_snippet("python", "result", "chunks").unwrap();
1001 assert!(snip.contains("async for chunk in result"), "python: {snip}");
1002 assert!(snip.contains("chunks.append(chunk)"), "python: {snip}");
1003 }
1004
1005 #[test]
1006 fn accessor_stream_content_python_uses_join() {
1007 let expr = StreamingFieldResolver::accessor("stream_content", "python", "chunks").unwrap();
1008 assert!(expr.contains("\"\".join("), "python stream_content: {expr}");
1009 assert!(expr.contains("c.choices"), "python stream_content: {expr}");
1010 }
1011
1012 #[test]
1013 fn accessor_stream_complete_python_uses_finish_reason() {
1014 let expr = StreamingFieldResolver::accessor("stream_complete", "python", "chunks").unwrap();
1015 assert!(
1016 expr.contains("finish_reason is not None"),
1017 "python stream_complete: {expr}"
1018 );
1019 }
1020
1021 #[test]
1022 fn accessor_finish_reason_python_uses_last_chunk() {
1023 let expr = StreamingFieldResolver::accessor("finish_reason", "python", "chunks").unwrap();
1024 assert!(expr.contains("chunks[-1]"), "python finish_reason: {expr}");
1025 assert!(
1027 expr.starts_with("(str(") || expr.contains("str(chunks"),
1028 "python finish_reason must wrap in str(): {expr}"
1029 );
1030 }
1031
1032 #[test]
1033 fn accessor_tool_calls_python_uses_list_comprehension() {
1034 let expr = StreamingFieldResolver::accessor("tool_calls", "python", "chunks").unwrap();
1035 assert!(expr.contains("for c in chunks"), "python tool_calls: {expr}");
1036 assert!(expr.contains("tool_calls"), "python tool_calls: {expr}");
1037 }
1038
1039 #[test]
1040 fn accessor_usage_python_uses_last_chunk() {
1041 let expr = StreamingFieldResolver::accessor("usage", "python", "chunks").unwrap();
1042 assert!(
1043 expr.contains("chunks[-1].usage"),
1044 "python usage: expected chunks[-1].usage, got: {expr}"
1045 );
1046 }
1047
1048 #[test]
1049 fn accessor_usage_total_tokens_does_not_route_via_chunks() {
1050 assert!(StreamingFieldResolver::accessor("usage.total_tokens", "python", "chunks").is_none());
1054 }
1055
1056 #[test]
1057 fn accessor_unknown_field_returns_none() {
1058 assert_eq!(
1059 StreamingFieldResolver::accessor("nonexistent_field", "rust", "chunks"),
1060 None
1061 );
1062 }
1063
1064 #[test]
1069 fn is_streaming_virtual_field_recognizes_deep_tool_calls_paths() {
1070 assert!(
1071 is_streaming_virtual_field("tool_calls[0].function.name"),
1072 "tool_calls[0].function.name should be recognized"
1073 );
1074 assert!(
1075 is_streaming_virtual_field("tool_calls[0].id"),
1076 "tool_calls[0].id should be recognized"
1077 );
1078 assert!(
1079 is_streaming_virtual_field("tool_calls[1].function.arguments"),
1080 "tool_calls[1].function.arguments should be recognized"
1081 );
1082 assert!(is_streaming_virtual_field("tool_calls"));
1084 assert!(!is_streaming_virtual_field("tool_calls_extra.name"));
1086 assert!(!is_streaming_virtual_field("nonexistent[0].field"));
1087 }
1088
1089 #[test]
1096 fn deep_tool_calls_function_name_snapshot_rust_kotlin_ts() {
1097 let field = "tool_calls[0].function.name";
1098
1099 let rust = StreamingFieldResolver::accessor(field, "rust", "chunks").unwrap();
1100 assert!(
1104 rust.contains(".nth(0)"),
1105 "rust deep tool_calls: expected .nth(0) iterator index, got: {rust}"
1106 );
1107 assert!(
1108 rust.contains("x.function.as_ref()"),
1109 "rust deep tool_calls: expected Option-aware function access, got: {rust}"
1110 );
1111 assert!(
1112 rust.contains("x.name.as_deref()"),
1113 "rust deep tool_calls: expected Option-aware name leaf, got: {rust}"
1114 );
1115 assert!(
1116 !rust.contains("// skipped"),
1117 "rust deep tool_calls: must not emit skip comment, got: {rust}"
1118 );
1119
1120 let kotlin = StreamingFieldResolver::accessor(field, "kotlin", "chunks").unwrap();
1121 assert!(
1123 kotlin.contains(".first()"),
1124 "kotlin deep tool_calls: expected .first() for index 0, got: {kotlin}"
1125 );
1126 assert!(
1127 kotlin.contains(".function()"),
1128 "kotlin deep tool_calls: expected .function() method call, got: {kotlin}"
1129 );
1130 assert!(
1131 kotlin.contains(".name()"),
1132 "kotlin deep tool_calls: expected .name() method call, got: {kotlin}"
1133 );
1134
1135 let ts = StreamingFieldResolver::accessor(field, "node", "chunks").unwrap();
1136 assert!(
1138 ts.contains("[0]"),
1139 "ts/node deep tool_calls: expected [0] index, got: {ts}"
1140 );
1141 assert!(
1142 ts.contains(".function"),
1143 "ts/node deep tool_calls: expected .function segment, got: {ts}"
1144 );
1145 assert!(
1146 ts.contains(".name"),
1147 "ts/node deep tool_calls: expected .name segment, got: {ts}"
1148 );
1149 }
1150
1151 #[test]
1152 fn deep_tool_calls_id_snapshot_all_langs() {
1153 let field = "tool_calls[0].id";
1154
1155 let rust = StreamingFieldResolver::accessor(field, "rust", "chunks").unwrap();
1156 assert!(rust.contains(".nth(0)"), "rust: {rust}");
1157 assert!(rust.contains("x.id.as_deref()"), "rust: {rust}");
1158
1159 let go = StreamingFieldResolver::accessor(field, "go", "chunks").unwrap();
1160 assert!(go.contains("[0]"), "go: {go}");
1161 assert!(go.contains(".ID"), "go: expected .ID initialism, got: {go}");
1163
1164 let python = StreamingFieldResolver::accessor(field, "python", "chunks").unwrap();
1165 assert!(python.contains("[0]"), "python: {python}");
1166 assert!(python.contains(".id"), "python: {python}");
1167
1168 let php = StreamingFieldResolver::accessor(field, "php", "chunks").unwrap();
1169 assert!(php.contains("[0]"), "php: {php}");
1170 assert!(php.contains("->id"), "php: expected ->id, got: {php}");
1171
1172 let java = StreamingFieldResolver::accessor(field, "java", "chunks").unwrap();
1173 assert!(java.contains(".get(0)"), "java: expected .get(0), got: {java}");
1174 assert!(java.contains(".id()"), "java: expected .id() method call, got: {java}");
1175
1176 let csharp = StreamingFieldResolver::accessor(field, "csharp", "chunks").unwrap();
1177 assert!(csharp.contains("[0]"), "csharp: {csharp}");
1178 assert!(
1179 csharp.contains(".Id"),
1180 "csharp: expected .Id (PascalCase), got: {csharp}"
1181 );
1182
1183 let elixir = StreamingFieldResolver::accessor(field, "elixir", "chunks").unwrap();
1184 assert!(elixir.contains("Enum.at("), "elixir: expected Enum.at(, got: {elixir}");
1185 assert!(elixir.contains(".id"), "elixir: {elixir}");
1186 }
1187
1188 #[test]
1189 fn deep_tool_calls_function_name_snapshot_python_elixir_zig() {
1190 let field = "tool_calls[0].function.name";
1191
1192 let python = StreamingFieldResolver::accessor(field, "python", "chunks").unwrap();
1193 assert!(python.contains("[0]"), "python: {python}");
1194 assert!(python.contains(".function"), "python: {python}");
1195 assert!(python.contains(".name"), "python: {python}");
1196
1197 let elixir = StreamingFieldResolver::accessor(field, "elixir", "chunks").unwrap();
1198 assert!(elixir.contains("Enum.at("), "elixir: {elixir}");
1200 assert!(elixir.contains(".function"), "elixir: {elixir}");
1201 assert!(elixir.contains(".name"), "elixir: {elixir}");
1202
1203 assert!(
1207 StreamingFieldResolver::accessor(field, "zig", "chunks").is_none(),
1208 "zig: expected None for deep tool_calls path"
1209 );
1210 }
1211
1212 #[test]
1213 fn parse_tail_parses_index_then_field_segments() {
1214 let segs = parse_tail("[0].function.name");
1215 assert_eq!(segs.len(), 3, "expected 3 segments, got: {segs:?}");
1216 assert_eq!(segs[0], TailSeg::Index(0));
1217 assert_eq!(segs[1], TailSeg::Field("function".to_string()));
1218 assert_eq!(segs[2], TailSeg::Field("name".to_string()));
1219 }
1220
1221 #[test]
1222 fn parse_tail_parses_simple_index_field() {
1223 let segs = parse_tail("[0].id");
1224 assert_eq!(segs.len(), 2, "expected 2 segments, got: {segs:?}");
1225 assert_eq!(segs[0], TailSeg::Index(0));
1226 assert_eq!(segs[1], TailSeg::Field("id".to_string()));
1227 }
1228
1229 #[test]
1230 fn parse_tail_handles_nonzero_index() {
1231 let segs = parse_tail("[2].function.arguments");
1232 assert_eq!(segs[0], TailSeg::Index(2));
1233 assert_eq!(segs[1], TailSeg::Field("function".to_string()));
1234 assert_eq!(segs[2], TailSeg::Field("arguments".to_string()));
1235 }
1236
1237 #[test]
1242 fn accessor_chunks_length_swift_uses_count() {
1243 let swift = StreamingFieldResolver::accessor("chunks.length", "swift", "chunks").unwrap();
1244 assert_eq!(swift, "chunks.count", "swift chunks.length: {swift}");
1245 }
1246
1247 #[test]
1248 fn accessor_stream_content_swift_uses_swift_closures() {
1249 let expr = StreamingFieldResolver::accessor("stream_content", "swift", "chunks").unwrap();
1250 assert!(
1252 expr.contains("{ c in"),
1253 "swift stream_content must use Swift closure syntax, got: {expr}"
1254 );
1255 assert!(
1256 !expr.contains("=>"),
1257 "swift stream_content must not contain JS arrow `=>`, got: {expr}"
1258 );
1259 assert!(
1261 expr.contains("choices()"),
1262 "swift stream_content must use .choices() method call, got: {expr}"
1263 );
1264 assert!(
1265 expr.contains("delta()"),
1266 "swift stream_content must use .delta() method call, got: {expr}"
1267 );
1268 assert!(
1269 expr.contains("content()"),
1270 "swift stream_content must use .content() method call, got: {expr}"
1271 );
1272 assert!(
1273 expr.contains(".toString()"),
1274 "swift stream_content must convert RustString via .toString(), got: {expr}"
1275 );
1276 assert!(
1277 expr.contains(".joined()"),
1278 "swift stream_content must join with .joined(), got: {expr}"
1279 );
1280 assert!(
1282 !expr.contains(".length"),
1283 "swift stream_content must not use JS .length, got: {expr}"
1284 );
1285 assert!(
1286 !expr.contains(".join("),
1287 "swift stream_content must not use JS .join(, got: {expr}"
1288 );
1289 }
1290
1291 #[test]
1292 fn accessor_stream_complete_swift_uses_swift_syntax() {
1293 let expr = StreamingFieldResolver::accessor("stream_complete", "swift", "chunks").unwrap();
1294 assert!(
1296 expr.contains("isEmpty"),
1297 "swift stream_complete must use .isEmpty, got: {expr}"
1298 );
1299 assert!(
1300 expr.contains(".last!"),
1301 "swift stream_complete must use .last!, got: {expr}"
1302 );
1303 assert!(
1304 expr.contains("choices()"),
1305 "swift stream_complete must use .choices() method call, got: {expr}"
1306 );
1307 assert!(
1308 expr.contains("finish_reason()"),
1309 "swift stream_complete must use .finish_reason(), got: {expr}"
1310 );
1311 assert!(
1312 !expr.contains(".length"),
1313 "swift stream_complete must not use JS .length, got: {expr}"
1314 );
1315 assert!(
1316 !expr.contains("!= null"),
1317 "swift stream_complete must not use JS `!= null`, got: {expr}"
1318 );
1319 }
1320
1321 #[test]
1322 fn accessor_tool_calls_swift_uses_swift_flatmap() {
1323 let expr = StreamingFieldResolver::accessor("tool_calls", "swift", "chunks").unwrap();
1324 assert!(
1326 !expr.contains("=>"),
1327 "swift tool_calls must not contain JS arrow `=>`, got: {expr}"
1328 );
1329 assert!(
1330 expr.contains("flatMap"),
1331 "swift tool_calls must use flatMap, got: {expr}"
1332 );
1333 assert!(
1334 expr.contains("choices()"),
1335 "swift tool_calls must use .choices() method call, got: {expr}"
1336 );
1337 assert!(
1338 expr.contains("delta()"),
1339 "swift tool_calls must use .delta() method call, got: {expr}"
1340 );
1341 assert!(
1342 expr.contains("tool_calls()"),
1343 "swift tool_calls must use .tool_calls() method call, got: {expr}"
1344 );
1345 }
1346
1347 #[test]
1348 fn accessor_finish_reason_swift_uses_swift_syntax() {
1349 let expr = StreamingFieldResolver::accessor("finish_reason", "swift", "chunks").unwrap();
1350 assert!(
1352 expr.contains("isEmpty"),
1353 "swift finish_reason must use .isEmpty, got: {expr}"
1354 );
1355 assert!(
1356 expr.contains(".last!"),
1357 "swift finish_reason must use .last!, got: {expr}"
1358 );
1359 assert!(
1360 expr.contains("finish_reason()"),
1361 "swift finish_reason must use .finish_reason() method call, got: {expr}"
1362 );
1363 assert!(
1364 expr.contains(".toString()"),
1365 "swift finish_reason must convert RustString via .toString(), got: {expr}"
1366 );
1367 assert!(
1368 !expr.contains("undefined"),
1369 "swift finish_reason must not use JS `undefined`, got: {expr}"
1370 );
1371 assert!(
1372 !expr.contains(".length"),
1373 "swift finish_reason must not use JS .length, got: {expr}"
1374 );
1375 }
1376
1377 #[test]
1378 fn accessor_usage_swift_uses_swift_syntax() {
1379 let expr = StreamingFieldResolver::accessor("usage", "swift", "chunks").unwrap();
1380 assert!(expr.contains("isEmpty"), "swift usage must use .isEmpty, got: {expr}");
1382 assert!(expr.contains(".last!"), "swift usage must use .last!, got: {expr}");
1383 assert!(
1384 expr.contains("usage()"),
1385 "swift usage must use .usage() method call, got: {expr}"
1386 );
1387 assert!(
1388 !expr.contains("undefined"),
1389 "swift usage must not use JS `undefined`, got: {expr}"
1390 );
1391 assert!(
1392 !expr.contains(".length"),
1393 "swift usage must not use JS .length, got: {expr}"
1394 );
1395 }
1396}