Add tests

This commit is contained in:
2026-03-03 04:11:11 +00:00
parent cb631479a1
commit c2b6945cab
13 changed files with 5492 additions and 5 deletions

918
internal/api/types_test.go Normal file
View File

@@ -0,0 +1,918 @@
package api
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestInputUnion_UnmarshalJSON(t *testing.T) {
tests := []struct {
name string
input string
expectError bool
validate func(t *testing.T, u InputUnion)
}{
{
name: "string input",
input: `"hello world"`,
validate: func(t *testing.T, u InputUnion) {
require.NotNil(t, u.String)
assert.Equal(t, "hello world", *u.String)
assert.Nil(t, u.Items)
},
},
{
name: "empty string input",
input: `""`,
validate: func(t *testing.T, u InputUnion) {
require.NotNil(t, u.String)
assert.Equal(t, "", *u.String)
assert.Nil(t, u.Items)
},
},
{
name: "null input",
input: `null`,
validate: func(t *testing.T, u InputUnion) {
assert.Nil(t, u.String)
assert.Nil(t, u.Items)
},
},
{
name: "array input with single message",
input: `[{
"type": "message",
"role": "user",
"content": "hello"
}]`,
validate: func(t *testing.T, u InputUnion) {
assert.Nil(t, u.String)
require.Len(t, u.Items, 1)
assert.Equal(t, "message", u.Items[0].Type)
assert.Equal(t, "user", u.Items[0].Role)
},
},
{
name: "array input with multiple messages",
input: `[{
"type": "message",
"role": "user",
"content": "hello"
}, {
"type": "message",
"role": "assistant",
"content": "hi there"
}]`,
validate: func(t *testing.T, u InputUnion) {
assert.Nil(t, u.String)
require.Len(t, u.Items, 2)
assert.Equal(t, "user", u.Items[0].Role)
assert.Equal(t, "assistant", u.Items[1].Role)
},
},
{
name: "empty array",
input: `[]`,
validate: func(t *testing.T, u InputUnion) {
assert.Nil(t, u.String)
require.NotNil(t, u.Items)
assert.Len(t, u.Items, 0)
},
},
{
name: "array with function_call_output",
input: `[{
"type": "function_call_output",
"call_id": "call_123",
"name": "get_weather",
"output": "{\"temperature\": 72}"
}]`,
validate: func(t *testing.T, u InputUnion) {
assert.Nil(t, u.String)
require.Len(t, u.Items, 1)
assert.Equal(t, "function_call_output", u.Items[0].Type)
assert.Equal(t, "call_123", u.Items[0].CallID)
assert.Equal(t, "get_weather", u.Items[0].Name)
assert.Equal(t, `{"temperature": 72}`, u.Items[0].Output)
},
},
{
name: "invalid JSON",
input: `{invalid json}`,
expectError: true,
},
{
name: "invalid type - number",
input: `123`,
expectError: true,
},
{
name: "invalid type - object",
input: `{"key": "value"}`,
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var u InputUnion
err := json.Unmarshal([]byte(tt.input), &u)
if tt.expectError {
assert.Error(t, err)
return
}
require.NoError(t, err)
if tt.validate != nil {
tt.validate(t, u)
}
})
}
}
func TestInputUnion_MarshalJSON(t *testing.T) {
tests := []struct {
name string
input InputUnion
expected string
}{
{
name: "string value",
input: InputUnion{
String: stringPtr("hello world"),
},
expected: `"hello world"`,
},
{
name: "empty string",
input: InputUnion{
String: stringPtr(""),
},
expected: `""`,
},
{
name: "array value",
input: InputUnion{
Items: []InputItem{
{Type: "message", Role: "user"},
},
},
expected: `[{"type":"message","role":"user"}]`,
},
{
name: "empty array",
input: InputUnion{
Items: []InputItem{},
},
expected: `[]`,
},
{
name: "nil values",
input: InputUnion{},
expected: `null`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := json.Marshal(tt.input)
require.NoError(t, err)
assert.JSONEq(t, tt.expected, string(data))
})
}
}
func TestInputUnion_RoundTrip(t *testing.T) {
tests := []struct {
name string
input InputUnion
}{
{
name: "string",
input: InputUnion{
String: stringPtr("test message"),
},
},
{
name: "array with messages",
input: InputUnion{
Items: []InputItem{
{Type: "message", Role: "user", Content: json.RawMessage(`"hello"`)},
{Type: "message", Role: "assistant", Content: json.RawMessage(`"hi"`)},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Marshal
data, err := json.Marshal(tt.input)
require.NoError(t, err)
// Unmarshal
var result InputUnion
err = json.Unmarshal(data, &result)
require.NoError(t, err)
// Verify equivalence
if tt.input.String != nil {
require.NotNil(t, result.String)
assert.Equal(t, *tt.input.String, *result.String)
}
if tt.input.Items != nil {
require.NotNil(t, result.Items)
assert.Len(t, result.Items, len(tt.input.Items))
}
})
}
}
func TestResponseRequest_NormalizeInput(t *testing.T) {
tests := []struct {
name string
request ResponseRequest
validate func(t *testing.T, msgs []Message)
}{
{
name: "string input creates user message",
request: ResponseRequest{
Input: InputUnion{
String: stringPtr("hello world"),
},
},
validate: func(t *testing.T, msgs []Message) {
require.Len(t, msgs, 1)
assert.Equal(t, "user", msgs[0].Role)
require.Len(t, msgs[0].Content, 1)
assert.Equal(t, "input_text", msgs[0].Content[0].Type)
assert.Equal(t, "hello world", msgs[0].Content[0].Text)
},
},
{
name: "message with string content",
request: ResponseRequest{
Input: InputUnion{
Items: []InputItem{
{
Type: "message",
Role: "user",
Content: json.RawMessage(`"what is the weather?"`),
},
},
},
},
validate: func(t *testing.T, msgs []Message) {
require.Len(t, msgs, 1)
assert.Equal(t, "user", msgs[0].Role)
require.Len(t, msgs[0].Content, 1)
assert.Equal(t, "input_text", msgs[0].Content[0].Type)
assert.Equal(t, "what is the weather?", msgs[0].Content[0].Text)
},
},
{
name: "assistant message with string content uses output_text",
request: ResponseRequest{
Input: InputUnion{
Items: []InputItem{
{
Type: "message",
Role: "assistant",
Content: json.RawMessage(`"The weather is sunny"`),
},
},
},
},
validate: func(t *testing.T, msgs []Message) {
require.Len(t, msgs, 1)
assert.Equal(t, "assistant", msgs[0].Role)
require.Len(t, msgs[0].Content, 1)
assert.Equal(t, "output_text", msgs[0].Content[0].Type)
assert.Equal(t, "The weather is sunny", msgs[0].Content[0].Text)
},
},
{
name: "message with content blocks array",
request: ResponseRequest{
Input: InputUnion{
Items: []InputItem{
{
Type: "message",
Role: "user",
Content: json.RawMessage(`[
{"type": "input_text", "text": "hello"},
{"type": "input_text", "text": "world"}
]`),
},
},
},
},
validate: func(t *testing.T, msgs []Message) {
require.Len(t, msgs, 1)
assert.Equal(t, "user", msgs[0].Role)
require.Len(t, msgs[0].Content, 2)
assert.Equal(t, "input_text", msgs[0].Content[0].Type)
assert.Equal(t, "hello", msgs[0].Content[0].Text)
assert.Equal(t, "input_text", msgs[0].Content[1].Type)
assert.Equal(t, "world", msgs[0].Content[1].Text)
},
},
{
name: "message with tool_use blocks",
request: ResponseRequest{
Input: InputUnion{
Items: []InputItem{
{
Type: "message",
Role: "assistant",
Content: json.RawMessage(`[
{
"type": "tool_use",
"id": "call_123",
"name": "get_weather",
"input": {"location": "San Francisco"}
}
]`),
},
},
},
},
validate: func(t *testing.T, msgs []Message) {
require.Len(t, msgs, 1)
assert.Equal(t, "assistant", msgs[0].Role)
assert.Len(t, msgs[0].Content, 0)
require.Len(t, msgs[0].ToolCalls, 1)
assert.Equal(t, "call_123", msgs[0].ToolCalls[0].ID)
assert.Equal(t, "get_weather", msgs[0].ToolCalls[0].Name)
assert.JSONEq(t, `{"location":"San Francisco"}`, msgs[0].ToolCalls[0].Arguments)
},
},
{
name: "message with mixed text and tool_use",
request: ResponseRequest{
Input: InputUnion{
Items: []InputItem{
{
Type: "message",
Role: "assistant",
Content: json.RawMessage(`[
{
"type": "output_text",
"text": "Let me check the weather"
},
{
"type": "tool_use",
"id": "call_456",
"name": "get_weather",
"input": {"location": "Boston"}
}
]`),
},
},
},
},
validate: func(t *testing.T, msgs []Message) {
require.Len(t, msgs, 1)
assert.Equal(t, "assistant", msgs[0].Role)
require.Len(t, msgs[0].Content, 1)
assert.Equal(t, "output_text", msgs[0].Content[0].Type)
assert.Equal(t, "Let me check the weather", msgs[0].Content[0].Text)
require.Len(t, msgs[0].ToolCalls, 1)
assert.Equal(t, "call_456", msgs[0].ToolCalls[0].ID)
},
},
{
name: "multiple tool_use blocks",
request: ResponseRequest{
Input: InputUnion{
Items: []InputItem{
{
Type: "message",
Role: "assistant",
Content: json.RawMessage(`[
{
"type": "tool_use",
"id": "call_1",
"name": "get_weather",
"input": {"location": "NYC"}
},
{
"type": "tool_use",
"id": "call_2",
"name": "get_time",
"input": {"timezone": "EST"}
}
]`),
},
},
},
},
validate: func(t *testing.T, msgs []Message) {
require.Len(t, msgs, 1)
require.Len(t, msgs[0].ToolCalls, 2)
assert.Equal(t, "call_1", msgs[0].ToolCalls[0].ID)
assert.Equal(t, "get_weather", msgs[0].ToolCalls[0].Name)
assert.Equal(t, "call_2", msgs[0].ToolCalls[1].ID)
assert.Equal(t, "get_time", msgs[0].ToolCalls[1].Name)
},
},
{
name: "function_call_output item",
request: ResponseRequest{
Input: InputUnion{
Items: []InputItem{
{
Type: "function_call_output",
CallID: "call_123",
Name: "get_weather",
Output: `{"temperature": 72, "condition": "sunny"}`,
},
},
},
},
validate: func(t *testing.T, msgs []Message) {
require.Len(t, msgs, 1)
assert.Equal(t, "tool", msgs[0].Role)
assert.Equal(t, "call_123", msgs[0].CallID)
assert.Equal(t, "get_weather", msgs[0].Name)
require.Len(t, msgs[0].Content, 1)
assert.Equal(t, "input_text", msgs[0].Content[0].Type)
assert.Equal(t, `{"temperature": 72, "condition": "sunny"}`, msgs[0].Content[0].Text)
},
},
{
name: "multiple messages in conversation",
request: ResponseRequest{
Input: InputUnion{
Items: []InputItem{
{
Type: "message",
Role: "user",
Content: json.RawMessage(`"what is 2+2?"`),
},
{
Type: "message",
Role: "assistant",
Content: json.RawMessage(`"The answer is 4"`),
},
{
Type: "message",
Role: "user",
Content: json.RawMessage(`"thanks!"`),
},
},
},
},
validate: func(t *testing.T, msgs []Message) {
require.Len(t, msgs, 3)
assert.Equal(t, "user", msgs[0].Role)
assert.Equal(t, "assistant", msgs[1].Role)
assert.Equal(t, "user", msgs[2].Role)
},
},
{
name: "complete tool calling flow",
request: ResponseRequest{
Input: InputUnion{
Items: []InputItem{
{
Type: "message",
Role: "user",
Content: json.RawMessage(`"what is the weather?"`),
},
{
Type: "message",
Role: "assistant",
Content: json.RawMessage(`[
{
"type": "tool_use",
"id": "call_abc",
"name": "get_weather",
"input": {"location": "Seattle"}
}
]`),
},
{
Type: "function_call_output",
CallID: "call_abc",
Name: "get_weather",
Output: `{"temp": 55, "condition": "rainy"}`,
},
},
},
},
validate: func(t *testing.T, msgs []Message) {
require.Len(t, msgs, 3)
assert.Equal(t, "user", msgs[0].Role)
assert.Equal(t, "assistant", msgs[1].Role)
require.Len(t, msgs[1].ToolCalls, 1)
assert.Equal(t, "tool", msgs[2].Role)
assert.Equal(t, "call_abc", msgs[2].CallID)
},
},
{
name: "message without type defaults to message",
request: ResponseRequest{
Input: InputUnion{
Items: []InputItem{
{
Role: "user",
Content: json.RawMessage(`"hello"`),
},
},
},
},
validate: func(t *testing.T, msgs []Message) {
require.Len(t, msgs, 1)
assert.Equal(t, "user", msgs[0].Role)
},
},
{
name: "message with nil content",
request: ResponseRequest{
Input: InputUnion{
Items: []InputItem{
{
Type: "message",
Role: "user",
Content: nil,
},
},
},
},
validate: func(t *testing.T, msgs []Message) {
require.Len(t, msgs, 1)
assert.Equal(t, "user", msgs[0].Role)
assert.Len(t, msgs[0].Content, 0)
},
},
{
name: "tool_use with empty input",
request: ResponseRequest{
Input: InputUnion{
Items: []InputItem{
{
Type: "message",
Role: "assistant",
Content: json.RawMessage(`[
{
"type": "tool_use",
"id": "call_xyz",
"name": "no_args_function",
"input": {}
}
]`),
},
},
},
},
validate: func(t *testing.T, msgs []Message) {
require.Len(t, msgs, 1)
require.Len(t, msgs[0].ToolCalls, 1)
assert.Equal(t, "call_xyz", msgs[0].ToolCalls[0].ID)
assert.JSONEq(t, `{}`, msgs[0].ToolCalls[0].Arguments)
},
},
{
name: "content blocks with unknown types ignored",
request: ResponseRequest{
Input: InputUnion{
Items: []InputItem{
{
Type: "message",
Role: "user",
Content: json.RawMessage(`[
{"type": "input_text", "text": "visible"},
{"type": "unknown_type", "data": "ignored"},
{"type": "input_text", "text": "also visible"}
]`),
},
},
},
},
validate: func(t *testing.T, msgs []Message) {
require.Len(t, msgs, 1)
require.Len(t, msgs[0].Content, 2)
assert.Equal(t, "visible", msgs[0].Content[0].Text)
assert.Equal(t, "also visible", msgs[0].Content[1].Text)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
msgs := tt.request.NormalizeInput()
if tt.validate != nil {
tt.validate(t, msgs)
}
})
}
}
func TestResponseRequest_Validate(t *testing.T) {
tests := []struct {
name string
request *ResponseRequest
expectError bool
errorMsg string
}{
{
name: "valid request with string input",
request: &ResponseRequest{
Model: "gpt-4",
Input: InputUnion{
String: stringPtr("hello"),
},
},
expectError: false,
},
{
name: "valid request with array input",
request: &ResponseRequest{
Model: "gpt-4",
Input: InputUnion{
Items: []InputItem{
{Type: "message", Role: "user", Content: json.RawMessage(`"hello"`)},
},
},
},
expectError: false,
},
{
name: "nil request",
request: nil,
expectError: true,
errorMsg: "request is nil",
},
{
name: "missing model",
request: &ResponseRequest{
Model: "",
Input: InputUnion{
String: stringPtr("hello"),
},
},
expectError: true,
errorMsg: "model is required",
},
{
name: "missing input",
request: &ResponseRequest{
Model: "gpt-4",
Input: InputUnion{},
},
expectError: true,
errorMsg: "input is required",
},
{
name: "empty string input is invalid",
request: &ResponseRequest{
Model: "gpt-4",
Input: InputUnion{
String: stringPtr(""),
},
},
expectError: false, // Empty string is technically valid
},
{
name: "empty array input is invalid",
request: &ResponseRequest{
Model: "gpt-4",
Input: InputUnion{
Items: []InputItem{},
},
},
expectError: true,
errorMsg: "input is required",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.request.Validate()
if tt.expectError {
assert.Error(t, err)
if tt.errorMsg != "" {
assert.Contains(t, err.Error(), tt.errorMsg)
}
return
}
assert.NoError(t, err)
})
}
}
func TestGetStringField(t *testing.T) {
tests := []struct {
name string
input map[string]interface{}
key string
expected string
}{
{
name: "existing string field",
input: map[string]interface{}{
"name": "value",
},
key: "name",
expected: "value",
},
{
name: "missing field",
input: map[string]interface{}{
"other": "value",
},
key: "name",
expected: "",
},
{
name: "wrong type - int",
input: map[string]interface{}{
"name": 123,
},
key: "name",
expected: "",
},
{
name: "wrong type - bool",
input: map[string]interface{}{
"name": true,
},
key: "name",
expected: "",
},
{
name: "wrong type - object",
input: map[string]interface{}{
"name": map[string]string{"nested": "value"},
},
key: "name",
expected: "",
},
{
name: "empty string value",
input: map[string]interface{}{
"name": "",
},
key: "name",
expected: "",
},
{
name: "nil map",
input: nil,
key: "name",
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getStringField(tt.input, tt.key)
assert.Equal(t, tt.expected, result)
})
}
}
func TestInputItem_ComplexContent(t *testing.T) {
tests := []struct {
name string
itemJSON string
validate func(t *testing.T, item InputItem)
}{
{
name: "content with nested objects",
itemJSON: `{
"type": "message",
"role": "assistant",
"content": [{
"type": "tool_use",
"id": "call_complex",
"name": "search",
"input": {
"query": "test",
"filters": {
"category": "docs",
"date": "2024-01-01"
},
"limit": 10
}
}]
}`,
validate: func(t *testing.T, item InputItem) {
assert.Equal(t, "message", item.Type)
assert.Equal(t, "assistant", item.Role)
assert.NotNil(t, item.Content)
},
},
{
name: "content with array in input",
itemJSON: `{
"type": "message",
"role": "assistant",
"content": [{
"type": "tool_use",
"id": "call_arr",
"name": "batch_process",
"input": {
"items": ["a", "b", "c"]
}
}]
}`,
validate: func(t *testing.T, item InputItem) {
assert.Equal(t, "message", item.Type)
assert.NotNil(t, item.Content)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var item InputItem
err := json.Unmarshal([]byte(tt.itemJSON), &item)
require.NoError(t, err)
if tt.validate != nil {
tt.validate(t, item)
}
})
}
}
func TestResponseRequest_CompleteWorkflow(t *testing.T) {
requestJSON := `{
"model": "gpt-4",
"input": [{
"type": "message",
"role": "user",
"content": "What's the weather in NYC and LA?"
}, {
"type": "message",
"role": "assistant",
"content": [{
"type": "output_text",
"text": "Let me check both locations for you."
}, {
"type": "tool_use",
"id": "call_1",
"name": "get_weather",
"input": {"location": "New York City"}
}, {
"type": "tool_use",
"id": "call_2",
"name": "get_weather",
"input": {"location": "Los Angeles"}
}]
}, {
"type": "function_call_output",
"call_id": "call_1",
"name": "get_weather",
"output": "{\"temp\": 45, \"condition\": \"cloudy\"}"
}, {
"type": "function_call_output",
"call_id": "call_2",
"name": "get_weather",
"output": "{\"temp\": 72, \"condition\": \"sunny\"}"
}],
"stream": true,
"temperature": 0.7
}`
var req ResponseRequest
err := json.Unmarshal([]byte(requestJSON), &req)
require.NoError(t, err)
// Validate
err = req.Validate()
require.NoError(t, err)
// Normalize
msgs := req.NormalizeInput()
require.Len(t, msgs, 4)
// Check user message
assert.Equal(t, "user", msgs[0].Role)
assert.Len(t, msgs[0].Content, 1)
// Check assistant message with tool calls
assert.Equal(t, "assistant", msgs[1].Role)
assert.Len(t, msgs[1].Content, 1)
assert.Len(t, msgs[1].ToolCalls, 2)
assert.Equal(t, "call_1", msgs[1].ToolCalls[0].ID)
assert.Equal(t, "call_2", msgs[1].ToolCalls[1].ID)
// Check tool responses
assert.Equal(t, "tool", msgs[2].Role)
assert.Equal(t, "call_1", msgs[2].CallID)
assert.Equal(t, "tool", msgs[3].Role)
assert.Equal(t, "call_2", msgs[3].CallID)
}
// Helper functions
func stringPtr(s string) *string {
return &s
}