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
521 lines
16 KiB
Go
521 lines
16 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/tutti-os/tutti/packages/agent/daemon/providerregistry"
|
|
agenthost "github.com/tutti-os/tutti/packages/agent/host"
|
|
workspaceagentbiz "github.com/tutti-os/tutti/services/tuttid/biz/workspaceagent"
|
|
agentservice "github.com/tutti-os/tutti/services/tuttid/service/agent"
|
|
reporterservice "github.com/tutti-os/tutti/services/tuttid/service/reporter"
|
|
workspaceservice "github.com/tutti-os/tutti/services/tuttid/service/workspace"
|
|
tuttitypes "github.com/tutti-os/tutti/services/tuttid/types"
|
|
)
|
|
|
|
type recordingIssueRunAgentSessionCreator struct {
|
|
workspaceID string
|
|
input agentservice.CreateSessionInput
|
|
}
|
|
|
|
func (r *recordingIssueRunAgentSessionCreator) CreateWithResult(
|
|
_ context.Context,
|
|
workspaceID string,
|
|
input agentservice.CreateSessionInput,
|
|
) (agentservice.CreateSessionResult, error) {
|
|
r.workspaceID = workspaceID
|
|
r.input = input
|
|
return agentservice.CreateSessionResult{}, nil
|
|
}
|
|
|
|
func TestIssueRunAgentLauncherForwardsSourceRailPlacement(t *testing.T) {
|
|
creator := &recordingIssueRunAgentSessionCreator{}
|
|
launcher := issueRunAgentLauncher{Sessions: creator}
|
|
|
|
err := launcher.Launch(context.Background(), workspaceservice.IssueRunLaunch{
|
|
WorkspaceID: "workspace-1",
|
|
ClientSubmitID: "issue-run:run-1",
|
|
AgentSessionID: "delegate-1",
|
|
AgentTargetID: "local-codex",
|
|
RunID: "run-1",
|
|
Title: "Delegated task",
|
|
Prompt: "Implement the task",
|
|
ExecutionDirectory: "/tmp/task-worktree",
|
|
RailPlacement: &workspaceservice.IssueRunRailPlacement{
|
|
Kind: " project ",
|
|
ProjectPath: " /workspace/project ",
|
|
SectionKey: " project:/workspace/project ",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Launch() error = %v", err)
|
|
}
|
|
if creator.workspaceID != "workspace-1" {
|
|
t.Fatalf("Create() workspace = %q, want workspace-1", creator.workspaceID)
|
|
}
|
|
if creator.input.Cwd == nil || *creator.input.Cwd != "/tmp/task-worktree" {
|
|
t.Fatalf("Create() cwd = %#v, want isolated worktree", creator.input.Cwd)
|
|
}
|
|
if creator.input.RailPlacement == nil {
|
|
t.Fatal("Create() rail placement is nil")
|
|
}
|
|
if creator.input.RailPlacement.Version != 1 ||
|
|
creator.input.RailPlacement.Kind != "project" ||
|
|
creator.input.RailPlacement.ProjectPath != "/workspace/project" ||
|
|
creator.input.RailPlacement.SectionKey != "project:/workspace/project" {
|
|
t.Fatalf("Create() rail placement = %#v", creator.input.RailPlacement)
|
|
}
|
|
}
|
|
|
|
func TestIssueRunAgentLauncherForwardsImageAttachments(t *testing.T) {
|
|
creator := &recordingIssueRunAgentSessionCreator{}
|
|
err := (issueRunAgentLauncher{Sessions: creator}).Launch(
|
|
context.Background(),
|
|
workspaceservice.IssueRunLaunch{
|
|
WorkspaceID: "workspace-1",
|
|
ClientSubmitID: "issue-run:run-1",
|
|
AgentSessionID: "delegate-1",
|
|
AgentTargetID: "local-codex",
|
|
Prompt: "Inspect this screenshot",
|
|
Attachments: []workspaceservice.IssueRunImageAttachment{{
|
|
MimeType: "image/png",
|
|
Name: "capture.png",
|
|
Path: "/state/agent-prompt-assets/issues/capture.png",
|
|
}},
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Launch() error = %v", err)
|
|
}
|
|
if len(creator.input.InitialContent) != 2 {
|
|
t.Fatalf("InitialContent = %#v, want text and image", creator.input.InitialContent)
|
|
}
|
|
image := creator.input.InitialContent[1]
|
|
if image.Type != "image" || image.MimeType != "image/png" || image.Name != "capture.png" || image.Path != "/state/agent-prompt-assets/issues/capture.png" {
|
|
t.Fatalf("image content = %#v", image)
|
|
}
|
|
}
|
|
|
|
func TestIssueRunAgentLauncherForwardsSessionVisibility(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
hideSession bool
|
|
wantVisible bool
|
|
}{
|
|
{name: "default launches stay visible", hideSession: false, wantVisible: true},
|
|
{name: "hidden launches create invisible sessions", hideSession: true, wantVisible: false},
|
|
}
|
|
for _, testCase := range cases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
creator := &recordingIssueRunAgentSessionCreator{}
|
|
launcher := issueRunAgentLauncher{Sessions: creator}
|
|
|
|
err := launcher.Launch(context.Background(), workspaceservice.IssueRunLaunch{
|
|
WorkspaceID: "workspace-1",
|
|
ClientSubmitID: "issue-run:run-1",
|
|
AgentSessionID: "delegate-1",
|
|
AgentTargetID: "local-codex",
|
|
RunID: "run-1",
|
|
Title: "Delegated task",
|
|
Prompt: "Implement the task",
|
|
HideSession: testCase.hideSession,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Launch() error = %v", err)
|
|
}
|
|
if creator.input.Visible == nil || *creator.input.Visible != testCase.wantVisible {
|
|
t.Fatalf("Create() visible = %#v, want %v", creator.input.Visible, testCase.wantVisible)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type fakeIssueSourceSessionReader struct {
|
|
session agentservice.PersistedSession
|
|
found bool
|
|
}
|
|
|
|
func (r fakeIssueSourceSessionReader) GetSession(string, string) (agentservice.PersistedSession, bool) {
|
|
return r.session, r.found
|
|
}
|
|
|
|
func (fakeIssueSourceSessionReader) ListSessions(string) ([]agentservice.PersistedSession, bool) {
|
|
return nil, false
|
|
}
|
|
|
|
func (fakeIssueSourceSessionReader) SessionDeleted(context.Context, string, string) (bool, error) {
|
|
return false, nil
|
|
}
|
|
|
|
func TestIssueSourceSessionContextResolverReturnsExecutionAndRailIdentity(t *testing.T) {
|
|
resolver := issueSourceSessionContextResolver{Sessions: fakeIssueSourceSessionReader{
|
|
found: true,
|
|
session: agentservice.PersistedSession{
|
|
Cwd: " /workspace/project ",
|
|
RailSectionKind: " project ",
|
|
RailProjectPath: " /workspace/project ",
|
|
RailSectionKey: " project:/workspace/project ",
|
|
},
|
|
}}
|
|
|
|
got, ok := resolver.ResolveSourceSessionContext("workspace-1", "planning-session")
|
|
if !ok {
|
|
t.Fatal("ResolveSourceSessionContext() found = false")
|
|
}
|
|
if got.WorkingDirectory != "/workspace/project" {
|
|
t.Fatalf("working directory = %q, want /workspace/project", got.WorkingDirectory)
|
|
}
|
|
if got.RailPlacement == nil ||
|
|
got.RailPlacement.Kind != "project" ||
|
|
got.RailPlacement.ProjectPath != "/workspace/project" ||
|
|
got.RailPlacement.SectionKey != "project:/workspace/project" {
|
|
t.Fatalf("rail placement = %#v", got.RailPlacement)
|
|
}
|
|
}
|
|
|
|
func TestIssueSourceSessionContextResolverFallsBackToProjectPathWhenCwdIsEmpty(t *testing.T) {
|
|
resolver := issueSourceSessionContextResolver{Sessions: fakeIssueSourceSessionReader{
|
|
found: true,
|
|
session: agentservice.PersistedSession{
|
|
RailSectionKind: "project",
|
|
RailProjectPath: " /workspace/project ",
|
|
RailSectionKey: "project:/workspace/project",
|
|
},
|
|
}}
|
|
|
|
got, ok := resolver.ResolveSourceSessionContext("workspace-1", "planning-session")
|
|
if !ok {
|
|
t.Fatal("ResolveSourceSessionContext() found = false")
|
|
}
|
|
if got.WorkingDirectory != "/workspace/project" {
|
|
t.Fatalf("working directory = %q, want project-path fallback", got.WorkingDirectory)
|
|
}
|
|
}
|
|
|
|
type fakeAnalyticsDebugEventStream struct{}
|
|
|
|
func (fakeAnalyticsDebugEventStream) PublishFromServer(context.Context, string, []byte) error {
|
|
return nil
|
|
}
|
|
|
|
func TestResolveAnalyticsDebugPublisherAllowsProductionAnalyticsDebugStream(t *testing.T) {
|
|
got := resolveAnalyticsDebugPublisher(tuttitypes.AnalyticsConfig{
|
|
AppID: 20004092,
|
|
AppKey: "app-key",
|
|
ChannelDomain: "https://example.test",
|
|
}, fakeAnalyticsDebugEventStream{})
|
|
|
|
if _, ok := got.(analyticsDebugEventPublisher); !ok {
|
|
t.Fatalf("debug publisher = %T, want analyticsDebugEventPublisher", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveAnalyticsDebugPublisherSkipsDisabledAnalytics(t *testing.T) {
|
|
got := resolveAnalyticsDebugPublisher(tuttitypes.AnalyticsConfig{
|
|
Disabled: true,
|
|
AppID: 20004092,
|
|
AppKey: "app-key",
|
|
ChannelDomain: "https://example.test",
|
|
}, fakeAnalyticsDebugEventStream{})
|
|
|
|
if got != nil {
|
|
t.Fatalf("debug publisher = %T, want nil", got)
|
|
}
|
|
}
|
|
|
|
func TestTuttiDesktopCommandNetworkAccessPolicyAllowsOptedInAppServers(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, test := range []struct {
|
|
provider string
|
|
want bool
|
|
}{
|
|
{provider: providerregistry.TuttiAgentProviderID, want: true},
|
|
{provider: providerregistry.CodexProviderID, want: true},
|
|
{provider: providerregistry.ClaudeCodeProviderID, want: false},
|
|
{provider: "acp:custom-agent", want: false},
|
|
{provider: "", want: false},
|
|
} {
|
|
if got := tuttiDesktopCommandNetworkAccessPolicy(test.provider); got != test.want {
|
|
t.Fatalf(
|
|
"tuttiDesktopCommandNetworkAccessPolicy(%q) = %t, want %t",
|
|
test.provider,
|
|
got,
|
|
test.want,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTuttiModeWakeRecoveryStartsOnlyFromListenerReadyHook(t *testing.T) {
|
|
calls := 0
|
|
wiring := &tuttiWiring{
|
|
tuttiModeWakeRecoveryStarter: func() {
|
|
calls++
|
|
},
|
|
}
|
|
if calls != 0 {
|
|
t.Fatalf("wake recovery started during wiring construction: calls=%d", calls)
|
|
}
|
|
|
|
wiring.startTuttiModeWakeRecovery()
|
|
if calls != 1 {
|
|
t.Fatalf("listener-ready wake recovery calls=%d, want 1", calls)
|
|
}
|
|
wiring.startTuttiModeWakeRecovery()
|
|
if calls != 1 {
|
|
t.Fatalf("listener-ready wake recovery replay calls=%d, want one-shot", calls)
|
|
}
|
|
}
|
|
|
|
type recordingWorkspaceAgentTargetResolverSetter struct {
|
|
resolver agentservice.WorkspaceAgentTargetResolver
|
|
}
|
|
|
|
func (r *recordingWorkspaceAgentTargetResolverSetter) SetWorkspaceAgentTargetResolver(
|
|
resolver agentservice.WorkspaceAgentTargetResolver,
|
|
) {
|
|
r.resolver = resolver
|
|
}
|
|
|
|
type fakeWorkspaceAgentTargetResolver struct{}
|
|
|
|
func (fakeWorkspaceAgentTargetResolver) GetWorkspaceAgent(
|
|
context.Context,
|
|
string,
|
|
string,
|
|
) (workspaceagentbiz.Agent, error) {
|
|
return workspaceagentbiz.Agent{}, nil
|
|
}
|
|
|
|
func TestConfigureWorkspaceAgentProjectionWiresProjectionResolver(t *testing.T) {
|
|
activityProjection := &recordingWorkspaceAgentTargetResolverSetter{}
|
|
workspaceAgentTargets := fakeWorkspaceAgentTargetResolver{}
|
|
|
|
configureWorkspaceAgentProjection(
|
|
activityProjection,
|
|
workspaceAgentTargets,
|
|
)
|
|
|
|
if activityProjection.resolver != workspaceAgentTargets {
|
|
t.Fatalf(
|
|
"activity projection WorkspaceAgentTargetResolver = %T, want workspace agent service",
|
|
activityProjection.resolver,
|
|
)
|
|
}
|
|
}
|
|
|
|
var _ reporterservice.DebugPublisher = analyticsDebugEventPublisher{}
|
|
|
|
type recordingIssueRunSessionCreator struct {
|
|
workspaceID string
|
|
input agentservice.CreateSessionInput
|
|
result agentservice.CreateSessionResult
|
|
err error
|
|
createCalls int
|
|
legacyCalls int
|
|
}
|
|
|
|
type recordingIssueRunTurnFinder struct {
|
|
turnID string
|
|
found bool
|
|
err error
|
|
calls int
|
|
ref agenthost.SessionRef
|
|
clientSubmitID string
|
|
}
|
|
|
|
func (finder *recordingIssueRunTurnFinder) FindTurnByClientSubmitID(
|
|
_ context.Context,
|
|
ref agenthost.SessionRef,
|
|
clientSubmitID string,
|
|
) (string, bool, error) {
|
|
finder.calls++
|
|
finder.ref = ref
|
|
finder.clientSubmitID = clientSubmitID
|
|
return finder.turnID, finder.found, finder.err
|
|
}
|
|
|
|
func (creator *recordingIssueRunSessionCreator) Create(
|
|
_ context.Context,
|
|
workspaceID string,
|
|
input agentservice.CreateSessionInput,
|
|
) (agentservice.Session, error) {
|
|
creator.legacyCalls++
|
|
creator.workspaceID = workspaceID
|
|
creator.input = input
|
|
return creator.result.Session, creator.err
|
|
}
|
|
|
|
func (creator *recordingIssueRunSessionCreator) CreateWithResult(
|
|
_ context.Context,
|
|
workspaceID string,
|
|
input agentservice.CreateSessionInput,
|
|
) (agentservice.CreateSessionResult, error) {
|
|
creator.createCalls++
|
|
creator.workspaceID = workspaceID
|
|
creator.input = input
|
|
return creator.result, creator.err
|
|
}
|
|
|
|
func TestIssueRunAgentLauncherUsesPersistedClientSubmitID(t *testing.T) {
|
|
creator := &recordingIssueRunSessionCreator{}
|
|
err := (issueRunAgentLauncher{Sessions: creator}).Launch(
|
|
context.Background(),
|
|
workspaceservice.IssueRunLaunch{
|
|
WorkspaceID: "workspace-1", RunID: "run-1",
|
|
ClientSubmitID: "persisted-submit-id", AgentSessionID: "session-1",
|
|
AgentTargetID: "local:codex", Title: "Task",
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Launch() error = %v", err)
|
|
}
|
|
if creator.workspaceID != "workspace-1" ||
|
|
creator.input.ClientSubmitID != "persisted-submit-id" {
|
|
t.Fatalf("Create() scope/input = %q/%#v", creator.workspaceID, creator.input)
|
|
}
|
|
}
|
|
|
|
func TestIssueRunAgentLauncherClassifiesPreTurnFailureAsNotStarted(t *testing.T) {
|
|
cause := errors.New("provider rejected launch")
|
|
creator := &recordingIssueRunSessionCreator{err: cause}
|
|
finder := &recordingIssueRunTurnFinder{}
|
|
|
|
err := (issueRunAgentLauncher{Sessions: creator, Host: finder}).Launch(
|
|
context.Background(),
|
|
workspaceservice.IssueRunLaunch{
|
|
WorkspaceID: "workspace-1", ClientSubmitID: "submit-1",
|
|
AgentSessionID: "session-1", AgentTargetID: "local:codex", Title: "Task",
|
|
},
|
|
)
|
|
|
|
if !errors.Is(err, cause) {
|
|
t.Fatalf("Launch() error = %v, want wrapped cause %v", err, cause)
|
|
}
|
|
if err == cause || fmt.Sprintf("%T", err) != "workspace.issueRunLaunchNotStartedError" {
|
|
t.Fatalf("Launch() error type = %T, want typed IssueRunLaunchNotStartedError", err)
|
|
}
|
|
if creator.createCalls != 1 || creator.legacyCalls != 0 {
|
|
t.Fatalf(
|
|
"session creation calls = CreateWithResult:%d Create:%d, want 1/0",
|
|
creator.createCalls,
|
|
creator.legacyCalls,
|
|
)
|
|
}
|
|
if finder.calls != 1 ||
|
|
finder.ref != (agenthost.SessionRef{WorkspaceID: "workspace-1", AgentSessionID: "session-1"}) ||
|
|
finder.clientSubmitID != "submit-1" {
|
|
t.Fatalf(
|
|
"canonical Turn lookup = calls:%d ref:%#v submit:%q, want 1/workspace-1/session-1/submit-1",
|
|
finder.calls,
|
|
finder.ref,
|
|
finder.clientSubmitID,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestIssueRunAgentLauncherKeepsDeliveryUnknownRecoverable(t *testing.T) {
|
|
deliveryErr := fmt.Errorf("provider response lost: %w", agentservice.ErrSubmitDeliveryUnknown)
|
|
creator := &recordingIssueRunSessionCreator{err: deliveryErr}
|
|
finder := &recordingIssueRunTurnFinder{}
|
|
|
|
err := (issueRunAgentLauncher{Sessions: creator, Host: finder}).Launch(
|
|
context.Background(),
|
|
workspaceservice.IssueRunLaunch{
|
|
WorkspaceID: "workspace-1", ClientSubmitID: "submit-1",
|
|
AgentSessionID: "session-1", AgentTargetID: "local:codex", Title: "Task",
|
|
},
|
|
)
|
|
|
|
if err != deliveryErr {
|
|
t.Fatalf("Launch() error = %v (%T), want unchanged ambiguous error", err, err)
|
|
}
|
|
if creator.createCalls != 1 || creator.legacyCalls != 0 {
|
|
t.Fatalf(
|
|
"session creation calls = CreateWithResult:%d Create:%d, want 1/0",
|
|
creator.createCalls,
|
|
creator.legacyCalls,
|
|
)
|
|
}
|
|
if finder.calls != 0 {
|
|
t.Fatalf("canonical Turn lookup calls = %d, want 0 for delivery-unknown", finder.calls)
|
|
}
|
|
}
|
|
|
|
func TestIssueRunAgentLauncherKeepsTurnIdentifiedFailureRecoverable(t *testing.T) {
|
|
cause := errors.New("response failed after canonical turn")
|
|
creator := &recordingIssueRunSessionCreator{
|
|
result: agentservice.CreateSessionResult{TurnID: "turn-1"},
|
|
err: cause,
|
|
}
|
|
finder := &recordingIssueRunTurnFinder{}
|
|
|
|
err := (issueRunAgentLauncher{Sessions: creator, Host: finder}).Launch(
|
|
context.Background(),
|
|
workspaceservice.IssueRunLaunch{
|
|
WorkspaceID: "workspace-1", ClientSubmitID: "submit-1",
|
|
AgentSessionID: "session-1", AgentTargetID: "local:codex", Title: "Task",
|
|
},
|
|
)
|
|
|
|
if err != cause {
|
|
t.Fatalf("Launch() error = %v (%T), want unchanged recoverable error", err, err)
|
|
}
|
|
if creator.createCalls != 1 || creator.legacyCalls != 0 {
|
|
t.Fatalf(
|
|
"session creation calls = CreateWithResult:%d Create:%d, want 1/0",
|
|
creator.createCalls,
|
|
creator.legacyCalls,
|
|
)
|
|
}
|
|
if finder.calls != 0 {
|
|
t.Fatalf("canonical Turn lookup calls = %d, want 0 for identified Turn", finder.calls)
|
|
}
|
|
}
|
|
|
|
func TestIssueRunAgentLauncherKeepsCanonicalTurnLookupHitRecoverable(t *testing.T) {
|
|
cause := errors.New("submit claim read failed after canonical creation")
|
|
creator := &recordingIssueRunSessionCreator{err: cause}
|
|
finder := &recordingIssueRunTurnFinder{turnID: "turn-existing", found: true}
|
|
|
|
err := (issueRunAgentLauncher{Sessions: creator, Host: finder}).Launch(
|
|
context.Background(),
|
|
workspaceservice.IssueRunLaunch{
|
|
WorkspaceID: "workspace-1", ClientSubmitID: "submit-1",
|
|
AgentSessionID: "session-1", AgentTargetID: "local:codex", Title: "Task",
|
|
},
|
|
)
|
|
|
|
if err != cause {
|
|
t.Fatalf("Launch() error = %v (%T), want unchanged recoverable error", err, err)
|
|
}
|
|
if finder.calls != 1 {
|
|
t.Fatalf("canonical Turn lookup calls = %d, want 1", finder.calls)
|
|
}
|
|
}
|
|
|
|
func TestIssueRunAgentLauncherKeepsCanonicalTurnLookupErrorRecoverable(t *testing.T) {
|
|
cause := errors.New("submit claim read failed")
|
|
lookupErr := errors.New("canonical store is temporarily unavailable")
|
|
creator := &recordingIssueRunSessionCreator{err: cause}
|
|
finder := &recordingIssueRunTurnFinder{err: lookupErr}
|
|
|
|
err := (issueRunAgentLauncher{Sessions: creator, Host: finder}).Launch(
|
|
context.Background(),
|
|
workspaceservice.IssueRunLaunch{
|
|
WorkspaceID: "workspace-1", ClientSubmitID: "submit-1",
|
|
AgentSessionID: "session-1", AgentTargetID: "local:codex", Title: "Task",
|
|
},
|
|
)
|
|
|
|
if err != cause {
|
|
t.Fatalf("Launch() error = %v (%T), want unchanged recoverable error", err, err)
|
|
}
|
|
if finder.calls != 1 {
|
|
t.Fatalf("canonical Turn lookup calls = %d, want 1", finder.calls)
|
|
}
|
|
}
|