Files
tutti/services/tuttid/api/daemon_tutti_mode_execution_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

254 lines
9.5 KiB
Go

package api
import (
"bytes"
"context"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
tuttigenerated "github.com/tutti-os/tutti/services/tuttid/api/generated"
executionbiz "github.com/tutti-os/tutti/services/tuttid/biz/tuttimodeexecution"
tuttimodeexecutionservice "github.com/tutti-os/tutti/services/tuttid/service/tuttimodeexecution"
)
func TestCancelTuttiModeExecutionUsesManagedExecutionPath(t *testing.T) {
t.Parallel()
var receivedWorkspaceID string
var receivedIssueID string
service := issueExecutionAPIStub{
cancelTuttiMode: func(_ context.Context, workspaceID, issueID string) (int, error) {
receivedWorkspaceID = workspaceID
receivedIssueID = issueID
return 2, nil
},
}
mux := http.NewServeMux()
RegisterRoutes(mux, NewRoutes(DaemonAPI{IssueExecutionService: service}))
request := httptest.NewRequest(
http.MethodPost,
"/v1/workspaces/workspace-1/tutti-executions/issue-1/cancel-execution",
nil,
)
response := httptest.NewRecorder()
mux.ServeHTTP(response, request)
if response.Code != http.StatusOK {
t.Fatalf("status=%d, want %d; body=%s", response.Code, http.StatusOK, response.Body.String())
}
if receivedWorkspaceID != "workspace-1" || receivedIssueID != "issue-1" {
t.Fatalf("managed cancel received workspace=%q issue=%q", receivedWorkspaceID, receivedIssueID)
}
if strings.TrimSpace(response.Body.String()) != `{"canceledRunCount":2}` {
t.Fatalf("body=%s", response.Body.String())
}
}
func TestArchiveTuttiModeExecutionUsesAuthenticatedLocalOperator(t *testing.T) {
t.Parallel()
var received tuttimodeexecutionservice.ArchiveInput
now := time.UnixMilli(1_700_003_000_000).UTC()
api := DaemonAPI{TuttiModeExecutionService: tuttiModeExecutionAPIStub{
archive: func(_ context.Context, input tuttimodeexecutionservice.ArchiveInput) (executionbiz.ArchiveOperation, error) {
received = input
return executionbiz.ArchiveOperation{
WorkspaceID: input.WorkspaceID, ExecutionID: "execution-1", IssueID: input.IssueID,
OperationID: "archive-1", RequestID: input.RequestID,
Status: executionbiz.ArchiveStatusCompleted, RequestedBy: input.RequestedBy,
Reason: input.Reason, CreatedAt: now, UpdatedAt: now, CompletedAt: now,
}, nil
},
}}
_, err := api.ArchiveTuttiModeExecution(context.Background(), tuttigenerated.ArchiveTuttiModeExecutionRequestObject{
WorkspaceID: "workspace-1", IssueID: "issue-1",
Body: &tuttigenerated.ArchiveTuttiModeExecutionRequest{
RequestId: "request-1", Reason: "stop",
},
})
if err != nil {
t.Fatalf("ArchiveTuttiModeExecution() error=%v", err)
}
if received.RequestedBy != authenticatedLocalOperatorActorID {
t.Fatalf("RequestedBy=%q, want authenticated actor %q", received.RequestedBy, authenticatedLocalOperatorActorID)
}
}
func TestArchiveTuttiModeExecutionHTTPContractRejectsSpoofedActor(t *testing.T) {
t.Parallel()
now := time.UnixMilli(1_700_003_000_000).UTC()
service := tuttiModeExecutionAPIStub{
archive: func(_ context.Context, input tuttimodeexecutionservice.ArchiveInput) (executionbiz.ArchiveOperation, error) {
return executionbiz.ArchiveOperation{
WorkspaceID: input.WorkspaceID, ExecutionID: "execution-1", IssueID: input.IssueID,
OperationID: "archive-1", RequestID: input.RequestID,
Status: executionbiz.ArchiveStatusCompleted, RequestedBy: input.RequestedBy,
Reason: input.Reason, CreatedAt: now, UpdatedAt: now, CompletedAt: now,
}, nil
},
}
mux := http.NewServeMux()
RegisterRoutes(mux, NewRoutes(DaemonAPI{TuttiModeExecutionService: service}))
request := httptest.NewRequest(
http.MethodPost,
"/v1/workspaces/workspace-1/tutti-executions/issue-1/archive",
bytes.NewBufferString(`{
"requestId":"request-1",
"requestedBy":"spoofed-user",
"reason":"stop"
}`),
)
request.Header.Set("Content-Type", "application/json")
response := httptest.NewRecorder()
mux.ServeHTTP(response, request)
if response.Code != http.StatusBadRequest {
t.Fatalf("status=%d, want %d; body=%s", response.Code, http.StatusBadRequest, response.Body.String())
}
}
func TestArchiveTuttiModeExecutionHTTPContractReturnsTypedConflict(t *testing.T) {
t.Parallel()
service := tuttiModeExecutionAPIStub{
archive: func(_ context.Context, _ tuttimodeexecutionservice.ArchiveInput) (executionbiz.ArchiveOperation, error) {
return executionbiz.ArchiveOperation{}, executionbiz.ErrExecutionConflict
},
}
mux := http.NewServeMux()
RegisterRoutes(mux, NewRoutes(DaemonAPI{TuttiModeExecutionService: service}))
request := httptest.NewRequest(
http.MethodPost,
"/v1/workspaces/workspace-1/tutti-executions/issue-1/archive",
bytes.NewBufferString(`{
"requestId":"request-1",
"reason":"stop"
}`),
)
request.Header.Set("Content-Type", "application/json")
response := httptest.NewRecorder()
mux.ServeHTTP(response, request)
if response.Code != http.StatusConflict {
t.Fatalf("status=%d, want %d; body=%s", response.Code, http.StatusConflict, response.Body.String())
}
body := response.Body.String()
for _, expected := range []string{
`"code":"tutti_mode_archive_conflict"`,
`"reason":"tutti_archive_request_conflict"`,
} {
if !strings.Contains(body, expected) {
t.Fatalf("body=%s, want %s", body, expected)
}
}
}
func TestArchiveTuttiModeExecutionReturnsDurableFailedOperation(t *testing.T) {
t.Parallel()
now := time.UnixMilli(1_700_003_000_000).UTC()
api := DaemonAPI{TuttiModeExecutionService: tuttiModeExecutionAPIStub{
archive: func(_ context.Context, input tuttimodeexecutionservice.ArchiveInput) (executionbiz.ArchiveOperation, error) {
return executionbiz.ArchiveOperation{
WorkspaceID: input.WorkspaceID, ExecutionID: "execution-1", IssueID: input.IssueID,
OperationID: "archive-1", RequestID: input.RequestID,
Status: executionbiz.ArchiveStatusFailed, RequestedBy: input.RequestedBy,
Reason: input.Reason, LastError: "cancel failed", CreatedAt: now, UpdatedAt: now,
}, errors.New("cancel failed")
},
}}
response, err := api.ArchiveTuttiModeExecution(context.Background(), tuttigenerated.ArchiveTuttiModeExecutionRequestObject{
WorkspaceID: "workspace-1", IssueID: "issue-1",
Body: &tuttigenerated.ArchiveTuttiModeExecutionRequest{
RequestId: "request-1", Reason: "stop",
},
})
if err != nil {
t.Fatalf("ArchiveTuttiModeExecution() error=%v", err)
}
operation, ok := response.(tuttigenerated.ArchiveTuttiModeExecution200JSONResponse)
if !ok || operation.Status != tuttigenerated.TuttiModeArchiveOperationStatusFailed ||
operation.LastError != "cancel failed" {
t.Fatalf("ArchiveTuttiModeExecution() response=%#v", response)
}
}
func TestProtectedSourceDeletionMapsSingleBatchAndClearToTypedConflict(t *testing.T) {
t.Parallel()
err := &executionbiz.ProtectedSourceError{
WorkspaceID: "workspace-1",
Issues: []executionbiz.ProtectedIssue{{
IssueID: "issue-1", ExecutionID: "execution-1",
SourceSessionID: "source-1", Status: executionbiz.StatusRunning,
}},
}
if _, ok := writeDeleteWorkspaceAgentSessionError(err).(tuttigenerated.DeleteWorkspaceAgentSession409JSONResponse); !ok {
t.Fatal("single deletion did not return 409")
}
if _, ok := writeDeleteWorkspaceAgentSessionsBatchError(err).(tuttigenerated.DeleteWorkspaceAgentSessionsBatch409JSONResponse); !ok {
t.Fatal("batch deletion did not return 409")
}
if _, ok := writeClearWorkspaceAgentSessionsError(err).(tuttigenerated.ClearWorkspaceAgentSessions409JSONResponse); !ok {
t.Fatal("clear deletion did not return 409")
}
}
func TestGetTuttiModeArchiveOperationRejectsOperationFromAnotherIssue(t *testing.T) {
t.Parallel()
api := DaemonAPI{TuttiModeExecutionService: tuttiModeExecutionAPIStub{
get: func(_ context.Context, workspaceID, operationID string) (executionbiz.ArchiveOperation, error) {
return executionbiz.ArchiveOperation{
WorkspaceID: workspaceID, IssueID: "issue-2", OperationID: operationID,
}, nil
},
}}
response, err := api.GetTuttiModeArchiveOperation(
context.Background(),
tuttigenerated.GetTuttiModeArchiveOperationRequestObject{
WorkspaceID: "workspace-1", IssueID: "issue-1",
Params: tuttigenerated.GetTuttiModeArchiveOperationParams{OperationId: "archive-1"},
},
)
if err != nil {
t.Fatalf("GetTuttiModeArchiveOperation() error=%v", err)
}
if _, ok := response.(tuttigenerated.GetTuttiModeArchiveOperation404JSONResponse); !ok {
t.Fatalf("GetTuttiModeArchiveOperation() response=%#v, want 404", response)
}
}
type tuttiModeExecutionAPIStub struct {
archive func(context.Context, tuttimodeexecutionservice.ArchiveInput) (executionbiz.ArchiveOperation, error)
get func(context.Context, string, string) (executionbiz.ArchiveOperation, error)
}
type issueExecutionAPIStub struct {
cancelTuttiMode func(context.Context, string, string) (int, error)
}
func (issueExecutionAPIStub) CancelIssueExecution(
context.Context, string, string,
) (int, error) {
panic("generic Issue cancellation must not serve the Tutti execution route")
}
func (stub issueExecutionAPIStub) CancelTuttiModeIssueExecution(
ctx context.Context, workspaceID, issueID string,
) (int, error) {
return stub.cancelTuttiMode(ctx, workspaceID, issueID)
}
func (stub tuttiModeExecutionAPIStub) Archive(
ctx context.Context, input tuttimodeexecutionservice.ArchiveInput,
) (executionbiz.ArchiveOperation, error) {
return stub.archive(ctx, input)
}
func (stub tuttiModeExecutionAPIStub) GetArchive(
ctx context.Context, workspaceID, operationID string,
) (executionbiz.ArchiveOperation, error) {
if stub.get == nil {
return executionbiz.ArchiveOperation{}, executionbiz.ErrExecutionNotFound
}
return stub.get(ctx, workspaceID, operationID)
}