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
254 lines
9.5 KiB
Go
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)
|
|
}
|