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

228 lines
8.9 KiB
Go

package api
import (
"context"
"net/http"
"testing"
agenthost "github.com/tutti-os/tutti/packages/agent/host"
tuttigenerated "github.com/tutti-os/tutti/services/tuttid/api/generated"
agentservice "github.com/tutti-os/tutti/services/tuttid/service/agent"
agentmaintenance "github.com/tutti-os/tutti/services/tuttid/service/agentmaintenance"
)
type stubWorkspaceDeletedAgentSessionService struct {
stubAgentSessionService
listDeletedFn func(context.Context, string, agentservice.ListDeletedSessionsInput) (agentservice.DeletedSessionPage, error)
restoreDeletedFn func(context.Context, string, string) (agentservice.RestoreDeletedSessionResult, error)
}
func (s stubWorkspaceDeletedAgentSessionService) ListDeletedSessions(
ctx context.Context,
workspaceID string,
input agentservice.ListDeletedSessionsInput,
) (agentservice.DeletedSessionPage, error) {
if s.listDeletedFn == nil {
return agentservice.DeletedSessionPage{}, nil
}
return s.listDeletedFn(ctx, workspaceID, input)
}
func (s stubWorkspaceDeletedAgentSessionService) RestoreDeletedSession(
ctx context.Context,
workspaceID string,
agentSessionID string,
) (agentservice.RestoreDeletedSessionResult, error) {
if s.restoreDeletedFn == nil {
return agentservice.RestoreDeletedSessionResult{}, nil
}
return s.restoreDeletedFn(ctx, workspaceID, agentSessionID)
}
func TestDaemonAPIListsWorkspaceDeletedAgentSessions(t *testing.T) {
mux := http.NewServeMux()
RegisterRoutes(mux, NewRoutes(DaemonAPI{
AgentSessionService: stubWorkspaceDeletedAgentSessionService{
listDeletedFn: func(_ context.Context, workspaceID string, input agentservice.ListDeletedSessionsInput) (agentservice.DeletedSessionPage, error) {
if workspaceID != "ws-1" || input.SearchQuery != "hello" || input.Cursor != "20|session-0" || input.Limit != 25 {
t.Fatalf("workspace/input = %q/%#v", workspaceID, input)
}
if input.RailSectionKey == nil || *input.RailSectionKey != "conversations" {
t.Fatalf("rail section key = %#v, want conversations", input.RailSectionKey)
}
return agentservice.DeletedSessionPage{
Sessions: []agentservice.DeletedSessionSummary{{
AgentSessionID: "session-1", Title: "Hello", UpdatedAtUnixMS: 20,
RailSectionKey: "conversations",
DeletedAtUnixMS: 30, Restorable: false, UnavailableReason: "legacyDataUnavailable",
}},
ProjectOptions: []agentservice.DeletedSessionProjectOption{{
RailSectionKey: "project:/projects/tutti", ProjectPath: "/projects/tutti",
ProjectLabel: "tutti", ProjectAvailable: false,
}},
TotalCount: 1, WorkspaceTotalCount: 3, HasMore: true, NextCursor: "10|session-2",
}, nil
},
},
}))
recorder := performGeneratedRouteRequest(
t,
mux,
http.MethodGet,
"/v1/workspaces/ws-1/deleted-agent-sessions?searchQuery=hello&railSectionKey=conversations&cursor=20%7Csession-0&limit=25",
nil,
)
if recorder.Code != http.StatusOK {
t.Fatalf("status = %d; body: %s", recorder.Code, recorder.Body.String())
}
var response tuttigenerated.WorkspaceDeletedAgentSessionListResponse
decodeGeneratedRouteResponse(t, recorder, &response)
if len(response.Sessions) != 1 || response.Sessions[0].RailSectionKey != "conversations" || response.Sessions[0].ProjectPath != nil || response.Sessions[0].UnavailableReason == nil {
t.Fatalf("sessions = %#v", response.Sessions)
}
if response.TotalCount != 1 || response.WorkspaceTotalCount != 3 || !response.HasMore || response.NextCursor == nil {
t.Fatalf("page = %#v", response)
}
if len(response.ProjectOptions) != 1 || response.ProjectOptions[0].RailSectionKey != "project:/projects/tutti" || response.ProjectOptions[0].ProjectAvailable {
t.Fatalf("project options = %#v", response.ProjectOptions)
}
}
func TestDaemonAPIRejectsAmbiguousDeletedSessionProjectFilter(t *testing.T) {
mux := http.NewServeMux()
RegisterRoutes(mux, NewRoutes(DaemonAPI{
AgentSessionService: stubWorkspaceDeletedAgentSessionService{},
}))
recorder := performGeneratedRouteRequest(
t,
mux,
http.MethodGet,
"/v1/workspaces/ws-1/deleted-agent-sessions?railSectionKey=conversations&projectPath=%2Fprojects%2Ftutti",
nil,
)
if recorder.Code != http.StatusBadRequest {
t.Fatalf("status = %d; body: %s", recorder.Code, recorder.Body.String())
}
}
func TestDaemonAPIRestoresWorkspaceDeletedAgentSession(t *testing.T) {
mux := http.NewServeMux()
RegisterRoutes(mux, NewRoutes(DaemonAPI{
AgentSessionService: stubWorkspaceDeletedAgentSessionService{
restoreDeletedFn: func(_ context.Context, workspaceID string, agentSessionID string) (agentservice.RestoreDeletedSessionResult, error) {
if workspaceID != "ws-1" || agentSessionID != "session-1" {
t.Fatalf("workspace/session = %q/%q", workspaceID, agentSessionID)
}
return agentservice.RestoreDeletedSessionResult{Restored: true}, nil
},
},
}))
recorder := performGeneratedRouteRequest(
t, mux, http.MethodPost,
"/v1/workspaces/ws-1/deleted-agent-sessions/session-1/restore", nil,
)
if recorder.Code != http.StatusOK {
t.Fatalf("status = %d; body: %s", recorder.Code, recorder.Body.String())
}
var response tuttigenerated.RestoreWorkspaceDeletedAgentSessionResponse
decodeGeneratedRouteResponse(t, recorder, &response)
if response.AgentSessionId != "session-1" || !response.Restored {
t.Fatalf("response = %#v", response)
}
}
func TestDaemonAPIMapsUnavailableDeletedSessionRestoreToConflict(t *testing.T) {
mux := http.NewServeMux()
RegisterRoutes(mux, NewRoutes(DaemonAPI{
AgentSessionService: stubWorkspaceDeletedAgentSessionService{
restoreDeletedFn: func(context.Context, string, string) (agentservice.RestoreDeletedSessionResult, error) {
return agentservice.RestoreDeletedSessionResult{}, agenthost.ErrDeletedSessionNotRestorable
},
},
}))
recorder := performGeneratedRouteRequest(
t, mux, http.MethodPost,
"/v1/workspaces/ws-1/deleted-agent-sessions/session-1/restore", nil,
)
if recorder.Code != http.StatusConflict {
t.Fatalf("status = %d; body: %s", recorder.Code, recorder.Body.String())
}
}
func TestDaemonAPIPurgesWorkspaceDeletedAgentSessionsThroughMaintenanceGate(t *testing.T) {
result := agentmaintenance.PurgeResult{RemovedSessions: 2, RemovedMessages: 5, PayloadBytes: 128}
mux := http.NewServeMux()
RegisterRoutes(mux, NewRoutes(DaemonAPI{
AgentMaintenanceService: stubAgentMaintenanceService{
purgeWorkspaceFn: func(_ context.Context, workspaceID string) (agentmaintenance.PurgeResult, error) {
if workspaceID != "ws-1" {
t.Fatalf("workspace = %q", workspaceID)
}
return result, nil
},
purgeSessionFn: func(_ context.Context, workspaceID string, agentSessionID string) (agentmaintenance.PurgeResult, error) {
if workspaceID != "ws-1" || agentSessionID != "session-1" {
t.Fatalf("workspace/session = %q/%q", workspaceID, agentSessionID)
}
return result, nil
},
},
}))
for _, path := range []string{
"/v1/workspaces/ws-1/deleted-agent-sessions",
"/v1/workspaces/ws-1/deleted-agent-sessions/session-1",
} {
recorder := performGeneratedRouteRequest(t, mux, http.MethodDelete, path, nil)
if recorder.Code != http.StatusOK {
t.Fatalf("%s status = %d; body: %s", path, recorder.Code, recorder.Body.String())
}
var response tuttigenerated.DeletedAgentConversationPurgeResult
decodeGeneratedRouteResponse(t, recorder, &response)
if response.RemovedSessions != 2 || response.RemovedMessages != 5 || response.PayloadBytes != 128 {
t.Fatalf("response = %#v", response)
}
}
}
func TestDaemonAPIMapsWorkspaceDeletedAgentSessionPurgeBusy(t *testing.T) {
mux := http.NewServeMux()
RegisterRoutes(mux, NewRoutes(DaemonAPI{
AgentMaintenanceService: stubAgentMaintenanceService{err: agentmaintenance.ErrBusy},
}))
for _, path := range []string{
"/v1/workspaces/ws-1/deleted-agent-sessions",
"/v1/workspaces/ws-1/deleted-agent-sessions/session-1",
} {
recorder := performGeneratedRouteRequest(t, mux, http.MethodDelete, path, nil)
if recorder.Code != http.StatusServiceUnavailable {
t.Fatalf("%s status = %d; body: %s", path, recorder.Code, recorder.Body.String())
}
}
}
func TestDaemonAPIMapsMissingDeletedAgentSession(t *testing.T) {
mux := http.NewServeMux()
RegisterRoutes(mux, NewRoutes(DaemonAPI{
AgentSessionService: stubWorkspaceDeletedAgentSessionService{
restoreDeletedFn: func(context.Context, string, string) (agentservice.RestoreDeletedSessionResult, error) {
return agentservice.RestoreDeletedSessionResult{}, agenthost.ErrDeletedSessionNotFound
},
},
AgentMaintenanceService: stubAgentMaintenanceService{err: agenthost.ErrDeletedSessionNotFound},
}))
requests := []struct {
method string
path string
}{
{http.MethodPost, "/v1/workspaces/ws-1/deleted-agent-sessions/missing/restore"},
{http.MethodDelete, "/v1/workspaces/ws-1/deleted-agent-sessions/missing"},
}
for _, request := range requests {
recorder := performGeneratedRouteRequest(t, mux, request.method, request.path, nil)
if recorder.Code != http.StatusNotFound {
t.Fatalf("%s status = %d; body: %s", request.path, recorder.Code, recorder.Body.String())
}
}
}