// streaming filter regression tests (reasoning + tool_response passthrough) // Created by Luxferre in 2026, released into the public domain with no warranties package main import ( "strings" "testing" ) // build tags without embedding the literal token, so tooling cannot mangle it. func thinkOpen() string { return "<" + "think" + ">" } func thinkClose() string { return "<" + "/think" + ">" } // runPipeline mirrors the gateway streaming chain: thinking -> tool_call. func runPipeline(allowed map[string]bool, chunks []string) (string, string, []ToolCall) { thinkFilter := NewStreamThinkingFilter() toolFilter := NewStreamToolCallFilter(allowed) var content, reasoning []string var calls []ToolCall onContent := func(s string) { content = append(content, s) } onReasoning := func(s string) { reasoning = append(reasoning, s) } onCall := func(tc ToolCall) { calls = append(calls, tc) } forward := func(clean string) { toolFilter.Feed(clean, onContent, onReasoning, onCall) } for _, c := range chunks { thinkFilter.Feed(c, forward, onReasoning) } thinkFilter.Flush(forward, onReasoning) toolFilter.Flush(onContent, onReasoning, onCall) return strings.Join(content, ""), strings.Join(reasoning, ""), calls } func chunkify(s string, n int) []string { var out []string for i := 0; i < len(s); i += n { end := i + n if end > len(s) { end = len(s) } out = append(out, s[i:end]) } return out } func TestStreamThinkingFilterExtractsReasoning(t *testing.T) { payload := "Sure thing.\n" + thinkOpen() + "The user wants a greeting.\nI will comply." + thinkClose() + "\nHello, friend!" content, reasoning, calls := runPipeline(nil, []string{payload}) if len(calls) != 0 { t.Fatalf("unexpected calls: %v", calls) } if !strings.Contains(content, "Sure thing.") || !strings.Contains(content, "Hello, friend!") { t.Fatalf("content missing visible text: %q", content) } if strings.Contains(content, "The user wants a greeting") { t.Fatalf("reasoning leaked into content: %q", content) } if !strings.Contains(reasoning, "The user wants a greeting") || !strings.Contains(reasoning, "I will comply.") { t.Fatalf("reasoning not captured: %q", reasoning) } if strings.Contains(content, "think") { t.Fatalf("raw thinking tag leaked into content: %q", content) } } func TestStreamThinkingFilterBoundaryStability(t *testing.T) { payload := "A" + thinkOpen() + "B" + thinkClose() + "C" refContent, refReasoning, _ := runPipeline(nil, []string{payload}) if refContent != "AC" || refReasoning != "B" { t.Fatalf("unexpected reference split: content=%q reasoning=%q", refContent, refReasoning) } for _, n := range []int{1, 2, 3, 4, 5} { c, r, _ := runPipeline(nil, chunkify(payload, n)) if c != refContent || r != refReasoning { t.Fatalf("chunk size %d mismatch: content=%q reasoning=%q", n, c, r) } } } // compound echoes its own tool_response blocks as plain text; they must be // preserved verbatim in the content stream (no tools requested). func TestToolResponsePassthroughNoTools(t *testing.T) { tr := "\n{\"name\": \"web_search\", \"content\": \"tokyo=14M\"}\n\n" payload := tr + "Here is the result." content, _, calls := runPipeline(nil, []string{payload}) if len(calls) != 0 { t.Fatalf("unexpected calls: %v", calls) } if !strings.Contains(content, "") || !strings.Contains(content, "") { t.Fatalf("tool_response tags dropped: %q", content) } if !strings.Contains(content, "tokyo=14M") || !strings.Contains(content, "Here is the result.") { t.Fatalf("tool_response body or trailing text dropped: %q", content) } for _, n := range []int{1, 2, 3, 5, 9, 17} { c, _, _ := runPipeline(nil, chunkify(payload, n)) if !strings.Contains(c, "") || !strings.Contains(c, "") || !strings.Contains(c, "tokyo=14M") { t.Fatalf("chunk size %d lost tool_response content: %q", n, c) } } } // With tools enabled, an echoed tool_response plus a real tool_call: the call // is extracted and the echoed tool_response must survive (content or reasoning). func TestToolResponsePlusRealToolCall(t *testing.T) { allowed := map[string]bool{"web_search": true} tr := "\n{\"name\": \"web_search\", \"content\": \"cached\"}\n\n" call := "\n{\"name\": \"web_search\", \"arguments\": {\"q\": \"more\"}}\n" payload := tr + "Let me search more.\n" + thinkOpen() + "I will call the tool now." + thinkClose() + call content, reasoning, calls := runPipeline(allowed, []string{payload}) if len(calls) != 1 || calls[0].Function.Name != "web_search" { t.Fatalf("expected one web_search call, got %v", calls) } combined := content + reasoning if !strings.Contains(combined, "") || !strings.Contains(combined, "") { t.Fatalf("echoed tool_response tags dropped: content=%q reasoning=%q", content, reasoning) } if !strings.Contains(combined, "cached") { t.Fatalf("echoed tool_response body dropped: content=%q reasoning=%q", content, reasoning) } if !strings.Contains(reasoning, "call the tool now") { t.Fatalf("reasoning lost: %q", reasoning) } if strings.Contains(content, "") { t.Fatalf("raw tool_call leaked into content: %q", content) } } // An unregistered tool_call must never be emitted as a call, and must be kept. func TestCompoundUnregisteredToolCallKept(t *testing.T) { allowed := map[string]bool{"web_search": true} payload := "\n{\"name\": \"weather\", \"arguments\": {}}\n" content, _, calls := runPipeline(allowed, []string{payload}) if len(calls) != 0 { t.Fatalf("unregistered tool must not be emitted, got %v", calls) } if !strings.Contains(content, "weather") { t.Fatalf("unregistered tool call lost from content: %q", content) } }