Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
335 lines
10 KiB
Go
335 lines
10 KiB
Go
package agentruntime
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
)
|
|
|
|
func TestControllerUpdateSettingsMergesModelPatchWithoutRequiringPermissionMode(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
adapter := &statefulInteractiveAdapter{}
|
|
controller := NewController([]Adapter{adapter}, nil)
|
|
|
|
started, err := controller.Start(context.Background(), StartInput{
|
|
RoomID: "room-1",
|
|
AgentSessionID: "agent-session-1",
|
|
Provider: ProviderCodex,
|
|
CWD: "/workspace",
|
|
Title: "Codex",
|
|
Settings: &SessionSettings{
|
|
CodexSaverMode: true,
|
|
Model: "gpt-5.2-codex",
|
|
ReasoningEffort: "high",
|
|
Speed: "standard",
|
|
PlanMode: true,
|
|
PermissionModeID: "full-access",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
if started.Session.Settings == nil {
|
|
t.Fatal("session settings = nil, want round-tripped settings")
|
|
}
|
|
|
|
updated, err := controller.UpdateSettings(context.Background(), UpdateSettingsInput{
|
|
RoomID: "room-1",
|
|
AgentSessionID: "agent-session-1",
|
|
Settings: SessionSettingsPatch{
|
|
Model: stringPtr("gpt-5.4"),
|
|
Speed: stringPtr("fast"),
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("UpdateSettings: %v", err)
|
|
}
|
|
if updated.Settings.PermissionModeID != "full-access" {
|
|
t.Fatalf("updated settings permission mode = %q, want %q", updated.Settings.PermissionModeID, "full-access")
|
|
}
|
|
if updated.Settings.Model != "gpt-5.4" {
|
|
t.Fatalf("updated settings model = %q, want %q", updated.Settings.Model, "gpt-5.4")
|
|
}
|
|
if updated.Settings.Speed != "fast" {
|
|
t.Fatalf("updated settings speed = %q, want fast", updated.Settings.Speed)
|
|
}
|
|
if updated.Settings.ReasoningEffort != "high" || !updated.Settings.PlanMode || !updated.Settings.CodexSaverMode {
|
|
t.Fatalf("updated settings = %#v, want non-updated fields preserved", updated.Settings)
|
|
}
|
|
}
|
|
|
|
func TestControllerUpdateSettingsAppliesLiveAdapterSettingsPatch(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
adapter := &statefulInteractiveAdapter{
|
|
snapshot: SessionStateSnapshot{
|
|
Settings: &SessionSettings{
|
|
Model: "gpt-5.2-codex",
|
|
ReasoningEffort: "high",
|
|
PlanMode: false,
|
|
PermissionModeID: "full-access",
|
|
},
|
|
},
|
|
}
|
|
controller := NewController([]Adapter{adapter}, nil)
|
|
|
|
started, err := controller.Start(context.Background(), StartInput{
|
|
RoomID: "room-1",
|
|
AgentSessionID: "agent-session-1",
|
|
Provider: ProviderCodex,
|
|
CWD: "/workspace",
|
|
Title: "Codex",
|
|
Settings: &SessionSettings{
|
|
Model: "gpt-5.2-codex",
|
|
ReasoningEffort: "high",
|
|
PlanMode: false,
|
|
PermissionModeID: "full-access",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
|
|
if _, err := controller.UpdateSettings(context.Background(), UpdateSettingsInput{
|
|
RoomID: "room-1",
|
|
AgentSessionID: started.Session.AgentSessionID,
|
|
Settings: SessionSettingsPatch{
|
|
Model: stringPtr("gpt-5.4"),
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("UpdateSettings: %v", err)
|
|
}
|
|
|
|
if len(adapter.appliedSettings) != 1 {
|
|
t.Fatalf("applied settings = %#v, want one live patch", adapter.appliedSettings)
|
|
}
|
|
if adapter.appliedSettings[0].Model == nil || *adapter.appliedSettings[0].Model != "gpt-5.4" {
|
|
t.Fatalf("applied settings model = %#v, want gpt-5.4", adapter.appliedSettings[0].Model)
|
|
}
|
|
|
|
state, err := controller.State("room-1", started.Session.AgentSessionID)
|
|
if err != nil {
|
|
t.Fatalf("State: %v", err)
|
|
}
|
|
if state.Settings == nil || state.Settings.Model != "gpt-5.4" {
|
|
t.Fatalf("state settings = %#v, want live model override updated", state.Settings)
|
|
}
|
|
}
|
|
|
|
func TestControllerUpdateSettingsRejectsSettingsThatRequireNewSession(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
adapter := &statefulInteractiveAdapter{requiresNewSession: true}
|
|
controller := NewController([]Adapter{adapter}, nil)
|
|
|
|
started, err := controller.Start(context.Background(), StartInput{
|
|
RoomID: "room-1",
|
|
AgentSessionID: "agent-session-1",
|
|
Provider: ProviderCodex,
|
|
CWD: "/workspace",
|
|
Title: "Codex",
|
|
Settings: &SessionSettings{
|
|
Model: "gpt-5.2-codex",
|
|
ReasoningEffort: "high",
|
|
PermissionModeID: "full-access",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
|
|
_, err = controller.UpdateSettings(context.Background(), UpdateSettingsInput{
|
|
RoomID: "room-1",
|
|
AgentSessionID: started.Session.AgentSessionID,
|
|
Settings: SessionSettingsPatch{
|
|
Model: stringPtr("gpt-5.3-codex-spark"),
|
|
},
|
|
})
|
|
if !errors.Is(err, ErrSessionSettingsRequireNewSession) {
|
|
t.Fatalf("UpdateSettings error = %v, want ErrSessionSettingsRequireNewSession", err)
|
|
}
|
|
if len(adapter.appliedSettings) != 0 {
|
|
t.Fatalf("applied live settings = %#v, want none for new-session-only setting", adapter.appliedSettings)
|
|
}
|
|
session, ok := controller.Session("room-1", started.Session.AgentSessionID)
|
|
if !ok {
|
|
t.Fatal("Session returned ok=false after rejected update")
|
|
}
|
|
if session.Settings == nil || session.Settings.Model != "gpt-5.2-codex" {
|
|
t.Fatalf("session settings after rejected update = %#v, want original model preserved", session.Settings)
|
|
}
|
|
}
|
|
|
|
func TestControllerUpdateSettingsDoesNotPersistFailedLivePatch(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
adapter := &statefulInteractiveAdapter{
|
|
applySettingsErr: errors.New("live settings unavailable"),
|
|
}
|
|
controller := NewController([]Adapter{adapter}, nil)
|
|
|
|
started, err := controller.Start(context.Background(), StartInput{
|
|
RoomID: "room-1",
|
|
AgentSessionID: "agent-session-1",
|
|
Provider: ProviderCodex,
|
|
CWD: "/workspace",
|
|
Title: "Codex",
|
|
Settings: &SessionSettings{
|
|
Model: "gpt-5.2-codex",
|
|
ReasoningEffort: "high",
|
|
PermissionModeID: "full-access",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
|
|
_, err = controller.UpdateSettings(context.Background(), UpdateSettingsInput{
|
|
RoomID: "room-1",
|
|
AgentSessionID: started.Session.AgentSessionID,
|
|
Settings: SessionSettingsPatch{
|
|
Model: stringPtr("gpt-5.4"),
|
|
},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("UpdateSettings: expected live settings failure")
|
|
}
|
|
|
|
session, ok := controller.Session("room-1", started.Session.AgentSessionID)
|
|
if !ok {
|
|
t.Fatal("Session returned ok=false after failed update")
|
|
}
|
|
if session.Settings == nil {
|
|
t.Fatal("session settings = nil after failed update")
|
|
}
|
|
if session.Settings.Model != "gpt-5.2-codex" {
|
|
t.Fatalf("session settings model = %q, want original value after failed update", session.Settings.Model)
|
|
}
|
|
|
|
state, err := controller.State("room-1", started.Session.AgentSessionID)
|
|
if err != nil {
|
|
t.Fatalf("State: %v", err)
|
|
}
|
|
if state.Settings == nil {
|
|
t.Fatal("state settings = nil after failed update")
|
|
}
|
|
if state.Settings.Model != "gpt-5.2-codex" {
|
|
t.Fatalf("state settings model = %q, want original value after failed update", state.Settings.Model)
|
|
}
|
|
}
|
|
|
|
func TestControllerUpdateSettingsDoesNotAdvanceSessionUpdatedAt(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
adapter := &statefulInteractiveAdapter{}
|
|
controller := NewController([]Adapter{adapter}, nil)
|
|
|
|
started, err := controller.Start(context.Background(), StartInput{
|
|
RoomID: "room-1",
|
|
AgentSessionID: "agent-session-1",
|
|
Provider: ProviderCodex,
|
|
CWD: "/workspace",
|
|
Title: "Codex",
|
|
Settings: &SessionSettings{
|
|
Model: "gpt-5.2-codex",
|
|
ReasoningEffort: "high",
|
|
PermissionModeID: "full-access",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
|
|
controller.mu.Lock()
|
|
session := controller.sessions[sessionKey("room-1", started.Session.AgentSessionID)]
|
|
session.UpdatedAtUnixMS = 123
|
|
controller.sessions[sessionKey("room-1", started.Session.AgentSessionID)] = session
|
|
controller.mu.Unlock()
|
|
|
|
if _, err := controller.UpdateSettings(context.Background(), UpdateSettingsInput{
|
|
RoomID: "room-1",
|
|
AgentSessionID: started.Session.AgentSessionID,
|
|
Settings: SessionSettingsPatch{
|
|
Model: stringPtr("gpt-5.4"),
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("UpdateSettings: %v", err)
|
|
}
|
|
|
|
updated, ok := controller.Session("room-1", started.Session.AgentSessionID)
|
|
if !ok {
|
|
t.Fatal("Session returned ok=false after update")
|
|
}
|
|
if updated.UpdatedAtUnixMS != 123 {
|
|
t.Fatalf("UpdatedAtUnixMS = %d, want preserved value 123", updated.UpdatedAtUnixMS)
|
|
}
|
|
|
|
state, err := controller.State("room-1", started.Session.AgentSessionID)
|
|
if err != nil {
|
|
t.Fatalf("State: %v", err)
|
|
}
|
|
if state.UpdatedAtUnixMS != 123 {
|
|
t.Fatalf("state UpdatedAtUnixMS = %d, want preserved value 123", state.UpdatedAtUnixMS)
|
|
}
|
|
}
|
|
|
|
func TestControllerStateAppliesAdapterSettingsOverride(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
adapter := &statefulInteractiveAdapter{
|
|
snapshot: SessionStateSnapshot{
|
|
PermissionModeID: "full-access",
|
|
Settings: &SessionSettings{
|
|
Model: "opus",
|
|
ReasoningEffort: "low",
|
|
PlanMode: true,
|
|
PermissionModeID: "full-access",
|
|
},
|
|
},
|
|
}
|
|
controller := NewController([]Adapter{adapter}, nil)
|
|
|
|
_, err := controller.Start(context.Background(), StartInput{
|
|
RoomID: "room-1",
|
|
AgentSessionID: "agent-session-1",
|
|
Provider: ProviderCodex,
|
|
CWD: "/workspace",
|
|
Title: "Codex",
|
|
Settings: &SessionSettings{
|
|
Model: "gpt-5.2-codex",
|
|
ReasoningEffort: "high",
|
|
PermissionModeID: "read-only",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
|
|
state, err := controller.State("room-1", "agent-session-1")
|
|
if err != nil {
|
|
t.Fatalf("State: %v", err)
|
|
}
|
|
if state.PermissionModeID != "full-access" {
|
|
t.Fatalf("state permission mode = %q, want %q", state.PermissionModeID, "full-access")
|
|
}
|
|
if state.Settings == nil {
|
|
t.Fatal("state settings = nil, want adapter override")
|
|
}
|
|
if state.Settings.Model != "opus" || state.Settings.ReasoningEffort != "low" || !state.Settings.PlanMode {
|
|
t.Fatalf("state settings = %#v, want adapter override", state.Settings)
|
|
}
|
|
if state.Settings.PermissionModeID != "full-access" {
|
|
t.Fatalf("state settings permission mode = %q, want %q", state.Settings.PermissionModeID, "full-access")
|
|
}
|
|
if got := asString(state.RuntimeContext["model"]); got != "opus" {
|
|
t.Fatalf("runtime context model = %q, want opus", got)
|
|
}
|
|
if got := asString(state.RuntimeContext["reasoningEffort"]); got != "low" {
|
|
t.Fatalf("runtime context reasoningEffort = %q, want low", got)
|
|
}
|
|
if got, _ := state.RuntimeContext["planMode"].(bool); !got {
|
|
t.Fatalf("runtime context planMode = %#v, want true", state.RuntimeContext["planMode"])
|
|
}
|
|
}
|