Files
tutti/services/tuttid/agent_runtime_adapter_test.go
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

487 lines
17 KiB
Go

package main
import (
"context"
"errors"
"fmt"
"testing"
"time"
agentsessionstore "github.com/tutti-os/tutti/packages/agent/daemon/activity"
activityshared "github.com/tutti-os/tutti/packages/agent/daemon/activity/events"
agentruntime "github.com/tutti-os/tutti/packages/agent/daemon/runtime"
agenthost "github.com/tutti-os/tutti/packages/agent/host"
"github.com/tutti-os/tutti/packages/agent/store-sqlite/canonical"
agentservice "github.com/tutti-os/tutti/services/tuttid/service/agent"
)
type unavailableAgentExtensionResumeResolver struct{}
func (unavailableAgentExtensionResumeResolver) ResolveAdapter(context.Context, agentruntime.AdapterResolveInput) (agentruntime.Adapter, error) {
return nil, errors.New("adapter resolution must not run during resume eligibility checks")
}
type submitProvenanceAdapterTestProvider struct{}
func (submitProvenanceAdapterTestProvider) Provider() string { return "submit-provenance-test" }
func (submitProvenanceAdapterTestProvider) Start(context.Context, agentruntime.Session) ([]activityshared.Event, error) {
return nil, nil
}
func (submitProvenanceAdapterTestProvider) Resume(context.Context, agentruntime.Session) error {
return nil
}
func (submitProvenanceAdapterTestProvider) Close(context.Context, agentruntime.Session) error {
return nil
}
func (submitProvenanceAdapterTestProvider) Exec(context.Context, agentruntime.Session, []agentruntime.PromptContentBlock, string, string, agentruntime.EventSink, agentruntime.CommandSnapshotSink) ([]activityshared.Event, error) {
return nil, nil
}
func (submitProvenanceAdapterTestProvider) Cancel(context.Context, agentruntime.Session, string) ([]activityshared.Event, error) {
return nil, nil
}
func (submitProvenanceAdapterTestProvider) SessionState(session agentruntime.Session) agentruntime.SessionStateSnapshot {
return agentruntime.SessionStateSnapshot{
RoomID: session.RoomID,
AgentSessionID: session.AgentSessionID,
Provider: session.Provider,
Capabilities: canonical.NewCapabilitySnapshot([]string{"imageInput", "interrupt"}),
}
}
type providerAcceptanceMappingTestAdapter struct {
execCalls int
acceptanceExecCalls int
failure error
}
func (*providerAcceptanceMappingTestAdapter) Provider() string {
return "provider-acceptance-mapping-test"
}
func (*providerAcceptanceMappingTestAdapter) Start(
_ context.Context,
session agentruntime.Session,
) ([]activityshared.Event, error) {
event := activityshared.NewSessionStarted(activityshared.EventContext{
EventID: "session-started",
Provider: activityshared.Provider(session.Provider),
ProviderSessionID: "provider-session-1",
AgentSessionID: session.AgentSessionID,
SessionKind: "root",
RootAgentSessionID: session.AgentSessionID,
})
return []activityshared.Event{event}, nil
}
func (*providerAcceptanceMappingTestAdapter) Resume(
context.Context,
agentruntime.Session,
) error {
return nil
}
func (*providerAcceptanceMappingTestAdapter) Close(
context.Context,
agentruntime.Session,
) error {
return nil
}
func (a *providerAcceptanceMappingTestAdapter) Exec(
context.Context,
agentruntime.Session,
[]agentruntime.PromptContentBlock,
string,
string,
agentruntime.EventSink,
agentruntime.CommandSnapshotSink,
) ([]activityshared.Event, error) {
a.execCalls++
return nil, nil
}
func (a *providerAcceptanceMappingTestAdapter) ExecWithProviderAcceptance(
_ context.Context,
_ agentruntime.Session,
_ []agentruntime.PromptContentBlock,
_ string,
_ string,
_ agentruntime.EventSink,
_ agentruntime.CommandSnapshotSink,
reportDispatch agentruntime.ProviderDispatchSink,
acceptProviderTurn agentruntime.ProviderAcceptanceBarrier,
) ([]activityshared.Event, error) {
a.acceptanceExecCalls++
if a.failure != nil {
reportDispatch(agentruntime.ProviderDispatchResult{
Disposition: agentruntime.DispatchDispositionRejected,
Failure: a.failure,
})
return nil, a.failure
}
if err := acceptProviderTurn(agentruntime.ProviderAcceptanceReceipt{
Source: agentruntime.AcceptanceSourceTurnStartResponse,
ProviderSessionID: "provider-session-1",
ProviderTurnID: "provider-turn-1",
}); err != nil {
return nil, err
}
return nil, nil
}
func (*providerAcceptanceMappingTestAdapter) Cancel(
context.Context,
agentruntime.Session,
string,
) ([]activityshared.Event, error) {
return nil, nil
}
func (*providerAcceptanceMappingTestAdapter) ForkCapabilities(
context.Context,
agentruntime.Session,
) (agentruntime.SessionForkCapabilities, error) {
return agentruntime.SessionForkCapabilities{ThroughTurn: true}, nil
}
func (*providerAcceptanceMappingTestAdapter) Fork(
context.Context,
agentruntime.SessionForkInput,
) (agentruntime.SessionForkResult, error) {
return agentruntime.SessionForkResult{}, nil
}
type submitProvenanceAdapterTestReporter struct {
provenance agentsessionstore.ReportActivityInput
}
func (*submitProvenanceAdapterTestReporter) Report(context.Context, agentsessionstore.ReportActivityInput) error {
return nil
}
func (r *submitProvenanceAdapterTestReporter) ReportSubmitProvenance(_ context.Context, input agentsessionstore.ReportActivityInput) error {
r.provenance = input
return nil
}
func TestMapAgentRuntimeErrorPreservesInteractiveRecoveryCodes(t *testing.T) {
tests := []struct {
runtimeErr error
serviceErr error
}{
{agentruntime.ErrInteractiveRequestNotLive, agentservice.ErrInteractiveRequestNotLive},
{agentruntime.ErrInteractiveAlreadyAnswered, agentservice.ErrInteractiveAlreadyAnswered},
{agentruntime.ErrSessionDisconnected, agentservice.ErrRuntimeSessionDisconnected},
}
for _, test := range tests {
if err := mapAgentRuntimeError(test.runtimeErr); !errors.Is(err, test.serviceErr) {
t.Fatalf("mapAgentRuntimeError(%v) = %v, want %v", test.runtimeErr, err, test.serviceErr)
}
}
}
func TestMapAgentRuntimeErrorPreservesStructuredProviderFailure(t *testing.T) {
cause := errors.New("provider process rejected startup")
runtimeErr := &agentruntime.AppError{
Code: "provider_auth_required",
Message: "Agent provider needs authentication",
DebugMessage: "provider exited with status 1",
Cause: cause,
}
mapped := mapAgentRuntimeError(fmt.Errorf("runtime start: %w", runtimeErr))
var providerErr *agenthost.ProviderError
if !errors.As(mapped, &providerErr) {
t.Fatalf("mapped error = %v, want ProviderError", mapped)
}
if providerErr.Code != runtimeErr.Code || providerErr.Message != runtimeErr.Message || providerErr.DebugMessage != runtimeErr.DebugMessage {
t.Fatalf("ProviderError = %#v, want diagnostics from %#v", providerErr, runtimeErr)
}
if !errors.Is(mapped, cause) || !errors.Is(mapped, runtimeErr) {
t.Fatalf("mapped error did not preserve runtime error chain: %v", mapped)
}
}
func TestMapAgentRuntimeErrorDoesNotClassifyProviderTimeoutAsDefinitive(t *testing.T) {
runtimeErr := &agentruntime.AppError{
Code: "request_failed",
Message: "Agent provider request failed",
Cause: fmt.Errorf("provider response: %w", context.DeadlineExceeded),
}
mapped := mapAgentRuntimeError(runtimeErr)
var providerErr *agenthost.ProviderError
if errors.As(mapped, &providerErr) {
t.Fatalf("mapped error = %#v, want recoverable timeout", providerErr)
}
if !errors.Is(mapped, context.DeadlineExceeded) {
t.Fatalf("mapped error = %v, want deadline in error chain", mapped)
}
}
func TestAgentRuntimeAdapterCanResumePreservesExtensionTargetBinding(t *testing.T) {
controller := agentruntime.NewControllerWithAdapterResolver(nil, nil, unavailableAgentExtensionResumeResolver{})
adapter := newAgentRuntimeAdapter(controller)
if !adapter.CanResume(agentservice.RuntimeResumeInput{
WorkspaceID: "workspace-1",
AgentSessionID: "session-1",
AgentTargetID: "extension:codebuddy",
Provider: "acp:codebuddy",
ProviderSessionID: "provider-session-1",
ProviderTargetRef: map[string]any{
"kind": "agent_extension",
"provider": "acp:codebuddy",
"targetId": "extension:codebuddy",
"extensionInstallationId": "codebuddy@1.0.0",
},
}) {
t.Fatal("CanResume() = false, want authorized extension session to remain resumable across the tuttid runtime adapter")
}
}
func TestRuntimeCapabilityReferencesFromServicePreservesStructuredProvenance(t *testing.T) {
got := runtimeCapabilityReferencesFromService([]agentservice.CapabilityReference{{
Capability: "tutti",
Source: "slash_command",
}})
if len(got) != 1 || got[0] != (agentruntime.CapabilityReference{Capability: "tutti", Source: "slash_command"}) {
t.Fatalf("runtime capability refs = %#v", got)
}
}
func TestRuntimeTuttiModeSnapshotFromServicePreservesTypedRevision(t *testing.T) {
source := &agentservice.TuttiModeTurnSnapshot{
ActivationID: "activation-1",
RevisionID: "revision-7",
Revision: 7,
State: "active",
Source: "slash_command",
}
got := runtimeTuttiModeSnapshotFromService(source)
want := agentruntime.TuttiModeTurnSnapshot{
ActivationID: "activation-1",
RevisionID: "revision-7",
Revision: 7,
State: "active",
Source: "slash_command",
}
if got == nil || *got != want {
t.Fatalf("runtime Tutti mode snapshot = %#v, want %#v", got, want)
}
// Runtime input owns its copy; later service mutations must not rewrite the
// turn snapshot that the controller freezes.
source.State = "inactive"
if got.State != "active" {
t.Fatalf("runtime Tutti mode snapshot mutated with source: %#v", got)
}
}
func TestAgentRuntimeAdapterRejectsNewTurnWithoutCanonicalTurnID(t *testing.T) {
adapter := newAgentRuntimeAdapter(nil)
_, err := adapter.Exec(context.Background(), agentservice.RuntimeExecInput{
WorkspaceID: "workspace-1",
AgentSessionID: "session-1",
})
if !errors.Is(err, agentservice.ErrInvalidArgument) {
t.Fatalf("Exec() error = %v, want ErrInvalidArgument", err)
}
}
func TestAgentRuntimeAdapterPreservesProviderAcceptanceRequirement(t *testing.T) {
provider := &providerAcceptanceMappingTestAdapter{}
reporter := &submitProvenanceAdapterTestReporter{}
controller := agentruntime.NewController(
[]agentruntime.Adapter{provider},
reporter,
)
adapter := newAgentRuntimeAdapter(controller)
startResult, err := adapter.Start(t.Context(), agentservice.RuntimeStartInput{
WorkspaceID: "workspace-1", AgentSessionID: "session-1",
Provider: provider.Provider(),
})
if err != nil {
t.Fatal(err)
}
session := startResult.Session
result, err := adapter.Exec(t.Context(), agentservice.RuntimeExecInput{
WorkspaceID: "workspace-1", AgentSessionID: "session-1",
TurnID: "canonical-turn-1", ClientSubmitID: "opaque-submit-1",
CanonicalSubmitOccurredAtUnixMS: 1,
Content: agentservice.TextPromptContent("hello"),
RequireProviderAcceptance: true,
})
if err != nil {
t.Fatal(err)
}
if result.ProviderDispatch.Disposition != agenthost.RuntimeDispatchDispositionApplied ||
result.ProviderDispatch.Acceptance == nil ||
result.ProviderDispatch.Acceptance.ProviderTurnID != "provider-turn-1" {
t.Fatalf("provider dispatch = %#v", result.ProviderDispatch)
}
if provider.acceptanceExecCalls != 1 || provider.execCalls != 0 {
t.Fatalf(
"provider calls acceptance=%d ordinary=%d",
provider.acceptanceExecCalls,
provider.execCalls,
)
}
if session.ProviderSessionID != "provider-session-1" {
t.Fatalf("provider session id = %q", session.ProviderSessionID)
}
}
func TestAgentRuntimeAdapterPreservesRejectedDispatchWithProviderFailure(t *testing.T) {
providerFailure := &agentruntime.AppError{
Code: "auth_required",
Message: "Claude Code needs authentication",
}
provider := &providerAcceptanceMappingTestAdapter{failure: providerFailure}
controller := agentruntime.NewController(
[]agentruntime.Adapter{provider},
&submitProvenanceAdapterTestReporter{},
)
adapter := newAgentRuntimeAdapter(controller)
if _, err := adapter.Start(t.Context(), agentservice.RuntimeStartInput{
WorkspaceID: "workspace-1", AgentSessionID: "session-rejected",
Provider: provider.Provider(),
}); err != nil {
t.Fatal(err)
}
result, err := adapter.Exec(t.Context(), agentservice.RuntimeExecInput{
WorkspaceID: "workspace-1", AgentSessionID: "session-rejected",
TurnID: "canonical-turn-rejected", Content: agentservice.TextPromptContent("hello"),
RequireProviderAcceptance: true,
})
var mapped *agenthost.ProviderError
if !errors.As(err, &mapped) || mapped.Code != "auth_required" {
t.Fatalf("Exec() error = %#v, want auth_required ProviderError", err)
}
if result.ProviderDispatch.Disposition != agenthost.RuntimeDispatchDispositionRejected ||
result.ProviderDispatch.Acceptance != nil {
t.Fatalf("provider dispatch = %#v, want rejected", result.ProviderDispatch)
}
}
func TestAgentRuntimeAdapterDelegatesTypedDurableSubmitProvenance(t *testing.T) {
reporter := &submitProvenanceAdapterTestReporter{}
controller := agentruntime.NewController(
[]agentruntime.Adapter{submitProvenanceAdapterTestProvider{}},
reporter,
)
if _, err := controller.Start(context.Background(), agentruntime.StartInput{
RoomID: "workspace-1", AgentSessionID: "session-1", Provider: "submit-provenance-test", CWD: t.TempDir(),
}); err != nil {
t.Fatalf("Start() error = %v", err)
}
adapter := newAgentRuntimeAdapter(controller)
if err := adapter.DurablyReportSubmitProvenance(context.Background(), agentservice.RuntimeSubmitProvenanceInput{
WorkspaceID: "workspace-1", AgentSessionID: "session-1", TurnID: "turn-1",
ClientSubmitID: "submit-1", Content: agentservice.TextPromptContent("hello"),
CanonicalSubmitOccurredAtUnixMS: 1_234,
DisplayPrompt: "Visible hello",
}); err != nil {
t.Fatalf("DurablyReportSubmitProvenance() error = %v", err)
}
got := reporter.provenance
if got.WorkspaceID != "workspace-1" || got.Source.AgentID != "session-1" || len(got.MessageUpdates) != 1 {
t.Fatalf("provenance report = %#v", got)
}
message := got.MessageUpdates[0]
if message.TurnID != "turn-1" || message.Seq != 1_234 || message.OccurredAtUnixMS != 1_234 ||
message.Payload["clientSubmitId"] != "submit-1" || message.Payload["displayPrompt"] != "Visible hello" {
t.Fatalf("provenance message = %#v", message)
}
}
func TestAgentRuntimeAdapterPreservesProviderCapabilities(t *testing.T) {
controller := agentruntime.NewController(
[]agentruntime.Adapter{submitProvenanceAdapterTestProvider{}},
nil,
)
adapter := newAgentRuntimeAdapter(controller)
result, err := adapter.Start(t.Context(), agentservice.RuntimeStartInput{
WorkspaceID: "workspace-1",
AgentSessionID: "session-capabilities",
Provider: submitProvenanceAdapterTestProvider{}.Provider(),
})
if err != nil {
t.Fatalf("Start() error = %v", err)
}
if result.Session.Capabilities == nil ||
len(result.Session.Capabilities.Values) != 2 ||
result.Session.Capabilities.Values[0] != "imageInput" ||
result.Session.Capabilities.Values[1] != "interrupt" {
t.Fatalf("capabilities = %#v, want provider state capabilities", result.Session.Capabilities)
}
}
func TestAgentRuntimeAdapterReturnsClaudeSDKModelConfigOptions(t *testing.T) {
t.Setenv("TUTTI_CLAUDE_SDK_SIDECAR_TEST_DRIVER", "1")
t.Setenv("TUTTI_CLAUDE_SDK_SIDECAR_ENTRY_PATH", "")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
controller := agentruntime.NewController(
[]agentruntime.Adapter{agentruntime.NewClaudeCodeSDKAdapter(agentruntime.NewLocalProcessTransport())},
nil,
)
adapter := newAgentRuntimeAdapter(controller)
startResult, err := adapter.Start(ctx, agentservice.RuntimeStartInput{
WorkspaceID: "workspace-1",
AgentSessionID: "agent-session-1",
Provider: agentruntime.ProviderClaudeCode,
Cwd: t.TempDir(),
Title: "Claude Code",
Model: "haiku",
})
if err != nil {
t.Fatalf("Start() error = %v", err)
}
session := startResult.Session
defer func() {
_ = adapter.Close(context.Background(), agentservice.RuntimeCloseInput{
WorkspaceID: session.WorkspaceID,
AgentSessionID: session.ID,
})
}()
if !runtimeContextHasClaudeSDKModelConfigOptions(session.RuntimeContext) {
t.Fatalf("RuntimeContext = %#v, want SDK model config options", session.RuntimeContext)
}
}
func runtimeContextHasClaudeSDKModelConfigOptions(runtimeContext map[string]any) bool {
options, ok := runtimeContext["configOptions"].([]map[string]any)
if !ok {
return false
}
for _, option := range options {
if option["id"] != "model" || option["currentValue"] != "haiku" {
continue
}
models, ok := option["options"].([]map[string]string)
if !ok {
return false
}
var sawDefault bool
var sawHaiku bool
for _, model := range models {
if model["value"] == "default" {
sawDefault = true
}
if model["value"] == "haiku" {
sawHaiku = true
}
}
return sawDefault && sawHaiku
}
return false
}