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
353 lines
10 KiB
Go
353 lines
10 KiB
Go
package agentruntime
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
agentsessionstore "github.com/tutti-os/tutti/packages/agent/daemon/activity"
|
|
)
|
|
|
|
func TestControllerUpdateSettingsAppliesOpenProviderPermissionMode(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
transport := newStandardACPTransport("Example Agent", "example-session-1")
|
|
transport.conn.configOptions = []map[string]any{{
|
|
"id": "mode",
|
|
"currentValue": "default",
|
|
"options": []any{
|
|
map[string]any{"name": "Default", "value": "default"},
|
|
map[string]any{"name": "Automatic", "value": "auto"},
|
|
},
|
|
}}
|
|
adapter, err := NewStandardACPAdapter(StandardACPAdapterConfig{
|
|
Provider: "acp:example",
|
|
Name: "example-acp",
|
|
DisplayName: "Example Agent",
|
|
Command: []string{"example", "--acp"},
|
|
PermissionModes: map[string]string{
|
|
"default": "default",
|
|
"auto": "auto",
|
|
},
|
|
}, transport, LegacyHostMetadata())
|
|
if err != nil {
|
|
t.Fatalf("NewStandardACPAdapter: %v", err)
|
|
}
|
|
controller := NewController([]Adapter{adapter}, nil)
|
|
started, err := controller.Start(context.Background(), StartInput{
|
|
RoomID: "room-1",
|
|
AgentSessionID: "agent-session-1",
|
|
Provider: "acp:example",
|
|
CWD: "/workspace",
|
|
Title: "Example Agent",
|
|
Settings: &SessionSettings{
|
|
PermissionModeID: "default",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
|
|
updated, err := controller.UpdateSettings(context.Background(), UpdateSettingsInput{
|
|
RoomID: "room-1",
|
|
AgentSessionID: started.Session.AgentSessionID,
|
|
Settings: SessionSettingsPatch{
|
|
PermissionModeID: stringPtr("auto"),
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("UpdateSettings: %v", err)
|
|
}
|
|
if updated.Settings.PermissionModeID != "auto" {
|
|
t.Fatalf("updated permission mode = %q, want auto", updated.Settings.PermissionModeID)
|
|
}
|
|
session, ok := controller.Session("room-1", started.Session.AgentSessionID)
|
|
if !ok {
|
|
t.Fatal("Session returned ok=false")
|
|
}
|
|
if session.PermissionModeID != "auto" {
|
|
t.Fatalf("session permission mode = %q, want auto", session.PermissionModeID)
|
|
}
|
|
if transport.conn.lastModeID() != "auto" {
|
|
t.Fatalf("mode id = %q, want auto", transport.conn.lastModeID())
|
|
}
|
|
}
|
|
|
|
//nolint:unused // Retain the migrated config-option fixture for focused controller tests.
|
|
func configOptionDescriptorValues(descriptors []map[string]any, configID string) []string {
|
|
for _, descriptor := range descriptors {
|
|
if strings.TrimSpace(asString(descriptor["id"])) != configID {
|
|
continue
|
|
}
|
|
switch options := descriptor["options"].(type) {
|
|
case []any:
|
|
values := make([]string, 0, len(options))
|
|
for _, option := range options {
|
|
record, ok := option.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if value := strings.TrimSpace(asString(record["value"])); value != "" {
|
|
values = append(values, value)
|
|
}
|
|
}
|
|
return values
|
|
case []map[string]any:
|
|
values := make([]string, 0, len(options))
|
|
for _, option := range options {
|
|
if value := strings.TrimSpace(asString(option["value"])); value != "" {
|
|
values = append(values, value)
|
|
}
|
|
}
|
|
return values
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
//nolint:unused // Retain the migrated config-option fixture for focused controller tests.
|
|
func configOptionDescriptorOptionDescription(descriptors []map[string]any, configID string, value string) string {
|
|
for _, descriptor := range descriptors {
|
|
if strings.TrimSpace(asString(descriptor["id"])) != configID {
|
|
continue
|
|
}
|
|
switch options := descriptor["options"].(type) {
|
|
case []any:
|
|
for _, option := range options {
|
|
record, ok := option.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if strings.TrimSpace(asString(record["value"])) == value {
|
|
return strings.TrimSpace(asString(record["description"]))
|
|
}
|
|
}
|
|
case []map[string]any:
|
|
for _, option := range options {
|
|
if strings.TrimSpace(asString(option["value"])) == value {
|
|
return strings.TrimSpace(asString(option["description"]))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func TestControllerPublishesIdleStandardACPCommandUpdatesAfterStart(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
transport := newStandardACPTransport("OpenCode", "opencode-session-idle-commands")
|
|
adapter := newOpenCodeTestAdapter(transport)
|
|
controller := NewController([]Adapter{adapter}, nil)
|
|
session := standardTestSession(ProviderOpenCode)
|
|
|
|
started, err := controller.Start(context.Background(), StartInput{
|
|
RoomID: session.RoomID,
|
|
AgentSessionID: session.AgentSessionID,
|
|
Provider: session.Provider,
|
|
CWD: session.CWD,
|
|
PermissionModeID: session.PermissionModeID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
|
|
stream, unsubscribe, ok := controller.Subscribe(started.Session.RoomID, started.Session.AgentSessionID)
|
|
if !ok {
|
|
t.Fatal("Subscribe ok=false, want live session stream")
|
|
}
|
|
defer unsubscribe()
|
|
|
|
transport.conn.sendAvailableCommandsUpdate()
|
|
|
|
deadline := time.After(time.Second)
|
|
for {
|
|
select {
|
|
case event := <-stream:
|
|
if event.EventType != StreamEventAvailableCommands {
|
|
continue
|
|
}
|
|
snapshot, ok := event.Data.(AgentSessionCommandSnapshot)
|
|
if !ok {
|
|
t.Fatalf("event data = %#v, want AgentSessionCommandSnapshot", event.Data)
|
|
}
|
|
if len(snapshot.Commands) == 1 && snapshot.Commands[0].Name == "web" {
|
|
return
|
|
}
|
|
case <-deadline:
|
|
t.Fatal("idle available_commands_update was not published")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestControllerPublishesIdleStandardACPGoalUpdatesAfterStart(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
transport := newStandardACPTransport("OpenCode", "opencode-session-idle-goal")
|
|
adapter := newOpenCodeTestAdapter(transport)
|
|
controller := NewController([]Adapter{adapter}, nil)
|
|
session := standardTestSession(ProviderOpenCode)
|
|
|
|
started, err := controller.Start(context.Background(), StartInput{
|
|
RoomID: session.RoomID,
|
|
AgentSessionID: session.AgentSessionID,
|
|
Provider: session.Provider,
|
|
CWD: session.CWD,
|
|
PermissionModeID: session.PermissionModeID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
|
|
stream, unsubscribe, ok := controller.Subscribe(started.Session.RoomID, started.Session.AgentSessionID)
|
|
if !ok {
|
|
t.Fatal("Subscribe ok=false, want live session stream")
|
|
}
|
|
defer unsubscribe()
|
|
|
|
transport.conn.sendJSON(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"method": acpMethodUpdate,
|
|
"params": map[string]any{
|
|
"sessionId": transport.conn.sessionID,
|
|
"update": map[string]any{
|
|
"sessionUpdate": "thread_goal_update",
|
|
"goal": map[string]any{
|
|
"objective": "ship slash commands",
|
|
"status": "active",
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
deadline := time.After(time.Second)
|
|
for {
|
|
select {
|
|
case event := <-stream:
|
|
if event.EventType != StreamEventStatePatch {
|
|
continue
|
|
}
|
|
patch, ok := event.Data.(agentsessionstore.WorkspaceAgentStatePatch)
|
|
if !ok {
|
|
t.Fatalf("event data = %#v, want WorkspaceAgentStatePatch", event.Data)
|
|
}
|
|
goal := payloadObject(patch.RuntimeContext["goal"])
|
|
if asString(goal["objective"]) == "ship slash commands" {
|
|
return
|
|
}
|
|
case <-deadline:
|
|
t.Fatal("idle thread_goal_update was not published")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestControllerSyncCursorPlanModeFromACPUpdate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
transport := newStandardACPTransport("Cursor Agent", "cursor-session-plan-sync")
|
|
adapter := newCursorAdapterWithHostMetadata(transport, LegacyHostMetadata(), nil)
|
|
controller := NewController([]Adapter{adapter}, nil)
|
|
session := standardTestSession(ProviderCursor)
|
|
session.PermissionModeID = "agent"
|
|
session.Settings = &SessionSettings{
|
|
PermissionModeID: "agent",
|
|
PlanMode: false,
|
|
}
|
|
|
|
started, err := controller.Start(context.Background(), StartInput{
|
|
RoomID: session.RoomID,
|
|
AgentSessionID: session.AgentSessionID,
|
|
Provider: session.Provider,
|
|
CWD: session.CWD,
|
|
PermissionModeID: session.PermissionModeID,
|
|
Settings: session.Settings,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
|
|
stream, unsubscribe, ok := controller.Subscribe(started.Session.RoomID, started.Session.AgentSessionID)
|
|
if !ok {
|
|
t.Fatal("Subscribe ok=false, want live session stream")
|
|
}
|
|
defer unsubscribe()
|
|
|
|
transport.conn.sendJSON(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"method": acpMethodUpdate,
|
|
"params": map[string]any{
|
|
"sessionId": transport.conn.sessionID,
|
|
"update": map[string]any{
|
|
"sessionUpdate": "current_mode_update",
|
|
"currentModeId": "plan",
|
|
},
|
|
},
|
|
})
|
|
|
|
deadline := time.After(time.Second)
|
|
for {
|
|
select {
|
|
case event := <-stream:
|
|
if event.EventType != StreamEventStatePatch {
|
|
continue
|
|
}
|
|
patch, ok := event.Data.(agentsessionstore.WorkspaceAgentStatePatch)
|
|
if !ok {
|
|
t.Fatalf("event data = %#v, want WorkspaceAgentStatePatch", event.Data)
|
|
}
|
|
if patch.Settings != nil && patch.Settings["planMode"] == true {
|
|
stored, ok := controller.get(started.Session.RoomID, started.Session.AgentSessionID)
|
|
if !ok || stored.Settings == nil || !stored.Settings.PlanMode {
|
|
t.Fatalf("stored session settings = %#v, want planMode true", stored.Settings)
|
|
}
|
|
if stored.PermissionModeID != "agent" {
|
|
t.Fatalf("permission mode = %q, want unchanged agent", stored.PermissionModeID)
|
|
}
|
|
return
|
|
}
|
|
case <-deadline:
|
|
t.Fatal("cursor current_mode_update did not publish planMode state patch")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestControllerPublishesIdleStandardACPConfigOptionsUpdatesAfterStart(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
transport := newStandardACPTransport("OpenCode", "opencode-session-idle-config-options")
|
|
adapter := newOpenCodeTestAdapter(transport)
|
|
controller := NewController([]Adapter{adapter}, nil)
|
|
session := standardTestSession(ProviderOpenCode)
|
|
|
|
started, err := controller.Start(context.Background(), StartInput{
|
|
RoomID: session.RoomID,
|
|
AgentSessionID: session.AgentSessionID,
|
|
Provider: session.Provider,
|
|
CWD: session.CWD,
|
|
PermissionModeID: session.PermissionModeID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
|
|
stream, unsubscribe, ok := controller.Subscribe(started.Session.RoomID, started.Session.AgentSessionID)
|
|
if !ok {
|
|
t.Fatal("Subscribe ok=false, want live session stream")
|
|
}
|
|
defer unsubscribe()
|
|
|
|
transport.conn.sendConfigOptionsUpdate("model", "opus")
|
|
|
|
event := waitForStreamEventType(t, stream, StreamEventConfigOptions)
|
|
update, ok := event.Data.(AgentSessionConfigOptionsUpdate)
|
|
if !ok {
|
|
t.Fatalf("event data = %#v, want AgentSessionConfigOptionsUpdate", event.Data)
|
|
}
|
|
if update.AgentSessionID != started.Session.AgentSessionID || update.ConfigOptionKey != "model" {
|
|
t.Fatalf("config options update = %#v, want model update for session", update)
|
|
}
|
|
}
|