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
293 lines
9.6 KiB
Go
293 lines
9.6 KiB
Go
package agentruntime
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
agentsessionstore "github.com/tutti-os/tutti/packages/agent/daemon/activity"
|
|
activityshared "github.com/tutti-os/tutti/packages/agent/daemon/activity/events"
|
|
)
|
|
|
|
func TestControllerStateMergesAdapterRuntimeContextWithoutDroppingLaunchContext(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
adapter := &statefulInteractiveAdapter{
|
|
provider: ProviderCodex,
|
|
snapshot: SessionStateSnapshot{
|
|
RuntimeContext: map[string]any{
|
|
"providerState": "ready",
|
|
"shared": "provider",
|
|
},
|
|
},
|
|
}
|
|
controller := NewController([]Adapter{adapter}, nil)
|
|
session := Session{
|
|
RoomID: "room-1",
|
|
AgentSessionID: "agent-session-1",
|
|
Provider: ProviderCodex,
|
|
CWD: "/Users/example/Documents/tutti/session-agent-session-1",
|
|
Title: "No project session",
|
|
Visible: true,
|
|
RuntimeContext: map[string]any{
|
|
"noProject": true,
|
|
"shared": "session",
|
|
},
|
|
}
|
|
controller.store(session)
|
|
|
|
snapshot, err := controller.State(session.RoomID, session.AgentSessionID)
|
|
if err != nil {
|
|
t.Fatalf("State() error = %v", err)
|
|
}
|
|
if snapshot.RuntimeContext["noProject"] != true {
|
|
t.Fatalf("runtime context = %#v, want noProject launch marker", snapshot.RuntimeContext)
|
|
}
|
|
if snapshot.RuntimeContext["providerState"] != "ready" {
|
|
t.Fatalf("runtime context = %#v, want provider state", snapshot.RuntimeContext)
|
|
}
|
|
if snapshot.RuntimeContext["shared"] != "provider" {
|
|
t.Fatalf("runtime context shared = %#v, want provider override", snapshot.RuntimeContext["shared"])
|
|
}
|
|
}
|
|
|
|
func TestEnrichReportWithSessionSnapshotPreservesNoProjectLaunchContext(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
adapter := &statefulInteractiveAdapter{
|
|
provider: ProviderCodex,
|
|
snapshot: SessionStateSnapshot{
|
|
RuntimeContext: map[string]any{"providerState": "ready"},
|
|
},
|
|
}
|
|
controller := NewController([]Adapter{adapter}, nil)
|
|
session := Session{
|
|
RoomID: "room-1",
|
|
AgentSessionID: "agent-session-1",
|
|
Provider: ProviderCodex,
|
|
CWD: "/Users/example/Documents/tutti/session-agent-session-1",
|
|
RuntimeContext: map[string]any{"noProject": true},
|
|
}
|
|
controller.store(session)
|
|
report := agentsessionstore.ReportActivityInput{WorkspaceID: session.RoomID}
|
|
|
|
controller.enrichReportWithSessionSnapshot(session, &report)
|
|
|
|
if len(report.StatePatches) != 1 {
|
|
t.Fatalf("state patch count = %d, want 1", len(report.StatePatches))
|
|
}
|
|
if report.StatePatches[0].RuntimeContext["noProject"] != true {
|
|
t.Fatalf("runtime context = %#v, want noProject launch marker", report.StatePatches[0].RuntimeContext)
|
|
}
|
|
}
|
|
|
|
func TestControllerStateRoundTripsSessionSettingsAndPermissionUpdate(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",
|
|
PlanMode: false,
|
|
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")
|
|
}
|
|
if started.Session.PermissionModeID != "full-access" {
|
|
t.Fatalf("session permission mode = %q, want %q", started.Session.PermissionModeID, "full-access")
|
|
}
|
|
if started.Session.Settings.Model != "gpt-5.2-codex" ||
|
|
started.Session.Settings.ReasoningEffort != "high" ||
|
|
started.Session.Settings.PlanMode ||
|
|
started.Session.Settings.PermissionModeID != "full-access" {
|
|
t.Fatalf("session settings = %#v", started.Session.Settings)
|
|
}
|
|
|
|
state, err := controller.State("room-1", "agent-session-1")
|
|
if err != nil {
|
|
t.Fatalf("State: %v", err)
|
|
}
|
|
if state.Settings == nil {
|
|
t.Fatal("state settings = nil, want round-tripped settings")
|
|
}
|
|
if state.Settings.Model != "gpt-5.2-codex" ||
|
|
state.Settings.ReasoningEffort != "high" ||
|
|
state.Settings.PlanMode ||
|
|
state.Settings.PermissionModeID != "full-access" {
|
|
t.Fatalf("state settings = %#v", state.Settings)
|
|
}
|
|
|
|
updated, err := controller.UpdateSettings(context.Background(), UpdateSettingsInput{
|
|
RoomID: "room-1",
|
|
AgentSessionID: "agent-session-1",
|
|
Settings: SessionSettingsPatch{
|
|
PermissionModeID: stringPtr("auto"),
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("UpdateSettings: %v", err)
|
|
}
|
|
if updated.Settings.PermissionModeID != "auto" {
|
|
t.Fatalf("updated settings permission mode = %q, want %q", updated.Settings.PermissionModeID, "auto")
|
|
}
|
|
if updated.Settings.Model != "gpt-5.2-codex" ||
|
|
updated.Settings.ReasoningEffort != "high" ||
|
|
updated.Settings.PlanMode {
|
|
t.Fatalf("updated settings = %#v, want launch facts preserved", updated.Settings)
|
|
}
|
|
|
|
session, ok := controller.Session("room-1", "agent-session-1")
|
|
if !ok {
|
|
t.Fatal("Session returned ok=false after update")
|
|
}
|
|
if session.PermissionModeID != "auto" {
|
|
t.Fatalf("session permission mode after update = %q, want %q", session.PermissionModeID, "auto")
|
|
}
|
|
if session.Settings == nil || session.Settings.PermissionModeID != "auto" {
|
|
t.Fatalf("session settings after update = %#v", session.Settings)
|
|
}
|
|
if session.Settings.Model != "gpt-5.2-codex" ||
|
|
session.Settings.ReasoningEffort != "high" ||
|
|
session.Settings.PlanMode {
|
|
t.Fatalf("session settings after update = %#v, want launch facts preserved", session.Settings)
|
|
}
|
|
|
|
state, err = controller.State("room-1", "agent-session-1")
|
|
if err != nil {
|
|
t.Fatalf("State after update: %v", err)
|
|
}
|
|
if state.PermissionModeID != "auto" {
|
|
t.Fatalf("state permission mode after update = %q, want %q", state.PermissionModeID, "auto")
|
|
}
|
|
if state.Settings == nil || state.Settings.PermissionModeID != "auto" {
|
|
t.Fatalf("state settings after update = %#v", state.Settings)
|
|
}
|
|
if state.Settings.Model != "gpt-5.2-codex" ||
|
|
state.Settings.ReasoningEffort != "high" ||
|
|
state.Settings.PlanMode {
|
|
t.Fatalf("state settings after update = %#v, want launch facts preserved", state.Settings)
|
|
}
|
|
}
|
|
|
|
func TestControllerStateUsesAdapterSnapshot(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
adapter := &statefulInteractiveAdapter{
|
|
snapshot: SessionStateSnapshot{
|
|
AuthState: "auth_required",
|
|
RuntimeContext: map[string]any{
|
|
"mode": "plan",
|
|
},
|
|
PendingInteractive: &SessionInteractivePrompt{
|
|
Kind: "ask-user",
|
|
RequestID: "request-1",
|
|
},
|
|
},
|
|
}
|
|
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)
|
|
}
|
|
|
|
snapshot, err := controller.State("room-1", started.Session.AgentSessionID)
|
|
if err != nil {
|
|
t.Fatalf("State: %v", err)
|
|
}
|
|
if snapshot.AuthState != "auth_required" {
|
|
t.Fatalf("auth state = %q, want auth_required", snapshot.AuthState)
|
|
}
|
|
if snapshot.PendingInteractive == nil || snapshot.PendingInteractive.RequestID != "request-1" {
|
|
t.Fatalf("pending interactive = %#v, want request-1", snapshot.PendingInteractive)
|
|
}
|
|
if got := asString(snapshot.RuntimeContext["mode"]); got != "plan" {
|
|
t.Fatalf("runtime context mode = %q, want plan", got)
|
|
}
|
|
}
|
|
|
|
func TestShouldAdvanceSessionUpdatedAtFromEvents(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
session := Session{AgentSessionID: "agent-session-1", Provider: ProviderCodex}
|
|
tests := []struct {
|
|
name string
|
|
events []activityshared.Event
|
|
want bool
|
|
}{
|
|
{
|
|
name: "turn started advances recency",
|
|
events: []activityshared.Event{newTurnActivityEvent(session, EventTurnStarted, "turn-1", SessionStatusWorking, "", "", nil)},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "turn completed advances recency",
|
|
events: []activityshared.Event{newTurnActivityEvent(session, EventTurnCompleted, "turn-1", SessionStatusReady, "", "", nil)},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "turn canceled advances recency",
|
|
events: []activityshared.Event{newTurnActivityEvent(session, EventTurnCanceled, "turn-1", SessionStatusCanceled, "", "", nil)},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "turn failed advances recency",
|
|
events: []activityshared.Event{newTurnActivityEvent(session, EventTurnFailed, "turn-1", SessionStatusFailed, "", "", nil)},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "turn updated does not advance recency",
|
|
events: []activityshared.Event{newTurnActivityEvent(session, EventTurnUpdated, "turn-1", SessionStatusWaiting, "", "", nil)},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "turn updated waiting advances recency",
|
|
events: []activityshared.Event{newTurnActivityEvent(session, EventTurnUpdated, "turn-1", SessionStatusWaiting, "", "", map[string]any{"phase": string(activityshared.TurnPhaseWaitingApproval)})},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "session update does not advance recency",
|
|
events: []activityshared.Event{newSessionTitleActivityEvent(session, "Provider title")},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "message does not advance recency by itself",
|
|
events: []activityshared.Event{newTurnActivityEvent(session, EventMessage, "turn-1", "", RoleAssistant, "hello", nil)},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "tool call does not advance recency by itself",
|
|
events: []activityshared.Event{newTurnActivityEvent(session, EventCallStarted, "turn-1", messageStreamStateStreaming, "", "Read files", nil)},
|
|
want: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
if got := shouldAdvanceSessionUpdatedAtFromEvents(tt.events); got != tt.want {
|
|
t.Fatalf("shouldAdvanceSessionUpdatedAtFromEvents() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|