WSv2 egress relays upstream events verbatim without the HTTP-path namespace restore, so flattening requests that take the WSv2 branch would surface flattened tool names the client cannot match. Resolve the WS transport decision before flattening and skip flattening only when the request will actually go WSv2 (passthrough accounts return via HTTP before the WSv2 branch and still flatten). Also check all type assertions in responses_namespace_test.go to satisfy golangci-lint errcheck.
165 lines
6.7 KiB
Go
165 lines
6.7 KiB
Go
package apicompat
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestFlattenResponsesNamespaces_RewritesDeclarationHistoryAndChoice(t *testing.T) {
|
|
req := map[string]any{
|
|
"model": "gpt-5.5",
|
|
"tools": []any{
|
|
map[string]any{"type": "function", "name": "plain", "description": "keep"},
|
|
map[string]any{
|
|
"type": "namespace",
|
|
"name": "collaboration",
|
|
"tools": []any{
|
|
map[string]any{"type": "function", "name": "spawn_agent", "description": "spawn", "parameters": map[string]any{"type": "object"}},
|
|
},
|
|
},
|
|
},
|
|
"tool_choice": map[string]any{"type": "function", "name": "spawn_agent", "namespace": "collaboration"},
|
|
"input": []any{
|
|
map[string]any{"type": "function_call", "call_id": "call_1", "name": "spawn_agent", "namespace": "collaboration", "arguments": "{}"},
|
|
map[string]any{"type": "message", "role": "user", "content": "hi", "name": "spawn_agent", "namespace": "collaboration"},
|
|
},
|
|
}
|
|
|
|
names, changed, err := FlattenResponsesNamespaces(req)
|
|
require.NoError(t, err)
|
|
require.True(t, changed)
|
|
require.Equal(t, ResponsesNamespaceName{Namespace: "collaboration", Name: "spawn_agent"}, names["collaboration__spawn_agent"])
|
|
|
|
tools, ok := req["tools"].([]any)
|
|
require.True(t, ok)
|
|
require.Len(t, tools, 2)
|
|
plainTool, ok := tools[0].(map[string]any)
|
|
require.True(t, ok)
|
|
require.Equal(t, "plain", plainTool["name"])
|
|
flatTool, ok := tools[1].(map[string]any)
|
|
require.True(t, ok)
|
|
require.Equal(t, "collaboration__spawn_agent", flatTool["name"])
|
|
require.Equal(t, "spawn", flatTool["description"])
|
|
|
|
choice, ok := req["tool_choice"].(map[string]any)
|
|
require.True(t, ok)
|
|
require.Equal(t, "collaboration__spawn_agent", choice["name"])
|
|
require.NotContains(t, choice, "namespace")
|
|
|
|
input, ok := req["input"].([]any)
|
|
require.True(t, ok)
|
|
require.Len(t, input, 2)
|
|
call, ok := input[0].(map[string]any)
|
|
require.True(t, ok)
|
|
require.Equal(t, "collaboration__spawn_agent", call["name"])
|
|
require.NotContains(t, call, "namespace")
|
|
message, ok := input[1].(map[string]any)
|
|
require.True(t, ok)
|
|
require.Equal(t, "spawn_agent", message["name"])
|
|
require.Equal(t, "collaboration", message["namespace"])
|
|
require.Equal(t, "gpt-5.5", req["model"])
|
|
}
|
|
|
|
func TestFlattenResponsesNamespaces_RejectsFlatNameCollision(t *testing.T) {
|
|
req := map[string]any{"tools": []any{
|
|
map[string]any{"type": "function", "name": "collaboration__spawn_agent"},
|
|
map[string]any{"type": "namespace", "name": "collaboration", "tools": []any{
|
|
map[string]any{"type": "function", "name": "spawn_agent"},
|
|
}},
|
|
}}
|
|
|
|
_, _, err := FlattenResponsesNamespaces(req)
|
|
require.ErrorContains(t, err, "conflicts with a top-level tool")
|
|
}
|
|
|
|
func TestFlattenResponsesNamespaces_NamespaceGroupChoiceFallsBackToAuto(t *testing.T) {
|
|
req := map[string]any{
|
|
"tools": []any{map[string]any{
|
|
"type": "namespace", "name": "collaboration", "tools": []any{
|
|
map[string]any{"type": "function", "name": "spawn_agent"},
|
|
map[string]any{"type": "function", "name": "send_message"},
|
|
},
|
|
}},
|
|
"tool_choice": map[string]any{"type": "namespace", "name": "collaboration"},
|
|
}
|
|
|
|
_, changed, err := FlattenResponsesNamespaces(req)
|
|
require.NoError(t, err)
|
|
require.True(t, changed)
|
|
require.Equal(t, "auto", req["tool_choice"])
|
|
}
|
|
|
|
func TestFlattenResponsesNamespacesExcept_PreservesBuiltInNamespaceAndChoice(t *testing.T) {
|
|
req := map[string]any{
|
|
"tools": []any{
|
|
map[string]any{"type": "namespace", "name": "image_gen", "tools": []any{
|
|
map[string]any{"type": "function", "name": "imagegen"},
|
|
}},
|
|
map[string]any{"type": "namespace", "name": "collaboration", "tools": []any{
|
|
map[string]any{"type": "function", "name": "spawn_agent"},
|
|
}},
|
|
},
|
|
"tool_choice": map[string]any{"type": "namespace", "name": "image_gen"},
|
|
}
|
|
|
|
names, changed, err := FlattenResponsesNamespacesExcept(req, map[string]bool{"image_gen": true})
|
|
require.NoError(t, err)
|
|
require.True(t, changed)
|
|
require.Contains(t, names, "collaboration__spawn_agent")
|
|
tools, ok := req["tools"].([]any)
|
|
require.True(t, ok)
|
|
require.Len(t, tools, 2)
|
|
preservedTool, ok := tools[0].(map[string]any)
|
|
require.True(t, ok)
|
|
require.Equal(t, "namespace", preservedTool["type"])
|
|
require.Equal(t, "image_gen", preservedTool["name"])
|
|
flatTool, ok := tools[1].(map[string]any)
|
|
require.True(t, ok)
|
|
require.Equal(t, "function", flatTool["type"])
|
|
require.Equal(t, "collaboration__spawn_agent", flatTool["name"])
|
|
require.Equal(t, map[string]any{"type": "namespace", "name": "image_gen"}, req["tool_choice"])
|
|
}
|
|
|
|
func TestFlattenResponsesNamespaces_RejectsNamespaceCollision(t *testing.T) {
|
|
req := map[string]any{"tools": []any{
|
|
map[string]any{"type": "namespace", "name": "a", "tools": []any{
|
|
map[string]any{"type": "function", "name": "b__c"},
|
|
}},
|
|
map[string]any{"type": "namespace", "name": "a__b", "tools": []any{
|
|
map[string]any{"type": "function", "name": "c"},
|
|
}},
|
|
}}
|
|
|
|
_, _, err := FlattenResponsesNamespaces(req)
|
|
require.ErrorContains(t, err, "both flatten")
|
|
}
|
|
|
|
func TestRestoreResponsesNamespaceCalls_RewritesOnlyFunctionCalls(t *testing.T) {
|
|
payload := []byte(`{"type":"response.completed","response":{"output":[{"type":"function_call","name":"collaboration__spawn_agent","call_id":"call_1","arguments":"{}","extra":"keep"},{"type":"function_call","name":"plain","arguments":"{}"},{"type":"message","name":"collaboration__spawn_agent","content":"<tag>&value</tag>"}]}}`)
|
|
names := map[string]ResponsesNamespaceName{
|
|
"collaboration__spawn_agent": {Namespace: "collaboration", Name: "spawn_agent"},
|
|
}
|
|
|
|
got, changed, err := RestoreResponsesNamespaceCalls(payload, names)
|
|
require.NoError(t, err)
|
|
require.True(t, changed)
|
|
require.JSONEq(t, `{"type":"response.completed","response":{"output":[{"type":"function_call","name":"spawn_agent","namespace":"collaboration","call_id":"call_1","arguments":"{}","extra":"keep"},{"type":"function_call","name":"plain","arguments":"{}"},{"type":"message","name":"collaboration__spawn_agent","content":"<tag>&value</tag>"}]}}`, string(got))
|
|
require.Contains(t, string(got), "<tag>&value</tag>")
|
|
require.NotContains(t, string(got), `\u003c`)
|
|
}
|
|
|
|
func TestRestoreResponsesNamespaceCalls_RewritesLifecycleItems(t *testing.T) {
|
|
for _, eventType := range []string{"response.output_item.added", "response.output_item.done"} {
|
|
t.Run(eventType, func(t *testing.T) {
|
|
payload := []byte(`{"type":"` + eventType + `","item":{"type":"function_call","name":"collaboration__spawn_agent","arguments":"{}"}}`)
|
|
got, changed, err := RestoreResponsesNamespaceCalls(payload, map[string]ResponsesNamespaceName{
|
|
"collaboration__spawn_agent": {Namespace: "collaboration", Name: "spawn_agent"},
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, changed)
|
|
require.JSONEq(t, `{"type":"`+eventType+`","item":{"type":"function_call","name":"spawn_agent","namespace":"collaboration","arguments":"{}"}}`, string(got))
|
|
})
|
|
}
|
|
}
|