v0.45.0 cb619f7

openai_responses_continuation_test.go RawHistoryFolder

1package provider
2
3import (
4	"context"
5	"strings"
6	"testing"
7)
8
9// runResponsesCompletionFixture feeds scripted Responses SSE payloads
10// through the shared completion parser and returns the terminal event
11// plus the reported usage.
12func runResponsesCompletionFixture(t *testing.T, payloads ...string) (EventDone, Usage) {
13	t.Helper()
14	c := newOpenAICodexClient("token", "acct", "")
15	raw := make(chan sseEvent, len(payloads)+1)
16	for _, payload := range payloads {
17		raw <- sseEvent{Data: payload}
18	}
19	close(raw)
20	out := make(chan Event, 32)
21	go c.runResponseEventsWithFirst(context.Background(), Request{Model: "gpt-5.2"}, out, raw, nil, nil)
22	var usage Usage
23	for ev := range out {
24		switch e := ev.(type) {
25		case EventUsage:
26			usage = e.Usage
27		case EventDone:
28			return e, usage
29		}
30	}
31	t.Fatal("parser stream ended without EventDone")
32	return EventDone{}, Usage{}
33}
34
35func completedPayload(event, response string) string {
36	return `{"type":"` + event + `","response":{` + response + `}}`
37}
38
39func TestResponsesContinuation(t *testing.T) {
40	textItem := []string{
41		`{"type":"response.output_item.added","output_index":0,"item":{"type":"message"}}`,
42		`{"type":"response.output_text.delta","output_index":0,"delta":"part one"}`,
43	}
44	usage := `"id":"resp-1","usage":{"input_tokens":3,"output_tokens":4}`
45
46	for _, tt := range []struct {
47		name    string
48		event   string
49		endTurn string // raw end_turn field, empty when absent
50		want    StopReason
51	}{
52		{name: "explicit false continues", event: "response.completed", endTurn: `"end_turn":false`, want: StopContinue},
53		{name: "done alias continues", event: "response.done", endTurn: `"end_turn":false`, want: StopContinue},
54		{name: "explicit true ends", event: "response.completed", endTurn: `"end_turn":true`, want: StopEnd},
55		{name: "absent ends", event: "response.completed", endTurn: ``, want: StopEnd},
56		{name: "null ends", event: "response.completed", endTurn: `"end_turn":null`, want: StopEnd},
57	} {
58		t.Run(tt.name, func(t *testing.T) {
59			response := usage
60			if tt.endTurn != "" {
61				response += "," + tt.endTurn
62			}
63			payloads := append(append([]string{}, textItem...), completedPayload(tt.event, response))
64			done, gotUsage := runResponsesCompletionFixture(t, payloads...)
65			if done.Stop != tt.want {
66				t.Fatalf("stop = %q, want %q", done.Stop, tt.want)
67			}
68			if done.Err != nil {
69				t.Fatalf("err = %v, want nil", done.Err)
70			}
71			if gotUsage.InputTokens != 3 || gotUsage.OutputTokens != 4 {
72				t.Fatalf("usage = %+v, want input 3 output 4", gotUsage)
73			}
74			var text strings.Builder
75			for _, c := range done.Message.Content {
76				if tb, ok := c.(TextBlock); ok {
77					text.WriteString(tb.Text)
78				}
79			}
80			if text.String() != "part one" {
81				t.Fatalf("text = %q, want %q", text.String(), "part one")
82			}
83		})
84	}
85}
86
87func TestResponsesContinuationReasoningOnly(t *testing.T) {
88	done, _ := runResponsesCompletionFixture(t,
89		`{"type":"response.output_item.added","output_index":0,"item":{"type":"reasoning","id":"rs_1","encrypted_content":"blob"}}`,
90		completedPayload("response.completed", `"id":"resp-1","end_turn":false,"usage":{"input_tokens":1,"output_tokens":1}`),
91	)
92	if done.Stop != StopContinue {
93		t.Fatalf("stop = %q, want %q", done.Stop, StopContinue)
94	}
95	var sawReasoning bool
96	for _, c := range done.Message.Content {
97		if rb, ok := c.(ReasoningBlock); ok && rb.ID == "rs_1" && rb.Encrypted == "blob" {
98			sawReasoning = true
99		}
100	}
101	if !sawReasoning {
102		t.Fatalf("reasoning not preserved: %#v", done.Message.Content)
103	}
104}
105
106func TestResponsesContinuationToolCallPrecedence(t *testing.T) {
107	toolItem := []string{
108		`{"type":"response.output_item.added","output_index":0,"item":{"type":"function_call","call_id":"call-1","name":"echo"}}`,
109		`{"type":"response.function_call_arguments.delta","output_index":0,"delta":"{}"}`,
110		`{"type":"response.output_item.done","output_index":0,"item":{"type":"function_call"}}`,
111	}
112	for _, endTurn := range []string{`"end_turn":false`, `"end_turn":true`, ``} {
113		response := `"id":"resp-1","usage":{"input_tokens":1,"output_tokens":1}`
114		if endTurn != "" {
115			response += "," + endTurn
116		}
117		payloads := append(append([]string{}, toolItem...),
118			completedPayload("response.completed", response))
119		done, _ := runResponsesCompletionFixture(t, payloads...)
120		if done.Stop != StopToolUse {
121			t.Fatalf("end_turn %s with tool call: stop = %q, want %q", endTurn, done.Stop, StopToolUse)
122		}
123		var sawCall bool
124		for _, c := range done.Message.Content {
125			if tc, ok := c.(ToolCallBlock); ok && tc.ID == "call-1" && tc.Name == "echo" {
126				sawCall = true
127			}
128		}
129		if !sawCall {
130			t.Fatalf("tool call not preserved: %#v", done.Message.Content)
131		}
132	}
133}
134
135func TestResponsesContinuationPreservesErrorPaths(t *testing.T) {
136	done, _ := runResponsesCompletionFixture(t,
137		`{"type":"response.failed","response":{"error":{"message":"boom"}}}`,
138	)
139	if done.Stop != StopError || done.Err == nil {
140		t.Fatalf("failed response: stop = %q err = %v, want error", done.Stop, done.Err)
141	}
142}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close