183 lines
5.6 KiB
Go
183 lines
5.6 KiB
Go
package sessionmodel
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"reflect"
|
|
"testing"
|
|
|
|
acp "github.com/coder/acp-go-sdk"
|
|
)
|
|
|
|
type fakeApplier struct {
|
|
configErr error
|
|
legacyErr error
|
|
calls []string
|
|
}
|
|
|
|
func (f *fakeApplier) SetConfigOption(_ context.Context, _ string, configID, value string) error {
|
|
f.calls = append(f.calls, "config:"+configID+":"+value)
|
|
return f.configErr
|
|
}
|
|
|
|
func (f *fakeApplier) SetModelLegacy(_ context.Context, sessionID, modelID string) error {
|
|
f.calls = append(f.calls, "legacy:"+sessionID+":"+modelID)
|
|
return f.legacyErr
|
|
}
|
|
|
|
func TestApply_UsesModelConfigOption(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
applier := &fakeApplier{}
|
|
method, err := Apply(context.Background(), applier, Request{
|
|
SessionID: "sess-1",
|
|
ModelID: "gpt-5.4-mini",
|
|
ConfigOptions: []ConfigOption{{
|
|
ID: "model",
|
|
Category: "model",
|
|
}},
|
|
})
|
|
|
|
if err != nil {
|
|
t.Fatalf("Apply() error = %v", err)
|
|
}
|
|
if method != MethodSetConfigOption {
|
|
t.Fatalf("method = %q, want %q", method, MethodSetConfigOption)
|
|
}
|
|
wantCalls := []string{"config:model:gpt-5.4-mini"}
|
|
if !reflect.DeepEqual(applier.calls, wantCalls) {
|
|
t.Fatalf("calls = %#v, want %#v", applier.calls, wantCalls)
|
|
}
|
|
}
|
|
|
|
// TestApply_FallsBackToLegacySetModel verifies the legacy fallback path:
|
|
// when the session advertises no model-shaped config option, Apply issues
|
|
// the pre-v0.13.5 session/set_model RPC restored by the kdlbs fork. This
|
|
// keeps model switching working for unmigrated agents (e.g. auggie 0.29.x).
|
|
func TestApply_FallsBackToLegacySetModel(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
applier := &fakeApplier{}
|
|
method, err := Apply(context.Background(), applier, Request{
|
|
SessionID: "sess-1",
|
|
ModelID: "claude-opus-4-7",
|
|
ConfigOptions: []ConfigOption{{ID: "mode", Category: "mode"}},
|
|
})
|
|
|
|
if err != nil {
|
|
t.Fatalf("Apply() error = %v", err)
|
|
}
|
|
if method != MethodSetModel {
|
|
t.Fatalf("method = %q, want %q", method, MethodSetModel)
|
|
}
|
|
wantCalls := []string{"legacy:sess-1:claude-opus-4-7"}
|
|
if !reflect.DeepEqual(applier.calls, wantCalls) {
|
|
t.Fatalf("calls = %#v, want %#v", applier.calls, wantCalls)
|
|
}
|
|
}
|
|
|
|
// TestApply_LegacyMethodNotFoundIsNoOp pins that when the legacy
|
|
// session/set_model RPC returns JSON-RPC -32601, Apply treats the request as
|
|
// a clean no-op (MethodNone, nil error). This is how agents that support
|
|
// neither surface (no config option AND no legacy RPC) signal "I don't
|
|
// support model selection".
|
|
func TestApply_LegacyMethodNotFoundIsNoOp(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
notFound := acp.NewMethodNotFound(acp.LegacyAgentMethodSessionSetModel)
|
|
applier := &fakeApplier{legacyErr: notFound}
|
|
method, err := Apply(context.Background(), applier, Request{
|
|
SessionID: "sess-1",
|
|
ModelID: "some-model",
|
|
ConfigOptions: []ConfigOption{{ID: "mode", Category: "mode"}},
|
|
})
|
|
|
|
if err != nil {
|
|
t.Fatalf("Apply() error = %v, want nil", err)
|
|
}
|
|
if method != MethodNone {
|
|
t.Fatalf("method = %q, want %q", method, MethodNone)
|
|
}
|
|
wantCalls := []string{"legacy:sess-1:some-model"}
|
|
if !reflect.DeepEqual(applier.calls, wantCalls) {
|
|
t.Fatalf("calls = %#v, want %#v", applier.calls, wantCalls)
|
|
}
|
|
}
|
|
|
|
// TestApply_PropagatesLegacySetModelError pins that non-32601 errors from
|
|
// the legacy RPC bubble up unchanged with MethodSetModel attribution.
|
|
func TestApply_PropagatesLegacySetModelError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
invalid := acp.NewInvalidParams(map[string]any{"message": "Unknown model"})
|
|
applier := &fakeApplier{legacyErr: invalid}
|
|
method, err := Apply(context.Background(), applier, Request{
|
|
SessionID: "sess-1",
|
|
ModelID: "bogus",
|
|
ConfigOptions: []ConfigOption{{ID: "mode", Category: "mode"}},
|
|
})
|
|
|
|
if !errors.Is(err, invalid) {
|
|
t.Fatalf("Apply() error = %v, want %v", err, invalid)
|
|
}
|
|
if method != MethodSetModel {
|
|
t.Fatalf("method = %q, want %q", method, MethodSetModel)
|
|
}
|
|
}
|
|
|
|
func TestApply_PropagatesSetConfigOptionError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
invalid := acp.NewInvalidParams(map[string]any{"message": "Invalid model value"})
|
|
applier := &fakeApplier{configErr: invalid}
|
|
method, err := Apply(context.Background(), applier, Request{
|
|
SessionID: "sess-1",
|
|
ModelID: "claude-haiku-4-5",
|
|
ConfigOptions: []ConfigOption{{
|
|
ID: "model",
|
|
Category: "model",
|
|
}},
|
|
})
|
|
|
|
if !errors.Is(err, invalid) {
|
|
t.Fatalf("Apply() error = %v, want %v", err, invalid)
|
|
}
|
|
if method != MethodSetConfigOption {
|
|
t.Fatalf("method = %q, want %q", method, MethodSetConfigOption)
|
|
}
|
|
wantCalls := []string{"config:model:claude-haiku-4-5"}
|
|
if !reflect.DeepEqual(applier.calls, wantCalls) {
|
|
t.Fatalf("calls = %#v, want %#v", applier.calls, wantCalls)
|
|
}
|
|
}
|
|
|
|
// TestApply_SetConfigOptionMethodNotFoundFallsBackToLegacy pins the
|
|
// partial-migration fallthrough: when the session advertises a model-shaped
|
|
// config option but the agent answers session/set_config_option with -32601,
|
|
// Apply falls through to the legacy session/set_model RPC instead of
|
|
// surfacing the error.
|
|
func TestApply_SetConfigOptionMethodNotFoundFallsBackToLegacy(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
applier := &fakeApplier{configErr: acp.NewMethodNotFound(acp.AgentMethodSessionSetConfigOption)}
|
|
method, err := Apply(context.Background(), applier, Request{
|
|
SessionID: "sess-1",
|
|
ModelID: "claude-opus-4-7",
|
|
ConfigOptions: []ConfigOption{{
|
|
ID: "model",
|
|
Category: "model",
|
|
}},
|
|
})
|
|
|
|
if err != nil {
|
|
t.Fatalf("Apply() error = %v, want nil", err)
|
|
}
|
|
if method != MethodSetModel {
|
|
t.Fatalf("method = %q, want %q", method, MethodSetModel)
|
|
}
|
|
wantCalls := []string{"config:model:claude-opus-4-7", "legacy:sess-1:claude-opus-4-7"}
|
|
if !reflect.DeepEqual(applier.calls, wantCalls) {
|
|
t.Fatalf("calls = %#v, want %#v", applier.calls, wantCalls)
|
|
}
|
|
}
|