| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186 |
- package apicompat
- import (
- "encoding/json"
- "testing"
- "github.com/stretchr/testify/require"
- )
- // marshalEvent marshals through the custom MarshalJSON and returns the decoded
- // object plus the set of top-level keys.
- func marshalEvent(t *testing.T, e ResponsesStreamEvent) map[string]any {
- t.Helper()
- b, err := json.Marshal(e)
- require.NoError(t, err)
- var m map[string]any
- require.NoError(t, json.Unmarshal(b, &m))
- return m
- }
- // TestWire_IndexFieldsPresentAtZero guards the omitempty trap: output_index/
- // content_index/summary_index must serialize even when 0.
- func TestWire_IndexFieldsPresentAtZero(t *testing.T) {
- m := marshalEvent(t, ResponsesStreamEvent{
- Type: "response.output_text.delta", OutputIndex: 0, ContentIndex: 0, ItemID: "msg_1", Delta: "hi",
- })
- require.Contains(t, m, "output_index")
- require.Contains(t, m, "content_index")
- require.EqualValues(t, 0, m["output_index"])
- r := marshalEvent(t, ResponsesStreamEvent{
- Type: "response.reasoning_summary_text.delta", OutputIndex: 0, SummaryIndex: 0, ItemID: "rs_1", Delta: "think",
- })
- require.Contains(t, r, "output_index")
- require.Contains(t, r, "summary_index")
- }
- // TestWire_FunctionCallItemAlwaysComplete guards that a function_call item
- // always carries call_id/name/arguments, including arguments:"" on .added.
- func TestWire_FunctionCallItemAlwaysComplete(t *testing.T) {
- added := marshalEvent(t, ResponsesStreamEvent{
- Type: "response.output_item.added",
- OutputIndex: 1,
- Item: &ResponsesOutput{Type: "function_call", ID: "fc_1", CallID: "call_a", Name: "exec", Status: "in_progress"},
- })
- item, ok := added["item"].(map[string]any)
- require.True(t, ok, "item must be an object")
- for _, k := range []string{"call_id", "name", "arguments"} {
- require.Containsf(t, item, k, "function_call item missing %q", k)
- }
- require.Equal(t, "", item["arguments"])
- }
- // TestWire_MessageItemContentAlwaysArray guards content:[] presence.
- func TestWire_MessageItemContentAlwaysArray(t *testing.T) {
- m := marshalEvent(t, ResponsesStreamEvent{
- Type: "response.output_item.added",
- OutputIndex: 0,
- Item: &ResponsesOutput{Type: "message", ID: "msg_1", Role: "assistant", Status: "in_progress"},
- })
- item, ok := m["item"].(map[string]any)
- require.True(t, ok, "item must be an object")
- require.Contains(t, item, "content")
- _, ok = item["content"].([]any)
- require.True(t, ok, "content must be an array")
- }
- // TestWire_ReasoningItemSummaryAlwaysArray guards summary:[] presence.
- func TestWire_ReasoningItemSummaryAlwaysArray(t *testing.T) {
- m := marshalEvent(t, ResponsesStreamEvent{
- Type: "response.output_item.added",
- OutputIndex: 0,
- Item: &ResponsesOutput{Type: "reasoning", ID: "rs_1", Status: "in_progress"},
- })
- item, ok := m["item"].(map[string]any)
- require.True(t, ok, "item must be an object")
- require.Contains(t, item, "summary")
- _, ok = item["summary"].([]any)
- require.True(t, ok, "summary must be an array")
- }
- // TestWire_ContentPartCarriesAnnotationsLogprobs guards the output_text part shape.
- func TestWire_ContentPartCarriesAnnotationsLogprobs(t *testing.T) {
- m := marshalEvent(t, ResponsesStreamEvent{
- Type: "response.content_part.added", OutputIndex: 0, ContentIndex: 0, ItemID: "msg_1",
- Part: &ResponsesContentPart{Type: "output_text", Text: ""},
- })
- part, ok := m["part"].(map[string]any)
- require.True(t, ok, "part must be an object")
- require.Equal(t, "output_text", part["type"])
- require.Contains(t, part, "text")
- require.Contains(t, part, "annotations")
- require.Contains(t, part, "logprobs")
- }
- // TestWire_ArgumentsDonePresentEvenEmpty guards arguments presence on done.
- func TestWire_ArgumentsDonePresentEvenEmpty(t *testing.T) {
- m := marshalEvent(t, ResponsesStreamEvent{
- Type: "response.function_call_arguments.done", OutputIndex: 1, ItemID: "fc_1", CallID: "call_a", Name: "exec", Arguments: "",
- })
- require.Contains(t, m, "arguments")
- require.Equal(t, "", m["arguments"])
- }
- // TestWire_CustomToolCallInputIndexPresentAtZero guards the omitempty trap for
- // custom_tool_call_input.delta/done: output_index must serialize even when 0
- // (custom tool call as the first output item).
- func TestWire_CustomToolCallInputIndexPresentAtZero(t *testing.T) {
- d := marshalEvent(t, ResponsesStreamEvent{
- Type: "response.custom_tool_call_input.delta", OutputIndex: 0, ItemID: "ct_1", Delta: "dir",
- })
- require.Contains(t, d, "output_index")
- require.EqualValues(t, 0, d["output_index"])
- require.Equal(t, "dir", d["delta"])
- done := marshalEvent(t, ResponsesStreamEvent{
- Type: "response.custom_tool_call_input.done", OutputIndex: 0, ItemID: "ct_1", CallID: "call_1", Name: "exec", Input: "dir",
- })
- require.Contains(t, done, "output_index")
- require.EqualValues(t, 0, done["output_index"])
- require.Equal(t, "dir", done["input"])
- require.NotContains(t, done, "delta")
- }
- // TestWire_UnknownEventFallsBackToDefault ensures non-streamed event types keep
- // default marshalling (the response object is preserved).
- func TestWire_UnknownEventFallsBackToDefault(t *testing.T) {
- m := marshalEvent(t, ResponsesStreamEvent{
- Type: "response.completed",
- Response: &ResponsesResponse{ID: "resp_1", Object: "response", Status: "completed"},
- })
- require.Contains(t, m, "response")
- }
- func TestResponsesOutputUnmarshal_ToolSearchObjectArguments(t *testing.T) {
- var item ResponsesOutput
- require.NoError(t, json.Unmarshal([]byte(`{
- "type":"tool_search_call",
- "id":"item_1",
- "call_id":"call_1",
- "execution":"client",
- "arguments":{"query":"gmail","limit":2}
- }`), &item))
- require.Equal(t, "tool_search_call", item.Type)
- require.Equal(t, `{"query":"gmail","limit":2}`, item.Arguments)
- wire, err := json.Marshal(item)
- require.NoError(t, err)
- var decoded map[string]any
- require.NoError(t, json.Unmarshal(wire, &decoded))
- args, ok := decoded["arguments"].(map[string]any)
- require.True(t, ok, "tool_search_call arguments must remain an object")
- require.Equal(t, "gmail", args["query"])
- }
- func TestResponsesResponseUnmarshal_ToolSearchObjectArguments(t *testing.T) {
- var response ResponsesResponse
- require.NoError(t, json.Unmarshal([]byte(`{
- "id":"response_1",
- "object":"response",
- "status":"completed",
- "output":[{
- "type":"tool_search_call",
- "id":"item_1",
- "call_id":"call_1",
- "arguments":{"query":"gmail"}
- }]
- }`), &response))
- require.Len(t, response.Output, 1)
- require.Equal(t, `{"query":"gmail"}`, response.Output[0].Arguments)
- }
- func TestResponsesStreamEventUnmarshal_ToolSearchObjectArguments(t *testing.T) {
- var event ResponsesStreamEvent
- require.NoError(t, json.Unmarshal([]byte(`{
- "type":"response.output_item.done",
- "item":{
- "type":"tool_search_call",
- "id":"item_1",
- "call_id":"call_1",
- "arguments":{"query":"gmail"}
- }
- }`), &event))
- require.NotNil(t, event.Item)
- require.Equal(t, `{"query":"gmail"}`, event.Item.Arguments)
- }
|