Files
tutti/packages/agent/daemon/runtime/standard_acp_controller_test.go
agent-radar 2ed3415fb7
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
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

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