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
570 lines
21 KiB
Go
570 lines
21 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"errors"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
workspaceissues "github.com/tutti-os/tutti/packages/workspace/issues"
|
|
tuttigenerated "github.com/tutti-os/tutti/services/tuttid/api/generated"
|
|
"github.com/tutti-os/tutti/services/tuttid/apierrors"
|
|
workspacebiz "github.com/tutti-os/tutti/services/tuttid/biz/workspace"
|
|
workspacedata "github.com/tutti-os/tutti/services/tuttid/data/workspace"
|
|
workspaceservice "github.com/tutti-os/tutti/services/tuttid/service/workspace"
|
|
)
|
|
|
|
type recordingIssueRunLauncher struct {
|
|
launches []workspaceservice.IssueRunLaunch
|
|
}
|
|
|
|
func (launcher *recordingIssueRunLauncher) Launch(_ context.Context, launch workspaceservice.IssueRunLaunch) error {
|
|
launcher.launches = append(launcher.launches, launch)
|
|
return nil
|
|
}
|
|
|
|
func TestDaemonAPIGeneratedRoutesCreateWorkspaceIssueMapsDuplicateIDTo409(t *testing.T) {
|
|
store := openIssueRouteSQLiteStore(t)
|
|
ctx := context.Background()
|
|
if err := store.Create(ctx, workspacebiz.Summary{
|
|
ID: "ws-issue-route-1",
|
|
Name: "Issue Route Workspace",
|
|
}); err != nil {
|
|
t.Fatalf("Create() workspace error = %v", err)
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
RegisterRoutes(mux, NewRoutes(DaemonAPI{
|
|
IssueService: workspaceservice.IssueManagerService{Store: store},
|
|
}))
|
|
|
|
first := performGeneratedRouteRequest(t, mux, http.MethodPost, "/v1/workspaces/ws-issue-route-1/issues", map[string]any{
|
|
"issueId": "issue-fixed",
|
|
"topicId": workspaceissues.DefaultTopicID,
|
|
"title": "First issue",
|
|
})
|
|
if first.Code != http.StatusCreated {
|
|
t.Fatalf("first status = %d, want %d; body: %s", first.Code, http.StatusCreated, first.Body.String())
|
|
}
|
|
|
|
second := performGeneratedRouteRequest(t, mux, http.MethodPost, "/v1/workspaces/ws-issue-route-1/issues", map[string]any{
|
|
"issueId": "issue-fixed",
|
|
"topicId": workspaceissues.DefaultTopicID,
|
|
"title": "Duplicate issue",
|
|
})
|
|
if second.Code != http.StatusConflict {
|
|
t.Fatalf("second status = %d, want %d; body: %s", second.Code, http.StatusConflict, second.Body.String())
|
|
}
|
|
|
|
assertGeneratedRouteError(
|
|
t,
|
|
second,
|
|
tuttigenerated.WorkspaceIssueResourceExists,
|
|
apierrors.ReasonWorkspaceIssueExists,
|
|
workspaceissues.ErrIssueAlreadyExists.Error(),
|
|
)
|
|
}
|
|
|
|
func TestDaemonAPIGeneratedRoutesRejectInvalidIssueAttachments(t *testing.T) {
|
|
store := openIssueRouteSQLiteStore(t)
|
|
ctx := context.Background()
|
|
const workspaceID = "ws-issue-attachment-contract"
|
|
if err := store.Create(ctx, workspacebiz.Summary{ID: workspaceID, Name: "Attachment contract"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mux := http.NewServeMux()
|
|
RegisterRoutes(mux, NewRoutes(DaemonAPI{
|
|
IssueService: workspaceservice.IssueManagerService{
|
|
AttachmentFiles: workspacedata.IssueAttachmentFiles{StateDir: t.TempDir()},
|
|
Store: store,
|
|
},
|
|
}))
|
|
tooMany := make([]map[string]any, 9)
|
|
for index := range tooMany {
|
|
tooMany[index] = map[string]any{"dataBase64": "iVBORw0KGgo=", "mimeType": "image/png"}
|
|
}
|
|
testCases := []struct {
|
|
name string
|
|
attachments any
|
|
}{
|
|
{name: "invalid UUID", attachments: []map[string]any{{"attachmentId": "not-a-uuid", "dataBase64": "iVBORw0KGgo=", "mimeType": "image/png"}}},
|
|
{name: "invalid MIME", attachments: []map[string]any{{"dataBase64": "iVBORw0KGgo=", "mimeType": "image/gif"}}},
|
|
{name: "invalid base64", attachments: []map[string]any{{"dataBase64": "not base64!", "mimeType": "image/png"}}},
|
|
{name: "too many", attachments: tooMany},
|
|
}
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
recorder := performGeneratedRouteRequest(t, mux, http.MethodPost, "/v1/workspaces/"+workspaceID+"/issues", map[string]any{
|
|
"attachments": testCase.attachments,
|
|
"topicId": workspaceissues.DefaultTopicID,
|
|
"title": "Invalid attachment",
|
|
})
|
|
if recorder.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusBadRequest, recorder.Body.String())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDaemonAPIStartsIssueRunWithTextAndImageAttachment(t *testing.T) {
|
|
store := openIssueRouteSQLiteStore(t)
|
|
ctx := context.Background()
|
|
const workspaceID = "ws-issue-attachment-launch"
|
|
if err := store.Create(ctx, workspacebiz.Summary{ID: workspaceID, Name: "Attachment launch"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
launcher := &recordingIssueRunLauncher{}
|
|
issueService := &workspaceservice.IssueManagerService{
|
|
AttachmentFiles: workspacedata.IssueAttachmentFiles{StateDir: t.TempDir()},
|
|
MutationLocks: workspaceservice.NewIssueMutationLocks(),
|
|
RunLauncher: launcher,
|
|
Store: store,
|
|
}
|
|
mux := http.NewServeMux()
|
|
RegisterRoutes(mux, NewRoutes(DaemonAPI{IssueService: issueService}))
|
|
|
|
create := performGeneratedRouteRequest(t, mux, http.MethodPost, "/v1/workspaces/"+workspaceID+"/issues", map[string]any{
|
|
"attachments": []map[string]any{{
|
|
"dataBase64": base64.StdEncoding.EncodeToString([]byte("\x89PNG\r\n\x1a\ncontent")),
|
|
"displayName": "capture.png",
|
|
"mimeType": "image/png",
|
|
}},
|
|
"content": "Check the broken layout",
|
|
"topicId": workspaceissues.DefaultTopicID,
|
|
"title": "Inspect screenshot",
|
|
})
|
|
if create.Code != http.StatusCreated {
|
|
t.Fatalf("create status = %d, want %d; body: %s", create.Code, http.StatusCreated, create.Body.String())
|
|
}
|
|
var created tuttigenerated.IssueManagerIssueResponse
|
|
decodeGeneratedRouteResponse(t, create, &created)
|
|
|
|
start := performGeneratedRouteRequest(t, mux, http.MethodPost, "/v1/workspaces/"+workspaceID+"/issues/"+created.Issue.IssueId+"/run-launches", map[string]any{
|
|
"agentTargetId": "local:codex",
|
|
})
|
|
if start.Code != http.StatusCreated {
|
|
t.Fatalf("start status = %d, want %d; body: %s", start.Code, http.StatusCreated, start.Body.String())
|
|
}
|
|
if len(launcher.launches) != 1 {
|
|
t.Fatalf("launches = %#v, want one", launcher.launches)
|
|
}
|
|
launch := launcher.launches[0]
|
|
if !strings.Contains(launch.Prompt, "Inspect screenshot") || !strings.Contains(launch.Prompt, "Check the broken layout") {
|
|
t.Fatalf("prompt = %q, want title and note", launch.Prompt)
|
|
}
|
|
if len(launch.Attachments) != 1 || launch.Attachments[0].MimeType != "image/png" || launch.Attachments[0].Name != "capture.png" {
|
|
t.Fatalf("attachments = %#v, want one PNG", launch.Attachments)
|
|
}
|
|
if !issueService.AttachmentFiles.IsManagedPath(launch.Attachments[0].Path) {
|
|
t.Fatalf("attachment path = %q, want managed path", launch.Attachments[0].Path)
|
|
}
|
|
}
|
|
|
|
func TestDaemonAPIPlanIssueSchemaRejectsAttachments(t *testing.T) {
|
|
store := openIssueRouteSQLiteStore(t)
|
|
ctx := context.Background()
|
|
const workspaceID = "ws-plan-attachment-contract"
|
|
if err := store.Create(ctx, workspacebiz.Summary{ID: workspaceID, Name: "Plan attachment contract"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mux := http.NewServeMux()
|
|
RegisterRoutes(mux, NewRoutes(DaemonAPI{IssueService: &workspaceservice.IssueManagerService{Store: store}}))
|
|
recorder := performGeneratedRouteRequest(t, mux, http.MethodPost, "/v1/workspaces/"+workspaceID+"/issues/from-plan", map[string]any{
|
|
"issue": map[string]any{
|
|
"attachments": []map[string]any{{"dataBase64": "iVBORw0KGgo=", "mimeType": "image/png"}},
|
|
"planningSource": "traditional_plan",
|
|
"topicId": workspaceissues.DefaultTopicID,
|
|
"title": "Plan",
|
|
},
|
|
"tasks": []map[string]any{{"title": "Implement"}},
|
|
})
|
|
if recorder.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusBadRequest, recorder.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestDaemonAPIGeneratedRoutesRejectForgedTuttiModeIssueProvenance(t *testing.T) {
|
|
store := openIssueRouteSQLiteStore(t)
|
|
ctx := context.Background()
|
|
const workspaceID = "ws-issue-route-tutti-provenance"
|
|
if err := store.Create(ctx, workspacebiz.Summary{
|
|
ID: workspaceID,
|
|
Name: "Issue Tutti Provenance Workspace",
|
|
}); err != nil {
|
|
t.Fatalf("Create() workspace error = %v", err)
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
RegisterRoutes(mux, NewRoutes(DaemonAPI{
|
|
IssueService: workspaceservice.IssueManagerService{Store: store},
|
|
}))
|
|
recorder := performGeneratedRouteRequest(t, mux, http.MethodPost, "/v1/workspaces/"+workspaceID+"/issues", map[string]any{
|
|
"issueId": "ordinary-forged-tutti",
|
|
"topicId": workspaceissues.DefaultTopicID,
|
|
"title": "Forged Tutti provenance",
|
|
"planningSource": string(workspaceissues.PlanningSourceTuttiModePlan),
|
|
"sourceSessionId": "session-1",
|
|
})
|
|
|
|
if recorder.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusBadRequest, recorder.Body.String())
|
|
}
|
|
if _, err := store.GetIssue(ctx, workspaceID, "ordinary-forged-tutti"); !errors.Is(err, workspaceissues.ErrIssueNotFound) {
|
|
t.Fatalf("GetIssue() error = %v, want no forged Issue", err)
|
|
}
|
|
}
|
|
|
|
func TestDaemonAPIGeneratedRoutesIssueTopicLifecycle(t *testing.T) {
|
|
store := openIssueRouteSQLiteStore(t)
|
|
ctx := context.Background()
|
|
if err := store.Create(ctx, workspacebiz.Summary{
|
|
ID: "ws-issue-topic-route",
|
|
Name: "Issue Topic Route Workspace",
|
|
}); err != nil {
|
|
t.Fatalf("Create() workspace error = %v", err)
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
RegisterRoutes(mux, NewRoutes(DaemonAPI{
|
|
IssueService: workspaceservice.IssueManagerService{Store: store},
|
|
}))
|
|
|
|
create := performGeneratedRouteRequest(t, mux, http.MethodPost, "/v1/workspaces/ws-issue-topic-route/issue-topics", map[string]any{
|
|
"summary": "Renderer migration issues",
|
|
"title": "Renderer",
|
|
"topicId": "topic-renderer",
|
|
})
|
|
if create.Code != http.StatusCreated {
|
|
t.Fatalf("create topic status = %d, want %d; body: %s", create.Code, http.StatusCreated, create.Body.String())
|
|
}
|
|
var createResponse tuttigenerated.IssueManagerTopicResponse
|
|
decodeGeneratedRouteResponse(t, create, &createResponse)
|
|
if createResponse.Topic.TopicId != "topic-renderer" || createResponse.Topic.Summary != "Renderer migration issues" {
|
|
t.Fatalf("created topic = %+v", createResponse.Topic)
|
|
}
|
|
|
|
pinned := true
|
|
update := performGeneratedRouteRequest(t, mux, http.MethodPatch, "/v1/workspaces/ws-issue-topic-route/issue-topics/topic-renderer", map[string]any{
|
|
"pinned": pinned,
|
|
"summary": "Updated summary",
|
|
})
|
|
if update.Code != http.StatusOK {
|
|
t.Fatalf("update topic status = %d, want %d; body: %s", update.Code, http.StatusOK, update.Body.String())
|
|
}
|
|
var updateResponse tuttigenerated.IssueManagerTopicResponse
|
|
decodeGeneratedRouteResponse(t, update, &updateResponse)
|
|
if updateResponse.Topic.PinnedAtUnix == 0 || updateResponse.Topic.Summary != "Updated summary" {
|
|
t.Fatalf("updated topic = %+v", updateResponse.Topic)
|
|
}
|
|
|
|
list := performGeneratedRouteRequest(t, mux, http.MethodGet, "/v1/workspaces/ws-issue-topic-route/issue-topics", nil)
|
|
if list.Code != http.StatusOK {
|
|
t.Fatalf("list topic status = %d, want %d; body: %s", list.Code, http.StatusOK, list.Body.String())
|
|
}
|
|
var listResponse tuttigenerated.IssueManagerTopicListResponse
|
|
decodeGeneratedRouteResponse(t, list, &listResponse)
|
|
if len(listResponse.Topics) != 2 || listResponse.Topics[0].TopicId != "topic-renderer" {
|
|
t.Fatalf("topic list = %+v", listResponse.Topics)
|
|
}
|
|
|
|
deleteTopic := performGeneratedRouteRequest(t, mux, http.MethodDelete, "/v1/workspaces/ws-issue-topic-route/issue-topics/topic-renderer", nil)
|
|
if deleteTopic.Code != http.StatusOK {
|
|
t.Fatalf("delete topic status = %d, want %d; body: %s", deleteTopic.Code, http.StatusOK, deleteTopic.Body.String())
|
|
}
|
|
var deleteResponse tuttigenerated.DeleteIssueManagerTopicResponse
|
|
decodeGeneratedRouteResponse(t, deleteTopic, &deleteResponse)
|
|
if !deleteResponse.Removed {
|
|
t.Fatal("delete topic removed = false, want true")
|
|
}
|
|
|
|
nonEmptyTopic := performGeneratedRouteRequest(t, mux, http.MethodPost, "/v1/workspaces/ws-issue-topic-route/issue-topics", map[string]any{
|
|
"title": "Non empty",
|
|
"topicId": "topic-non-empty",
|
|
})
|
|
if nonEmptyTopic.Code != http.StatusCreated {
|
|
t.Fatalf("create non-empty topic status = %d, want %d; body: %s", nonEmptyTopic.Code, http.StatusCreated, nonEmptyTopic.Body.String())
|
|
}
|
|
issue := performGeneratedRouteRequest(t, mux, http.MethodPost, "/v1/workspaces/ws-issue-topic-route/issues", map[string]any{
|
|
"title": "Keep topic",
|
|
"topicId": "topic-non-empty",
|
|
})
|
|
if issue.Code != http.StatusCreated {
|
|
t.Fatalf("create issue status = %d, want %d; body: %s", issue.Code, http.StatusCreated, issue.Body.String())
|
|
}
|
|
deleteNonEmptyTopic := performGeneratedRouteRequest(t, mux, http.MethodDelete, "/v1/workspaces/ws-issue-topic-route/issue-topics/topic-non-empty", nil)
|
|
if deleteNonEmptyTopic.Code != http.StatusConflict {
|
|
t.Fatalf("delete non-empty topic status = %d, want %d; body: %s", deleteNonEmptyTopic.Code, http.StatusConflict, deleteNonEmptyTopic.Body.String())
|
|
}
|
|
assertGeneratedRouteError(
|
|
t,
|
|
deleteNonEmptyTopic,
|
|
tuttigenerated.WorkspaceIssueResourceExists,
|
|
apierrors.ReasonWorkspaceIssueTopicNotEmpty,
|
|
workspaceissues.ErrTopicNotEmpty.Error(),
|
|
)
|
|
}
|
|
|
|
func TestDaemonAPIGeneratedRoutesRequireIssueTopicID(t *testing.T) {
|
|
store := openIssueRouteSQLiteStore(t)
|
|
ctx := context.Background()
|
|
if err := store.Create(ctx, workspacebiz.Summary{
|
|
ID: "ws-issue-topic-required",
|
|
Name: "Issue Topic Required Workspace",
|
|
}); err != nil {
|
|
t.Fatalf("Create() workspace error = %v", err)
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
RegisterRoutes(mux, NewRoutes(DaemonAPI{
|
|
IssueService: workspaceservice.IssueManagerService{Store: store},
|
|
}))
|
|
|
|
createMissingTopic := performGeneratedRouteRequest(t, mux, http.MethodPost, "/v1/workspaces/ws-issue-topic-required/issues", map[string]any{
|
|
"title": "Missing topic",
|
|
})
|
|
if createMissingTopic.Code != http.StatusBadRequest {
|
|
t.Fatalf("create missing topic status = %d, want %d; body: %s", createMissingTopic.Code, http.StatusBadRequest, createMissingTopic.Body.String())
|
|
}
|
|
|
|
listMissingTopic := performGeneratedRouteRequest(t, mux, http.MethodGet, "/v1/workspaces/ws-issue-topic-required/issues", nil)
|
|
if listMissingTopic.Code != http.StatusBadRequest {
|
|
t.Fatalf("list missing topic status = %d, want %d; body: %s", listMissingTopic.Code, http.StatusBadRequest, listMissingTopic.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestDaemonAPIGeneratedRoutesMapMissingIssueTopicTo404(t *testing.T) {
|
|
store := openIssueRouteSQLiteStore(t)
|
|
ctx := context.Background()
|
|
if err := store.Create(ctx, workspacebiz.Summary{
|
|
ID: "ws-issue-topic-missing",
|
|
Name: "Issue Topic Missing Workspace",
|
|
}); err != nil {
|
|
t.Fatalf("Create() workspace error = %v", err)
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
RegisterRoutes(mux, NewRoutes(DaemonAPI{
|
|
IssueService: workspaceservice.IssueManagerService{Store: store},
|
|
}))
|
|
|
|
recorder := performGeneratedRouteRequest(t, mux, http.MethodPost, "/v1/workspaces/ws-issue-topic-missing/issues", map[string]any{
|
|
"title": "Missing topic",
|
|
"topicId": "missing-topic",
|
|
})
|
|
if recorder.Code != http.StatusNotFound {
|
|
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusNotFound, recorder.Body.String())
|
|
}
|
|
|
|
assertGeneratedRouteError(
|
|
t,
|
|
recorder,
|
|
tuttigenerated.WorkspaceIssueResourceNotFound,
|
|
apierrors.ReasonWorkspaceIssueTopicNotFound,
|
|
workspaceissues.ErrTopicNotFound.Error(),
|
|
)
|
|
|
|
list := performGeneratedRouteRequest(t, mux, http.MethodGet, "/v1/workspaces/ws-issue-topic-missing/issues?topicId=missing-topic", nil)
|
|
if list.Code != http.StatusNotFound {
|
|
t.Fatalf("list status = %d, want %d; body: %s", list.Code, http.StatusNotFound, list.Body.String())
|
|
}
|
|
assertGeneratedRouteError(
|
|
t,
|
|
list,
|
|
tuttigenerated.WorkspaceIssueResourceNotFound,
|
|
apierrors.ReasonWorkspaceIssueTopicNotFound,
|
|
workspaceissues.ErrTopicNotFound.Error(),
|
|
)
|
|
}
|
|
|
|
func TestDaemonAPIGeneratedRoutesUpdateWorkspaceIssueStatus(t *testing.T) {
|
|
store := openIssueRouteSQLiteStore(t)
|
|
ctx := context.Background()
|
|
if err := store.Create(ctx, workspacebiz.Summary{
|
|
ID: "ws-issue-route-status",
|
|
Name: "Issue Route Status Workspace",
|
|
}); err != nil {
|
|
t.Fatalf("Create() workspace error = %v", err)
|
|
}
|
|
|
|
issueService := workspaceservice.IssueManagerService{Store: store}
|
|
issue, err := issueService.CreateIssue(ctx, "ws-issue-route-status", workspaceservice.CreateIssueManagerIssueInput{
|
|
IssueID: "issue-status",
|
|
TopicID: workspaceissues.DefaultTopicID,
|
|
Title: "Mark me done",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreateIssue() error = %v", err)
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
RegisterRoutes(mux, NewRoutes(DaemonAPI{IssueService: issueService}))
|
|
|
|
recorder := performGeneratedRouteRequest(
|
|
t,
|
|
mux,
|
|
http.MethodPatch,
|
|
"/v1/workspaces/ws-issue-route-status/issues/issue-status",
|
|
map[string]any{"status": "completed"},
|
|
)
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusOK, recorder.Body.String())
|
|
}
|
|
|
|
var response tuttigenerated.IssueManagerIssueResponse
|
|
decodeGeneratedRouteResponse(t, recorder, &response)
|
|
if response.Issue.Status != tuttigenerated.IssueManagerStatusCompleted {
|
|
t.Fatalf("response issue status = %q", response.Issue.Status)
|
|
}
|
|
|
|
detail, err := issueService.GetIssueDetail(ctx, "ws-issue-route-status", issue.IssueID)
|
|
if err != nil {
|
|
t.Fatalf("GetIssueDetail() error = %v", err)
|
|
}
|
|
if detail.Issue.Status != workspaceissues.StatusCompleted {
|
|
t.Fatalf("stored issue status = %q", detail.Issue.Status)
|
|
}
|
|
}
|
|
|
|
func TestDaemonAPIGeneratedRoutesCreateWorkspaceIssueTasksPreservesOrder(t *testing.T) {
|
|
store := openIssueRouteSQLiteStore(t)
|
|
ctx := context.Background()
|
|
if err := store.Create(ctx, workspacebiz.Summary{
|
|
ID: "ws-issue-route-batch",
|
|
Name: "Issue Route Batch Workspace",
|
|
}); err != nil {
|
|
t.Fatalf("Create() workspace error = %v", err)
|
|
}
|
|
|
|
issueService := workspaceservice.IssueManagerService{Store: store}
|
|
issue, err := issueService.CreateIssue(ctx, "ws-issue-route-batch", workspaceservice.CreateIssueManagerIssueInput{
|
|
IssueID: "issue-batch",
|
|
TopicID: workspaceissues.DefaultTopicID,
|
|
Title: "Break down work",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreateIssue() error = %v", err)
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
RegisterRoutes(mux, NewRoutes(DaemonAPI{IssueService: issueService}))
|
|
|
|
recorder := performGeneratedRouteRequest(
|
|
t,
|
|
mux,
|
|
http.MethodPost,
|
|
"/v1/workspaces/ws-issue-route-batch/issues/issue-batch/tasks/batch-create",
|
|
map[string]any{"tasks": []map[string]any{
|
|
{"taskId": "task-1", "title": "1. Baseline", "content": "Capture current state"},
|
|
{"taskId": "task-2", "title": "2. Metrics", "priority": "high"},
|
|
}},
|
|
)
|
|
if recorder.Code != http.StatusCreated {
|
|
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusCreated, recorder.Body.String())
|
|
}
|
|
|
|
var response tuttigenerated.IssueManagerTasksResponse
|
|
decodeGeneratedRouteResponse(t, recorder, &response)
|
|
if len(response.Tasks) != 2 || response.Tasks[0].TaskId != "task-1" || response.Tasks[0].SortIndex != 1 || response.Tasks[1].TaskId != "task-2" || response.Tasks[1].SortIndex != 2 {
|
|
t.Fatalf("response tasks = %#v", response.Tasks)
|
|
}
|
|
|
|
detail, err := issueService.GetIssueDetail(ctx, "ws-issue-route-batch", issue.IssueID)
|
|
if err != nil {
|
|
t.Fatalf("GetIssueDetail() error = %v", err)
|
|
}
|
|
if len(detail.Tasks) != 2 || detail.Tasks[0].TaskID != "task-1" || detail.Tasks[1].TaskID != "task-2" {
|
|
t.Fatalf("stored tasks = %#v", detail.Tasks)
|
|
}
|
|
}
|
|
|
|
func TestDaemonAPIGeneratedRoutesRemoveWorkspaceIssueTaskContextRef(t *testing.T) {
|
|
store := openIssueRouteSQLiteStore(t)
|
|
ctx := context.Background()
|
|
if err := store.Create(ctx, workspacebiz.Summary{
|
|
ID: "ws-issue-route-2",
|
|
Name: "Issue Route Workspace Two",
|
|
}); err != nil {
|
|
t.Fatalf("Create() workspace error = %v", err)
|
|
}
|
|
|
|
issueService := workspaceservice.IssueManagerService{Store: store}
|
|
issue, err := issueService.CreateIssue(ctx, "ws-issue-route-2", workspaceservice.CreateIssueManagerIssueInput{
|
|
IssueID: "issue-1",
|
|
TopicID: workspaceissues.DefaultTopicID,
|
|
Title: "Scoped delete",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreateIssue() error = %v", err)
|
|
}
|
|
task, err := issueService.CreateTask(ctx, "ws-issue-route-2", issue.IssueID, workspaceservice.CreateIssueManagerTaskInput{
|
|
TaskID: "task-1",
|
|
Title: "Delete a ref",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreateTask() error = %v", err)
|
|
}
|
|
refs, err := issueService.AddTaskContextRefs(ctx, "ws-issue-route-2", issue.IssueID, task.TaskID, workspaceservice.AddIssueManagerContextRefsInput{
|
|
Refs: []workspaceissues.AddContextRefInput{{
|
|
ContextRefID: "task-ref-1",
|
|
RefType: "file",
|
|
Path: "/workspace/docs/task.md",
|
|
}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("AddTaskContextRefs() error = %v", err)
|
|
}
|
|
if len(refs) != 1 {
|
|
t.Fatalf("refs len = %d, want 1", len(refs))
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
RegisterRoutes(mux, NewRoutes(DaemonAPI{IssueService: issueService}))
|
|
|
|
recorder := performGeneratedRouteRequest(
|
|
t,
|
|
mux,
|
|
http.MethodDelete,
|
|
"/v1/workspaces/ws-issue-route-2/issues/issue-1/tasks/task-1/context-refs/task-ref-1",
|
|
nil,
|
|
)
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d; body: %s", recorder.Code, http.StatusOK, recorder.Body.String())
|
|
}
|
|
|
|
var response struct {
|
|
Removed bool `json:"removed"`
|
|
}
|
|
decodeGeneratedRouteResponse(t, recorder, &response)
|
|
if !response.Removed {
|
|
t.Fatal("removed = false, want true")
|
|
}
|
|
|
|
detail, err := issueService.GetTaskDetail(ctx, "ws-issue-route-2", issue.IssueID, task.TaskID)
|
|
if err != nil {
|
|
t.Fatalf("GetTaskDetail() error = %v", err)
|
|
}
|
|
if len(detail.ContextRefs) != 0 {
|
|
t.Fatalf("task context refs len = %d, want 0", len(detail.ContextRefs))
|
|
}
|
|
}
|
|
|
|
func openIssueRouteSQLiteStore(t *testing.T) *workspacedata.SQLiteStore {
|
|
t.Helper()
|
|
|
|
dbPath := filepath.Join(t.TempDir(), "tuttid.db")
|
|
store, err := workspacedata.OpenSQLiteStore(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("OpenSQLiteStore() error = %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_ = store.Close()
|
|
})
|
|
if err := store.Migrate(context.Background()); err != nil {
|
|
t.Fatalf("Migrate() error = %v", err)
|
|
}
|
|
return store
|
|
}
|