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

344 lines
9.1 KiB
Go

package agentdaemon
import (
"context"
"errors"
"sync"
"testing"
"time"
activityshared "github.com/tutti-os/tutti/packages/agent/daemon/activity/events"
agentruntime "github.com/tutti-os/tutti/packages/agent/daemon/runtime"
)
func TestNewRuntimeCreatesDefaultController(t *testing.T) {
t.Parallel()
runtime, err := NewRuntime(Config{
ProcessTransport: NewLocalProcessTransport(),
HostMetadata: testHostMetadata(),
})
if err != nil {
t.Fatalf("NewRuntime() error = %v", err)
}
t.Cleanup(runtime.Close)
if runtime.Controller() == nil {
t.Fatal("Controller() = nil, want controller")
}
if runtime.done == nil {
t.Fatal("default runtime did not start live session reaper")
}
}
func TestNewRuntimeAppliesCommandNetworkAccessPolicyToDefaultAppServerAdapters(t *testing.T) {
t.Parallel()
policyProviders := make(map[string]int)
runtime, err := NewRuntime(Config{
ProcessTransport: NewLocalProcessTransport(),
HostMetadata: testHostMetadata(),
CommandNetworkAccessPolicy: func(provider string) bool {
policyProviders[provider]++
return provider == agentruntime.ProviderTuttiAgent
},
})
if err != nil {
t.Fatalf("NewRuntime() error = %v", err)
}
t.Cleanup(runtime.Close)
if policyProviders[agentruntime.ProviderCodex] != 1 ||
policyProviders[agentruntime.ProviderTuttiAgent] != 1 ||
len(policyProviders) != 2 {
t.Fatalf("command network access policy providers = %#v", policyProviders)
}
}
func TestNewRuntimeRequiresHostMetadataForDefaultAdapters(t *testing.T) {
t.Parallel()
_, err := NewRuntime(Config{})
if !errors.Is(err, ErrHostMetadataRequired) {
t.Fatalf("NewRuntime() error = %v, want ErrHostMetadataRequired", err)
}
}
func TestNewRuntimeRequiresProcessTransportForDefaultAdapters(t *testing.T) {
t.Parallel()
_, err := NewRuntime(Config{HostMetadata: testHostMetadata()})
if !errors.Is(err, ErrProcessTransportRequired) {
t.Fatalf("NewRuntime() error = %v, want ErrProcessTransportRequired", err)
}
}
func TestNewRuntimeUsesCustomAdapters(t *testing.T) {
t.Parallel()
runtime, err := NewRuntime(Config{
Adapters: []agentruntime.Adapter{testAdapter{provider: "test-agent"}},
})
if err != nil {
t.Fatalf("NewRuntime() error = %v", err)
}
t.Cleanup(runtime.Close)
started, err := runtime.Controller().Start(context.Background(), agentruntime.StartInput{
RoomID: "workspace-1",
AgentSessionID: "agent-session-1",
Provider: "test-agent",
})
if err != nil {
t.Fatalf("Start() error = %v", err)
}
if started.Session.Provider != "test-agent" {
t.Fatalf("provider = %q, want test-agent", started.Session.Provider)
}
}
func TestNewRuntimeAppliesProviderLaunchPreparerToCustomAdapters(t *testing.T) {
t.Parallel()
adapter := &providerLaunchPreparerTestAdapter{
testAdapter: testAdapter{provider: "test-agent"},
}
_, err := NewRuntime(Config{
Adapters: []agentruntime.Adapter{adapter},
ProviderLaunchPreparer: func(context.Context, agentruntime.ProviderLaunchPrepareInput) (agentruntime.ProviderLaunchPrepareResult, error) {
return agentruntime.ProviderLaunchPrepareResult{}, nil
},
})
if err != nil {
t.Fatalf("NewRuntime() error = %v", err)
}
if adapter.preparer == nil {
t.Fatal("custom adapter did not receive ProviderLaunchPreparer")
}
}
func TestNewRuntimeCanDisableLiveSessionReaper(t *testing.T) {
t.Parallel()
enabled := false
runtime, err := NewRuntime(Config{
Adapters: []agentruntime.Adapter{testAdapter{provider: "test-agent"}},
LiveSessionReaper: LiveSessionReaperConfig{
Enabled: &enabled,
IdleAfter: time.Minute,
SweepInterval: time.Minute,
},
})
if err != nil {
t.Fatalf("NewRuntime() error = %v", err)
}
t.Cleanup(runtime.Close)
if runtime.done != nil {
t.Fatal("disabled live session reaper started a background loop")
}
}
// TestRuntimeCloseForceClosesLiveProviderSessions guards against orphaned
// provider subprocesses (e.g. a Codex app-server) surviving daemon
// shutdown. An OS process spawned by the daemon is not killed just because
// the daemon exits — it is reparented and keeps running — so Runtime.Close
// must proactively close every live session's provider process first.
func TestRuntimeCloseForceClosesLiveProviderSessions(t *testing.T) {
t.Parallel()
adapter := &liveSessionTestAdapter{provider: "test-agent", live: make(map[string]bool)}
runtime, err := NewRuntime(Config{
Adapters: []agentruntime.Adapter{adapter},
})
if err != nil {
t.Fatalf("NewRuntime() error = %v", err)
}
started, err := runtime.Controller().Start(context.Background(), agentruntime.StartInput{
RoomID: "workspace-1",
AgentSessionID: "agent-session-1",
Provider: "test-agent",
})
if err != nil {
t.Fatalf("Start() error = %v", err)
}
adapter.setLive(started.Session.AgentSessionID, true)
runtime.Close()
if adapter.closeCallCount(started.Session.AgentSessionID) != 1 {
t.Fatalf("adapter Close called %d times for live session, want exactly once", adapter.closeCallCount(started.Session.AgentSessionID))
}
if adapter.isLive(started.Session.AgentSessionID) {
t.Fatal("adapter still reports live session after Runtime.Close")
}
}
func TestRuntimeCloseFinalizesProcessTransport(t *testing.T) {
t.Parallel()
transport := &runtimeFinalizerTestTransport{}
runtime, err := NewRuntime(Config{
Adapters: []agentruntime.Adapter{testAdapter{provider: "test-agent"}},
ProcessTransport: transport,
})
if err != nil {
t.Fatal(err)
}
runtime.Close()
if transport.finalizeCalls != 1 {
t.Fatalf("Finalize() calls = %d, want 1", transport.finalizeCalls)
}
runtime.Close()
if transport.finalizeCalls != 1 {
t.Fatalf("Finalize() calls after second close = %d, want 1", transport.finalizeCalls)
}
}
type runtimeFinalizerTestTransport struct {
finalizeCalls int
}
func (*runtimeFinalizerTestTransport) Start(
context.Context,
agentruntime.ProcessSpec,
) (agentruntime.ProcessConnection, error) {
return nil, errors.New("unexpected process start")
}
func (t *runtimeFinalizerTestTransport) Finalize() error {
t.finalizeCalls++
return nil
}
func testHostMetadata() HostMetadata {
return HostMetadata{
ClientInfo: ClientInfo{
Name: "test-desktop",
Title: "Test Desktop",
Version: "1.0.0",
},
WorkspaceEnvName: "TEST_WORKSPACE_ID",
OpenClawSessionKeyPrefix: "agent:main:test-",
}
}
type testAdapter struct {
provider string
}
type providerLaunchPreparerTestAdapter struct {
testAdapter
preparer agentruntime.ProviderLaunchPreparer
}
func (a *providerLaunchPreparerTestAdapter) SetProviderLaunchPreparer(preparer agentruntime.ProviderLaunchPreparer) {
a.preparer = preparer
}
func (a testAdapter) Provider() string {
return a.provider
}
func (testAdapter) Start(context.Context, agentruntime.Session) ([]activityshared.Event, error) {
return nil, nil
}
func (testAdapter) Resume(context.Context, agentruntime.Session) error {
return nil
}
func (testAdapter) Close(context.Context, agentruntime.Session) error {
return nil
}
func (testAdapter) Exec(
context.Context,
agentruntime.Session,
[]agentruntime.PromptContentBlock,
string,
string,
agentruntime.EventSink,
agentruntime.CommandSnapshotSink,
) ([]activityshared.Event, error) {
return nil, nil
}
func (testAdapter) Cancel(
context.Context,
agentruntime.Session,
string,
) ([]activityshared.Event, error) {
return nil, nil
}
// liveSessionTestAdapter is a testAdapter variant that also implements
// agentruntime.LiveSessionProbeAdapter, so it can stand in for a provider
// (Codex app-server, Claude Code SDK) that holds a live, long-running
// subprocess per session.
type liveSessionTestAdapter struct {
provider string
mu sync.Mutex
live map[string]bool
closeCalls map[string]int
}
func (a *liveSessionTestAdapter) Provider() string { return a.provider }
func (*liveSessionTestAdapter) Start(context.Context, agentruntime.Session) ([]activityshared.Event, error) {
return nil, nil
}
func (*liveSessionTestAdapter) Resume(context.Context, agentruntime.Session) error { return nil }
func (a *liveSessionTestAdapter) Close(_ context.Context, session agentruntime.Session) error {
a.mu.Lock()
defer a.mu.Unlock()
if a.closeCalls == nil {
a.closeCalls = make(map[string]int)
}
a.closeCalls[session.AgentSessionID]++
a.live[session.AgentSessionID] = false
return nil
}
func (*liveSessionTestAdapter) Exec(
context.Context,
agentruntime.Session,
[]agentruntime.PromptContentBlock,
string,
string,
agentruntime.EventSink,
agentruntime.CommandSnapshotSink,
) ([]activityshared.Event, error) {
return nil, nil
}
func (*liveSessionTestAdapter) Cancel(
context.Context,
agentruntime.Session,
string,
) ([]activityshared.Event, error) {
return nil, nil
}
func (a *liveSessionTestAdapter) HasLiveSession(session agentruntime.Session) bool {
return a.isLive(session.AgentSessionID)
}
func (a *liveSessionTestAdapter) setLive(agentSessionID string, live bool) {
a.mu.Lock()
defer a.mu.Unlock()
a.live[agentSessionID] = live
}
func (a *liveSessionTestAdapter) isLive(agentSessionID string) bool {
a.mu.Lock()
defer a.mu.Unlock()
return a.live[agentSessionID]
}
func (a *liveSessionTestAdapter) closeCallCount(agentSessionID string) int {
a.mu.Lock()
defer a.mu.Unlock()
return a.closeCalls[agentSessionID]
}