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
481 lines
15 KiB
Go
481 lines
15 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"testing"
|
|
"time"
|
|
|
|
agentactivitybiz "github.com/tutti-os/tutti/packages/agent/store-sqlite"
|
|
tuttigenerated "github.com/tutti-os/tutti/services/tuttid/api/generated"
|
|
agentservice "github.com/tutti-os/tutti/services/tuttid/service/agent"
|
|
)
|
|
|
|
func TestDaemonAPIGeneratedRoutesReadAgentSessionAttachment(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
RegisterRoutes(mux, NewRoutes(DaemonAPI{
|
|
AgentSessionService: stubAgentSessionService{
|
|
readAttachmentFn: func(_ context.Context, workspaceID string, agentSessionID string, attachmentID string) (agentservice.PromptAttachment, error) {
|
|
if workspaceID != "ws-1" {
|
|
t.Fatalf("workspaceID = %q, want ws-1", workspaceID)
|
|
}
|
|
if agentSessionID != "session-1" {
|
|
t.Fatalf("agentSessionID = %q, want session-1", agentSessionID)
|
|
}
|
|
if attachmentID != "attachment-1" {
|
|
t.Fatalf("attachmentID = %q, want attachment-1", attachmentID)
|
|
}
|
|
return agentservice.PromptAttachment{
|
|
AttachmentID: attachmentID,
|
|
MimeType: "image/png",
|
|
Data: "aW1hZ2U=",
|
|
}, nil
|
|
},
|
|
},
|
|
}))
|
|
|
|
recorder := performGeneratedRouteRequest(
|
|
t,
|
|
mux,
|
|
http.MethodGet,
|
|
"/v1/workspaces/ws-1/agent-sessions/session-1/attachments/attachment-1",
|
|
nil,
|
|
)
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusOK, recorder.Body.String())
|
|
}
|
|
|
|
var response tuttigenerated.WorkspaceAgentSessionAttachmentResponse
|
|
decodeGeneratedRouteResponse(t, recorder, &response)
|
|
if response.AttachmentId != "attachment-1" {
|
|
t.Fatalf("attachmentId = %q, want attachment-1", response.AttachmentId)
|
|
}
|
|
if response.MimeType != tuttigenerated.Imagepng {
|
|
t.Fatalf("mimeType = %q, want image/png", response.MimeType)
|
|
}
|
|
if response.Data != "aW1hZ2U=" {
|
|
t.Fatalf("data = %q, want base64 payload", response.Data)
|
|
}
|
|
}
|
|
|
|
func TestDaemonAPIGeneratedRoutesListAgentSessionMessages(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
RegisterRoutes(mux, NewRoutes(DaemonAPI{
|
|
AgentSessionService: stubAgentSessionService{
|
|
listMessagesFn: func(_ context.Context, workspaceID string, agentSessionID string, input agentservice.ListMessagesInput) (agentservice.SessionMessagesPage, error) {
|
|
if workspaceID != "ws-1" {
|
|
t.Fatalf("workspaceID = %q, want ws-1", workspaceID)
|
|
}
|
|
if agentSessionID != "agent-session-1" {
|
|
t.Fatalf("agentSessionID = %q, want agent-session-1", agentSessionID)
|
|
}
|
|
if input.BeforeVersion != 9 {
|
|
t.Fatalf("beforeVersion = %d, want 9", input.BeforeVersion)
|
|
}
|
|
if input.Order != agentactivitybiz.MessageOrderDesc {
|
|
t.Fatalf("order = %q, want desc", input.Order)
|
|
}
|
|
if input.Limit != 25 {
|
|
t.Fatalf("limit = %d, want 25", input.Limit)
|
|
}
|
|
return agentservice.SessionMessagesPage{
|
|
AgentSessionID: agentSessionID,
|
|
Messages: []agentservice.SessionMessage{
|
|
{
|
|
ID: 8,
|
|
AgentSessionID: agentSessionID,
|
|
MessageID: "msg-1",
|
|
TurnID: "turn-1",
|
|
Role: "assistant",
|
|
Kind: "text",
|
|
Payload: map[string]any{"content": "Done."},
|
|
StartedAtUnixMS: 1717200001000,
|
|
Version: 8,
|
|
},
|
|
},
|
|
LatestVersion: 8,
|
|
HasMore: false,
|
|
}, nil
|
|
},
|
|
},
|
|
}))
|
|
|
|
recorder := performGeneratedRouteRequest(
|
|
t,
|
|
mux,
|
|
http.MethodGet,
|
|
"/v1/workspaces/ws-1/agent-sessions/agent-session-1/messages?beforeVersion=9&order=desc&limit=25",
|
|
nil,
|
|
)
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusOK, recorder.Body.String())
|
|
}
|
|
|
|
var response tuttigenerated.WorkspaceAgentSessionMessagesResponse
|
|
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
|
|
t.Fatalf("unmarshal response: %v", err)
|
|
}
|
|
if response.AgentSessionId != "agent-session-1" {
|
|
t.Fatalf("agentSessionId = %q, want agent-session-1", response.AgentSessionId)
|
|
}
|
|
if response.LatestVersion != 8 {
|
|
t.Fatalf("latestVersion = %d, want 8", response.LatestVersion)
|
|
}
|
|
if response.HasMore {
|
|
t.Fatal("hasMore = true, want false")
|
|
}
|
|
if len(response.Messages) != 1 {
|
|
t.Fatalf("len(messages) = %d, want 1", len(response.Messages))
|
|
}
|
|
if response.Messages[0].MessageId != "msg-1" {
|
|
t.Fatalf("messageId = %q, want msg-1", response.Messages[0].MessageId)
|
|
}
|
|
if response.Messages[0].Sequence != 8 {
|
|
t.Fatalf("sequence = %d, want durable message id 8", response.Messages[0].Sequence)
|
|
}
|
|
if response.Messages[0].TurnId == nil || *response.Messages[0].TurnId != "turn-1" {
|
|
t.Fatalf("turnId = %v, want turn-1", response.Messages[0].TurnId)
|
|
}
|
|
if response.Messages[0].OccurredAtUnixMs != 1717200001000 {
|
|
t.Fatalf("occurredAtUnixMs = %d, want startedAt fallback", response.Messages[0].OccurredAtUnixMs)
|
|
}
|
|
}
|
|
|
|
// Protocol v2: messages without turn attribution are legitimate
|
|
// session-level messages and project turnId null instead of failing.
|
|
func TestDaemonAPIGeneratedRoutesProjectTurnlessAgentSessionMessagesAsSessionLevel(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
RegisterRoutes(mux, NewRoutes(DaemonAPI{
|
|
AgentSessionService: stubAgentSessionService{
|
|
listMessagesFn: func(_ context.Context, _ string, agentSessionID string, _ agentservice.ListMessagesInput) (agentservice.SessionMessagesPage, error) {
|
|
return agentservice.SessionMessagesPage{
|
|
AgentSessionID: agentSessionID,
|
|
Messages: []agentservice.SessionMessage{
|
|
{
|
|
ID: 8,
|
|
AgentSessionID: agentSessionID,
|
|
MessageID: "msg-turnless",
|
|
Role: "assistant",
|
|
Kind: "text",
|
|
Payload: map[string]any{"content": "Done."},
|
|
StartedAtUnixMS: 1717200001000,
|
|
Version: 8,
|
|
},
|
|
},
|
|
LatestVersion: 8,
|
|
}, nil
|
|
},
|
|
},
|
|
}))
|
|
|
|
recorder := performGeneratedRouteRequest(
|
|
t,
|
|
mux,
|
|
http.MethodGet,
|
|
"/v1/workspaces/ws-1/agent-sessions/agent-session-1/messages",
|
|
nil,
|
|
)
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusOK, recorder.Body.String())
|
|
}
|
|
|
|
var response tuttigenerated.WorkspaceAgentSessionMessagesResponse
|
|
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
|
|
t.Fatalf("unmarshal response error = %v", err)
|
|
}
|
|
if len(response.Messages) != 1 {
|
|
t.Fatalf("len(messages) = %d, want 1", len(response.Messages))
|
|
}
|
|
if response.Messages[0].TurnId != nil {
|
|
t.Fatalf("turnId = %v, want null", *response.Messages[0].TurnId)
|
|
}
|
|
}
|
|
|
|
func TestGeneratedWorkspaceAgentSafeIntegerBounds(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
value, err := generatedWorkspaceAgentSafeInteger(
|
|
"message version",
|
|
maxWorkspaceAgentJSONSafeInteger,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("maximum safe integer rejected: %v", err)
|
|
}
|
|
if value != int64(maxWorkspaceAgentJSONSafeInteger) {
|
|
t.Fatalf("value = %d, want %d", value, maxWorkspaceAgentJSONSafeInteger)
|
|
}
|
|
if _, err := generatedWorkspaceAgentSafeInteger(
|
|
"message version",
|
|
maxWorkspaceAgentJSONSafeInteger+1,
|
|
); err == nil {
|
|
t.Fatal("maximum safe integer plus one accepted")
|
|
}
|
|
}
|
|
|
|
func TestDaemonAPIGeneratedRoutesAcceptSafeAgentMessageCursorQueries(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
RegisterRoutes(mux, NewRoutes(DaemonAPI{
|
|
AgentSessionService: stubAgentSessionService{
|
|
listMessagesFn: func(_ context.Context, _ string, _ string, input agentservice.ListMessagesInput) (agentservice.SessionMessagesPage, error) {
|
|
if input.AfterVersion != maxWorkspaceAgentJSONSafeInteger {
|
|
t.Fatalf("afterVersion = %d, want %d", input.AfterVersion, maxWorkspaceAgentJSONSafeInteger)
|
|
}
|
|
if input.BeforeVersion != maxWorkspaceAgentJSONSafeInteger {
|
|
t.Fatalf("beforeVersion = %d, want %d", input.BeforeVersion, maxWorkspaceAgentJSONSafeInteger)
|
|
}
|
|
return agentservice.SessionMessagesPage{
|
|
AgentSessionID: "agent-session-1",
|
|
Messages: []agentservice.SessionMessage{},
|
|
LatestVersion: maxWorkspaceAgentJSONSafeInteger,
|
|
}, nil
|
|
},
|
|
},
|
|
}))
|
|
|
|
recorder := performGeneratedRouteRequest(
|
|
t,
|
|
mux,
|
|
http.MethodGet,
|
|
fmt.Sprintf(
|
|
"/v1/workspaces/ws-1/agent-sessions/agent-session-1/messages?afterVersion=%d&beforeVersion=%d",
|
|
maxWorkspaceAgentJSONSafeInteger,
|
|
maxWorkspaceAgentJSONSafeInteger,
|
|
),
|
|
nil,
|
|
)
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusOK, recorder.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestDaemonAPIGeneratedRoutesRejectUnsafeAgentMessageCursorQueries(t *testing.T) {
|
|
for _, parameter := range []string{"afterVersion", "beforeVersion"} {
|
|
t.Run(parameter, func(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
RegisterRoutes(mux, NewRoutes(DaemonAPI{
|
|
AgentSessionService: stubAgentSessionService{
|
|
listMessagesFn: func(context.Context, string, string, agentservice.ListMessagesInput) (agentservice.SessionMessagesPage, error) {
|
|
t.Fatal("unsafe cursor reached the service")
|
|
return agentservice.SessionMessagesPage{}, nil
|
|
},
|
|
},
|
|
}))
|
|
|
|
recorder := performGeneratedRouteRequest(
|
|
t,
|
|
mux,
|
|
http.MethodGet,
|
|
fmt.Sprintf(
|
|
"/v1/workspaces/ws-1/agent-sessions/agent-session-1/messages?%s=%d",
|
|
parameter,
|
|
maxWorkspaceAgentJSONSafeInteger+1,
|
|
),
|
|
nil,
|
|
)
|
|
if recorder.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusBadRequest, recorder.Body.String())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDaemonAPIGeneratedRoutesRejectUnsafeAgentMessageResponseIntegers(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
page agentservice.SessionMessagesPage
|
|
}{
|
|
{
|
|
name: "sequence",
|
|
page: agentservice.SessionMessagesPage{
|
|
AgentSessionID: "agent-session-1",
|
|
LatestVersion: 1,
|
|
Messages: []agentservice.SessionMessage{{
|
|
ID: maxWorkspaceAgentJSONSafeInteger + 1,
|
|
AgentSessionID: "agent-session-1",
|
|
MessageID: "message-1",
|
|
Kind: "text",
|
|
Role: "assistant",
|
|
Version: 1,
|
|
}},
|
|
},
|
|
},
|
|
{
|
|
name: "message version",
|
|
page: agentservice.SessionMessagesPage{
|
|
AgentSessionID: "agent-session-1",
|
|
LatestVersion: maxWorkspaceAgentJSONSafeInteger + 1,
|
|
Messages: []agentservice.SessionMessage{{
|
|
ID: 1,
|
|
AgentSessionID: "agent-session-1",
|
|
MessageID: "message-1",
|
|
Kind: "text",
|
|
Role: "assistant",
|
|
Version: maxWorkspaceAgentJSONSafeInteger + 1,
|
|
}},
|
|
},
|
|
},
|
|
{
|
|
name: "latest version",
|
|
page: agentservice.SessionMessagesPage{
|
|
AgentSessionID: "agent-session-1",
|
|
LatestVersion: maxWorkspaceAgentJSONSafeInteger + 1,
|
|
Messages: []agentservice.SessionMessage{},
|
|
},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
RegisterRoutes(mux, NewRoutes(DaemonAPI{
|
|
AgentSessionService: stubAgentSessionService{
|
|
listMessagesFn: func(context.Context, string, string, agentservice.ListMessagesInput) (agentservice.SessionMessagesPage, error) {
|
|
return test.page, nil
|
|
},
|
|
},
|
|
}))
|
|
|
|
recorder := performGeneratedRouteRequest(
|
|
t,
|
|
mux,
|
|
http.MethodGet,
|
|
"/v1/workspaces/ws-1/agent-sessions/agent-session-1/messages",
|
|
nil,
|
|
)
|
|
if recorder.Code != http.StatusBadGateway {
|
|
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusBadGateway, recorder.Body.String())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDaemonAPIGeneratedRoutesRejectUnsafeSessionMessageVersion(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
RegisterRoutes(mux, NewRoutes(DaemonAPI{
|
|
AgentSessionService: stubAgentSessionService{
|
|
getDetailFn: func(context.Context, string, string) (agentservice.SessionDetail, error) {
|
|
return agentservice.SessionDetail{
|
|
Session: agentservice.Session{
|
|
ID: "agent-session-1",
|
|
Kind: agentactivitybiz.SessionKindRoot,
|
|
MessageVersion: maxWorkspaceAgentJSONSafeInteger + 1,
|
|
Provider: "codex",
|
|
RailSectionKey: "conversations",
|
|
CreatedAt: time.UnixMilli(1),
|
|
},
|
|
ChildSessions: []agentservice.Session{},
|
|
Turns: []agentactivitybiz.Turn{},
|
|
}, nil
|
|
},
|
|
},
|
|
}))
|
|
|
|
recorder := performGeneratedRouteRequest(
|
|
t,
|
|
mux,
|
|
http.MethodGet,
|
|
"/v1/workspaces/ws-1/agent-sessions/agent-session-1",
|
|
nil,
|
|
)
|
|
if recorder.Code != http.StatusBadGateway {
|
|
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusBadGateway, recorder.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestDaemonAPIGeneratedRoutesListAgentGeneratedFiles(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
RegisterRoutes(mux, NewRoutes(DaemonAPI{
|
|
AgentSessionService: stubAgentSessionService{
|
|
listGeneratedFilesFn: func(_ context.Context, workspaceID string, input agentservice.ListGeneratedFilesInput) (agentservice.GeneratedFileList, error) {
|
|
if workspaceID != "ws-1" {
|
|
t.Fatalf("workspaceID = %q, want ws-1", workspaceID)
|
|
}
|
|
if input.Query != "report" {
|
|
t.Fatalf("query = %q, want report", input.Query)
|
|
}
|
|
if input.SectionKey != "project:/workspace" {
|
|
t.Fatalf("sectionKey = %q, want project:/workspace", input.SectionKey)
|
|
}
|
|
if input.Limit != 25 {
|
|
t.Fatalf("limit = %d, want 25", input.Limit)
|
|
}
|
|
if input.Cursor != "v1:25" {
|
|
t.Fatalf("cursor = %q, want v1:25", input.Cursor)
|
|
}
|
|
return agentservice.GeneratedFileList{
|
|
WorkspaceID: workspaceID,
|
|
HasMore: true,
|
|
NextCursor: "v1:50",
|
|
Files: []agentservice.GeneratedFile{
|
|
{
|
|
Label: "report.md",
|
|
Path: "/workspace/report.md",
|
|
},
|
|
},
|
|
}, nil
|
|
},
|
|
},
|
|
}))
|
|
|
|
recorder := performGeneratedRouteRequest(
|
|
t,
|
|
mux,
|
|
http.MethodGet,
|
|
"/v1/workspaces/ws-1/agent-generated-files?query=report§ionKey=project%3A%2Fworkspace&agentTargetIds=local%3Acodex&agentTargetIds=local%3Aclaude-code&cursor=v1%3A25&limit=25",
|
|
nil,
|
|
)
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusOK, recorder.Body.String())
|
|
}
|
|
var response tuttigenerated.WorkspaceAgentGeneratedFileListResponse
|
|
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
|
|
t.Fatalf("unmarshal response: %v", err)
|
|
}
|
|
if response.WorkspaceId != "ws-1" {
|
|
t.Fatalf("workspaceId = %q, want ws-1", response.WorkspaceId)
|
|
}
|
|
if len(response.Entries) != 1 {
|
|
t.Fatalf("len(entries) = %d, want 1", len(response.Entries))
|
|
}
|
|
if response.Entries[0].Path != "/workspace/report.md" {
|
|
t.Fatalf("entry path = %q, want /workspace/report.md", response.Entries[0].Path)
|
|
}
|
|
if !response.HasMore || response.NextCursor == nil || *response.NextCursor != "v1:50" {
|
|
t.Fatalf("pagination = hasMore:%v nextCursor:%v", response.HasMore, response.NextCursor)
|
|
}
|
|
}
|
|
|
|
func TestDaemonAPIGeneratedRoutesRejectsTooManyAgentTargetFilters(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
serviceCalls := 0
|
|
RegisterRoutes(mux, NewRoutes(DaemonAPI{
|
|
AgentSessionService: stubAgentSessionService{
|
|
listGeneratedFilesFn: func(context.Context, string, agentservice.ListGeneratedFilesInput) (agentservice.GeneratedFileList, error) {
|
|
serviceCalls++
|
|
return agentservice.GeneratedFileList{}, nil
|
|
},
|
|
},
|
|
}))
|
|
|
|
query := make(url.Values)
|
|
query.Set("sectionKey", "conversations")
|
|
for index := 0; index <= agentservice.MaxGeneratedFileAgentTargetFilters; index++ {
|
|
query.Add("agentTargetIds", fmt.Sprintf("agent-%d", index))
|
|
}
|
|
recorder := performGeneratedRouteRequest(
|
|
t,
|
|
mux,
|
|
http.MethodGet,
|
|
"/v1/workspaces/ws-1/agent-generated-files?"+query.Encode(),
|
|
nil,
|
|
)
|
|
if recorder.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusBadRequest, recorder.Body.String())
|
|
}
|
|
if serviceCalls != 0 {
|
|
t.Fatalf("service calls = %d, want 0", serviceCalls)
|
|
}
|
|
}
|