Files
kandev/apps/backend/internal/agentctl/sessionmodel/sessionmodel_test.go
T

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)
}
}