From 458e4469e549db94a3dec28ebda4b7383b9203c7 Mon Sep 17 00:00:00 2001 From: shern-point Date: Tue, 28 Apr 2026 13:47:24 +0800 Subject: [PATCH] test: cover openai formatter string protection Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- internal/format/openai/render_test.go | 91 +++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/internal/format/openai/render_test.go b/internal/format/openai/render_test.go index 2f22a98..c1dc540 100644 --- a/internal/format/openai/render_test.go +++ b/internal/format/openai/render_test.go @@ -1,8 +1,11 @@ package openai import ( + "encoding/json" "strings" "testing" + + "ds2api/internal/toolcall" ) func TestBuildResponseObjectKeepsFencedToolPayloadAsText(t *testing.T) { @@ -13,6 +16,7 @@ func TestBuildResponseObjectKeepsFencedToolPayloadAsText(t *testing.T) { "", "```json\n{\"tool_calls\":[{\"name\":\"search\",\"input\":{\"q\":\"golang\"}}]}\n```", []string{"search"}, + nil, ) outputText, _ := obj["output_text"].(string) @@ -42,6 +46,7 @@ func TestBuildResponseObjectReasoningOnlyFallsBackToOutputText(t *testing.T) { "internal thinking content", "", nil, + nil, ) outputText, _ := obj["output_text"].(string) @@ -75,6 +80,7 @@ func TestBuildResponseObjectPromotesToolCallFromThinkingWhenTextEmpty(t *testing `from-thinking`, "", []string{"search"}, + nil, ) output, _ := obj["output"].([]any) @@ -86,3 +92,88 @@ func TestBuildResponseObjectPromotesToolCallFromThinkingWhenTextEmpty(t *testing t.Fatalf("expected function_call output, got %#v", first["type"]) } } + +func TestBuildChatCompletionWithToolCallsCoercesSchemaDeclaredStringArguments(t *testing.T) { + toolsRaw := []any{ + map[string]any{ + "type": "function", + "function": map[string]any{ + "name": "Write", + "parameters": map[string]any{ + "type": "object", + "properties": map[string]any{ + "content": map[string]any{"type": "string"}, + "taskId": map[string]any{"type": "string"}, + }, + }, + }, + }, + } + obj := BuildChatCompletionWithToolCalls( + "chat_test", + "gpt-4o", + "prompt", + "", + "", + []toolcall.ParsedToolCall{{ + Name: "Write", + Input: map[string]any{ + "content": map[string]any{"message": "hi"}, + "taskId": 1, + }, + }}, + toolsRaw, + ) + choices, _ := obj["choices"].([]map[string]any) + message, _ := choices[0]["message"].(map[string]any) + toolCalls, _ := message["tool_calls"].([]map[string]any) + fn, _ := toolCalls[0]["function"].(map[string]any) + args := map[string]any{} + if err := json.Unmarshal([]byte(fn["arguments"].(string)), &args); err != nil { + t.Fatalf("decode arguments failed: %v", err) + } + if args["content"] != `{"message":"hi"}` { + t.Fatalf("expected content stringified by schema, got %#v", args["content"]) + } + if args["taskId"] != "1" { + t.Fatalf("expected taskId stringified by schema, got %#v", args["taskId"]) + } +} + +func TestBuildResponseObjectWithToolCallsCoercesSchemaDeclaredStringArguments(t *testing.T) { + toolsRaw := []any{ + map[string]any{ + "type": "function", + "function": map[string]any{ + "name": "Write", + "parameters": map[string]any{ + "type": "object", + "properties": map[string]any{ + "content": map[string]any{"type": "string"}, + }, + }, + }, + }, + } + obj := BuildResponseObjectWithToolCalls( + "resp_test", + "gpt-4o", + "prompt", + "", + "", + []toolcall.ParsedToolCall{{ + Name: "Write", + Input: map[string]any{"content": []any{"a", 1}}, + }}, + toolsRaw, + ) + output, _ := obj["output"].([]any) + first, _ := output[0].(map[string]any) + args := map[string]any{} + if err := json.Unmarshal([]byte(first["arguments"].(string)), &args); err != nil { + t.Fatalf("decode response arguments failed: %v", err) + } + if args["content"] != `["a",1]` { + t.Fatalf("expected response content stringified by schema, got %#v", args["content"]) + } +}