Files
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

248 lines
9.0 KiB
Go

package agentruntime
import (
"context"
"errors"
"strings"
"testing"
"time"
activityshared "github.com/tutti-os/tutti/packages/agent/daemon/activity/events"
)
func TestGoalRecoveryCapabilitiesComeFromAdapterPolicy(t *testing.T) {
codex := (&CodexAppServerAdapter{}).GoalCapabilities()
if !codex.QuerySupported || !codex.ReplaySetAfterRestart {
t.Fatalf("codex capabilities=%#v", codex)
}
claude := (&ClaudeCodeSDKAdapter{}).GoalCapabilities()
if claude.QuerySupported || claude.ReplaySetAfterRestart {
t.Fatalf("claude capabilities=%#v", claude)
}
// A provider name is not a capability. Even an adapter registered under
// the codex string is rejected if it does not implement GoalAdapter.
adapter := &recordingStartAdapter{provider: ProviderCodex}
controller := NewController([]Adapter{adapter}, nil)
started, err := controller.Start(context.Background(), StartInput{RoomID: "room-policy", Provider: ProviderCodex})
if err != nil {
t.Fatal(err)
}
if _, err := controller.GoalCapabilities(context.Background(), GoalReconcileInput{RoomID: "room-policy", AgentSessionID: started.Session.AgentSessionID}); err == nil {
t.Fatal("provider-name-only adapter unexpectedly acquired goal recovery capabilities")
}
}
func TestBackgroundGoalControlsRequireExistingLiveProvider(t *testing.T) {
adapter := &requireLiveGoalAdapter{recordingStartAdapter: recordingStartAdapter{provider: "require-live-goal"}}
controller := NewController([]Adapter{adapter}, nil)
session, err := controller.Resume(context.Background(), ResumeInput{
RoomID: "room-require-live", AgentSessionID: "session-require-live", Provider: adapter.Provider(),
ProviderSessionID: "provider-session-require-live",
})
if err != nil {
t.Fatal(err)
}
adapter.live = false
adapter.resumeCalls = 0
adapter.calls = nil
if _, err := controller.GoalControl(context.Background(), GoalControlInput{
RoomID: session.RoomID, AgentSessionID: session.AgentSessionID,
Action: GoalControlClear, RequireLive: true,
}); !errors.Is(err, ErrSessionDisconnected) {
t.Fatalf("RequireLive GoalControl error=%v", err)
}
if err := controller.FenceGoalGeneration(context.Background(), GoalGenerationFenceRequest{
RoomID: session.RoomID, AgentSessionID: session.AgentSessionID,
OperationID: "goal-op", Revision: 1, RequireLive: true,
}); !errors.Is(err, ErrSessionDisconnected) {
t.Fatalf("RequireLive FenceGoalGeneration error=%v", err)
}
if adapter.resumeCalls != 0 || adapter.goalCalls != 0 || adapter.fenceCalls != 0 {
t.Fatalf("background controls resumed or reached provider: resume=%d goal=%d fence=%d",
adapter.resumeCalls, adapter.goalCalls, adapter.fenceCalls)
}
if _, err := controller.GoalControl(context.Background(), GoalControlInput{
RoomID: session.RoomID, AgentSessionID: session.AgentSessionID,
Action: GoalControlClear,
}); err != nil {
t.Fatal(err)
}
if adapter.resumeCalls != 1 || adapter.goalCalls != 1 || adapter.fenceCalls != 1 {
t.Fatalf("user control resume=%d goal=%d fence=%d", adapter.resumeCalls, adapter.goalCalls, adapter.fenceCalls)
}
if got, want := strings.Join(adapter.calls, ","), "resume,fence,goal"; got != want {
t.Fatalf("reconnect call order=%q want=%q", got, want)
}
// Releasing or losing the provider connection discards adapter-local
// fences. The Controller registry must reinstall them on the replacement
// connection before any user operation reaches the provider.
adapter.live = false
adapter.calls = nil
if _, err := controller.GoalControl(context.Background(), GoalControlInput{
RoomID: session.RoomID, AgentSessionID: session.AgentSessionID,
Action: GoalControlClear,
}); err != nil {
t.Fatal(err)
}
if got, want := strings.Join(adapter.calls, ","), "resume,fence,goal"; got != want {
t.Fatalf("replacement connection call order=%q want=%q", got, want)
}
adapter.live = false
adapter.calls = nil
adapter.fenceErr = errors.New("fence install failed")
if _, err := controller.GoalControl(context.Background(), GoalControlInput{
RoomID: session.RoomID, AgentSessionID: session.AgentSessionID,
Action: GoalControlClear,
}); err == nil {
t.Fatal("replacement connection unexpectedly survived fence installation failure")
}
if got, want := strings.Join(adapter.calls, ","), "resume,fence,close"; got != want {
t.Fatalf("failed replacement call order=%q want=%q", got, want)
}
if adapter.live {
t.Fatal("failed replacement remained live without its admission fences")
}
}
func TestResumeInstallsPreloadedGoalFencesBeforePublishingSession(t *testing.T) {
adapter := &requireLiveGoalAdapter{recordingStartAdapter: recordingStartAdapter{provider: "resume-goal-fences"}}
controller := NewController([]Adapter{adapter}, nil)
adapter.onFence = func(session Session) {
if published, found := controller.Session(session.RoomID, session.AgentSessionID); found {
t.Fatalf("session published before durable fences were installed: %#v", published)
}
}
inputFence := GoalGenerationFenceInput{
OperationID: "old-goal", Revision: 4, RepairEpoch: 2, Reason: "binding_revoked",
}
session, err := controller.Resume(context.Background(), ResumeInput{
RoomID: "room-resume-fences", AgentSessionID: "session-resume-fences", Provider: adapter.Provider(),
ProviderSessionID: "provider-session-resume-fences",
GoalGenerationFences: []GoalGenerationFenceInput{inputFence},
})
if err != nil {
t.Fatal(err)
}
if got, want := strings.Join(adapter.calls, ","), "resume,fence"; got != want {
t.Fatalf("resume fence order=%q want=%q", got, want)
}
if adapter.lastFence != inputFence {
t.Fatalf("installed fence=%#v want=%#v", adapter.lastFence, inputFence)
}
if published, found := controller.Session(session.RoomID, session.AgentSessionID); !found || published.AgentSessionID != session.AgentSessionID {
t.Fatalf("resumed session was not published after fence installation: %#v found=%v", published, found)
}
}
type requireLiveGoalAdapter struct {
recordingStartAdapter
live bool
resumeCalls int
goalCalls int
fenceCalls int
closeCalls int
calls []string
fenceErr error
lastFence GoalGenerationFenceInput
onFence func(Session)
}
func (a *requireLiveGoalAdapter) Start(ctx context.Context, session Session) ([]activityshared.Event, error) {
events, err := a.recordingStartAdapter.Start(ctx, session)
if err == nil {
a.live = true
}
return events, err
}
func (a *requireLiveGoalAdapter) Resume(context.Context, Session) error {
a.resumeCalls++
a.live = true
a.calls = append(a.calls, "resume")
return nil
}
func (a *requireLiveGoalAdapter) HasLiveSession(Session) bool {
return a.live
}
func (*requireLiveGoalAdapter) GoalCapabilities() GoalAdapterCapabilities {
return GoalAdapterCapabilities{}
}
func (a *requireLiveGoalAdapter) ApplyGoal(_ context.Context, _ Session, input GoalApplyInput) (GoalAdapterResult, error) {
a.goalCalls++
a.calls = append(a.calls, "goal")
return GoalAdapterResult{Observation: map[string]any{"action": string(input.Action)}}, nil
}
func (*requireLiveGoalAdapter) ReconcileGoal(context.Context, Session) (GoalAdapterResult, error) {
return GoalAdapterResult{}, nil
}
func (*requireLiveGoalAdapter) NormalizeGoalObservation(raw map[string]any) map[string]any {
return clonePayload(raw)
}
func (*requireLiveGoalAdapter) ExecGoalControl(context.Context, Session, []PromptContentBlock, string) ([]activityshared.Event, bool, error) {
return nil, false, nil
}
func (a *requireLiveGoalAdapter) FenceGoalGeneration(_ context.Context, session Session, input GoalGenerationFenceInput) error {
a.fenceCalls++
a.calls = append(a.calls, "fence")
a.lastFence = input
if a.onFence != nil {
a.onFence(session)
}
return a.fenceErr
}
func (a *requireLiveGoalAdapter) Close(context.Context, Session) error {
a.closeCalls++
a.live = false
a.calls = append(a.calls, "close")
return nil
}
func TestGoalCapabilitiesWaitObservesContextCancellation(t *testing.T) {
controller := NewController(nil, nil)
release := controller.acquireLifecycleLock("room-1", "session-1")
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
defer cancel()
_, err := controller.GoalCapabilities(ctx, GoalReconcileInput{RoomID: "room-1", AgentSessionID: "session-1"})
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("GoalCapabilities error = %v", err)
}
release()
controller.mu.Lock()
remaining := len(controller.lifecycleLocks)
controller.mu.Unlock()
if remaining != 0 {
t.Fatalf("lifecycle lock references leaked: %d", remaining)
}
}
func TestGoalControlWaitObservesContextCancellation(t *testing.T) {
controller := NewController(nil, nil)
release := controller.acquireLifecycleLock("room-1", "session-1")
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
defer cancel()
_, err := controller.GoalControl(ctx, GoalControlInput{RoomID: "room-1", AgentSessionID: "session-1"})
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("GoalControl error = %v", err)
}
release()
controller.mu.Lock()
remaining := len(controller.lifecycleLocks)
controller.mu.Unlock()
if remaining != 0 {
t.Fatalf("lifecycle lock references leaked: %d", remaining)
}
}