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

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
}