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
304 lines
11 KiB
Go
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())
|
|
}
|
|
}
|