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
122 lines
4.9 KiB
Go
122 lines
4.9 KiB
Go
package agentruntime
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
activityshared "github.com/tutti-os/tutti/packages/agent/daemon/activity/events"
|
|
)
|
|
|
|
type reprepareTestAdapter struct {
|
|
provider string
|
|
live bool
|
|
resumeInputs []Session
|
|
launchInputs []ProviderLaunchPrepareInput
|
|
releaseCalls int
|
|
}
|
|
|
|
func (a *reprepareTestAdapter) Provider() string { return a.provider }
|
|
|
|
func (*reprepareTestAdapter) Start(context.Context, Session) ([]activityshared.Event, error) {
|
|
return nil, errors.New("unexpected Start")
|
|
}
|
|
|
|
func (a *reprepareTestAdapter) Resume(ctx context.Context, session Session) error {
|
|
a.resumeInputs = append(a.resumeInputs, session)
|
|
_, _, err := prepareProviderLaunch(ctx, func(_ context.Context, input ProviderLaunchPrepareInput) (ProviderLaunchPrepareResult, error) {
|
|
a.launchInputs = append(a.launchInputs, input)
|
|
return ProviderLaunchPrepareResult{}, nil
|
|
}, session, ProcessSpec{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a.live = true
|
|
return nil
|
|
}
|
|
|
|
func (*reprepareTestAdapter) Close(context.Context, Session) error { return nil }
|
|
|
|
func (*reprepareTestAdapter) Exec(context.Context, Session, []PromptContentBlock, string, string, EventSink, CommandSnapshotSink) ([]activityshared.Event, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (*reprepareTestAdapter) Cancel(context.Context, Session, string) ([]activityshared.Event, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (a *reprepareTestAdapter) HasLiveSession(Session) bool { return a.live }
|
|
|
|
func (a *reprepareTestAdapter) ReleaseLiveSession(context.Context, Session) error {
|
|
a.releaseCalls++
|
|
a.live = false
|
|
return nil
|
|
}
|
|
|
|
func TestControllerRepreparePreservesSessionIdentityAcrossProviders(t *testing.T) {
|
|
for _, provider := range []string{ProviderCodex, ProviderClaudeCode, ProviderOpenCode} {
|
|
t.Run(provider, func(t *testing.T) {
|
|
adapter := &reprepareTestAdapter{provider: provider}
|
|
controller := NewController([]Adapter{adapter}, nil)
|
|
base := ResumeInput{
|
|
RoomID: "workspace-1", AgentSessionID: "session-1", AgentTargetID: "target-1",
|
|
Provider: provider, ProviderSessionID: "provider-session-1", CWD: "/workspace",
|
|
MCPServers: []MCPServerBinding{{Name: "connectors", Type: "http", URL: "http://127.0.0.1/old"}},
|
|
RuntimeContext: map[string]any{"canonical": true},
|
|
}
|
|
if _, err := controller.Resume(t.Context(), base); err != nil {
|
|
t.Fatalf("Resume() error = %v", err)
|
|
}
|
|
replacement := base
|
|
replacement.MCPServers = []MCPServerBinding{{Name: "connectors", Type: "http", URL: "http://127.0.0.1/new", Headers: map[string]string{"Authorization": "Bearer invocation"}}}
|
|
replacement.ProviderLaunchRuntimeContext = map[string]any{"canonical": true, "invocationId": "invocation-2"}
|
|
result, err := controller.Reprepare(t.Context(), replacement)
|
|
if err != nil {
|
|
t.Fatalf("Reprepare() error = %v", err)
|
|
}
|
|
if result.AgentSessionID != "session-1" || result.ProviderSessionID != "provider-session-1" || result.Provider != provider {
|
|
t.Fatalf("reprepared identity = %#v", result)
|
|
}
|
|
if adapter.releaseCalls != 1 || len(adapter.resumeInputs) != 2 {
|
|
t.Fatalf("release calls=%d resume inputs=%d, want 1/2", adapter.releaseCalls, len(adapter.resumeInputs))
|
|
}
|
|
if got := adapter.resumeInputs[1].MCPServers; len(got) != 1 || got[0].URL != "http://127.0.0.1/new" || got[0].Headers["Authorization"] != "Bearer invocation" {
|
|
t.Fatalf("replacement MCP binding = %#v", got)
|
|
}
|
|
if len(adapter.launchInputs) != 2 || adapter.launchInputs[1].Session.RuntimeContext["invocationId"] != "invocation-2" {
|
|
t.Fatalf("provider launch input = %#v", adapter.launchInputs)
|
|
}
|
|
if adapter.resumeInputs[1].RuntimeContext["invocationId"] != nil {
|
|
t.Fatalf("adapter retained ephemeral launch context: %#v", adapter.resumeInputs[1].RuntimeContext)
|
|
}
|
|
stored, ok := controller.Session("workspace-1", "session-1")
|
|
if !ok || stored.ProviderSessionID != "provider-session-1" || stored.MCPServers[0].URL != "http://127.0.0.1/new" || stored.RuntimeContext["invocationId"] != nil {
|
|
t.Fatalf("stored replacement = %#v found=%t", stored, ok)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestControllerReprepareRejectsActiveTurnBeforeRelease(t *testing.T) {
|
|
adapter := &reprepareTestAdapter{provider: ProviderCodex}
|
|
controller := NewController([]Adapter{adapter}, nil)
|
|
input := ResumeInput{
|
|
RoomID: "workspace-1", AgentSessionID: "session-1", Provider: ProviderCodex,
|
|
ProviderSessionID: "provider-session-1", CWD: "/workspace",
|
|
}
|
|
if _, err := controller.Resume(t.Context(), input); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
controller.mu.Lock()
|
|
controller.turns[sessionKey("workspace-1", "session-1")] = activeTurn{turnID: "turn-1"}
|
|
controller.mu.Unlock()
|
|
|
|
_, err := controller.Reprepare(t.Context(), input)
|
|
if !errors.Is(err, ErrSessionActiveTurn) {
|
|
t.Fatalf("Reprepare() error = %v, want ErrSessionActiveTurn", err)
|
|
}
|
|
if adapter.releaseCalls != 0 || len(adapter.resumeInputs) != 1 {
|
|
t.Fatalf("active reprepare release=%d resume=%d", adapter.releaseCalls, len(adapter.resumeInputs))
|
|
}
|
|
}
|