backend: 100% coverage for buildAnthropicMessages and streamAnthropicSSE
- Test all buildAnthropicMessages branches: system concat, assistant tool-calls-only, nil arguments default, tool batching, unknown role, full multi-turn conversation - Test streamAnthropicSSE done re-entry: events after message_stop and error are ignored - Remove unreachable done guard in process closure
This commit is contained in:
parent
38dd34ea20
commit
5cfa5a0df9
|
|
@ -193,9 +193,6 @@ func streamAnthropicSSE(body io.Reader, ch chan<- StreamEvent) {
|
|||
done := false
|
||||
|
||||
process := func(typ, data string) {
|
||||
if done {
|
||||
return
|
||||
}
|
||||
raw := []byte(data)
|
||||
switch typ {
|
||||
case "message_start":
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package backend
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
|
@ -184,3 +185,133 @@ func TestMapAnthropicStopReason(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- buildAnthropicMessages ---
|
||||
|
||||
func TestBuildAnthropicMessages_SystemConcat(t *testing.T) {
|
||||
sys, _ := buildAnthropicMessages([]Message{
|
||||
{Role: "system", Content: "a"},
|
||||
{Role: "system", Content: "b"},
|
||||
})
|
||||
if sys != "a\n\nb" {
|
||||
t.Errorf("system = %q", sys)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAnthropicMessages_AssistantToolCallsOnly(t *testing.T) {
|
||||
_, out := buildAnthropicMessages([]Message{
|
||||
{Role: "assistant", ToolCalls: []ToolCall{{ID: "c1", Name: "f", Arguments: json.RawMessage(`{"k":"v"}`)}}},
|
||||
})
|
||||
if len(out) != 1 {
|
||||
t.Fatalf("len = %d", len(out))
|
||||
}
|
||||
// No text block, only tool_use
|
||||
for _, b := range out[0].Content {
|
||||
if b.Type == "text" {
|
||||
t.Error("should not have text block when Content is empty")
|
||||
}
|
||||
}
|
||||
if out[0].Content[0].Type != "tool_use" {
|
||||
t.Errorf("type = %q", out[0].Content[0].Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAnthropicMessages_NilArguments(t *testing.T) {
|
||||
_, out := buildAnthropicMessages([]Message{
|
||||
{Role: "assistant", ToolCalls: []ToolCall{{ID: "c1", Name: "f", Arguments: nil}}},
|
||||
})
|
||||
block := out[0].Content[0]
|
||||
if string(block.Input) != "{}" {
|
||||
t.Errorf("input = %s; want {}", block.Input)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAnthropicMessages_ToolBatching(t *testing.T) {
|
||||
_, out := buildAnthropicMessages([]Message{
|
||||
{Role: "tool", Content: "r1", ToolCallID: "c1"},
|
||||
{Role: "tool", Content: "r2", ToolCallID: "c2"},
|
||||
})
|
||||
// Two consecutive tool messages should be batched into one user message.
|
||||
if len(out) != 1 {
|
||||
t.Fatalf("len = %d; want 1", len(out))
|
||||
}
|
||||
if out[0].Role != "user" {
|
||||
t.Errorf("role = %q", out[0].Role)
|
||||
}
|
||||
if len(out[0].Content) != 2 {
|
||||
t.Fatalf("content len = %d; want 2", len(out[0].Content))
|
||||
}
|
||||
if out[0].Content[0].Type != "tool_result" || out[0].Content[0].ToolUseID != "c1" {
|
||||
t.Errorf("block[0] = %+v", out[0].Content[0])
|
||||
}
|
||||
if out[0].Content[1].ToolUseID != "c2" {
|
||||
t.Errorf("block[1] = %+v", out[0].Content[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAnthropicMessages_UnknownRole(t *testing.T) {
|
||||
_, out := buildAnthropicMessages([]Message{
|
||||
{Role: "unknown", Content: "x"},
|
||||
{Role: "user", Content: "hi"},
|
||||
})
|
||||
// Unknown role should be skipped, user message should still appear.
|
||||
if len(out) != 1 || out[0].Role != "user" {
|
||||
t.Errorf("out = %+v", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAnthropicMessages_FullConversation(t *testing.T) {
|
||||
sys, out := buildAnthropicMessages([]Message{
|
||||
{Role: "system", Content: "sys"},
|
||||
{Role: "user", Content: "hi"},
|
||||
{Role: "assistant", Content: "let me check", ToolCalls: []ToolCall{{ID: "c1", Name: "f", Arguments: json.RawMessage(`{}`)}}},
|
||||
{Role: "tool", Content: "result", ToolCallID: "c1"},
|
||||
{Role: "assistant", Content: "done"},
|
||||
})
|
||||
if sys != "sys" {
|
||||
t.Errorf("system = %q", sys)
|
||||
}
|
||||
if len(out) != 4 {
|
||||
t.Fatalf("len = %d; want 4", len(out))
|
||||
}
|
||||
// assistant[1] should have both text and tool_use blocks
|
||||
if len(out[1].Content) != 2 {
|
||||
t.Errorf("assistant content len = %d; want 2", len(out[1].Content))
|
||||
}
|
||||
}
|
||||
|
||||
// --- streamAnthropicSSE: done re-entry and scanner break ---
|
||||
|
||||
func TestAnthropicSSE_EventsAfterStop(t *testing.T) {
|
||||
// Events after message_stop should be ignored (done flag).
|
||||
evs := collectAnthropic(
|
||||
"event: message_delta\ndata: {\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"output_tokens\":1}}\n\n" +
|
||||
"event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n" +
|
||||
"event: content_block_delta\ndata: {\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"ghost\"}}\n\n",
|
||||
)
|
||||
// Should get exactly one Done event, no "ghost" text.
|
||||
var text string
|
||||
var doneCount int
|
||||
for _, ev := range evs {
|
||||
text += ev.Content
|
||||
if ev.Done {
|
||||
doneCount++
|
||||
}
|
||||
}
|
||||
if text != "" {
|
||||
t.Errorf("text = %q; want empty", text)
|
||||
}
|
||||
if doneCount != 1 {
|
||||
t.Errorf("done count = %d; want 1", doneCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnthropicSSE_EventsAfterError(t *testing.T) {
|
||||
evs := collectAnthropic(
|
||||
"event: error\ndata: {\"error\":{\"message\":\"bad\"}}\n\n" +
|
||||
"event: content_block_delta\ndata: {\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"ghost\"}}\n\n",
|
||||
)
|
||||
if len(evs) != 1 || !evs[0].Done {
|
||||
t.Fatalf("expected single Done event; got %d", len(evs))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Reference in New Issue