v0.51.0 a9d674e

catalog_isolation_test.go RawHistoryFolder

1package sdk
2
3import (
4	"context"
5	"encoding/json"
6	"fmt"
7	"io"
8	"math"
9	"net/http"
10	"net/http/httptest"
11	"path/filepath"
12	"testing"
13	"time"
14
15	"github.com/bnema/zut/packages/provider"
16)
17
18func TestSDKRuntimeKeepsOpenCodeGoMetadataPerEndpoint(t *testing.T) {
19	const modelID = "scoped-opencode-model"
20	home := t.TempDir()
21	t.Setenv("ZUT_HOME", home)
22	provider.SetLiveModels(nil)
23	provider.SetUserModels(nil)
24	t.Cleanup(func() {
25		provider.SetLiveModels(nil)
26		provider.SetUserModels(nil)
27	})
28
29	newServer := func(apiKey, wantEffort string) *httptest.Server {
30		return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
31			if r.URL.Path != "/v1/chat/completions" {
32				http.NotFound(w, r)
33				return
34			}
35			if got := r.Header.Get("authorization"); got != "Bearer "+apiKey {
36				t.Errorf("authorization = %q, want bearer key %q", got, apiKey)
37			}
38			body, err := io.ReadAll(r.Body)
39			if err != nil {
40				t.Errorf("read request: %v", err)
41			}
42			var request struct {
43				Model           string `json:"model"`
44				ReasoningEffort string `json:"reasoning_effort"`
45			}
46			if err := json.Unmarshal(body, &request); err != nil {
47				t.Errorf("decode request: %v", err)
48			}
49			if request.Model != modelID || request.ReasoningEffort != wantEffort {
50				t.Errorf("request = model %q effort %q, want model %q effort %q", request.Model, request.ReasoningEffort, modelID, wantEffort)
51			}
52			w.Header().Set("Content-Type", "text/event-stream")
53			_, _ = fmt.Fprintln(w, `data: {"choices":[{"delta":{"content":"ok"},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":1}}`)
54			_, _ = fmt.Fprintln(w)
55			_, _ = fmt.Fprintln(w, "data: [DONE]")
56		}))
57	}
58	serverA := newServer("key-a", "minimal")
59	defer serverA.Close()
60	serverB := newServer("key-b", "")
61	defer serverB.Close()
62
63	saveCache := func(server *httptest.Server, apiKey string, reasoning bool, effort string) {
64		t.Helper()
65		model := provider.Model{
66			Provider:      provider.ProviderOpenCodeGo,
67			ID:            modelID,
68			Reasoning:     reasoning,
69			ContextWindow: 128000,
70			MaxOutput:     8192,
71			PriceInput:    100,
72			PriceOutput:   200,
73			BaseURL:       server.URL + "/v1",
74		}
75		if reasoning {
76			model.PriceInput = 10
77			model.PriceOutput = 20
78		}
79		if reasoning {
80			model.ReasoningLevelMap = map[string]string{"minimum": "minimum", "low": "", "medium": "", "high": "", "xhigh": "", "max": ""}
81			model.ReasoningEffortMap = map[string]string{"minimum": effort}
82		}
83		if err := provider.SaveCache(filepath.Join(home, "models-cache.json"), provider.ModelCache{
84			Version:                provider.ModelCacheVersion,
85			FetchedAt:              time.Now().UTC(),
86			Models:                 []provider.Model{model},
87			AuthoritativeProviders: []string{provider.ProviderOpenCodeGo},
88			ProviderScopes:         map[string]string{provider.ProviderOpenCodeGo: testCredentialEndpointScope(apiKey, server.URL+"/v1")},
89		}); err != nil {
90			t.Fatal(err)
91		}
92	}
93
94	saveCache(serverA, "key-a", true, "minimal")
95	runtimeA, err := New(Config{
96		Provider:  provider.ProviderOpenCodeGo,
97		Model:     modelID,
98		APIKey:    "key-a",
99		BaseURL:   serverA.URL + "/v1",
100		Reasoning: "minimum",
101		CWD:       t.TempDir(),
102		NoTools:   true,
103	})
104	if err != nil {
105		t.Fatal(err)
106	}
107
108	// Replace the disk cache and process-global catalog with another scope.
109	// Runtime A must continue using its own endpoint metadata afterward.
110	saveCache(serverB, "key-b", false, "")
111	runtimeB, err := New(Config{
112		Provider:  provider.ProviderOpenCodeGo,
113		Model:     modelID,
114		APIKey:    "key-b",
115		BaseURL:   serverB.URL + "/v1",
116		Reasoning: "minimum",
117		CWD:       t.TempDir(),
118		NoTools:   true,
119	})
120	if err != nil {
121		runtimeA.Close()
122		t.Fatal(err)
123	}
124
125	prompt := func(runtime *Runtime) {
126		t.Helper()
127		events, err := runtime.Prompt(context.Background(), "hello", nil)
128		if err != nil {
129			t.Fatal(err)
130		}
131		for event := range events {
132			if event.Type == "error" {
133				t.Fatalf("prompt error: %s", event.Error)
134			}
135		}
136	}
137	prompt(runtimeB)
138	prompt(runtimeA)
139
140	if got, want := runtimeB.Cost().CostUSD, 0.0003; math.Abs(got-want) > 1e-12 {
141		t.Fatalf("runtime B cost = %v, want %v", got, want)
142	}
143	if got, want := runtimeA.Cost().CostUSD, 0.00003; math.Abs(got-want) > 1e-12 {
144		t.Fatalf("runtime A cost = %v, want %v", got, want)
145	}
146	if err := runtimeB.Close(); err != nil {
147		t.Fatal(err)
148	}
149	if err := runtimeA.Close(); err != nil {
150		t.Fatal(err)
151	}
152}

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