1package agent
2
3import (
4 "bytes"
5 "strings"
6 "testing"
7
8 "github.com/bnema/zut/packages/core"
9 "github.com/bnema/zut/packages/provider"
10)
11
12func TestRPCSetModelRejectsBusyTurn(t *testing.T) {
13 var out bytes.Buffer
14 s := &rpcServer{
15 provider: provider.ProviderOpenCodeGo,
16 model: "old-model",
17 agent: &core.Agent{Model: "old-model", ContextWindow: 111, MaxTokens: 222},
18 out: &out,
19 }
20 s.turnMu.Lock()
21 s.dispatch("set_model", "1", []byte(`{"model":"new-model"}`))
22 s.turnMu.Unlock()
23 if s.agent.Model != "old-model" || s.model != "old-model" {
24 t.Fatalf("busy set_model changed active model: agent=%q server=%q", s.agent.Model, s.model)
25 }
26 if !strings.Contains(out.String(), `"success":false`) {
27 t.Fatalf("busy set_model response = %q", out.String())
28 }
29}
30
31func TestRPCSetModelAllowsUnlistedOpenCodeGoModel(t *testing.T) {
32 preserveProviderCatalog(t)
33 var out bytes.Buffer
34 const model = "muse-spark-1.2-contributor"
35 s := &rpcServer{
36 provider: "opencode-go",
37 model: "kimi-k2.6",
38 agent: &core.Agent{Model: "kimi-k2.6"},
39 out: &out,
40 }
41 s.dispatch("set_model", "1", []byte(`{"model":" `+model+` "}`))
42
43 if s.agent.Model != model || s.model != model {
44 t.Fatalf("model = agent:%q server:%q, want trimmed %q", s.agent.Model, s.model, model)
45 }
46 if s.agent.ContextWindow != 128000 || s.agent.MaxTokens != 16384 {
47 t.Fatalf("bootstrap limits = context %d output %d, want 128000/16384", s.agent.ContextWindow, s.agent.MaxTokens)
48 }
49 if !strings.Contains(out.String(), `"success":true`) || !strings.Contains(out.String(), `"model":"`+model+`"`) {
50 t.Fatalf("response = %q", out.String())
51 }
52}
53
54func TestRPCSetModelUsesKnownModelMetadata(t *testing.T) {
55 preserveProviderCatalog(t)
56 provider.SetLiveModels([]provider.Model{{
57 Provider: provider.ProviderOpenCodeGo,
58 ID: "served-model",
59 ContextWindow: 300000,
60 MaxOutput: 60000,
61 }})
62
63 var out bytes.Buffer
64 s := &rpcServer{
65 provider: provider.ProviderOpenCodeGo,
66 model: "old-model",
67 agent: &core.Agent{Model: "old-model", ContextWindow: 1, MaxTokens: 2},
68 out: &out,
69 }
70 s.dispatch("set_model", "1", []byte(`{"model":"served-model"}`))
71 if s.agent.ContextWindow != 300000 || s.agent.MaxTokens != 60000 {
72 t.Fatalf("known limits = context %d output %d, want 300000/60000", s.agent.ContextWindow, s.agent.MaxTokens)
73 }
74}
75
76func TestRPCSetModelPreservesExistingLimitsWhenMetadataIsMissing(t *testing.T) {
77 preserveProviderCatalog(t)
78 provider.SetLiveModels([]provider.Model{{
79 Provider: provider.ProviderOpenCodeGo,
80 ID: "metadata-without-limits",
81 }})
82
83 var out bytes.Buffer
84 s := &rpcServer{
85 provider: provider.ProviderOpenCodeGo,
86 model: "old-model",
87 agent: &core.Agent{Model: "old-model", ContextWindow: 111, MaxTokens: 222},
88 out: &out,
89 }
90 s.dispatch("set_model", "1", []byte(`{"model":"metadata-without-limits"}`))
91 if s.agent.ContextWindow != 111 || s.agent.MaxTokens != 222 {
92 t.Fatalf("missing limits changed agent values = context %d output %d, want 111/222", s.agent.ContextWindow, s.agent.MaxTokens)
93 }
94}
95
96func TestRPCSetModelRejectsBlankOpenCodeGoModel(t *testing.T) {
97 for _, model := range []string{"", " "} {
98 var out bytes.Buffer
99 s := &rpcServer{
100 provider: "opencode-go",
101 model: "kimi-k2.6",
102 agent: &core.Agent{Model: "kimi-k2.6"},
103 out: &out,
104 }
105 s.dispatch("set_model", "1", []byte(`{"model":"`+model+`"}`))
106 if s.agent.Model != "kimi-k2.6" || s.model != "kimi-k2.6" {
107 t.Fatalf("blank model %q changed active model: agent=%q server=%q", model, s.agent.Model, s.model)
108 }
109 if !strings.Contains(out.String(), `"success":false`) {
110 t.Fatalf("blank model %q response = %q", model, out.String())
111 }
112 }
113}
114
115func TestRPCSetReasoningMax(t *testing.T) {
116 var out bytes.Buffer
117 s := &rpcServer{agent: &core.Agent{}, out: &out}
118 s.dispatch("set_reasoning", "1", []byte(`{"reasoning":"max"}`))
119
120 if s.agent.Reasoning != "max" {
121 t.Fatalf("reasoning = %q, want max", s.agent.Reasoning)
122 }
123 if !strings.Contains(out.String(), `"reasoning":"max"`) {
124 t.Fatalf("response = %q", out.String())
125 }
126}
127
128func TestRPCSetReasoningRejectsUnknownLevel(t *testing.T) {
129 var out bytes.Buffer
130 s := &rpcServer{agent: &core.Agent{}, out: &out}
131 s.dispatch("set_reasoning", "1", []byte(`{"reasoning":"extreme"}`))
132
133 if s.agent.Reasoning != "" {
134 t.Fatalf("reasoning = %q, want unchanged", s.agent.Reasoning)
135 }
136 if !strings.Contains(out.String(), `"success":false`) {
137 t.Fatalf("response = %q", out.String())
138 }
139}