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

835 lines
28 KiB
Go

package agentruntime
import (
"context"
"errors"
"fmt"
"sync"
"sync/atomic"
"testing"
"time"
activityshared "github.com/tutti-os/tutti/packages/agent/daemon/activity/events"
)
func TestControllerStartFailureDoesNotCreateCanonicalSessionOrTurnlessMessage(t *testing.T) {
t.Parallel()
reporter := &recordingReporter{}
controller := NewController([]Adapter{failingStartAdapter{}}, reporter)
controller.pendingCommandSnapshots["agent-session-1"] = AgentSessionCommandSnapshot{AgentSessionID: "agent-session-1"}
controller.pendingConfigOptionsUpdates[sessionKey("room-1", "agent-session-1")] = []AgentSessionConfigOptionsUpdate{{AgentSessionID: "agent-session-1"}}
started, err := controller.Start(context.Background(), StartInput{
RoomID: "room-1",
AgentSessionID: "agent-session-1",
Provider: hermesExtensionTestProvider,
CWD: "/workspace",
Title: "Hermes",
})
if err == nil {
t.Fatal("Start error = nil")
}
if code := AppErrorCode(err); code != "process_exited" {
t.Fatalf("start error code = %q, want process_exited", code)
}
if detail := AppErrorDebugMessage(err); detail != "acp process exited with code 1: Config invalid" {
t.Fatalf("start error detail = %q", detail)
}
if started.Session.AgentSessionID != "" {
t.Fatalf("start result = %#v, want no failed session result", started)
}
if stored, ok := controller.get("room-1", "agent-session-1"); ok {
t.Fatalf("stored session = %#v, want no canonical session", stored)
}
if reports := reporter.snapshot(); len(reports) != 0 {
t.Fatalf("reports = %#v, want no turnless failure report", reports)
}
controller.mu.Lock()
defer controller.mu.Unlock()
if len(controller.pendingCommandSnapshots) != 0 || len(controller.pendingConfigOptionsUpdates) != 0 {
t.Fatalf("pending snapshots survived failed start: commands=%#v config=%#v", controller.pendingCommandSnapshots, controller.pendingConfigOptionsUpdates)
}
}
func TestControllerStartPreservesTypedProviderTimeoutWithoutChangingPresentationCode(t *testing.T) {
t.Parallel()
controller := NewController([]Adapter{typedProviderStartTimeoutAdapter{}}, nil)
_, err := controller.Start(context.Background(), StartInput{
RoomID: "room-timeout",
AgentSessionID: "agent-timeout",
Provider: hermesExtensionTestProvider,
CWD: "/workspace",
})
if code := AppErrorCode(err); code != "request_timed_out" {
t.Fatalf("Start error code = %q (err=%v), want request_timed_out", code, err)
}
if !errors.Is(err, ErrProviderStartTimeout) || !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("Start error = %v, want typed provider-start timeout and deadline cause", err)
}
if stored, ok := controller.get("room-timeout", "agent-timeout"); ok {
t.Fatalf("stored session = %#v, want no runtime session after start timeout", stored)
}
}
func TestControllerStartDoesNotInferProviderTimeoutFromCallerDeadline(t *testing.T) {
t.Parallel()
controller := NewController([]Adapter{callerDeadlineStartAdapter{}}, nil)
_, err := controller.Start(context.Background(), StartInput{
RoomID: "room-caller-timeout",
AgentSessionID: "agent-caller-timeout",
Provider: hermesExtensionTestProvider,
CWD: "/workspace",
})
if code := AppErrorCode(err); code != "request_timed_out" {
t.Fatalf("Start error code = %q (err=%v), want request_timed_out", code, err)
}
if errors.Is(err, ErrProviderStartTimeout) {
t.Fatalf("Start error = %v, arbitrary caller deadline gained provider-start verdict", err)
}
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("Start error = %v, want caller deadline preserved", err)
}
}
func TestControllerStartPreservesStructuredCleanupBackpressureError(t *testing.T) {
t.Parallel()
controller := NewController([]Adapter{cleanupPendingStartAdapter{}}, nil)
_, err := controller.Start(context.Background(), StartInput{
RoomID: "room-cleanup-pending",
AgentSessionID: "agent-cleanup-pending",
Provider: hermesExtensionTestProvider,
CWD: "/workspace",
})
if AppErrorCode(err) != AppErrorProcessCleanupPending {
t.Fatalf("Start error code = %q (err=%v), want %q", AppErrorCode(err), err, AppErrorProcessCleanupPending)
}
if AppErrorDebugMessage(err) != "old process close failed" {
t.Fatalf("Start debug message = %q", AppErrorDebugMessage(err))
}
}
func TestControllerStartupOperationsDoNotBlockIndependentSessions(t *testing.T) {
tests := []struct {
name string
blocking string
following string
followingProvider string
}{
{name: "other provider start while start is blocked", blocking: "start", following: "start", followingProvider: ProviderCodex},
{name: "other provider resume while start is blocked", blocking: "start", following: "resume", followingProvider: ProviderCodex},
{name: "other provider start while resume is blocked", blocking: "resume", following: "start", followingProvider: ProviderCodex},
{name: "other provider resume while resume is blocked", blocking: "resume", following: "resume", followingProvider: ProviderCodex},
{name: "same provider start while start is blocked", blocking: "start", following: "start", followingProvider: ProviderClaudeCode},
{name: "same provider resume while start is blocked", blocking: "start", following: "resume", followingProvider: ProviderClaudeCode},
{name: "same provider start while resume is blocked", blocking: "resume", following: "start", followingProvider: ProviderClaudeCode},
{name: "same provider resume while resume is blocked", blocking: "resume", following: "resume", followingProvider: ProviderClaudeCode},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
blocker := &blockingStartupAdapter{
recordingStartAdapter: recordingStartAdapter{provider: ProviderClaudeCode},
operation: test.blocking,
blockedSessionID: "blocked-session",
entered: make(chan struct{}),
release: make(chan struct{}),
}
adapters := []Adapter{blocker}
if test.followingProvider != ProviderClaudeCode {
adapters = append(adapters, &recordingStartAdapter{provider: test.followingProvider})
}
controller := NewController(adapters, nil)
blocked := make(chan error, 1)
go func() {
blocked <- invokeControllerStartupOperation(
context.Background(),
controller,
test.blocking,
ProviderClaudeCode,
"blocked-session",
)
}()
select {
case <-blocker.entered:
case <-time.After(time.Second):
t.Fatal("blocking startup operation was not entered")
}
completed := make(chan error, 1)
go func() {
completed <- invokeControllerStartupOperation(
context.Background(),
controller,
test.following,
test.followingProvider,
"independent-session",
)
}()
select {
case err := <-completed:
if err != nil {
close(blocker.release)
<-blocked
t.Fatalf("independent startup operation: %v", err)
}
case <-time.After(time.Second):
close(blocker.release)
<-blocked
<-completed
t.Fatal("independent provider startup was blocked")
}
close(blocker.release)
if err := <-blocked; err != nil {
t.Fatalf("blocking startup operation: %v", err)
}
})
}
}
func TestControllerCloseCanDiscardRuntimeWithoutCompletingCanonicalSession(t *testing.T) {
t.Parallel()
reporter := &recordingReporter{}
adapter := &recordingStartAdapter{provider: ProviderClaudeCode}
controller := NewController([]Adapter{adapter}, reporter)
if _, err := controller.Start(context.Background(), StartInput{
RoomID: "room-1", AgentSessionID: "agent-session-rejected", Provider: ProviderClaudeCode,
}); err != nil {
t.Fatalf("Start() error = %v", err)
}
reportsBeforeClose := len(reporter.snapshot())
if _, err := controller.Close(context.Background(), CloseInput{
RoomID: "room-1", AgentSessionID: "agent-session-rejected", PreserveCanonicalState: true,
}); err != nil {
t.Fatalf("Close() error = %v", err)
}
if adapter.closeCalls != 1 {
t.Fatalf("adapter close calls = %d, want 1", adapter.closeCalls)
}
if _, ok := controller.get("room-1", "agent-session-rejected"); ok {
t.Fatal("discarded runtime session remains live")
}
if reportsAfterClose := len(reporter.snapshot()); reportsAfterClose != reportsBeforeClose {
t.Fatalf("discard close reports = %d, want unchanged %d", reportsAfterClose, reportsBeforeClose)
}
}
func TestControllerDiscardRemovesRuntimeWhenProviderCloseFails(t *testing.T) {
t.Parallel()
closeErr := errors.New("provider close timed out")
adapter := &recordingStartAdapter{provider: ProviderClaudeCode, closeErr: closeErr}
controller := NewController([]Adapter{adapter}, nil)
if _, err := controller.Start(context.Background(), StartInput{
RoomID: "room-1", AgentSessionID: "agent-session-rejected", Provider: ProviderClaudeCode,
}); err != nil {
t.Fatalf("Start() error = %v", err)
}
_, err := controller.Close(context.Background(), CloseInput{
RoomID: "room-1", AgentSessionID: "agent-session-rejected", PreserveCanonicalState: true,
})
if !errors.Is(err, closeErr) {
t.Fatalf("Close() error = %v, want %v", err, closeErr)
}
if _, ok := controller.get("room-1", "agent-session-rejected"); ok {
t.Fatal("discarded runtime session remains registered after close failure")
}
}
func TestControllerStartupLockPreservesSameSessionSerialization(t *testing.T) {
tests := []struct {
name string
blocking string
following string
}{
{name: "start then start", blocking: "start", following: "start"},
{name: "start then resume", blocking: "start", following: "resume"},
{name: "resume then start", blocking: "resume", following: "start"},
{name: "resume then resume", blocking: "resume", following: "resume"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
adapter := &blockingStartupAdapter{
recordingStartAdapter: recordingStartAdapter{provider: ProviderClaudeCode},
operation: test.blocking,
blockedSessionID: "same-session",
entered: make(chan struct{}),
release: make(chan struct{}),
}
controller := NewController([]Adapter{adapter}, nil)
blocked := make(chan error, 1)
go func() {
blocked <- invokeControllerStartupOperation(
context.Background(),
controller,
test.blocking,
ProviderClaudeCode,
"same-session",
)
}()
select {
case <-adapter.entered:
case <-time.After(time.Second):
t.Fatal("blocking startup operation was not entered")
}
waitCtx, cancelWait := context.WithCancel(context.Background())
waiting := make(chan error, 1)
go func() {
waiting <- invokeControllerStartupOperation(
waitCtx,
controller,
test.following,
ProviderClaudeCode,
"same-session",
)
}()
cancelWait()
select {
case err := <-waiting:
if !errors.Is(err, context.Canceled) {
close(adapter.release)
<-blocked
t.Fatalf("waiting startup error = %v, want context canceled", err)
}
case <-time.After(time.Second):
close(adapter.release)
<-blocked
<-waiting
t.Fatal("same-session startup lock did not honor context cancellation")
}
close(adapter.release)
if err := <-blocked; err != nil {
t.Fatalf("blocking startup operation: %v", err)
}
if err := invokeControllerStartupOperation(
context.Background(),
controller,
test.following,
ProviderClaudeCode,
"same-session",
); err != nil {
t.Fatalf("startup replay: %v", err)
}
if calls := adapter.startCalls.Load() + adapter.resumeCalls.Load(); calls != 1 {
t.Fatalf("adapter startup calls = %d, want 1", calls)
}
if locks := len(controller.startupLocks); locks != 0 {
t.Fatalf("startup locks = %d, want 0", locks)
}
})
}
}
func TestControllerAnonymousStartsRemainProviderScopedAndIdempotent(t *testing.T) {
adapter := &blockingStartupAdapter{
recordingStartAdapter: recordingStartAdapter{provider: ProviderClaudeCode},
operation: "start",
entered: make(chan struct{}),
release: make(chan struct{}),
}
controller := NewController([]Adapter{adapter}, nil)
input := StartInput{RoomID: "room-1", Provider: ProviderClaudeCode}
firstResult := make(chan StartResult, 1)
firstErr := make(chan error, 1)
go func() {
result, err := controller.Start(context.Background(), input)
firstResult <- result
firstErr <- err
}()
select {
case <-adapter.entered:
case <-time.After(time.Second):
t.Fatal("blocking anonymous start was not entered")
}
waitCtx, cancelWait := context.WithCancel(context.Background())
waiting := make(chan error, 1)
go func() {
_, err := controller.Start(waitCtx, input)
waiting <- err
}()
cancelWait()
select {
case err := <-waiting:
if !errors.Is(err, context.Canceled) {
close(adapter.release)
<-firstResult
<-firstErr
t.Fatalf("waiting anonymous start error = %v, want context canceled", err)
}
case <-time.After(time.Second):
close(adapter.release)
<-firstResult
<-firstErr
<-waiting
t.Fatal("anonymous startup lock did not honor context cancellation")
}
close(adapter.release)
first := <-firstResult
if err := <-firstErr; err != nil {
t.Fatalf("first anonymous start: %v", err)
}
replayed, err := controller.Start(context.Background(), input)
if err != nil {
t.Fatalf("replayed anonymous start: %v", err)
}
if replayed.Session.AgentSessionID != first.Session.AgentSessionID {
t.Fatalf("replayed session = %q, want %q", replayed.Session.AgentSessionID, first.Session.AgentSessionID)
}
if !first.Created {
t.Fatal("first anonymous start Created = false, want true")
}
if replayed.Created {
t.Fatal("replayed anonymous start Created = true, want false")
}
if calls := adapter.startCalls.Load(); calls != 1 {
t.Fatalf("adapter start calls = %d, want 1", calls)
}
if locks := len(controller.startupLocks); locks != 0 {
t.Fatalf("startup locks = %d, want 0", locks)
}
}
func invokeControllerStartupOperation(
ctx context.Context,
controller *Controller,
operation string,
provider string,
agentSessionID string,
) error {
switch operation {
case "start":
_, err := controller.Start(ctx, StartInput{
RoomID: "room-1",
AgentSessionID: agentSessionID,
Provider: provider,
})
return err
case "resume":
_, err := controller.Resume(ctx, ResumeInput{
RoomID: "room-1",
AgentSessionID: agentSessionID,
Provider: provider,
ProviderSessionID: "provider-" + agentSessionID,
})
return err
default:
return fmt.Errorf("unsupported startup operation %q", operation)
}
}
func TestControllerProvisionalStartRollsBackWithoutCanonicalReport(t *testing.T) {
t.Parallel()
reporter := &recordingReporter{}
controller := NewController([]Adapter{&recordingStartAdapter{provider: ProviderCodex}}, reporter)
started, err := controller.Start(context.Background(), StartInput{
RoomID: "room-1", AgentSessionID: "agent-session-1", Provider: ProviderCodex,
CWD: "/workspace", Provisional: true,
})
if err != nil || started.Session.AgentSessionID != "agent-session-1" {
t.Fatalf("Start() = %#v, %v", started, err)
}
if reports := reporter.snapshot(); len(reports) != 0 {
t.Fatalf("reports before commit = %#v", reports)
}
controller.applySessionEventsByAgentSessionID("agent-session-1", []activityshared.Event{
newSessionActivityEvent(started.Session, EventSessionStarted, SessionStatusReady, nil),
})
controller.applyCommandSnapshotByAgentSessionID(AgentSessionCommandSnapshot{
AgentSessionID: "agent-session-1",
Commands: []AgentSessionCommand{{Name: "review"}},
})
controller.applyConfigOptionsUpdateByAgentSessionID(AgentSessionConfigOptionsUpdate{
RoomID: "room-1", AgentSessionID: "agent-session-1",
})
if reports := reporter.snapshot(); len(reports) != 0 {
t.Fatalf("provider callbacks leaked reports before commit = %#v", reports)
}
controller.mu.Lock()
if _, ok := controller.commands[sessionKey("room-1", "agent-session-1")]; ok {
controller.mu.Unlock()
t.Fatal("provider command callback became canonical before commit")
}
if len(controller.pendingCommandSnapshots) != 1 || len(controller.pendingConfigOptionsUpdates) != 1 {
controller.mu.Unlock()
t.Fatalf("provider callbacks were not retained transactionally: commands=%#v config=%#v", controller.pendingCommandSnapshots, controller.pendingConfigOptionsUpdates)
}
controller.mu.Unlock()
if _, err := controller.Close(context.Background(), CloseInput{RoomID: "room-1", AgentSessionID: "agent-session-1"}); err != nil {
t.Fatalf("Close() error = %v", err)
}
if _, ok := controller.get("room-1", "agent-session-1"); ok {
t.Fatal("provisional session survived rollback")
}
if reports := reporter.snapshot(); len(reports) != 0 {
t.Fatalf("rollback reports = %#v", reports)
}
controller.mu.Lock()
defer controller.mu.Unlock()
if len(controller.pendingCommandSnapshots) != 0 || len(controller.pendingConfigOptionsUpdates) != 0 {
t.Fatalf("rollback retained provider callbacks: commands=%#v config=%#v", controller.pendingCommandSnapshots, controller.pendingConfigOptionsUpdates)
}
}
func TestControllerProvisionalStartCommitsWithFirstTurn(t *testing.T) {
t.Parallel()
reporter := &recordingReporter{}
controller := NewController([]Adapter{&recordingStartAdapter{provider: ProviderCodex}}, reporter)
_, err := controller.Start(context.Background(), StartInput{
RoomID: "room-1", AgentSessionID: "agent-session-1", Provider: ProviderCodex,
CWD: "/workspace", Provisional: true,
})
if err != nil {
t.Fatalf("Start() error = %v", err)
}
if sessions := controller.Sessions("room-1"); len(sessions) != 0 {
t.Fatalf("Sessions() before first turn acceptance = %#v, want provisional session hidden", sessions)
}
result, err := controller.Exec(context.Background(), ExecInput{
RoomID: "room-1", AgentSessionID: "agent-session-1",
Content: []PromptContentBlock{{Type: "text", Text: "hello"}},
})
if err != nil || result.TurnID == "" {
t.Fatalf("Exec() = %#v, %v", result, err)
}
reports := reporter.waitForCalls(t, 1)
if len(reports[0].report.StatePatches) == 0 {
t.Fatalf("commit report = %#v, want session and turn state", reports[0].report)
}
if sessions := controller.Sessions("room-1"); len(sessions) != 1 || sessions[0].AgentSessionID != "agent-session-1" {
t.Fatalf("Sessions() after first turn acceptance = %#v, want committed session", sessions)
}
controller.mu.Lock()
defer controller.mu.Unlock()
if controller.provisionalSessions[sessionKey("room-1", "agent-session-1")] {
t.Fatal("session remained provisional after first turn acceptance")
}
}
func TestControllerStartPassesProviderTargetRefToAdapterSession(t *testing.T) {
t.Parallel()
adapter := &recordingStartAdapter{provider: ProviderCodex}
controller := NewController([]Adapter{adapter}, nil)
ref := map[string]any{
"kind": "sharedAgent",
"provider": ProviderCodex,
"sharedAgentId": "agent-1",
}
started, err := controller.Start(context.Background(), StartInput{
RoomID: "room-1",
AgentSessionID: "agent-session-1",
Provider: ProviderCodex,
CWD: "/workspace",
ProviderTargetRef: ref,
})
if err != nil {
t.Fatalf("Start: %v", err)
}
if adapter.started.ProviderTargetRef["kind"] != "sharedAgent" ||
adapter.started.ProviderTargetRef["sharedAgentId"] != "agent-1" {
t.Fatalf("adapter provider target ref = %#v, want shared agent ref", adapter.started.ProviderTargetRef)
}
if started.Session.ProviderTargetRef["kind"] != "sharedAgent" {
t.Fatalf("started provider target ref = %#v, want shared agent ref", started.Session.ProviderTargetRef)
}
}
func TestControllerPreflightsPathBackedImageButExecRequiresHydratedContent(t *testing.T) {
t.Parallel()
adapter := &recordingPromptAdapter{
recordingStartAdapter: recordingStartAdapter{provider: ProviderCodex},
}
controller := NewController([]Adapter{adapter}, nil)
_, err := controller.Start(context.Background(), StartInput{
RoomID: "room-1",
AgentSessionID: "agent-session-1",
Provider: ProviderCodex,
CWD: "/workspace",
})
if err != nil {
t.Fatalf("Start: %v", err)
}
content := []PromptContentBlock{{
Type: "image",
MimeType: "image/png",
Path: "/managed/agent-prompt-assets/screen.png",
}}
if err := controller.ValidatePromptContent(context.Background(), ExecInput{
RoomID: "room-1",
AgentSessionID: "agent-session-1",
Content: content,
}); err != nil {
t.Fatalf("ValidatePromptContent: %v", err)
}
if len(adapter.validated) != 1 || adapter.validated[0].Path != content[0].Path {
t.Fatalf("adapter validated content = %#v, want path-backed image", adapter.validated)
}
if _, err := controller.Exec(context.Background(), ExecInput{
RoomID: "room-1",
AgentSessionID: "agent-session-1",
Content: content,
}); !errors.Is(err, ErrPromptImageUnsupported) {
t.Fatalf("Exec error = %v, want ErrPromptImageUnsupported", err)
}
}
func TestControllerStartDoesNotReuseSessionWithDifferentProviderTargetRef(t *testing.T) {
t.Parallel()
adapter := &recordingStartAdapter{provider: ProviderCodex}
controller := NewController([]Adapter{adapter}, nil)
first, err := controller.Start(context.Background(), StartInput{
RoomID: "room-1",
Provider: ProviderCodex,
CWD: "/workspace",
ProviderTargetRef: map[string]any{
"kind": "sharedAgent",
"provider": ProviderCodex,
"sharedAgentId": "agent-1",
},
})
if err != nil {
t.Fatalf("first Start: %v", err)
}
second, err := controller.Start(context.Background(), StartInput{
RoomID: "room-1",
Provider: ProviderCodex,
CWD: "/workspace",
ProviderTargetRef: map[string]any{
"kind": "sharedAgent",
"provider": ProviderCodex,
"sharedAgentId": "agent-2",
},
})
if err != nil {
t.Fatalf("second Start: %v", err)
}
if second.Session.AgentSessionID == first.Session.AgentSessionID {
t.Fatalf("second start reused session %q for a different provider target", second.Session.AgentSessionID)
}
if adapter.started.ProviderTargetRef["sharedAgentId"] != "agent-2" {
t.Fatalf("adapter provider target ref = %#v, want second target", adapter.started.ProviderTargetRef)
}
}
func TestControllerStartDoesNotReuseTargetSessionWithDifferentProviderTargetRef(t *testing.T) {
t.Parallel()
adapter := &recordingStartAdapter{provider: ProviderCodex}
controller := NewController([]Adapter{adapter}, nil)
first, err := controller.Start(context.Background(), StartInput{
RoomID: "room-1",
Provider: ProviderCodex,
AgentTargetID: "local:codex",
CWD: "/workspace",
ProviderTargetRef: map[string]any{
"kind": "local_cli",
"provider": ProviderCodex,
"targetId": "local:codex",
},
})
if err != nil {
t.Fatalf("first Start: %v", err)
}
second, err := controller.Start(context.Background(), StartInput{
RoomID: "room-1",
Provider: ProviderCodex,
AgentTargetID: "local:codex",
CWD: "/workspace",
ProviderTargetRef: map[string]any{
"kind": "local_cli",
"provider": ProviderCodex,
"targetId": "alternate-codex",
},
})
if err != nil {
t.Fatalf("second Start: %v", err)
}
if second.Session.AgentSessionID == first.Session.AgentSessionID {
t.Fatalf("second start reused session %q for a different provider target ref", second.Session.AgentSessionID)
}
if adapter.started.ProviderTargetRef["targetId"] != "alternate-codex" {
t.Fatalf("adapter provider target ref = %#v, want alternate-codex", adapter.started.ProviderTargetRef)
}
}
func TestControllerStartReusesTargetSessionWithSameProviderTargetRef(t *testing.T) {
t.Parallel()
adapter := &recordingStartAdapter{provider: ProviderCodex}
controller := NewController([]Adapter{adapter}, nil)
ref := map[string]any{
"kind": "local_cli",
"provider": ProviderCodex,
"targetId": "local:codex",
}
first, err := controller.Start(context.Background(), StartInput{
RoomID: "room-1",
Provider: ProviderCodex,
AgentTargetID: "local:codex",
CWD: "/workspace",
ProviderTargetRef: ref,
})
if err != nil {
t.Fatalf("first Start: %v", err)
}
second, err := controller.Start(context.Background(), StartInput{
RoomID: "room-1",
Provider: ProviderCodex,
AgentTargetID: "local:codex",
CWD: "/workspace",
ProviderTargetRef: map[string]any{
"kind": "local_cli",
"provider": ProviderCodex,
"targetId": "local:codex",
},
})
if err != nil {
t.Fatalf("second Start: %v", err)
}
if second.Session.AgentSessionID != first.Session.AgentSessionID {
t.Fatalf("second start session = %q, want reused %q", second.Session.AgentSessionID, first.Session.AgentSessionID)
}
if !first.Created {
t.Fatal("first start Created = false, want true")
}
if second.Created {
t.Fatal("reused start Created = true, want false")
}
}
type blockingStartupAdapter struct {
recordingStartAdapter
operation string
blockedSessionID string
entered chan struct{}
release chan struct{}
enterOnce sync.Once
startCalls atomic.Int32
resumeCalls atomic.Int32
}
func (a *blockingStartupAdapter) Start(ctx context.Context, session Session) ([]activityshared.Event, error) {
a.startCalls.Add(1)
if a.operation == "start" && a.blocks(session) {
if err := a.wait(ctx); err != nil {
return nil, err
}
}
return []activityshared.Event{
newSessionActivityEvent(session, EventSessionStarted, SessionStatusReady, nil),
}, nil
}
func (a *blockingStartupAdapter) Resume(ctx context.Context, session Session) error {
a.resumeCalls.Add(1)
if a.operation == "resume" && a.blocks(session) {
return a.wait(ctx)
}
return nil
}
func (a *blockingStartupAdapter) blocks(session Session) bool {
return a.blockedSessionID == "" || a.blockedSessionID == session.AgentSessionID
}
func (a *blockingStartupAdapter) wait(ctx context.Context) error {
a.enterOnce.Do(func() { close(a.entered) })
select {
case <-a.release:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
type recordingPromptAdapter struct {
recordingStartAdapter
validated []PromptContentBlock
}
func (a *recordingPromptAdapter) ValidatePromptContent(_ Session, content []PromptContentBlock) error {
a.validated = append([]PromptContentBlock(nil), content...)
return validatePromptContentImagesForPreflight(content)
}
type failingStartAdapter struct{}
type typedProviderStartTimeoutAdapter struct{ failingStartAdapter }
func (typedProviderStartTimeoutAdapter) Start(context.Context, Session) ([]activityshared.Event, error) {
return nil, errors.Join(ErrProviderStartTimeout, fmt.Errorf("provider start: %w", context.DeadlineExceeded))
}
type callerDeadlineStartAdapter struct{ failingStartAdapter }
func (callerDeadlineStartAdapter) Start(context.Context, Session) ([]activityshared.Event, error) {
return nil, fmt.Errorf("caller request: %w", context.DeadlineExceeded)
}
type cleanupPendingStartAdapter struct{ failingStartAdapter }
func (cleanupPendingStartAdapter) Start(context.Context, Session) ([]activityshared.Event, error) {
return nil, &AppError{
Code: AppErrorProcessCleanupPending,
Message: "agent process cleanup is still pending",
DebugMessage: "old process close failed",
}
}
func (failingStartAdapter) Provider() string { return hermesExtensionTestProvider }
func (failingStartAdapter) Start(context.Context, Session) ([]activityshared.Event, error) {
return nil, errors.New("\x1b[33macp process exited with code 1: Config invalid\x1b[39m")
}
func (failingStartAdapter) Resume(context.Context, Session) error {
return nil
}
func (failingStartAdapter) Close(context.Context, Session) error {
return nil
}
func (failingStartAdapter) Exec(context.Context, Session, []PromptContentBlock, string, string, EventSink, CommandSnapshotSink) ([]activityshared.Event, error) {
return nil, nil
}
func (failingStartAdapter) Cancel(context.Context, Session, string) ([]activityshared.Event, error) {
return nil, nil
}
func TestControllerRejectsUnsupportedProvider(t *testing.T) {
t.Parallel()
_, err := NewController(nil, nil).Start(context.Background(), StartInput{
RoomID: "room-1",
Provider: "unknown",
})
if err == nil {
t.Fatal("Start returned nil error for unsupported provider")
}
}