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
560 lines
20 KiB
Go
560 lines
20 KiB
Go
package agentruntime
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
agentsessionstore "github.com/tutti-os/tutti/packages/agent/daemon/activity"
|
|
)
|
|
|
|
func TestControllerRoutesChildInteractionThroughRootLiveSession(t *testing.T) {
|
|
t.Parallel()
|
|
adapter := &statefulInteractiveAdapter{}
|
|
adapter.submitHook = func(session Session) {
|
|
if session.AgentSessionID != "root-session" {
|
|
t.Fatalf("adapter session = %q, want root live session", session.AgentSessionID)
|
|
}
|
|
}
|
|
controller := NewController([]Adapter{adapter}, nil)
|
|
if _, err := controller.Start(context.Background(), StartInput{
|
|
RoomID: "room-1", AgentSessionID: "root-session", Provider: ProviderCodex, Title: "Codex",
|
|
}); err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
if _, err := controller.SubmitInteractive(context.Background(), SubmitInteractiveInput{
|
|
RoomID: "room-1",
|
|
RootAgentSessionID: "root-session",
|
|
AgentSessionID: "child-session",
|
|
TurnID: "child-turn",
|
|
RequestID: "child-request",
|
|
OptionID: "allow",
|
|
}); err != nil {
|
|
t.Fatalf("SubmitInteractive: %v", err)
|
|
}
|
|
if adapter.interactiveInput.AgentSessionID != "child-session" ||
|
|
adapter.interactiveInput.TurnID != "child-turn" ||
|
|
adapter.interactiveInput.RequestID != "child-request" {
|
|
t.Fatalf("adapter target = %#v", adapter.interactiveInput)
|
|
}
|
|
}
|
|
|
|
func TestControllerSubmitInteractiveDelegatesToAdapter(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",
|
|
PermissionModeID: "auto",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
|
|
result, err := controller.SubmitInteractive(context.Background(), SubmitInteractiveInput{
|
|
RoomID: "room-1",
|
|
RootAgentSessionID: started.Session.AgentSessionID,
|
|
AgentSessionID: started.Session.AgentSessionID,
|
|
RequestID: "request-1",
|
|
Action: "submit",
|
|
OptionID: "option-1",
|
|
Payload: map[string]any{
|
|
"answer": "Use the task renderer",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("SubmitInteractive: %v", err)
|
|
}
|
|
if !result.Accepted {
|
|
t.Fatalf("submit result = %#v, want accepted", result)
|
|
}
|
|
if adapter.interactiveInput.RequestID != "request-1" || adapter.interactiveInput.OptionID != "option-1" {
|
|
t.Fatalf("interactive input = %#v", adapter.interactiveInput)
|
|
}
|
|
}
|
|
|
|
func TestControllerSubmitInteractiveSyncsClaudeCodePermissionModeSelection(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
initial string
|
|
optionID string
|
|
payload map[string]any
|
|
resolved string
|
|
wantMode string
|
|
}{
|
|
{name: "accept edits", initial: "default", optionID: "acceptEdits", wantMode: "acceptEdits"},
|
|
{name: "bypass permissions", initial: "default", optionID: "bypassPermissions", wantMode: "bypassPermissions"},
|
|
{name: "default", initial: "acceptEdits", optionID: "default", wantMode: "default"},
|
|
{name: "legacy auto", initial: "default", optionID: "auto", wantMode: "acceptEdits"},
|
|
{name: "dont ask from payload", initial: "default", payload: map[string]any{"optionId": "dontAsk"}, resolved: "dontAsk", wantMode: "dontAsk"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
adapter := &statefulInteractiveAdapter{provider: ProviderClaudeCode, interactiveOptionID: tt.resolved}
|
|
controller := NewController([]Adapter{adapter}, nil)
|
|
started, err := controller.Start(context.Background(), StartInput{
|
|
RoomID: "room-1",
|
|
AgentSessionID: "agent-session-" + strings.ReplaceAll(tt.name, " ", "-"),
|
|
Provider: ProviderClaudeCode,
|
|
CWD: "/workspace",
|
|
Title: "Claude Code",
|
|
PermissionModeID: tt.initial,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
|
|
events, unsubscribe, ok := controller.Subscribe("room-1", started.Session.AgentSessionID)
|
|
if !ok {
|
|
t.Fatal("Subscribe returned ok=false")
|
|
}
|
|
defer unsubscribe()
|
|
_ = waitForStreamEventType(t, events, StreamEventStatePatch)
|
|
|
|
if _, err := controller.SubmitInteractive(context.Background(), SubmitInteractiveInput{
|
|
RoomID: "room-1",
|
|
RootAgentSessionID: started.Session.AgentSessionID,
|
|
AgentSessionID: started.Session.AgentSessionID,
|
|
RequestID: "permission-1",
|
|
OptionID: tt.optionID,
|
|
Payload: tt.payload,
|
|
}); err != nil {
|
|
t.Fatalf("SubmitInteractive: %v", err)
|
|
}
|
|
|
|
session, ok := controller.Session("room-1", started.Session.AgentSessionID)
|
|
if !ok {
|
|
t.Fatal("Session returned ok=false")
|
|
}
|
|
if session.PermissionModeID != tt.wantMode {
|
|
t.Fatalf("session permission mode = %q, want %q", session.PermissionModeID, tt.wantMode)
|
|
}
|
|
if session.Settings == nil || session.Settings.PermissionModeID != tt.wantMode {
|
|
t.Fatalf("session settings = %#v, want permission mode %q", session.Settings, tt.wantMode)
|
|
}
|
|
|
|
event := waitForStatePatchPermissionMode(t, events, tt.wantMode)
|
|
patch, ok := event.Data.(agentsessionstore.WorkspaceAgentStatePatch)
|
|
if !ok {
|
|
t.Fatalf("event data = %#v, want state patch", event.Data)
|
|
}
|
|
if patch.Settings["permissionModeId"] != tt.wantMode {
|
|
t.Fatalf("patch settings = %#v, want permission mode %q", patch.Settings, tt.wantMode)
|
|
}
|
|
if patch.Settings["planMode"] != false {
|
|
t.Fatalf("patch settings planMode = %#v, want false", patch.Settings["planMode"])
|
|
}
|
|
if patch.RuntimeContext != nil {
|
|
t.Fatalf("patch runtime context snapshot = %#v, want nil", patch.RuntimeContext)
|
|
}
|
|
if patch.RuntimeContextPatch == nil || patch.RuntimeContextPatch.Set["permissionModeId"] != tt.wantMode {
|
|
t.Fatalf("patch runtime context update = %#v, want permission mode %q", patch.RuntimeContextPatch, tt.wantMode)
|
|
}
|
|
if patch.RuntimeContextPatch.Set["planMode"] != false {
|
|
t.Fatalf("patch runtime context planMode = %#v, want false", patch.RuntimeContextPatch.Set["planMode"])
|
|
}
|
|
if patch.LifecycleStatus != "" || patch.CurrentPhase != "" {
|
|
t.Fatalf("patch status fields = %q/%q, want empty permission-only patch", patch.LifecycleStatus, patch.CurrentPhase)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClaudeCodeModeFromID(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
modeID string
|
|
wantPlan bool
|
|
wantPermission string
|
|
wantOK bool
|
|
}{
|
|
{modeID: "plan", wantPlan: true, wantPermission: "", wantOK: true},
|
|
{modeID: "default", wantPlan: false, wantPermission: "default", wantOK: true},
|
|
{modeID: "acceptEdits", wantPlan: false, wantPermission: "acceptEdits", wantOK: true},
|
|
{modeID: "bypassPermissions", wantPlan: false, wantPermission: "bypassPermissions", wantOK: true},
|
|
{modeID: "auto", wantPlan: false, wantPermission: "acceptEdits", wantOK: true},
|
|
{modeID: "dontAsk", wantPlan: false, wantPermission: "dontAsk", wantOK: true},
|
|
{modeID: "allow_once", wantOK: false},
|
|
{modeID: "reject", wantOK: false},
|
|
{modeID: "", wantOK: false},
|
|
}
|
|
for _, tt := range tests {
|
|
plan, permission, ok := claudeCodeModeFromID(tt.modeID)
|
|
if ok != tt.wantOK || plan != tt.wantPlan || permission != tt.wantPermission {
|
|
t.Fatalf("claudeCodeModeFromID(%q) = (%v, %q, %v), want (%v, %q, %v)",
|
|
tt.modeID, plan, permission, ok, tt.wantPlan, tt.wantPermission, tt.wantOK)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestControllerSubmitInteractiveExitsPlanModeOnPermissionSelection(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
adapter := &statefulInteractiveAdapter{provider: ProviderClaudeCode}
|
|
controller := NewController([]Adapter{adapter}, nil)
|
|
started, err := controller.Start(context.Background(), StartInput{
|
|
RoomID: "room-1",
|
|
AgentSessionID: "agent-session-exit-plan",
|
|
Provider: ProviderClaudeCode,
|
|
CWD: "/workspace",
|
|
Title: "Claude Code",
|
|
PermissionModeID: "default",
|
|
Settings: &SessionSettings{PlanMode: true, PermissionModeID: "default"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
|
|
events, unsubscribe, ok := controller.Subscribe("room-1", started.Session.AgentSessionID)
|
|
if !ok {
|
|
t.Fatal("Subscribe returned ok=false")
|
|
}
|
|
defer unsubscribe()
|
|
_ = waitForStreamEventType(t, events, StreamEventStatePatch)
|
|
|
|
if _, err := controller.SubmitInteractive(context.Background(), SubmitInteractiveInput{
|
|
RoomID: "room-1",
|
|
RootAgentSessionID: started.Session.AgentSessionID,
|
|
AgentSessionID: started.Session.AgentSessionID,
|
|
RequestID: "permission-1",
|
|
OptionID: "auto",
|
|
}); err != nil {
|
|
t.Fatalf("SubmitInteractive: %v", err)
|
|
}
|
|
|
|
session, ok := controller.Session("room-1", started.Session.AgentSessionID)
|
|
if !ok {
|
|
t.Fatal("Session returned ok=false")
|
|
}
|
|
if session.PermissionModeID != "acceptEdits" {
|
|
t.Fatalf("session permission mode = %q, want acceptEdits", session.PermissionModeID)
|
|
}
|
|
if session.Settings == nil || session.Settings.PlanMode {
|
|
t.Fatalf("session settings = %#v, want plan mode cleared", session.Settings)
|
|
}
|
|
|
|
event := waitForStatePatchPermissionMode(t, events, "acceptEdits")
|
|
patch, ok := event.Data.(agentsessionstore.WorkspaceAgentStatePatch)
|
|
if !ok {
|
|
t.Fatalf("event data = %#v, want state patch", event.Data)
|
|
}
|
|
if patch.Settings["planMode"] != false {
|
|
t.Fatalf("patch settings planMode = %#v, want false", patch.Settings["planMode"])
|
|
}
|
|
}
|
|
|
|
func TestControllerSubmitInteractiveModeSurvivesActiveTurnStaleSession(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
adapter := newBlockingExecAdapter()
|
|
adapter.provider = ProviderClaudeCode
|
|
adapter.interactiveOptionID = "bypassPermissions"
|
|
controller := NewController([]Adapter{adapter}, nil)
|
|
started, err := controller.Start(context.Background(), StartInput{
|
|
RoomID: "room-1",
|
|
AgentSessionID: "agent-session-exit-plan-stale-turn",
|
|
Provider: ProviderClaudeCode,
|
|
CWD: "/workspace",
|
|
Title: "Claude Code",
|
|
PermissionModeID: "default",
|
|
Settings: &SessionSettings{PlanMode: true, PermissionModeID: "default"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
|
|
if _, err := controller.Exec(context.Background(), ExecInput{
|
|
RoomID: "room-1",
|
|
AgentSessionID: started.Session.AgentSessionID,
|
|
Content: []PromptContentBlock{{Type: "text", Text: "implement"}},
|
|
}); err != nil {
|
|
t.Fatalf("Exec: %v", err)
|
|
}
|
|
adapter.waitForPrompt(t, "implement")
|
|
|
|
if _, err := controller.SubmitInteractive(context.Background(), SubmitInteractiveInput{
|
|
RoomID: "room-1",
|
|
RootAgentSessionID: started.Session.AgentSessionID,
|
|
AgentSessionID: started.Session.AgentSessionID,
|
|
RequestID: "permission-1",
|
|
OptionID: "bypassPermissions",
|
|
}); err != nil {
|
|
t.Fatalf("SubmitInteractive: %v", err)
|
|
}
|
|
session, ok := controller.Session("room-1", started.Session.AgentSessionID)
|
|
if !ok {
|
|
t.Fatal("Session returned ok=false")
|
|
}
|
|
if session.Settings == nil || session.Settings.PlanMode || session.PermissionModeID != "bypassPermissions" {
|
|
t.Fatalf("session after exit = %#v, want planMode=false and bypassPermissions", session)
|
|
}
|
|
|
|
adapter.releaseNext()
|
|
session = waitForSessionStatus(t, controller, "room-1", started.Session.AgentSessionID, SessionStatusReady)
|
|
if session.Settings == nil || session.Settings.PlanMode || session.PermissionModeID != "bypassPermissions" {
|
|
t.Fatalf("session after stale turn completion = %#v, want planMode=false and bypassPermissions", session)
|
|
}
|
|
if got, _ := session.RuntimeContext["planMode"].(bool); got {
|
|
t.Fatalf("runtime context planMode = %#v, want false", session.RuntimeContext["planMode"])
|
|
}
|
|
if got, _ := session.RuntimeContext["permissionModeId"].(string); got != "bypassPermissions" {
|
|
t.Fatalf("runtime context permissionModeId = %#v, want bypassPermissions", session.RuntimeContext["permissionModeId"])
|
|
}
|
|
}
|
|
|
|
func TestControllerSubmitInteractiveKeepPlanningStaysInPlanMode(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
adapter := &statefulInteractiveAdapter{provider: ProviderClaudeCode}
|
|
controller := NewController([]Adapter{adapter}, nil)
|
|
started, err := controller.Start(context.Background(), StartInput{
|
|
RoomID: "room-1",
|
|
AgentSessionID: "agent-session-keep-planning",
|
|
Provider: ProviderClaudeCode,
|
|
CWD: "/workspace",
|
|
Title: "Claude Code",
|
|
PermissionModeID: "default",
|
|
Settings: &SessionSettings{PlanMode: true, PermissionModeID: "default"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
|
|
events, unsubscribe, ok := controller.Subscribe("room-1", started.Session.AgentSessionID)
|
|
if !ok {
|
|
t.Fatal("Subscribe returned ok=false")
|
|
}
|
|
defer unsubscribe()
|
|
_ = waitForStreamEventType(t, events, StreamEventStatePatch)
|
|
|
|
if _, err := controller.SubmitInteractive(context.Background(), SubmitInteractiveInput{
|
|
RoomID: "room-1",
|
|
RootAgentSessionID: started.Session.AgentSessionID,
|
|
AgentSessionID: started.Session.AgentSessionID,
|
|
RequestID: "permission-1",
|
|
OptionID: "plan",
|
|
}); err != nil {
|
|
t.Fatalf("SubmitInteractive: %v", err)
|
|
}
|
|
|
|
session, ok := controller.Session("room-1", started.Session.AgentSessionID)
|
|
if !ok {
|
|
t.Fatal("Session returned ok=false")
|
|
}
|
|
if session.Settings == nil || !session.Settings.PlanMode {
|
|
t.Fatalf("session settings = %#v, want plan mode preserved", session.Settings)
|
|
}
|
|
if session.PermissionModeID != "default" {
|
|
t.Fatalf("session permission mode = %q, want unchanged default", session.PermissionModeID)
|
|
}
|
|
// Keeping planning is a no-op for the mode, so no state patch is published.
|
|
expectNoStreamEventType(t, events, StreamEventStatePatch)
|
|
}
|
|
|
|
func TestControllerSubmitInteractiveDoesNotSyncUnsupportedPermissionSelections(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
provider string
|
|
optionID string
|
|
resolved string
|
|
}{
|
|
{name: "ordinary allow", provider: ProviderClaudeCode, optionID: "allow_once"},
|
|
{name: "reject", provider: ProviderClaudeCode, optionID: "reject"},
|
|
{name: "raw permission alias resolves to ordinary allow", provider: ProviderClaudeCode, optionID: "acceptEdits", resolved: "allow_once"},
|
|
{name: "non claude", provider: ProviderCodex, optionID: "acceptEdits"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
adapter := &statefulInteractiveAdapter{provider: tt.provider, interactiveOptionID: tt.resolved}
|
|
controller := NewController([]Adapter{adapter}, nil)
|
|
initialMode := defaultPermissionModeIDForProvider(tt.provider)
|
|
started, err := controller.Start(context.Background(), StartInput{
|
|
RoomID: "room-1",
|
|
AgentSessionID: "agent-session-" + strings.ReplaceAll(tt.name, " ", "-"),
|
|
Provider: tt.provider,
|
|
CWD: "/workspace",
|
|
Title: "Agent",
|
|
PermissionModeID: initialMode,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
|
|
events, unsubscribe, ok := controller.Subscribe("room-1", started.Session.AgentSessionID)
|
|
if !ok {
|
|
t.Fatal("Subscribe returned ok=false")
|
|
}
|
|
defer unsubscribe()
|
|
_ = waitForStreamEventType(t, events, StreamEventStatePatch)
|
|
|
|
if _, err := controller.SubmitInteractive(context.Background(), SubmitInteractiveInput{
|
|
RoomID: "room-1",
|
|
RootAgentSessionID: started.Session.AgentSessionID,
|
|
AgentSessionID: started.Session.AgentSessionID,
|
|
RequestID: "permission-1",
|
|
OptionID: tt.optionID,
|
|
}); err != nil {
|
|
t.Fatalf("SubmitInteractive: %v", err)
|
|
}
|
|
|
|
session, ok := controller.Session("room-1", started.Session.AgentSessionID)
|
|
if !ok {
|
|
t.Fatal("Session returned ok=false")
|
|
}
|
|
if session.PermissionModeID != initialMode {
|
|
t.Fatalf("session permission mode = %q, want unchanged %q", session.PermissionModeID, initialMode)
|
|
}
|
|
expectNoStreamEventType(t, events, StreamEventStatePatch)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestControllerKeepsTerminalInteractiveDispositionAfterSessionRemoval(t *testing.T) {
|
|
controller := NewController(nil, nil)
|
|
controller.recordTerminalInteractiveDisposition("session-1", "turn-1", "request-1", InteractiveDispositionAnswered)
|
|
|
|
if got := controller.InteractiveDisposition("room-1", "session-1", "session-1", "turn-1", "request-1"); got != InteractiveDispositionAnswered {
|
|
t.Fatalf("disposition without live session = %q, want answered", got)
|
|
}
|
|
if got := controller.InteractiveDisposition("room-1", "session-1", "session-1", "turn-2", "request-1"); got != InteractiveDispositionUnknown {
|
|
t.Fatalf("different-turn disposition = %q, want unknown", got)
|
|
}
|
|
}
|
|
|
|
func TestControllerSubmitInteractivePermissionSyncPreservesCurrentSessionState(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
controller := NewController(nil, nil)
|
|
adapter := &statefulInteractiveAdapter{
|
|
provider: ProviderClaudeCode,
|
|
interactiveOptionID: "acceptEdits",
|
|
}
|
|
adapter.submitHook = func(session Session) {
|
|
current, ok := controller.Session(session.RoomID, session.AgentSessionID)
|
|
if !ok {
|
|
return
|
|
}
|
|
current.Status = SessionStatusCompleted
|
|
current.Title = "Finished title"
|
|
current.ProviderSessionID = "latest-provider-session"
|
|
controller.store(current)
|
|
}
|
|
controller = NewController([]Adapter{adapter}, nil)
|
|
started, err := controller.Start(context.Background(), StartInput{
|
|
RoomID: "room-1",
|
|
AgentSessionID: "agent-session-1",
|
|
Provider: ProviderClaudeCode,
|
|
CWD: "/workspace",
|
|
Title: "Claude Code",
|
|
PermissionModeID: "default",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
|
|
if _, err := controller.SubmitInteractive(context.Background(), SubmitInteractiveInput{
|
|
RoomID: "room-1",
|
|
RootAgentSessionID: started.Session.AgentSessionID,
|
|
AgentSessionID: started.Session.AgentSessionID,
|
|
RequestID: "permission-1",
|
|
OptionID: "acceptEdits",
|
|
}); err != nil {
|
|
t.Fatalf("SubmitInteractive: %v", err)
|
|
}
|
|
|
|
session, ok := controller.Session("room-1", started.Session.AgentSessionID)
|
|
if !ok {
|
|
t.Fatal("Session returned ok=false")
|
|
}
|
|
if session.PermissionModeID != "acceptEdits" {
|
|
t.Fatalf("session permission mode = %q, want acceptEdits", session.PermissionModeID)
|
|
}
|
|
if session.Status != SessionStatusCompleted || session.Title != "Finished title" || session.ProviderSessionID != "latest-provider-session" {
|
|
t.Fatalf("session after sync = %#v, want latest non-permission fields preserved", session)
|
|
}
|
|
}
|
|
|
|
func TestControllerSubmitInteractiveStartsDenyFollowUpAfterActiveTurn(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
adapter := newBlockingExecAdapter()
|
|
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",
|
|
PermissionModeID: "auto",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
|
|
if _, err := controller.Exec(context.Background(), ExecInput{
|
|
RoomID: "room-1",
|
|
AgentSessionID: started.Session.AgentSessionID,
|
|
Content: textPrompt("run tests"),
|
|
}); err != nil {
|
|
t.Fatalf("Exec: %v", err)
|
|
}
|
|
adapter.waitForPrompt(t, "run tests")
|
|
|
|
result, err := controller.SubmitInteractive(context.Background(), SubmitInteractiveInput{
|
|
RoomID: "room-1",
|
|
RootAgentSessionID: started.Session.AgentSessionID,
|
|
AgentSessionID: started.Session.AgentSessionID,
|
|
RequestID: "permission-1",
|
|
Action: "deny",
|
|
OptionID: "abort",
|
|
Payload: map[string]any{
|
|
"denyMessage": "Please split the work into smaller steps.",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("SubmitInteractive: %v", err)
|
|
}
|
|
if !result.Accepted {
|
|
t.Fatalf("submit result = %#v, want accepted", result)
|
|
}
|
|
|
|
if result.FollowUpPrompt != "Please split the work into smaller steps." {
|
|
t.Fatalf("follow-up prompt = %q, want Host-owned follow-up intent", result.FollowUpPrompt)
|
|
}
|
|
adapter.releaseNext()
|
|
waitForSessionStatus(t, controller, "room-1", started.Session.AgentSessionID, SessionStatusReady)
|
|
if got := adapter.prompts(); len(got) != 1 || got[0] != "run tests" {
|
|
t.Fatalf("adapter prompts = %#v, want only the original prompt", got)
|
|
}
|
|
}
|
|
|
|
func TestInteractiveDenyFollowUpSkipsClaudeSDKAdapter(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if adapterShouldReceiveInteractiveDenyFollowUp(NewClaudeCodeSDKAdapter(nil)) {
|
|
t.Fatal("Claude SDK adapter should consume deny feedback without daemon follow-up")
|
|
}
|
|
if adapterShouldReceiveInteractiveDenyFollowUp(NewCodexAppServerAdapter(nil)) {
|
|
t.Fatal("Codex app-server adapter should steer deny feedback without daemon follow-up")
|
|
}
|
|
if !adapterShouldReceiveInteractiveDenyFollowUp(newBlockingExecAdapter()) {
|
|
t.Fatal("ACP-style adapter should keep daemon deny follow-up")
|
|
}
|
|
}
|