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

304 lines
11 KiB
Go

package api
import (
"context"
"net/http"
"testing"
tuttigenerated "github.com/tutti-os/tutti/services/tuttid/api/generated"
userprojectbiz "github.com/tutti-os/tutti/services/tuttid/biz/userproject"
userprojectservice "github.com/tutti-os/tutti/services/tuttid/service/userproject"
)
type stubUserProjectService struct {
checkPathFn func(context.Context, userprojectservice.CheckPathInput) (userprojectservice.PathCheck, error)
deleteFn func(context.Context, userprojectservice.DeleteInput) error
useFn func(context.Context, userprojectservice.UseInput) (userprojectbiz.Project, error)
listFn func(context.Context) ([]userprojectbiz.Project, error)
moveFn func(context.Context, userprojectservice.MoveInput) ([]userprojectbiz.Project, error)
pinFn func(context.Context, userprojectservice.PinInput) ([]userprojectbiz.Project, error)
useManyFn func(context.Context, userprojectservice.UseManyInput) []error
}
func (s stubUserProjectService) CheckPath(ctx context.Context, input userprojectservice.CheckPathInput) (userprojectservice.PathCheck, error) {
if s.checkPathFn == nil {
return userprojectservice.PathCheck{}, nil
}
return s.checkPathFn(ctx, input)
}
func (s stubUserProjectService) Delete(ctx context.Context, input userprojectservice.DeleteInput) error {
if s.deleteFn == nil {
return nil
}
return s.deleteFn(ctx, input)
}
func (s stubUserProjectService) Use(ctx context.Context, input userprojectservice.UseInput) (userprojectbiz.Project, error) {
if s.useFn == nil {
return userprojectbiz.Project{}, nil
}
return s.useFn(ctx, input)
}
func (s stubUserProjectService) UseMany(ctx context.Context, input userprojectservice.UseManyInput) []error {
if s.useManyFn == nil {
return make([]error, len(input.Paths))
}
return s.useManyFn(ctx, input)
}
func (s stubUserProjectService) List(ctx context.Context) ([]userprojectbiz.Project, error) {
if s.listFn == nil {
return nil, nil
}
return s.listFn(ctx)
}
func (s stubUserProjectService) Move(ctx context.Context, input userprojectservice.MoveInput) ([]userprojectbiz.Project, error) {
if s.moveFn == nil {
return nil, nil
}
return s.moveFn(ctx, input)
}
func (s stubUserProjectService) Pin(ctx context.Context, input userprojectservice.PinInput) ([]userprojectbiz.Project, error) {
if s.pinFn == nil {
return nil, nil
}
return s.pinFn(ctx, input)
}
func TestDaemonAPIRoutesCheckUserProjectPath(t *testing.T) {
mux := http.NewServeMux()
RegisterRoutes(mux, NewRoutes(DaemonAPI{
UserProjectService: stubUserProjectService{
checkPathFn: func(_ context.Context, input userprojectservice.CheckPathInput) (userprojectservice.PathCheck, error) {
if input.Path != "/workspace/tutti" {
t.Fatalf("path = %q, want /workspace/tutti", input.Path)
}
return userprojectservice.PathCheck{
Path: input.Path,
Exists: true,
IsDirectory: true,
}, nil
},
},
}))
recorder := performGeneratedRouteRequest(t, mux, http.MethodPost, "/v1/user-projects/check", map[string]any{
"path": "/workspace/tutti",
})
if recorder.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusOK, recorder.Body.String())
}
var response tuttigenerated.UserProjectPathCheckResponse
decodeGeneratedRouteResponse(t, recorder, &response)
if !response.Exists || !response.IsDirectory || response.Path != "/workspace/tutti" {
t.Fatalf("response = %#v, want existing directory", response)
}
}
func TestDaemonAPIRoutesUserProjects(t *testing.T) {
mux := http.NewServeMux()
RegisterRoutes(mux, NewRoutes(DaemonAPI{
UserProjectService: stubUserProjectService{
listFn: func(context.Context) ([]userprojectbiz.Project, error) {
return []userprojectbiz.Project{{
ID: "user_project_1",
Path: "/workspace/tutti",
Label: "tutti",
CreatedAtUnixMS: 1,
UpdatedAtUnixMS: 2,
PinnedAtUnixMS: 3,
}}, nil
},
},
}))
recorder := performGeneratedRouteRequest(t, mux, http.MethodGet, "/v1/user-projects", nil)
if recorder.Code != http.StatusOK {
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusOK, recorder.Body.String())
}
var response tuttigenerated.UserProjectListResponse
decodeGeneratedRouteResponse(t, recorder, &response)
if len(response.Projects) != 1 {
t.Fatalf("projects length = %d, want 1", len(response.Projects))
}
if response.Projects[0].Path != "/workspace/tutti" {
t.Fatalf("path = %q, want /workspace/tutti", response.Projects[0].Path)
}
if response.Projects[0].PinnedAtUnixMs != 3 {
t.Fatalf("pinnedAtUnixMs = %d, want 3", response.Projects[0].PinnedAtUnixMs)
}
}
func TestDaemonAPIRoutesDeleteUserProject(t *testing.T) {
mux := http.NewServeMux()
RegisterRoutes(mux, NewRoutes(DaemonAPI{
UserProjectService: stubUserProjectService{
deleteFn: func(_ context.Context, input userprojectservice.DeleteInput) error {
if input.Path != "/workspace/tutti" {
t.Fatalf("path = %q, want /workspace/tutti", input.Path)
}
return nil
},
},
}))
recorder := performGeneratedRouteRequest(t, mux, http.MethodDelete, "/v1/user-projects", map[string]any{
"path": "/workspace/tutti",
})
if recorder.Code != http.StatusNoContent {
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusNoContent, recorder.Body.String())
}
}
func TestDaemonAPIRoutesUseUserProject(t *testing.T) {
mux := http.NewServeMux()
RegisterRoutes(mux, NewRoutes(DaemonAPI{
UserProjectService: stubUserProjectService{
useFn: func(_ context.Context, input userprojectservice.UseInput) (userprojectbiz.Project, error) {
if input.Path != "/workspace/tutti" {
t.Fatalf("path = %q, want /workspace/tutti", input.Path)
}
return userprojectbiz.Project{
ID: "user_project_1",
Path: input.Path,
Label: "tutti",
CreatedAtUnixMS: 1,
UpdatedAtUnixMS: 2,
LastUsedAtUnixMS: 3,
}, nil
},
},
}))
recorder := performGeneratedRouteRequest(t, mux, http.MethodPost, "/v1/user-projects", map[string]any{
"path": "/workspace/tutti",
})
if recorder.Code != http.StatusCreated {
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusCreated, recorder.Body.String())
}
var response tuttigenerated.UserProjectResponse
decodeGeneratedRouteResponse(t, recorder, &response)
if response.Project.Path != "/workspace/tutti" {
t.Fatalf("path = %q, want /workspace/tutti", response.Project.Path)
}
}
func TestDaemonAPIRoutesMoveUserProject(t *testing.T) {
mux := http.NewServeMux()
RegisterRoutes(mux, NewRoutes(DaemonAPI{
UserProjectService: stubUserProjectService{
moveFn: func(_ context.Context, input userprojectservice.MoveInput) ([]userprojectbiz.Project, error) {
if input.ProjectID != "beta" || input.BeforeProjectID == nil || *input.BeforeProjectID != "alpha" {
t.Fatalf("move input = %#v", input)
}
return []userprojectbiz.Project{
{ID: "beta", Path: "/workspace/beta", Label: "beta"},
{ID: "alpha", Path: "/workspace/alpha", Label: "alpha"},
}, nil
},
},
}))
recorder := performGeneratedRouteRequest(t, mux, http.MethodPost, "/v1/user-projects/move", map[string]any{
"projectId": "beta", "beforeProjectId": "alpha",
})
if recorder.Code != http.StatusOK {
t.Fatalf("status = %d, want 200; body: %s", recorder.Code, recorder.Body.String())
}
var response tuttigenerated.UserProjectListResponse
decodeGeneratedRouteResponse(t, recorder, &response)
if len(response.Projects) != 2 || response.Projects[0].Id != "beta" {
t.Fatalf("response projects = %#v", response.Projects)
}
}
func TestDaemonAPIRoutesMoveUserProjectRejectsOmittedBeforeProjectID(t *testing.T) {
called := false
mux := http.NewServeMux()
RegisterRoutes(mux, NewRoutes(DaemonAPI{
UserProjectService: stubUserProjectService{
moveFn: func(context.Context, userprojectservice.MoveInput) ([]userprojectbiz.Project, error) {
called = true
return nil, nil
},
},
}))
recorder := performGeneratedRouteRequest(t, mux, http.MethodPost, "/v1/user-projects/move", map[string]any{
"projectId": "project-alpha",
})
if recorder.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusBadRequest, recorder.Body.String())
}
if called {
t.Fatal("Move called for request missing beforeProjectId")
}
}
func TestDaemonAPIRoutesMoveUserProjectRejectsUnknownID(t *testing.T) {
mux := http.NewServeMux()
RegisterRoutes(mux, NewRoutes(DaemonAPI{
UserProjectService: stubUserProjectService{
moveFn: func(context.Context, userprojectservice.MoveInput) ([]userprojectbiz.Project, error) {
return nil, userprojectservice.ErrInvalidArgument
},
},
}))
recorder := performGeneratedRouteRequest(t, mux, http.MethodPost, "/v1/user-projects/move", map[string]any{
"projectId": "unknown", "beforeProjectId": nil,
})
if recorder.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want 400; body: %s", recorder.Code, recorder.Body.String())
}
}
func TestDaemonAPIRoutesPinUserProject(t *testing.T) {
mux := http.NewServeMux()
RegisterRoutes(mux, NewRoutes(DaemonAPI{
UserProjectService: stubUserProjectService{
pinFn: func(_ context.Context, input userprojectservice.PinInput) ([]userprojectbiz.Project, error) {
if input.ProjectID != "project-alpha" || !input.Pinned {
t.Fatalf("pin input = %#v", input)
}
return []userprojectbiz.Project{{
ID: "project-alpha", Path: "/workspace/alpha", Label: "alpha", PinnedAtUnixMS: 10,
}}, nil
},
},
}))
recorder := performGeneratedRouteRequest(t, mux, http.MethodPost, "/v1/user-projects/pin", map[string]any{
"projectId": "project-alpha", "pinned": true,
})
if recorder.Code != http.StatusOK {
t.Fatalf("status = %d, want 200; body: %s", recorder.Code, recorder.Body.String())
}
var response tuttigenerated.UserProjectListResponse
decodeGeneratedRouteResponse(t, recorder, &response)
if len(response.Projects) != 1 || response.Projects[0].Id != "project-alpha" || response.Projects[0].PinnedAtUnixMs != 10 {
t.Fatalf("response projects = %#v", response.Projects)
}
}
func TestDaemonAPIRoutesPinUserProjectRejectsUnknownID(t *testing.T) {
mux := http.NewServeMux()
RegisterRoutes(mux, NewRoutes(DaemonAPI{
UserProjectService: stubUserProjectService{
pinFn: func(context.Context, userprojectservice.PinInput) ([]userprojectbiz.Project, error) {
return nil, userprojectservice.ErrInvalidArgument
},
},
}))
recorder := performGeneratedRouteRequest(t, mux, http.MethodPost, "/v1/user-projects/pin", map[string]any{
"projectId": "unknown", "pinned": true,
})
if recorder.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want 400; body: %s", recorder.Code, recorder.Body.String())
}
}