v0.38.0 58133ea

opencode_go_identity_test.go RawHistoryFolder

1package provider
2
3import (
4	"context"
5	"io"
6	"net/http"
7	"net/http/httptest"
8	"strings"
9	"sync"
10	"testing"
11)
12
13func TestOpenCodeGoRetriesKeepRequestIdentityStable(t *testing.T) {
14	const apiKey = "synthetic-key"
15	models := []Model{
16		{Provider: ProviderOpenCodeGo, ID: "retry-responses", API: APIResponses, MaxOutput: 1024},
17		{Provider: ProviderOpenCodeGo, ID: "retry-messages", API: APIAnthropicMessages, MaxOutput: 1024},
18		{Provider: ProviderOpenCodeGo, ID: "retry-chat", API: APICompletions, MaxOutput: 1024},
19	}
20	var (
21		mu    sync.Mutex
22		calls = make(map[string]int)
23		seen  = make(map[string][]http.Header)
24	)
25	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
26		switch r.URL.Path {
27		case "/v1/responses", "/v1/messages", "/v1/chat/completions":
28		default:
29			http.NotFound(w, r)
30			return
31		}
32		mu.Lock()
33		calls[r.URL.Path]++
34		attempt := calls[r.URL.Path]
35		seen[r.URL.Path] = append(seen[r.URL.Path], r.Header.Clone())
36		mu.Unlock()
37		if attempt == 1 {
38			http.Error(w, "temporary gateway failure", http.StatusInternalServerError)
39			return
40		}
41		writeTextResponse(w, r.URL.Path)
42	}))
43	defer server.Close()
44
45	client := NewOpenCodeGoWithModels(apiKey, server.URL+"/v1", models)
46	for _, model := range models {
47		req := Request{
48			Model: model.ID,
49			Context: RequestContext{
50				CacheSessionID: "cache-root",
51				ThreadID:       "thread-stable",
52				TurnID:         "turn-stable",
53			},
54			Messages: []Message{{Role: RoleUser, Content: []Content{TextBlock{Text: "hello"}}}},
55		}
56		if _, err := consumeTextStream(client, req); err != nil {
57			t.Fatalf("%s: %v", model.ID, err)
58		}
59	}
60
61	for _, model := range models {
62		path := map[string]string{
63			APIResponses:         "/v1/responses",
64			APIAnthropicMessages: "/v1/messages",
65			APICompletions:       "/v1/chat/completions",
66		}[model.API]
67		mu.Lock()
68		got := append([]http.Header(nil), seen[path]...)
69		mu.Unlock()
70		if len(got) != 2 {
71			t.Fatalf("%s attempts = %d, want initial failure and retry", path, len(got))
72		}
73		for i, headers := range got {
74			if headers.Get("x-opencode-client") != "zut" || headers.Get("user-agent") != openCodeGoUserAgent {
75				t.Fatalf("%s attempt %d identity = client %q user-agent %q", path, i+1, headers.Get("x-opencode-client"), headers.Get("user-agent"))
76			}
77			if headers.Get("x-opencode-session") != "thread-stable" || headers.Get("x-opencode-request") != "turn-stable" {
78				t.Fatalf("%s attempt %d correlation = session %q request %q", path, i+1, headers.Get("x-opencode-session"), headers.Get("x-opencode-request"))
79			}
80		}
81	}
82}
83
84func TestOpenCodeGoHeaderHelperScopesIdentity(t *testing.T) {
85	openCodeHeaders := make(http.Header)
86	setOpenCodeGoHeaders(openCodeHeaders, ProviderOpenCodeGo, RequestContext{CacheSessionID: "cache", TurnID: "turn"})
87	if openCodeHeaders.Get("x-opencode-session") != "cache" || openCodeHeaders.Get("x-opencode-request") != "turn" {
88		t.Fatalf("fallback identity = session %q request %q", openCodeHeaders.Get("x-opencode-session"), openCodeHeaders.Get("x-opencode-request"))
89	}
90	if openCodeHeaders.Get("x-opencode-client") != "zut" || openCodeHeaders.Get("user-agent") != openCodeGoUserAgent {
91		t.Fatalf("OpenCode Go identity = %v", openCodeHeaders)
92	}
93
94	ordinaryHeaders := make(http.Header)
95	setOpenCodeGoHeaders(ordinaryHeaders, ProviderOpenAI, RequestContext{ThreadID: "thread", TurnID: "turn"})
96	if ordinaryHeaders.Get("x-opencode-session") != "" || ordinaryHeaders.Get("x-opencode-request") != "" || ordinaryHeaders.Get("x-opencode-client") != "" || ordinaryHeaders.Get("user-agent") != "" {
97		t.Fatalf("ordinary provider received OpenCode headers: %v", ordinaryHeaders)
98	}
99
100	emptyHeaders := make(http.Header)
101	setOpenCodeGoHeaders(emptyHeaders, ProviderOpenCodeGo, RequestContext{})
102	if emptyHeaders.Get("x-opencode-session") != "" || emptyHeaders.Get("x-opencode-request") != "" {
103		t.Fatalf("empty context got correlation headers: %v", emptyHeaders)
104	}
105	if emptyHeaders.Get("x-opencode-client") != "zut" || emptyHeaders.Get("user-agent") != openCodeGoUserAgent {
106		t.Fatalf("empty context lost product identity: %v", emptyHeaders)
107	}
108
109	firstSession := make(http.Header)
110	secondSession := make(http.Header)
111	setOpenCodeGoHeaders(firstSession, ProviderOpenCodeGo, RequestContext{ThreadID: "thread-a"})
112	setOpenCodeGoHeaders(secondSession, ProviderOpenCodeGo, RequestContext{ThreadID: "thread-b"})
113	if firstSession.Get("x-opencode-session") == secondSession.Get("x-opencode-session") {
114		t.Fatalf("distinct conversations shared session identity %q", firstSession.Get("x-opencode-session"))
115	}
116}
117
118func TestOpenCodeGoResponsesRetainsIdentityThroughPublicTransport(t *testing.T) {
119	named := NewOpenAIResponsesNamed("synthetic-key", "https://example.test/v1", ProviderOpenCodeGo).(*renamedClient)
120	client := named.inner.(*codexClient)
121	client.SetModelMetadata(Model{Provider: ProviderOpenCodeGo, ID: "responses-model", MaxOutput: 1024})
122
123	var got *http.Request
124	client.http = openaiResponsesHTTPClient(&http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
125		got = r
126		return &http.Response{
127			StatusCode: http.StatusOK,
128			Header:     http.Header{"Content-Type": []string{"text/event-stream"}},
129			Body:       io.NopCloser(strings.NewReader("")),
130		}, nil
131	})})
132
133	stream, err := client.Stream(context.Background(), Request{
134		Model:   "responses-model",
135		Context: RequestContext{ThreadID: "thread", TurnID: "turn"},
136	})
137	if err != nil {
138		t.Fatal(err)
139	}
140	for range stream {
141	}
142	if got == nil {
143		t.Fatal("request was not sent")
144	}
145	if got.Header.Get("x-opencode-client") != "zut" || got.Header.Get("user-agent") != openCodeGoUserAgent {
146		t.Fatalf("identity stripped by public Responses transport: %v", got.Header)
147	}
148	if got.Header.Get("x-opencode-session") != "thread" || got.Header.Get("x-opencode-request") != "turn" {
149		t.Fatalf("correlation stripped by public Responses transport: %v", got.Header)
150	}
151	if got.Header.Get("chatgpt-account-id") != "" || got.Header.Get("originator") != "" || got.Header.Get("openai-beta") != "" {
152		t.Fatalf("Codex identity leaked through public Responses transport: %v", got.Header)
153	}
154}

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