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
756 lines
31 KiB
Go
756 lines
31 KiB
Go
package agentruntime
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestNexightSpawnCommandCarriesModelSettings(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []struct {
|
|
name string
|
|
settings *SessionSettings
|
|
want []string
|
|
}{
|
|
{
|
|
name: "spark model adds reasoning summary override",
|
|
settings: &SessionSettings{Model: "gpt-5.3-codex-spark", ReasoningEffort: "high"},
|
|
want: []string{
|
|
nexightACPCommand,
|
|
"--config", "model=gpt-5.3-codex-spark",
|
|
"--config", "model_reasoning_summary=none",
|
|
"--config", "model_reasoning_effort=high",
|
|
},
|
|
},
|
|
{
|
|
name: "plain model omits reasoning summary override",
|
|
settings: &SessionSettings{Model: "gpt-5.1-codex", ReasoningEffort: "medium"},
|
|
want: []string{
|
|
nexightACPCommand,
|
|
"--config", "model=gpt-5.1-codex",
|
|
"--config", "model_reasoning_effort=medium",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
transport := newStandardACPTransport("Nexight", "nexight-session-1")
|
|
adapter := NewNexightAdapter(transport)
|
|
session := standardTestSession(ProviderNexight)
|
|
session.Settings = tc.settings
|
|
if _, err := adapter.Start(context.Background(), session); err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
transport.mu.Lock()
|
|
specs := append([]ProcessSpec(nil), transport.specs...)
|
|
transport.mu.Unlock()
|
|
if len(specs) != 1 {
|
|
t.Fatalf("specs = %#v, want one process spawn", specs)
|
|
}
|
|
if !reflect.DeepEqual(specs[0].Command, tc.want) {
|
|
t.Fatalf("spawn command = %#v, want %#v", specs[0].Command, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNexightRequiresNewSessionWhenReasoningSummaryOverrideChanges(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
adapter := NewNexightAdapter(nil)
|
|
session := standardTestSession(ProviderNexight)
|
|
session.Settings = &SessionSettings{Model: "gpt-5.1-codex"}
|
|
|
|
sparkModel := "gpt-5.3-codex-spark"
|
|
if !adapter.RequiresNewSessionForSettings(session, SessionSettingsPatch{Model: &sparkModel}) {
|
|
t.Fatal("switching to a spark-family model must force a new session (spawn-time model_reasoning_summary override)")
|
|
}
|
|
plainModel := "gpt-5.2-codex"
|
|
if adapter.RequiresNewSessionForSettings(session, SessionSettingsPatch{Model: &plainModel}) {
|
|
t.Fatal("plain-to-plain model change must not force a new session")
|
|
}
|
|
if adapter.RequiresNewSessionForSettings(session, SessionSettingsPatch{}) {
|
|
t.Fatal("empty patch must not force a new session")
|
|
}
|
|
}
|
|
|
|
func TestStandardACPSpawnCommandUnchangedForOtherProviders(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
transport := newStandardACPTransport("Hermes Agent", "hermes-session-1")
|
|
adapter := newHermesExtensionTestAdapter(transport)
|
|
session := standardTestSession(hermesExtensionTestProvider)
|
|
session.Settings = &SessionSettings{Model: "gpt-5.3-codex-spark", ReasoningEffort: "high"}
|
|
if _, err := adapter.Start(context.Background(), session); err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
transport.mu.Lock()
|
|
specs := append([]ProcessSpec(nil), transport.specs...)
|
|
transport.mu.Unlock()
|
|
if len(specs) != 1 || !reflect.DeepEqual(specs[0].Command, []string{"hermes", "acp"}) {
|
|
t.Fatalf("spawn command = %#v, want bare hermes command", specs)
|
|
}
|
|
sparkModel := "gpt-5.3-codex-spark"
|
|
if adapter.RequiresNewSessionForSettings(session, SessionSettingsPatch{Model: &sparkModel}) {
|
|
t.Fatal("non-nexight providers must not force new sessions for model changes")
|
|
}
|
|
}
|
|
|
|
func TestStandardACPLaunchPermissionBuildsExactArgvAndDoesNotSetWorkflowMode(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
semantic string
|
|
want string
|
|
}{
|
|
{name: "default", want: "default"},
|
|
{name: "ask", semantic: "ask-before-write", want: "default"},
|
|
{name: "auto", semantic: "auto", want: "auto"},
|
|
{name: "full access", semantic: "full-access", want: "bypassPermissions"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
transport := newStandardACPTransport("Spawn ACP", "spawn-session-1")
|
|
adapterRaw, err := NewStandardACPAdapter(StandardACPAdapterConfig{
|
|
Provider: "acp:spawn",
|
|
Name: "spawn-acp",
|
|
DisplayName: "Spawn ACP",
|
|
Command: []string{"grok", "--no-auto-update", "--permission-mode", "${permissionMode}", "agent", "stdio"},
|
|
PermissionModes: map[string]string{"ask-before-write": "default", "auto": "auto", "full-access": "bypassPermissions"},
|
|
LaunchPermission: &StandardACPLaunchPermissionSetting{
|
|
Placeholder: "${permissionMode}",
|
|
Values: map[string]string{"ask-before-write": "default", "auto": "auto", "full-access": "bypassPermissions"},
|
|
},
|
|
}, transport, LegacyHostMetadata())
|
|
if err != nil {
|
|
t.Fatalf("NewStandardACPAdapter: %v", err)
|
|
}
|
|
adapter := adapterRaw.(*standardACPAdapter)
|
|
session := standardTestSession("acp:spawn")
|
|
session.PermissionModeID = tt.semantic
|
|
if _, err := adapter.Start(context.Background(), session); err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
transport.mu.Lock()
|
|
specs := append([]ProcessSpec(nil), transport.specs...)
|
|
transport.mu.Unlock()
|
|
want := []string{"grok", "--no-auto-update", "--permission-mode", tt.want, "agent", "stdio"}
|
|
if len(specs) != 1 || !reflect.DeepEqual(specs[0].Command, want) {
|
|
t.Fatalf("spawn command = %#v, want %#v", specs, want)
|
|
}
|
|
if got := transport.conn.lastSetModeParams(); got != nil {
|
|
t.Fatalf("session/set_mode params = %#v, want none for spawn-time permission", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStandardACPLaunchPermissionKeepsPlanAsIndependentWorkflowMode(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
transport := newStandardACPTransport("Spawn ACP", "spawn-plan-session")
|
|
transport.conn.modes = map[string]any{
|
|
"currentModeId": "default",
|
|
"availableModes": []any{
|
|
map[string]any{"id": "default", "name": "Default"},
|
|
map[string]any{"id": "plan", "name": "Plan"},
|
|
},
|
|
}
|
|
adapterRaw, err := NewStandardACPAdapter(StandardACPAdapterConfig{
|
|
Provider: "acp:spawn-plan",
|
|
Name: "spawn-plan-acp",
|
|
DisplayName: "Spawn Plan ACP",
|
|
Command: []string{"grok", "--no-auto-update", "--permission-mode", "${permissionMode}", "agent", "stdio"},
|
|
PermissionModes: map[string]string{"ask-before-write": "default", "auto": "auto", "full-access": "bypassPermissions"},
|
|
PlanModeRuntimeID: "plan",
|
|
PlanModeDisabledRuntimeID: "default",
|
|
LaunchPermission: &StandardACPLaunchPermissionSetting{
|
|
Placeholder: "${permissionMode}",
|
|
Values: map[string]string{"ask-before-write": "default", "auto": "auto", "full-access": "bypassPermissions"},
|
|
},
|
|
}, transport, LegacyHostMetadata())
|
|
if err != nil {
|
|
t.Fatalf("NewStandardACPAdapter: %v", err)
|
|
}
|
|
adapter := adapterRaw.(*standardACPAdapter)
|
|
session := standardTestSession("acp:spawn-plan")
|
|
session.PermissionModeID = "full-access"
|
|
session.Settings = &SessionSettings{PermissionModeID: "full-access"}
|
|
if _, err := adapter.Start(context.Background(), session); err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
if got := transport.conn.lastSetModeParams(); got != nil {
|
|
t.Fatalf("startup session/set_mode = %#v, want none", got)
|
|
}
|
|
for _, enabled := range []bool{true, false} {
|
|
session.Settings.PlanMode = enabled
|
|
if err := adapter.ValidateSessionSettings(session, SessionSettingsPatch{PlanMode: &enabled}); err != nil {
|
|
t.Fatalf("ValidateSessionSettings(%v): %v", enabled, err)
|
|
}
|
|
if err := adapter.ApplySessionSettings(context.Background(), session, SessionSettingsPatch{PlanMode: &enabled}); err != nil {
|
|
t.Fatalf("ApplySessionSettings(%v): %v", enabled, err)
|
|
}
|
|
want := "default"
|
|
if enabled {
|
|
want = "plan"
|
|
}
|
|
if got := transport.conn.lastModeID(); got != want {
|
|
t.Fatalf("plan=%v mode = %q, want %q", enabled, got, want)
|
|
}
|
|
}
|
|
changed := "auto"
|
|
if !adapter.RequiresNewSessionForSettings(session, SessionSettingsPatch{PermissionModeID: &changed}) {
|
|
t.Fatal("spawn-time permission update must require a new session")
|
|
}
|
|
if err := adapter.ApplySessionSettings(context.Background(), session, SessionSettingsPatch{PermissionModeID: &changed}); !errors.Is(err, ErrSessionSettingsRequireNewSession) {
|
|
t.Fatalf("ApplySessionSettings permission error = %v, want ErrSessionSettingsRequireNewSession", err)
|
|
}
|
|
}
|
|
|
|
func TestStandardACPLaunchPermissionWithoutPlanWorkflowNeverSendsPermissionAsMode(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
transport := newStandardACPTransport("Spawn ACP", "spawn-without-plan-session")
|
|
transport.conn.modes = map[string]any{
|
|
"currentModeId": "default",
|
|
"availableModes": []any{
|
|
map[string]any{"id": "default", "name": "Default"},
|
|
map[string]any{"id": "plan", "name": "Plan"},
|
|
},
|
|
}
|
|
adapterRaw, err := NewStandardACPAdapter(StandardACPAdapterConfig{
|
|
Provider: "acp:spawn-without-plan",
|
|
Name: "spawn-without-plan-acp",
|
|
DisplayName: "Spawn Without Plan ACP",
|
|
Command: []string{"spawn-acp", "--permission-mode", "${permissionMode}", "agent", "stdio"},
|
|
PermissionModes: map[string]string{"ask-before-write": "default", "auto": "auto", "full-access": "bypassPermissions"},
|
|
LaunchPermission: &StandardACPLaunchPermissionSetting{
|
|
Placeholder: "${permissionMode}",
|
|
Values: map[string]string{"ask-before-write": "default", "auto": "auto", "full-access": "bypassPermissions"},
|
|
},
|
|
}, transport, LegacyHostMetadata())
|
|
if err != nil {
|
|
t.Fatalf("NewStandardACPAdapter: %v", err)
|
|
}
|
|
adapter := adapterRaw.(*standardACPAdapter)
|
|
session := standardTestSession("acp:spawn-without-plan")
|
|
session.PermissionModeID = "ask-before-write"
|
|
session.Settings = &SessionSettings{PermissionModeID: "ask-before-write", PlanMode: true}
|
|
if _, err := adapter.Start(context.Background(), session); err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
if got := transport.conn.lastSetModeParams(); got != nil {
|
|
t.Fatalf("startup session/set_mode = %#v, want none", got)
|
|
}
|
|
enabled := true
|
|
if err := adapter.ValidateSessionSettings(session, SessionSettingsPatch{PlanMode: &enabled}); err == nil {
|
|
t.Fatal("ValidateSessionSettings error = nil, want missing signed Plan mapping")
|
|
}
|
|
}
|
|
|
|
func TestStandardACPDeclaredPlanModesDoNotRequireRuntimeModeCatalog(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
transport := newStandardACPTransport("Declared Plan ACP", "declared-plan-session")
|
|
adapterRaw, err := NewStandardACPAdapter(StandardACPAdapterConfig{
|
|
Provider: "acp:declared-plan",
|
|
Name: "declared-plan-acp",
|
|
DisplayName: "Declared Plan ACP",
|
|
Command: []string{"declared-plan", "agent", "stdio"},
|
|
PermissionModes: map[string]string{"ask-before-write": "default"},
|
|
PlanModeRuntimeID: "plan",
|
|
PlanModeDisabledRuntimeID: "default",
|
|
}, transport, LegacyHostMetadata())
|
|
if err != nil {
|
|
t.Fatalf("NewStandardACPAdapter: %v", err)
|
|
}
|
|
adapter := adapterRaw.(*standardACPAdapter)
|
|
session := standardTestSession("acp:declared-plan")
|
|
session.Settings = &SessionSettings{PermissionModeID: "ask-before-write"}
|
|
if _, err := adapter.Start(context.Background(), session); err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
|
|
for _, enabled := range []bool{true, false} {
|
|
if err := adapter.ValidateSessionSettings(session, SessionSettingsPatch{PlanMode: &enabled}); err != nil {
|
|
t.Fatalf("ValidateSessionSettings(%v): %v", enabled, err)
|
|
}
|
|
session.Settings.PlanMode = enabled
|
|
if err := adapter.ApplySessionSettings(context.Background(), session, SessionSettingsPatch{PlanMode: &enabled}); err != nil {
|
|
t.Fatalf("ApplySessionSettings(%v): %v", enabled, err)
|
|
}
|
|
want := "default"
|
|
if enabled {
|
|
want = "plan"
|
|
}
|
|
if got := transport.conn.lastModeID(); got != want {
|
|
t.Fatalf("plan=%v mode = %q, want %q", enabled, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStandardACPDeclaredPlanModeSurfacesRuntimeRejection(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
transport := newStandardACPTransport("Rejected Plan ACP", "rejected-plan-session")
|
|
adapterRaw, err := NewStandardACPAdapter(StandardACPAdapterConfig{
|
|
Provider: "acp:rejected-plan",
|
|
Name: "rejected-plan-acp",
|
|
DisplayName: "Rejected Plan ACP",
|
|
Command: []string{"rejected-plan", "agent", "stdio"},
|
|
PermissionModes: map[string]string{"ask-before-write": "default"},
|
|
PlanModeRuntimeID: "plan",
|
|
PlanModeDisabledRuntimeID: "default",
|
|
}, transport, LegacyHostMetadata())
|
|
if err != nil {
|
|
t.Fatalf("NewStandardACPAdapter: %v", err)
|
|
}
|
|
adapter := adapterRaw.(*standardACPAdapter)
|
|
session := standardTestSession("acp:rejected-plan")
|
|
session.Settings = &SessionSettings{PermissionModeID: "ask-before-write"}
|
|
if _, err := adapter.Start(context.Background(), session); err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
transport.conn.setModeError = &acpError{Code: -32602, Message: "unsupported mode"}
|
|
enabled := true
|
|
session.Settings.PlanMode = enabled
|
|
if err := adapter.ApplySessionSettings(context.Background(), session, SessionSettingsPatch{PlanMode: &enabled}); err == nil {
|
|
t.Fatal("ApplySessionSettings error = nil, want runtime rejection")
|
|
}
|
|
}
|
|
|
|
func TestStandardACPLaunchPermissionPlanRestartsAndLoadsSameSession(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
transport := &multiProcStandardACPTransport{
|
|
agentTitle: "Grok Build",
|
|
sessionID: "grok-provider-session",
|
|
supportsLoadSession: true,
|
|
}
|
|
adapterRaw, err := NewStandardACPAdapter(StandardACPAdapterConfig{
|
|
Provider: "acp:grok-launch-plan",
|
|
Name: "grok-launch-plan-acp",
|
|
DisplayName: "Grok Build",
|
|
Command: []string{"grok", "--permission-mode", "${permissionMode}", "agent", "stdio"},
|
|
PermissionModes: map[string]string{"ask-before-write": "default", "auto": "auto", "full-access": "bypassPermissions"},
|
|
PlanModeRuntimeID: "plan",
|
|
PlanModeDisabledRuntimeID: "default",
|
|
PlanModeUsesLaunchPermission: true,
|
|
LaunchPermission: &StandardACPLaunchPermissionSetting{
|
|
Placeholder: "${permissionMode}",
|
|
DefaultSemantic: "ask-before-write",
|
|
Values: map[string]string{"ask-before-write": "default", "auto": "auto", "full-access": "bypassPermissions"},
|
|
},
|
|
}, transport, LegacyHostMetadata())
|
|
if err != nil {
|
|
t.Fatalf("NewStandardACPAdapter: %v", err)
|
|
}
|
|
adapter := adapterRaw.(*standardACPAdapter)
|
|
session := standardTestSession("acp:grok-launch-plan")
|
|
session.PermissionModeID = "auto"
|
|
session.Settings = &SessionSettings{PermissionModeID: "auto"}
|
|
events, err := adapter.Start(context.Background(), session)
|
|
if err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
session.ProviderSessionID = events[0].ProviderSessionID
|
|
|
|
for _, enabled := range []bool{true, false} {
|
|
if err := adapter.ValidateSessionSettings(session, SessionSettingsPatch{PlanMode: &enabled}); err != nil {
|
|
t.Fatalf("ValidateSessionSettings(%v): %v", enabled, err)
|
|
}
|
|
session.Settings.PlanMode = enabled
|
|
if err := adapter.ApplySessionSettings(context.Background(), session, SessionSettingsPatch{PlanMode: &enabled}); err != nil {
|
|
t.Fatalf("ApplySessionSettings(%v): %v", enabled, err)
|
|
}
|
|
wantDecision := ""
|
|
if enabled {
|
|
wantDecision = "denied"
|
|
}
|
|
if got := adapter.automaticPermissionDecision(session.AgentSessionID); got != wantDecision {
|
|
t.Fatalf("plan=%v automatic permission decision = %q, want %q", enabled, got, wantDecision)
|
|
}
|
|
}
|
|
|
|
transport.mu.Lock()
|
|
specs := append([]ProcessSpec(nil), transport.specs...)
|
|
connections := append([]*standardACPConnection(nil), transport.conns...)
|
|
transport.mu.Unlock()
|
|
if len(specs) != 3 {
|
|
t.Fatalf("process starts = %d, want initial plus two Plan restarts", len(specs))
|
|
}
|
|
wantModes := []string{"auto", "plan", "auto"}
|
|
for index, want := range wantModes {
|
|
if got := specs[index].Command[2]; got != want {
|
|
t.Fatalf("process %d permission mode = %q, want %q", index, got, want)
|
|
}
|
|
}
|
|
for index := 1; index < len(connections); index++ {
|
|
if got := asString(connections[index].lastLoadSessionParams["sessionId"]); got != "grok-provider-session" {
|
|
t.Fatalf("restart %d loaded session = %q, want grok-provider-session", index, got)
|
|
}
|
|
}
|
|
spawned, live := transport.snapshot()
|
|
if spawned != 3 || len(live) != 1 {
|
|
t.Fatalf("spawned/live processes = %d/%d, want 3/1", spawned, len(live))
|
|
}
|
|
}
|
|
|
|
func TestStandardACPLaunchPermissionPlanPersistsWithoutSpawnAfterReleaseFailure(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
transport := &multiProcStandardACPTransport{
|
|
agentTitle: "Grok Build",
|
|
sessionID: "grok-provider-session-deferred-plan",
|
|
supportsLoadSession: true,
|
|
}
|
|
adapterRaw, err := NewStandardACPAdapter(StandardACPAdapterConfig{
|
|
Provider: "acp:grok-launch-plan-deferred",
|
|
Name: "grok-launch-plan-deferred-acp",
|
|
DisplayName: "Grok Build",
|
|
Command: []string{"grok", "--permission-mode", "${permissionMode}", "agent", "stdio"},
|
|
PermissionModes: map[string]string{"ask-before-write": "default", "auto": "auto", "full-access": "bypassPermissions"},
|
|
PlanModeRuntimeID: "plan",
|
|
PlanModeDisabledRuntimeID: "default",
|
|
PlanModeUsesLaunchPermission: true,
|
|
LaunchPermission: &StandardACPLaunchPermissionSetting{
|
|
Placeholder: "${permissionMode}",
|
|
DefaultSemantic: "ask-before-write",
|
|
Values: map[string]string{"ask-before-write": "default", "auto": "auto", "full-access": "bypassPermissions"},
|
|
},
|
|
}, transport, LegacyHostMetadata())
|
|
if err != nil {
|
|
t.Fatalf("NewStandardACPAdapter: %v", err)
|
|
}
|
|
adapter := adapterRaw.(*standardACPAdapter)
|
|
session := standardTestSession("acp:grok-launch-plan-deferred")
|
|
session.PermissionModeID = "auto"
|
|
session.Settings = &SessionSettings{PermissionModeID: "auto"}
|
|
events, err := adapter.Start(context.Background(), session)
|
|
if err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
session.ProviderSessionID = events[0].ProviderSessionID
|
|
transport.mu.Lock()
|
|
firstConnection := transport.conns[0]
|
|
transport.mu.Unlock()
|
|
firstConnection.mu.Lock()
|
|
firstConnection.closeFailures = 1
|
|
firstConnection.mu.Unlock()
|
|
if err := adapter.ReleaseLiveSession(context.Background(), session); err == nil {
|
|
t.Fatal("ReleaseLiveSession error = nil, want injected close failure")
|
|
}
|
|
|
|
enabled := true
|
|
session.Settings.PlanMode = enabled
|
|
if err := adapter.ApplySessionSettings(context.Background(), session, SessionSettingsPatch{PlanMode: &enabled}); err != nil {
|
|
t.Fatalf("ApplySessionSettings: %v", err)
|
|
}
|
|
spawned, _ := transport.snapshot()
|
|
if spawned != 1 {
|
|
t.Fatalf("process starts after settings persistence = %d, want 1", spawned)
|
|
}
|
|
if err := adapter.Resume(context.Background(), session); err != nil {
|
|
t.Fatalf("Resume: %v", err)
|
|
}
|
|
transport.mu.Lock()
|
|
specs := append([]ProcessSpec(nil), transport.specs...)
|
|
connections := append([]*standardACPConnection(nil), transport.conns...)
|
|
transport.mu.Unlock()
|
|
if len(specs) != 2 || specs[1].Command[2] != "plan" {
|
|
t.Fatalf("resume command = %#v, want persisted Plan launch", specs)
|
|
}
|
|
if got := asString(connections[1].lastLoadSessionParams["sessionId"]); got != session.ProviderSessionID {
|
|
t.Fatalf("resumed provider session = %q, want %q", got, session.ProviderSessionID)
|
|
}
|
|
}
|
|
|
|
func TestStandardACPLaunchPermissionRejectsUnknownSessionSemantic(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
transport := newStandardACPTransport("Spawn ACP", "spawn-invalid-session")
|
|
adapterRaw, err := NewStandardACPAdapter(StandardACPAdapterConfig{
|
|
Provider: "acp:spawn-invalid",
|
|
Name: "spawn-invalid-acp",
|
|
DisplayName: "Spawn Invalid ACP",
|
|
Command: []string{"spawn-acp", "${permissionMode}", "stdio"},
|
|
PermissionModes: map[string]string{"ask-before-write": "ask", "auto": "auto", "full-access": "always-approve"},
|
|
LaunchPermission: &StandardACPLaunchPermissionSetting{
|
|
Placeholder: "${permissionMode}",
|
|
Values: map[string]string{"ask-before-write": "ask", "auto": "auto", "full-access": "always-approve"},
|
|
},
|
|
}, transport, LegacyHostMetadata())
|
|
if err != nil {
|
|
t.Fatalf("NewStandardACPAdapter: %v", err)
|
|
}
|
|
session := standardTestSession("acp:spawn-invalid")
|
|
session.PermissionModeID = "unknown"
|
|
if _, err := adapterRaw.Start(context.Background(), session); err == nil {
|
|
t.Fatal("Start unknown permission semantic error = nil")
|
|
}
|
|
transport.mu.Lock()
|
|
defer transport.mu.Unlock()
|
|
if len(transport.specs) != 0 {
|
|
t.Fatalf("spawn specs = %#v, want fail before process start", transport.specs)
|
|
}
|
|
}
|
|
|
|
func TestStandardACPSessionNewAndLoadPassStandardClientMCPConfig(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
newTransport := newStandardACPTransport("MCP ACP", "mcp-new-session")
|
|
newAdapterRaw, err := NewStandardACPAdapter(StandardACPAdapterConfig{
|
|
Provider: "acp:mcp-new", Name: "mcp-new-acp", DisplayName: "MCP ACP", Command: []string{"mcp-acp", "stdio"},
|
|
}, newTransport, LegacyHostMetadata())
|
|
if err != nil {
|
|
t.Fatalf("NewStandardACPAdapter new: %v", err)
|
|
}
|
|
newSession := standardTestSession("acp:mcp-new")
|
|
newSession.MCPServers = []MCPServerBinding{{Name: "connector", Type: "http", URL: "http://127.0.0.1:1234/mcp/connector",
|
|
Headers: map[string]string{"Authorization": "Bearer test-token"}}}
|
|
if _, err := newAdapterRaw.Start(context.Background(), newSession); err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
if servers, ok := newTransport.conn.lastNewSessionParams["mcpServers"].([]any); !ok || len(servers) != 1 {
|
|
t.Fatalf("session/new mcpServers = %#v, want connector binding", newTransport.conn.lastNewSessionParams["mcpServers"])
|
|
}
|
|
|
|
loadTransport := newStandardACPTransport("MCP ACP", "mcp-load-session")
|
|
loadTransport.conn.supportsLoadSession = true
|
|
loadAdapterRaw, err := NewStandardACPAdapter(StandardACPAdapterConfig{
|
|
Provider: "acp:mcp-load", Name: "mcp-load-acp", DisplayName: "MCP ACP", Command: []string{"mcp-acp", "stdio"},
|
|
}, loadTransport, LegacyHostMetadata())
|
|
if err != nil {
|
|
t.Fatalf("NewStandardACPAdapter load: %v", err)
|
|
}
|
|
loadSession := standardTestSession("acp:mcp-load")
|
|
loadSession.ProviderSessionID = "mcp-load-session"
|
|
loadSession.MCPServers = cloneMCPServerBindings(newSession.MCPServers)
|
|
if err := loadAdapterRaw.Resume(context.Background(), loadSession); err != nil {
|
|
t.Fatalf("Resume: %v", err)
|
|
}
|
|
if servers, ok := loadTransport.conn.lastLoadSessionParams["mcpServers"].([]any); !ok || len(servers) != 1 {
|
|
t.Fatalf("session/load mcpServers = %#v, want connector binding", loadTransport.conn.lastLoadSessionParams["mcpServers"])
|
|
}
|
|
}
|
|
|
|
func TestStandardACPSessionFallsBackWithoutHTTPMCPWhenAgentDoesNotAdvertiseCapability(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
newTransport := newStandardACPTransport("MCP Unsupported ACP", "mcp-unsupported-session")
|
|
newTransport.conn.supportsHTTPMCP = false
|
|
newAdapter, err := NewStandardACPAdapter(StandardACPAdapterConfig{
|
|
Provider: "acp:mcp-unsupported", Name: "mcp-unsupported-acp", DisplayName: "MCP Unsupported ACP", Command: []string{"mcp-acp", "stdio"},
|
|
}, newTransport, LegacyHostMetadata())
|
|
if err != nil {
|
|
t.Fatalf("NewStandardACPAdapter: %v", err)
|
|
}
|
|
session := standardTestSession("acp:mcp-unsupported")
|
|
session.MCPServers = []MCPServerBinding{{Name: "connector", Type: "http", URL: "http://127.0.0.1:1234/mcp/connector"}}
|
|
if _, err := newAdapter.Start(context.Background(), session); err != nil {
|
|
t.Fatalf("Start error = %v", err)
|
|
}
|
|
if servers, ok := newTransport.conn.lastNewSessionParams["mcpServers"].([]any); ok && len(servers) != 0 {
|
|
t.Fatalf("session/new mcpServers = %#v, want empty fallback", newTransport.conn.lastNewSessionParams["mcpServers"])
|
|
}
|
|
|
|
loadTransport := newStandardACPTransport("MCP Unsupported ACP", "mcp-unsupported-load-session")
|
|
loadTransport.conn.supportsHTTPMCP = false
|
|
loadTransport.conn.supportsLoadSession = true
|
|
loadAdapter, err := NewStandardACPAdapter(StandardACPAdapterConfig{
|
|
Provider: "acp:mcp-unsupported", Name: "mcp-unsupported-acp", DisplayName: "MCP Unsupported ACP", Command: []string{"mcp-acp", "stdio"},
|
|
}, loadTransport, LegacyHostMetadata())
|
|
if err != nil {
|
|
t.Fatalf("NewStandardACPAdapter load: %v", err)
|
|
}
|
|
loadSession := standardTestSession("acp:mcp-unsupported")
|
|
loadSession.ProviderSessionID = "mcp-unsupported-load-session"
|
|
loadSession.MCPServers = cloneMCPServerBindings(session.MCPServers)
|
|
if err := loadAdapter.Resume(context.Background(), loadSession); err != nil {
|
|
t.Fatalf("Resume error = %v", err)
|
|
}
|
|
if servers, ok := loadTransport.conn.lastLoadSessionParams["mcpServers"].([]any); ok && len(servers) != 0 {
|
|
t.Fatalf("session/load mcpServers = %#v, want empty fallback", loadTransport.conn.lastLoadSessionParams["mcpServers"])
|
|
}
|
|
}
|
|
|
|
func TestStandardACPConnectorCapabilitiesRequireExplicitHTTPDeclaration(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
supported bool
|
|
}{
|
|
{name: "declared", supported: true},
|
|
{name: "missing", supported: false},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
transport := newStandardACPTransport("Capability ACP", "capability-session")
|
|
transport.conn.supportsHTTPMCP = test.supported
|
|
adapter, err := NewStandardACPAdapter(StandardACPAdapterConfig{
|
|
Provider: "acp:capability", Name: "capability-acp", DisplayName: "Capability ACP", Command: []string{"capability-acp", "stdio"},
|
|
}, transport, LegacyHostMetadata())
|
|
if err != nil {
|
|
t.Fatalf("NewStandardACPAdapter: %v", err)
|
|
}
|
|
capabilities, err := adapter.(ConnectorCapabilityAdapter).ConnectorCapabilities(t.Context(), standardTestSession("acp:capability"))
|
|
if err != nil {
|
|
t.Fatalf("ConnectorCapabilities: %v", err)
|
|
}
|
|
if capabilities.HTTPMCP != test.supported {
|
|
t.Fatalf("HTTPMCP = %v, want %v", capabilities.HTTPMCP, test.supported)
|
|
}
|
|
if transport.conn.lastNewSessionParams != nil {
|
|
t.Fatalf("capability probe created session: %#v", transport.conn.lastNewSessionParams)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStandardACPSetModelCarriesAdvertisedPerModelReasoningMetadata(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
transport := newStandardACPTransport("Model ACP", "model-session")
|
|
transport.conn.models = map[string]any{
|
|
"currentModelId": "reasoning-model",
|
|
"availableModels": []any{
|
|
map[string]any{
|
|
"modelId": "reasoning-model", "name": "Reasoning Model",
|
|
"supportsReasoningEffort": true, "reasoningEffort": "deep",
|
|
"reasoningEfforts": []any{
|
|
map[string]any{"value": "brief", "label": "Brief"},
|
|
map[string]any{"value": "deep", "label": "Deep", "default": true},
|
|
},
|
|
},
|
|
map[string]any{"modelId": "plain-model", "name": "Plain Model", "supportsReasoningEffort": false, "reasoningEfforts": []any{}},
|
|
},
|
|
}
|
|
adapterRaw, err := NewStandardACPAdapter(StandardACPAdapterConfig{
|
|
Provider: "acp:model-meta",
|
|
Name: "model-meta-acp",
|
|
DisplayName: "Model ACP",
|
|
Command: []string{"model-acp", "stdio"},
|
|
SetModelReasoningEffortMeta: true,
|
|
}, transport, LegacyHostMetadata())
|
|
if err != nil {
|
|
t.Fatalf("NewStandardACPAdapter: %v", err)
|
|
}
|
|
adapter := adapterRaw.(*standardACPAdapter)
|
|
session := standardTestSession("acp:model-meta")
|
|
if _, err := adapter.Start(context.Background(), session); err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
|
|
session.Settings = &SessionSettings{Model: "reasoning-model", ReasoningEffort: "brief"}
|
|
brief := "brief"
|
|
if err := adapter.ValidateSessionSettings(session, SessionSettingsPatch{ReasoningEffort: &brief}); err != nil {
|
|
t.Fatalf("ValidateSessionSettings reasoning: %v", err)
|
|
}
|
|
if err := adapter.ApplySessionSettings(context.Background(), session, SessionSettingsPatch{ReasoningEffort: &brief}); err != nil {
|
|
t.Fatalf("ApplySessionSettings reasoning: %v", err)
|
|
}
|
|
calls := transport.conn.setModelCalls()
|
|
if len(calls) != 1 || calls[0]["modelId"] != "reasoning-model" {
|
|
t.Fatalf("set_model calls = %#v, want reasoning-model", calls)
|
|
}
|
|
if meta := payloadObject(calls[0]["_meta"]); meta["reasoningEffort"] != "brief" {
|
|
t.Fatalf("set_model _meta = %#v, want brief", meta)
|
|
}
|
|
|
|
plain := "plain-model"
|
|
session.Settings = &SessionSettings{Model: plain, ReasoningEffort: "brief"}
|
|
if err := adapter.ValidateSessionSettings(session, SessionSettingsPatch{Model: &plain}); err != nil {
|
|
t.Fatalf("ValidateSessionSettings plain model: %v", err)
|
|
}
|
|
if err := adapter.ApplySessionSettings(context.Background(), session, SessionSettingsPatch{Model: &plain}); err != nil {
|
|
t.Fatalf("ApplySessionSettings plain model: %v", err)
|
|
}
|
|
calls = transport.conn.setModelCalls()
|
|
if len(calls) != 2 || calls[1]["modelId"] != plain {
|
|
t.Fatalf("set_model calls = %#v, want plain-model second", calls)
|
|
}
|
|
if _, exists := calls[1]["_meta"]; exists {
|
|
t.Fatalf("plain model set_model = %#v, want no reasoning metadata", calls[1])
|
|
}
|
|
unsupported := "brief"
|
|
if err := adapter.ValidateSessionSettings(session, SessionSettingsPatch{ReasoningEffort: &unsupported}); err == nil {
|
|
t.Fatal("unsupported model reasoning validation error = nil")
|
|
}
|
|
if calls := transport.conn.setConfigOptionCalls(); len(calls) != 0 {
|
|
t.Fatalf("config option calls = %#v, want standard session/set_model only", calls)
|
|
}
|
|
}
|
|
|
|
func TestStandardACPStartupFailsWhenRequestedModelIsRejected(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
transport := newStandardACPTransport("Model ACP", "model-rejected-session")
|
|
transport.conn.models = map[string]any{
|
|
"currentModelId": "provider-default",
|
|
"availableModels": []any{
|
|
map[string]any{"modelId": "requested-model", "name": "Requested Model"},
|
|
},
|
|
}
|
|
transport.conn.setModelError = &acpError{
|
|
Code: -32603,
|
|
Message: "Internal error",
|
|
Data: json.RawMessage(`{"details":"requested model is unavailable"}`),
|
|
}
|
|
adapterRaw, err := NewStandardACPAdapter(StandardACPAdapterConfig{
|
|
Provider: "acp:model-rejected", Name: "model-rejected-acp", DisplayName: "Model ACP", Command: []string{"model-acp", "stdio"},
|
|
}, transport, LegacyHostMetadata())
|
|
if err != nil {
|
|
t.Fatalf("NewStandardACPAdapter: %v", err)
|
|
}
|
|
session := standardTestSession("acp:model-rejected")
|
|
session.Settings = &SessionSettings{Model: "requested-model"}
|
|
|
|
if _, err := adapterRaw.Start(context.Background(), session); err == nil {
|
|
t.Fatal("Start error = nil, want rejected requested model to abort startup")
|
|
} else if !strings.Contains(err.Error(), "model configuration failed") {
|
|
t.Fatalf("Start error = %v, want model configuration failure", err)
|
|
}
|
|
}
|
|
|
|
func TestStandardACPStartupDoesNotReapplyCurrentRequestedModel(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
transport := newStandardACPTransport("Model ACP", "model-current-session")
|
|
transport.conn.models = map[string]any{
|
|
"currentModelId": "requested-model",
|
|
"availableModels": []any{
|
|
map[string]any{"modelId": "requested-model", "name": "Requested Model"},
|
|
},
|
|
}
|
|
transport.conn.setModelError = &acpError{
|
|
Code: -32603, Message: "duplicate model application rejected",
|
|
}
|
|
adapterRaw, err := NewStandardACPAdapter(StandardACPAdapterConfig{
|
|
Provider: "acp:model-current", Name: "model-current-acp", DisplayName: "Model ACP", Command: []string{"model-acp", "stdio"},
|
|
}, transport, LegacyHostMetadata())
|
|
if err != nil {
|
|
t.Fatalf("NewStandardACPAdapter: %v", err)
|
|
}
|
|
session := standardTestSession("acp:model-current")
|
|
session.Settings = &SessionSettings{Model: "requested-model"}
|
|
|
|
if _, err := adapterRaw.Start(context.Background(), session); err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
if calls := transport.conn.setModelCalls(); len(calls) != 0 {
|
|
t.Fatalf("set_model calls = %#v, want current model left unchanged", calls)
|
|
}
|
|
}
|