package assistantturn import ( "testing" "ds2api/internal/promptcompat" "ds2api/internal/sse" ) func TestBuildTurnFromCollectedTextCitation(t *testing.T) { turn := BuildTurnFromCollected(sse.CollectResult{ Text: "See [citation:1]", CitationLinks: map[int]string{1: "https://example.com"}, }, BuildOptions{Model: "deepseek-v4-flash", Prompt: "prompt", SearchEnabled: true}) if turn.Text != "See [1](https://example.com)" { t.Fatalf("text mismatch: %q", turn.Text) } if turn.StopReason != StopReasonStop { t.Fatalf("stop reason mismatch: %q", turn.StopReason) } if turn.Error != nil { t.Fatalf("unexpected error: %#v", turn.Error) } } func TestBuildTurnFromCollectedKeepsNonStreamReferenceLinks(t *testing.T) { turn := BuildTurnFromCollected(sse.CollectResult{ Text: "结论[reference:0],补充[reference:1]。", CitationLinks: map[int]string{ 1: "https://example.com/a", 2: "https://example.com/b", }, }, BuildOptions{Model: "deepseek-v4-flash-search", Prompt: "prompt", SearchEnabled: true}) want := "结论[0](https://example.com/a),补充[1](https://example.com/b)。" if turn.Text != want { t.Fatalf("text mismatch: got %q want %q", turn.Text, want) } } func TestBuildTurnFromCollectedToolCall(t *testing.T) { turn := BuildTurnFromCollected(sse.CollectResult{ Text: `{"x":1}`, }, BuildOptions{ ToolNames: []string{"Write"}, ToolsRaw: []any{map[string]any{ "name": "Write", "input_schema": map[string]any{ "type": "object", "properties": map[string]any{ "content": map[string]any{"type": "string"}, }, }, }}, }) if len(turn.ToolCalls) != 1 { t.Fatalf("expected one tool call, got %d", len(turn.ToolCalls)) } if turn.StopReason != StopReasonToolCalls { t.Fatalf("stop reason mismatch: %q", turn.StopReason) } if _, ok := turn.ToolCalls[0].Input["content"].(string); !ok { t.Fatalf("expected content coerced to string, got %#v", turn.ToolCalls[0].Input["content"]) } } func TestBuildTurnFromCollectedThinkingOnlyIsEmptyOutput(t *testing.T) { turn := BuildTurnFromCollected(sse.CollectResult{Thinking: "hidden"}, BuildOptions{}) if turn.Error == nil || turn.Error.Code != "upstream_empty_output" { t.Fatalf("expected empty output error, got %#v", turn.Error) } } func TestBuildTurnFromCollectedToolChoiceRequired(t *testing.T) { turn := BuildTurnFromCollected(sse.CollectResult{Text: "hello"}, BuildOptions{ ToolChoice: promptcompat.ToolChoicePolicy{Mode: promptcompat.ToolChoiceRequired}, }) if turn.Error == nil || turn.Error.Code != "tool_choice_violation" { t.Fatalf("expected tool choice violation, got %#v", turn.Error) } } func TestBuildTurnFromStreamSnapshotUsesVisibleTextAndRawToolDetection(t *testing.T) { turn := BuildTurnFromStreamSnapshot(StreamSnapshot{ RawText: `{"x":1}`, VisibleText: "", }, BuildOptions{ ToolNames: []string{"Write"}, ToolsRaw: []any{map[string]any{ "name": "Write", "schema": map[string]any{ "type": "object", "properties": map[string]any{ "content": map[string]any{"type": "string"}, }, }, }}, }) if len(turn.ToolCalls) != 1 { t.Fatalf("expected stream snapshot tool call, got %d", len(turn.ToolCalls)) } if _, ok := turn.ToolCalls[0].Input["content"].(string); !ok { t.Fatalf("expected stream snapshot schema coercion, got %#v", turn.ToolCalls[0].Input["content"]) } } func TestBuildTurnFromStreamSnapshotAlreadyEmittedToolAvoidsEmptyError(t *testing.T) { turn := BuildTurnFromStreamSnapshot(StreamSnapshot{AlreadyEmittedCalls: true}, BuildOptions{}) if turn.Error != nil { t.Fatalf("unexpected empty-output error after emitted tool call: %#v", turn.Error) } if turn.StopReason != StopReasonToolCalls { t.Fatalf("stop reason mismatch: %q", turn.StopReason) } } func TestFinalizeTurnStopOutcome(t *testing.T) { turn := BuildTurnFromCollected(sse.CollectResult{Text: "hello"}, BuildOptions{}) outcome := FinalizeTurn(turn, FinalizeOptions{}) if outcome.ShouldFail { t.Fatalf("unexpected failure: %#v", outcome.Error) } if outcome.FinishReason != "stop" || !outcome.HasVisibleText || !outcome.HasVisibleOutput { t.Fatalf("unexpected outcome: %#v", outcome) } } func TestFinalizeTurnToolCallsOutcome(t *testing.T) { turn := BuildTurnFromStreamSnapshot(StreamSnapshot{AlreadyEmittedCalls: true}, BuildOptions{}) outcome := FinalizeTurn(turn, FinalizeOptions{AlreadyEmittedToolCalls: true}) if outcome.ShouldFail || outcome.FinishReason != "tool_calls" || !outcome.HasToolCalls { t.Fatalf("unexpected tool outcome: %#v", outcome) } } func TestFinalizeTurnContentFilterOutcome(t *testing.T) { turn := BuildTurnFromCollected(sse.CollectResult{ContentFilter: true}, BuildOptions{}) outcome := FinalizeTurn(turn, FinalizeOptions{}) if !outcome.ShouldFail || outcome.Error == nil || outcome.Error.Code != "content_filter" { t.Fatalf("expected content filter failure, got %#v", outcome) } }