280 lines
9.2 KiB
Go
280 lines
9.2 KiB
Go
package proxy
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestExtractOpenAIMessageTextStructured(t *testing.T) {
|
|
content := []interface{}{
|
|
map[string]interface{}{"type": "text", "text": "alpha"},
|
|
map[string]interface{}{"type": "input_text", "text": "beta"},
|
|
}
|
|
|
|
if got := extractOpenAIMessageText(content); got != "alphabeta" {
|
|
t.Fatalf("expected concatenated structured text, got %q", got)
|
|
}
|
|
|
|
nested := map[string]interface{}{
|
|
"content": []interface{}{map[string]interface{}{"type": "text", "text": "nested"}},
|
|
}
|
|
if got := extractOpenAIMessageText(nested); got != "nested" {
|
|
t.Fatalf("expected nested content extraction, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestOpenAIToKiroPreservesStructuredAssistantAndToolContent(t *testing.T) {
|
|
req := &OpenAIRequest{
|
|
Model: "claude-sonnet-4.5",
|
|
Messages: []OpenAIMessage{
|
|
{
|
|
Role: "system",
|
|
Content: []interface{}{
|
|
map[string]interface{}{"type": "text", "text": "system-a"},
|
|
map[string]interface{}{"type": "text", "text": "system-b"},
|
|
},
|
|
},
|
|
{Role: "user", Content: "first-question"},
|
|
{
|
|
Role: "assistant",
|
|
Content: []interface{}{
|
|
map[string]interface{}{"type": "text", "text": "assistant-structured"},
|
|
},
|
|
},
|
|
{
|
|
Role: "tool",
|
|
ToolCallID: "call_1",
|
|
Content: []interface{}{
|
|
map[string]interface{}{"type": "text", "text": "tool-result-structured"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
payload := OpenAIToKiro(req, false)
|
|
|
|
if len(payload.ConversationState.History) != 2 {
|
|
t.Fatalf("expected 2 history items, got %d", len(payload.ConversationState.History))
|
|
}
|
|
|
|
firstHistoryUser := payload.ConversationState.History[0].UserInputMessage
|
|
if firstHistoryUser == nil {
|
|
t.Fatalf("expected first history item to be user message")
|
|
}
|
|
if !strings.Contains(firstHistoryUser.Content, "system-a") ||
|
|
!strings.Contains(firstHistoryUser.Content, "system-b") ||
|
|
!strings.Contains(firstHistoryUser.Content, "first-question") {
|
|
t.Fatalf("expected merged system+user content, got %q", firstHistoryUser.Content)
|
|
}
|
|
|
|
historyAssistant := payload.ConversationState.History[1].AssistantResponseMessage
|
|
if historyAssistant == nil {
|
|
t.Fatalf("expected second history item to be assistant message")
|
|
}
|
|
if historyAssistant.Content != "assistant-structured" {
|
|
t.Fatalf("expected assistant structured content to be preserved, got %q", historyAssistant.Content)
|
|
}
|
|
|
|
cur := payload.ConversationState.CurrentMessage.UserInputMessage
|
|
if !strings.Contains(cur.Content, "tool-result-structured") {
|
|
t.Fatalf("expected tool-result continuation content, got %q", cur.Content)
|
|
}
|
|
if cur.UserInputMessageContext == nil || len(cur.UserInputMessageContext.ToolResults) != 1 {
|
|
t.Fatalf("expected one tool result in current context")
|
|
}
|
|
gotToolText := cur.UserInputMessageContext.ToolResults[0].Content[0].Text
|
|
if gotToolText != "tool-result-structured" {
|
|
t.Fatalf("expected structured tool result text, got %q", gotToolText)
|
|
}
|
|
}
|
|
|
|
func TestOpenAIToKiroAssistantMapContentInHistory(t *testing.T) {
|
|
req := &OpenAIRequest{
|
|
Model: "claude-sonnet-4.5",
|
|
Messages: []OpenAIMessage{
|
|
{Role: "user", Content: "u1"},
|
|
{Role: "assistant", Content: map[string]interface{}{"type": "text", "text": "assistant-map"}},
|
|
{Role: "user", Content: "u2"},
|
|
},
|
|
}
|
|
|
|
payload := OpenAIToKiro(req, false)
|
|
|
|
if len(payload.ConversationState.History) != 2 {
|
|
t.Fatalf("expected 2 history entries, got %d", len(payload.ConversationState.History))
|
|
}
|
|
assistant := payload.ConversationState.History[1].AssistantResponseMessage
|
|
if assistant == nil {
|
|
t.Fatalf("expected second history entry to be assistant")
|
|
}
|
|
if assistant.Content != "assistant-map" {
|
|
t.Fatalf("expected assistant map content preserved, got %q", assistant.Content)
|
|
}
|
|
}
|
|
|
|
func TestOpenAIToKiroAssistantToolCallsDoNotInjectPlaceholder(t *testing.T) {
|
|
req := &OpenAIRequest{
|
|
Model: "claude-sonnet-4.5",
|
|
Messages: []OpenAIMessage{
|
|
{Role: "user", Content: "find weather"},
|
|
{
|
|
Role: "assistant",
|
|
Content: nil,
|
|
ToolCalls: []ToolCall{{
|
|
ID: "call_1",
|
|
Type: "function",
|
|
Function: struct {
|
|
Name string `json:"name"`
|
|
Arguments string `json:"arguments"`
|
|
}{Name: "get_weather", Arguments: "{}"},
|
|
}},
|
|
},
|
|
{Role: "user", Content: "continue"},
|
|
},
|
|
}
|
|
|
|
payload := OpenAIToKiro(req, false)
|
|
if len(payload.ConversationState.History) < 2 {
|
|
t.Fatalf("expected history with assistant tool call")
|
|
}
|
|
assistant := payload.ConversationState.History[1].AssistantResponseMessage
|
|
if assistant == nil {
|
|
t.Fatalf("expected assistant history entry")
|
|
}
|
|
if assistant.Content != "" {
|
|
t.Fatalf("expected empty assistant content for tool-call-only turn, got %q", assistant.Content)
|
|
}
|
|
}
|
|
|
|
func TestOpenAIConversationIDStableFromAnchor(t *testing.T) {
|
|
baseMessages := []OpenAIMessage{
|
|
{Role: "system", Content: "You are helpful"},
|
|
{Role: "user", Content: "Build calculator"},
|
|
{Role: "assistant", Content: "Sure"},
|
|
{Role: "user", Content: "Continue"},
|
|
}
|
|
|
|
reqA := &OpenAIRequest{Model: "claude-sonnet-4.5", Messages: baseMessages}
|
|
reqB := &OpenAIRequest{Model: "claude-sonnet-4.5", Messages: append(baseMessages, OpenAIMessage{Role: "assistant", Content: "Next step"})}
|
|
|
|
payloadA := OpenAIToKiro(reqA, false)
|
|
payloadB := OpenAIToKiro(reqB, false)
|
|
|
|
if payloadA.ConversationState.ConversationID == "" || payloadB.ConversationState.ConversationID == "" {
|
|
t.Fatalf("expected non-empty conversation IDs")
|
|
}
|
|
if payloadA.ConversationState.ConversationID != payloadB.ConversationState.ConversationID {
|
|
t.Fatalf("expected stable conversation ID across turns, got %q vs %q", payloadA.ConversationState.ConversationID, payloadB.ConversationState.ConversationID)
|
|
}
|
|
}
|
|
|
|
func TestClaudeConversationIDStableFromAnchor(t *testing.T) {
|
|
reqA := &ClaudeRequest{
|
|
Model: "claude-sonnet-4.5",
|
|
System: "sys",
|
|
Messages: []ClaudeMessage{
|
|
{Role: "user", Content: "hello"},
|
|
},
|
|
}
|
|
reqB := &ClaudeRequest{
|
|
Model: "claude-sonnet-4.5",
|
|
System: "sys",
|
|
Messages: []ClaudeMessage{
|
|
{Role: "user", Content: "hello"},
|
|
{Role: "assistant", Content: "ok"},
|
|
{Role: "user", Content: "next"},
|
|
},
|
|
}
|
|
|
|
payloadA := ClaudeToKiro(reqA, false)
|
|
payloadB := ClaudeToKiro(reqB, false)
|
|
|
|
if payloadA.ConversationState.ConversationID == "" || payloadB.ConversationState.ConversationID == "" {
|
|
t.Fatalf("expected non-empty conversation IDs")
|
|
}
|
|
if payloadA.ConversationState.ConversationID != payloadB.ConversationState.ConversationID {
|
|
t.Fatalf("expected stable conversation ID across turns, got %q vs %q", payloadA.ConversationState.ConversationID, payloadB.ConversationState.ConversationID)
|
|
}
|
|
}
|
|
|
|
func TestOpenAIConversationIDRandomForSyntheticAnchor(t *testing.T) {
|
|
req := &OpenAIRequest{
|
|
Model: "claude-sonnet-4.5",
|
|
Messages: []OpenAIMessage{
|
|
{Role: "assistant", Content: "prefill"},
|
|
},
|
|
}
|
|
|
|
payloadA := OpenAIToKiro(req, false)
|
|
payloadB := OpenAIToKiro(req, false)
|
|
|
|
if payloadA.ConversationState.ConversationID == payloadB.ConversationState.ConversationID {
|
|
t.Fatalf("expected synthetic anchor to generate non-deterministic conversation IDs")
|
|
}
|
|
}
|
|
|
|
func TestClaudeToKiroDropsLeadingAssistantHistory(t *testing.T) {
|
|
req := &ClaudeRequest{
|
|
Model: "claude-sonnet-4.5",
|
|
Messages: []ClaudeMessage{
|
|
{Role: "assistant", Content: "prefill"},
|
|
{Role: "user", Content: "real user message"},
|
|
},
|
|
}
|
|
|
|
payload := ClaudeToKiro(req, false)
|
|
|
|
if len(payload.ConversationState.History) != 0 {
|
|
t.Fatalf("expected leading assistant-only history to be dropped, got %d entries", len(payload.ConversationState.History))
|
|
}
|
|
|
|
if strings.Contains(payload.ConversationState.CurrentMessage.UserInputMessage.Content, "Begin conversation") {
|
|
t.Fatalf("unexpected synthetic Begin conversation injection in current content: %q", payload.ConversationState.CurrentMessage.UserInputMessage.Content)
|
|
}
|
|
}
|
|
|
|
func TestKiroToClaudeResponseCanEmitEmptyThinkingBlock(t *testing.T) {
|
|
resp := KiroToClaudeResponse("final answer", "", true, nil, 10, 20, "claude-sonnet-4.6")
|
|
|
|
if len(resp.Content) != 2 {
|
|
t.Fatalf("expected empty thinking block plus text block, got %d blocks", len(resp.Content))
|
|
}
|
|
if resp.Content[0].Type != "thinking" {
|
|
t.Fatalf("expected first block to be thinking, got %#v", resp.Content[0])
|
|
}
|
|
if resp.Content[0].Thinking != "" {
|
|
t.Fatalf("expected omitted thinking block to have empty content, got %#v", resp.Content[0].Thinking)
|
|
}
|
|
if resp.Content[1].Type != "text" || resp.Content[1].Text != "final answer" {
|
|
t.Fatalf("expected text block to be preserved, got %#v", resp.Content[1])
|
|
}
|
|
}
|
|
|
|
func TestToolResultsContinuationIncludesInstructionPrefix(t *testing.T) {
|
|
req := &OpenAIRequest{
|
|
Model: "claude-sonnet-4.5",
|
|
Messages: []OpenAIMessage{
|
|
{Role: "user", Content: "find data"},
|
|
{Role: "assistant", ToolCalls: []ToolCall{{
|
|
ID: "call_1",
|
|
Type: "function",
|
|
Function: struct {
|
|
Name string `json:"name"`
|
|
Arguments string `json:"arguments"`
|
|
}{Name: "fetch", Arguments: "{}"},
|
|
}}},
|
|
{Role: "tool", ToolCallID: "call_1", Content: "result-1"},
|
|
},
|
|
}
|
|
|
|
payload := OpenAIToKiro(req, false)
|
|
content := payload.ConversationState.CurrentMessage.UserInputMessage.Content
|
|
|
|
if !strings.Contains(content, toolResultsContinuationPrefix) {
|
|
t.Fatalf("expected tool continuation prefix, got %q", content)
|
|
}
|
|
if !strings.Contains(content, "result-1") {
|
|
t.Fatalf("expected tool result text in continuation content, got %q", content)
|
|
}
|
|
}
|