1package provider
2
3import (
4 "encoding/json"
5 "testing"
6)
7
8func TestReplayContentPreservesHistoryAndRestoresOrigin(t *testing.T) {
9 original := Message{Role: RoleAssistant, Meta: map[string]string{"provider": "google", "model": "model-a"}, Content: []Content{
10 ReasoningBlock{Encrypted: "opaque"}, TextBlock{Text: "answer", ThoughtSignature: "text-signature"}, ImageBlock{ThoughtSignature: "image-signature"}, ToolCallBlock{ID: "call_1", Name: "lookup", ThoughtSignature: "tool-signature"},
11 }}
12 before, _ := json.Marshal(original)
13 foreign := replayContent(original, "google", "model-b")
14 if len(foreign) != 3 || foreign[0].(TextBlock).ThoughtSignature != "" || foreign[1].(ImageBlock).ThoughtSignature != "" || foreign[2].(ToolCallBlock).ThoughtSignature != "" {
15 t.Fatalf("foreign content = %#v", foreign)
16 }
17 restored := replayContent(original, "google", "model-a")
18 if len(restored) != 4 || restored[0].(ReasoningBlock).Encrypted != "opaque" || restored[1].(TextBlock).ThoughtSignature != "text-signature" {
19 t.Fatal("switch back lost private state")
20 }
21 after, _ := json.Marshal(original)
22 if string(before) != string(after) {
23 t.Fatal("history mutated")
24 }
25}
26
27func TestResponsesContinuationAllowsHostOrigin(t *testing.T) {
28 prior := Request{Model: "model-a", Messages: []Message{{Role: RoleUser, Content: []Content{TextBlock{Text: "hello"}}}}}
29 output := Message{Role: RoleAssistant, Content: []Content{TextBlock{Text: "answer"}}}
30 session := responsesWebSocketSession{lastRequest: prior, lastOutput: output, lastResponseID: "resp_1", lastBaseline: "baseline"}
31 next := prior
32 next.Messages = append(append([]Message(nil), prior.Messages...), WithMessageOrigin(output, "openai-codex", "model-a"), Message{Role: RoleUser, Content: []Content{TextBlock{Text: "next"}}})
33 incremental, id := session.incrementalRequest(next, "cache", func(Request, string) (string, error) { return "baseline", nil })
34 if id != "resp_1" || len(incremental.Messages) != 1 {
35 t.Fatalf("continuation = %q, %d messages", id, len(incremental.Messages))
36 }
37}
38
39func TestResponsesReplayAcrossModels(t *testing.T) {
40 c := newOpenAICodexClient("token", "account", "")
41 for _, tt := range []struct {
42 name, origin, model, id string
43 want bool
44 }{
45 {"same model", "openai-codex", "gpt-5.2", "rs_native", true},
46 {"other provider", "opencode", "gpt-5.2", "rs_first:rs_second", false},
47 {"foreign valid ID", "opencode", "gpt-5.2", "rs_native", false},
48 {"other model", "openai-codex", "gpt-5", "rs_native", false},
49 {"unknown compound ID", "", "", "rs_first:rs_second", false},
50 } {
51 t.Run(tt.name, func(t *testing.T) {
52 messages := []Message{{Role: RoleAssistant, Meta: map[string]string{"provider": tt.origin, "model": tt.model}, Content: []Content{
53 ReasoningBlock{ID: tt.id, Encrypted: "opaque"}, TextBlock{Text: "answer"},
54 ToolCallBlock{ID: "call_1", Name: "lookup", Arguments: json.RawMessage(`{}`)},
55 }}, {Role: RoleTool, Content: []Content{ToolResultBlock{CallID: "call_1", Content: []Content{TextBlock{Text: "result"}}}}}}
56 before, _ := json.Marshal(messages)
57 wire, err := c.buildRequest(Request{Model: "gpt-5.2", Messages: messages})
58 if err != nil {
59 t.Fatal(err)
60 }
61 reasoning, text, call, result := 0, 0, 0, 0
62 for _, item := range wire.Input {
63 switch item.(type) {
64 case codexReasoningItem:
65 reasoning++
66 case codexOutputMessage:
67 text++
68 case codexFunctionCall:
69 call++
70 case codexFunctionCallOutput:
71 result++
72 }
73 }
74 if (reasoning == 1) != tt.want || text != 1 || call != 1 || result != 1 {
75 t.Fatalf("reasoning/text/call/result = %d/%d/%d/%d", reasoning, text, call, result)
76 }
77 after, _ := json.Marshal(messages)
78 if string(before) != string(after) {
79 t.Fatal("history mutated")
80 }
81 })
82 }
83}