v0.48.1 deaab59

reasoning_test.go RawHistoryFolder

1package sdk
2
3import (
4	"context"
5	"errors"
6	"testing"
7
8	"github.com/bnema/zut/packages/agent/tools"
9	"github.com/bnema/zut/packages/core"
10	"github.com/bnema/zut/packages/provider"
11)
12
13func TestNewContextHonorsCancellation(t *testing.T) {
14	t.Setenv("ZUT_HOME", t.TempDir())
15	t.Setenv("OPENCODE_API_KEY", "")
16	ctx, cancel := context.WithCancel(context.Background())
17	cancel()
18	if _, err := NewContext(ctx, Config{Provider: provider.ProviderOpenCodeGo}); !errors.Is(err, context.Canceled) {
19		t.Fatalf("NewContext error = %v, want context.Canceled", err)
20	}
21}
22
23func TestRuntimeSetModelAllowsUnlistedOpenCodeGoModel(t *testing.T) {
24	const model = "muse-spark-1.2-contributor"
25	r := &Runtime{
26		provider: "opencode-go",
27		model:    "kimi-k2.6",
28		agent:    &core.Agent{Model: "kimi-k2.6"},
29	}
30	if err := r.SetModel("  " + model + "  "); err != nil {
31		t.Fatal(err)
32	}
33	if r.model != model || r.agent.Model != model {
34		t.Fatalf("model = runtime:%q agent:%q, want trimmed %q", r.model, r.agent.Model, model)
35	}
36	if r.agent.ContextWindow != 128000 || r.agent.MaxTokens != 16384 {
37		t.Fatalf("bootstrap limits = context %d output %d, want 128000/16384", r.agent.ContextWindow, r.agent.MaxTokens)
38	}
39}
40
41func TestRuntimeSetModelUpdatesAgentMetadata(t *testing.T) {
42	const model = "served-opencode-model"
43	r := &Runtime{
44		provider: provider.ProviderOpenCodeGo,
45		model:    "old-model",
46		agent:    &core.Agent{Model: "old-model", ContextWindow: 1, MaxTokens: 2},
47		modelCatalog: []provider.Model{{
48			Provider:      provider.ProviderOpenCodeGo,
49			ID:            model,
50			ContextWindow: 200000,
51			MaxOutput:     50000,
52		}},
53	}
54	if err := r.SetModel(model); err != nil {
55		t.Fatal(err)
56	}
57	if r.agent.ContextWindow != 200000 || r.agent.MaxTokens != 50000 {
58		t.Fatalf("agent limits = context %d output %d, want 200000/50000", r.agent.ContextWindow, r.agent.MaxTokens)
59	}
60}
61
62func TestRuntimeSetModelPreservesExistingLimitsWhenMetadataIsMissing(t *testing.T) {
63	const model = "metadata-without-limits"
64	r := &Runtime{
65		provider: provider.ProviderOpenCodeGo,
66		model:    "old-model",
67		agent:    &core.Agent{Model: "old-model", ContextWindow: 111, MaxTokens: 222},
68		modelCatalog: []provider.Model{{
69			Provider: provider.ProviderOpenCodeGo,
70			ID:       model,
71		}},
72	}
73	if err := r.SetModel(model); err != nil {
74		t.Fatal(err)
75	}
76	if r.agent.ContextWindow != 111 || r.agent.MaxTokens != 222 {
77		t.Fatalf("missing limits changed agent values = context %d output %d, want 111/222", r.agent.ContextWindow, r.agent.MaxTokens)
78	}
79}
80
81func TestRuntimeSetModelRejectsUnknownAuthoritativeModel(t *testing.T) {
82	r := &Runtime{
83		provider:                  provider.ProviderOpenCodeGo,
84		model:                     "served-model",
85		modelCatalogAuthoritative: true,
86		modelCatalog:              []provider.Model{{Provider: provider.ProviderOpenCodeGo, ID: "served-model"}},
87		agent:                     &core.Agent{Model: "served-model"},
88	}
89	if err := r.SetModel("removed-model"); err == nil {
90		t.Fatal("authoritative SDK snapshot accepted removed model")
91	}
92	if r.model != "served-model" || r.agent.Model != "served-model" {
93		t.Fatalf("rejected switch changed model: runtime=%q agent=%q", r.model, r.agent.Model)
94	}
95}
96
97func TestRuntimeSetModelRejectsBlankOpenCodeGoModel(t *testing.T) {
98	for _, model := range []string{"", "   "} {
99		r := &Runtime{
100			provider: "opencode-go",
101			model:    "kimi-k2.6",
102			agent:    &core.Agent{Model: "kimi-k2.6"},
103		}
104		if err := r.SetModel(model); err == nil {
105			t.Fatalf("SetModel(%q) returned nil", model)
106		}
107		if r.model != "kimi-k2.6" || r.agent.Model != "kimi-k2.6" {
108			t.Fatalf("blank model %q changed active model: runtime=%q agent=%q", model, r.model, r.agent.Model)
109		}
110	}
111}
112
113func TestRuntimeSetModelRejectsChangesWhileBusy(t *testing.T) {
114	r := &Runtime{
115		provider:     "opencode-go",
116		model:        "old-model",
117		activeCancel: func() {},
118		agent:        &core.Agent{Model: "old-model"},
119	}
120	if err := r.SetModel("new-model"); err != ErrBusy {
121		t.Fatalf("SetModel while busy = %v, want ErrBusy", err)
122	}
123	if r.model != "old-model" || r.agent.Model != "old-model" {
124		t.Fatalf("busy SetModel changed model: runtime=%q agent=%q", r.model, r.agent.Model)
125	}
126}
127
128func TestRuntimeSetReasoningMax(t *testing.T) {
129	r := &Runtime{agent: &core.Agent{}}
130	if err := r.SetReasoning("max"); err != nil {
131		t.Fatal(err)
132	}
133	if r.agent.Reasoning != "max" {
134		t.Fatalf("reasoning = %q, want max", r.agent.Reasoning)
135	}
136}
137
138func TestRuntimeSetReasoningRejectsUnknownLevel(t *testing.T) {
139	r := &Runtime{agent: &core.Agent{}}
140	if err := r.SetReasoning("extreme"); err == nil {
141		t.Fatal("expected invalid reasoning error")
142	}
143}
144
145func TestNewRequiresExplicitWebSearchTool(t *testing.T) {
146	t.Setenv("ZUT_HOME", t.TempDir())
147	t.Setenv("OPENAI_API_KEY", "test-key")
148
149	withoutWeb, err := New(Config{Provider: "openai", Model: "gpt-5", Tools: []string{"read"}})
150	if err != nil {
151		t.Fatal(err)
152	}
153	defer withoutWeb.Close()
154	for _, name := range tools.WebCapabilityNames {
155		if _, ok := withoutWeb.agent.ToolsSnapshot()[name]; ok {
156			t.Fatalf("SDK registry inherited %s without explicit Config.Tools opt-in", name)
157		}
158	}
159
160	withWeb, err := New(Config{Provider: "openai", Model: "gpt-5", Tools: []string{"web_search"}})
161	if err != nil {
162		t.Fatal(err)
163	}
164	defer withWeb.Close()
165	for _, name := range tools.WebCapabilityNames {
166		if _, ok := withWeb.agent.ToolsSnapshot()[name]; !ok {
167			t.Fatalf("SDK registry omitted %s from explicit web_search capability", name)
168		}
169	}
170}

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 477

↑↓ moveenter openesc close