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}