Files
kandev/apps/backend/internal/task/service/service_child_task_test.go
T

276 lines
7.5 KiB
Go

package service
import (
"context"
"errors"
"strings"
"testing"
"github.com/kandev/kandev/internal/task/models"
)
func TestCreateChildTask_HappyPath_InheritsWorkflow(t *testing.T) {
svc, repo := setupOfficeTest(t)
ctx := context.Background()
parent, err := svc.CreateTask(ctx, &CreateTaskRequest{
WorkspaceID: "ws-1",
Title: "Parent",
ProjectID: "proj-1",
})
if err != nil {
t.Fatalf("create parent: %v", err)
}
childID, err := svc.CreateChildTask(ctx, parent, ChildTaskSpec{
Title: "Child",
Description: "Investigate",
})
if err != nil {
t.Fatalf("CreateChildTask: %v", err)
}
if childID == "" {
t.Fatalf("expected non-empty child id")
}
got, err := repo.GetTask(ctx, childID)
if err != nil {
t.Fatalf("GetTask: %v", err)
}
if got.ParentID != parent.ID {
t.Errorf("parent_id = %q, want %q", got.ParentID, parent.ID)
}
if got.WorkflowID != parent.WorkflowID {
t.Errorf("workflow_id = %q, want inherited %q", got.WorkflowID, parent.WorkflowID)
}
if got.WorkspaceID != parent.WorkspaceID {
t.Errorf("workspace_id = %q, want %q", got.WorkspaceID, parent.WorkspaceID)
}
if got.Origin != models.TaskOriginAgentCreated {
t.Errorf("origin = %q, want agent_created", got.Origin)
}
}
func TestCreateChildTask_OverridesWorkflow(t *testing.T) {
svc, _ := setupOfficeTest(t)
ctx := context.Background()
parent, err := svc.CreateTask(ctx, &CreateTaskRequest{
WorkspaceID: "ws-1",
Title: "Parent",
ProjectID: "proj-1",
})
if err != nil {
t.Fatalf("create parent: %v", err)
}
// Override with a workflow id different from parent's. The repository
// stores whatever workflow id we pass — even if there's no rows for
// it in workflow_steps, we just want to confirm the request shape.
otherWorkflowID := parent.WorkflowID + "-override"
childID, err := svc.CreateChildTask(ctx, parent, ChildTaskSpec{
Title: "Switch flow",
WorkflowID: otherWorkflowID,
})
if err != nil {
t.Fatalf("CreateChildTask: %v", err)
}
if childID == "" {
t.Fatalf("expected non-empty child id")
}
}
func TestCreateTask_Subtask_InheritsParentRepositories(t *testing.T) {
svc, repo := setupOfficeTest(t)
ctx := context.Background()
if err := repo.CreateRepository(ctx, &models.Repository{ID: "repo-x", WorkspaceID: "ws-1", Name: "Repo"}); err != nil {
t.Fatalf("CreateRepository: %v", err)
}
parent, err := svc.CreateTask(ctx, &CreateTaskRequest{
WorkspaceID: "ws-1",
Title: "Parent",
ProjectID: "proj-1",
Repositories: []TaskRepositoryInput{{
RepositoryID: "repo-x",
BaseBranch: "main",
CheckoutBranch: "feature/parent",
}},
})
if err != nil {
t.Fatalf("create parent: %v", err)
}
// Child created without explicit repositories — mirrors the UI inherit_parent
// flow, which omits repositories expecting the backend to inherit them.
child, err := svc.CreateTask(ctx, &CreateTaskRequest{
WorkspaceID: "ws-1",
Title: "Child",
ParentID: parent.ID,
ProjectID: "proj-1",
})
if err != nil {
t.Fatalf("create child: %v", err)
}
childRepos, err := repo.ListTaskRepositories(ctx, child.ID)
if err != nil {
t.Fatalf("ListTaskRepositories: %v", err)
}
if len(childRepos) != 1 {
t.Fatalf("child repos = %d, want 1 inherited from parent", len(childRepos))
}
if childRepos[0].RepositoryID != "repo-x" {
t.Errorf("repository_id = %q, want repo-x", childRepos[0].RepositoryID)
}
if childRepos[0].BaseBranch != "main" {
t.Errorf("base_branch = %q, want inherited main", childRepos[0].BaseBranch)
}
// CheckoutBranch is dropped: two worktrees can't share a working branch.
if childRepos[0].CheckoutBranch != "" {
t.Errorf("checkout_branch = %q, want empty (dropped on inherit)", childRepos[0].CheckoutBranch)
}
}
func TestCreateTask_Subtask_ExplicitRepositoriesNotOverridden(t *testing.T) {
svc, repo := setupOfficeTest(t)
ctx := context.Background()
for _, id := range []string{"repo-a", "repo-b"} {
if err := repo.CreateRepository(ctx, &models.Repository{ID: id, WorkspaceID: "ws-1", Name: id}); err != nil {
t.Fatalf("CreateRepository %s: %v", id, err)
}
}
parent, err := svc.CreateTask(ctx, &CreateTaskRequest{
WorkspaceID: "ws-1",
Title: "Parent",
ProjectID: "proj-1",
Repositories: []TaskRepositoryInput{{RepositoryID: "repo-a", BaseBranch: "main"}},
})
if err != nil {
t.Fatalf("create parent: %v", err)
}
child, err := svc.CreateTask(ctx, &CreateTaskRequest{
WorkspaceID: "ws-1",
Title: "Child",
ParentID: parent.ID,
ProjectID: "proj-1",
Repositories: []TaskRepositoryInput{{RepositoryID: "repo-b", BaseBranch: "dev"}},
})
if err != nil {
t.Fatalf("create child: %v", err)
}
childRepos, err := repo.ListTaskRepositories(ctx, child.ID)
if err != nil {
t.Fatalf("ListTaskRepositories: %v", err)
}
if len(childRepos) != 1 || childRepos[0].RepositoryID != "repo-b" {
t.Fatalf("child repos = %+v, want only explicit repo-b", childRepos)
}
}
func TestCreateChildTask_RequiresParent(t *testing.T) {
svc, _ := setupOfficeTest(t)
_, err := svc.CreateChildTask(context.Background(), nil, ChildTaskSpec{Title: "X"})
if err == nil || !strings.Contains(err.Error(), "parent") {
t.Fatalf("expected parent error, got: %v", err)
}
}
func TestCreateChildTask_RequiresTitle(t *testing.T) {
svc, _ := setupOfficeTest(t)
ctx := context.Background()
parent, _ := svc.CreateTask(ctx, &CreateTaskRequest{
WorkspaceID: "ws-1",
Title: "Parent",
ProjectID: "proj-1",
})
_, err := svc.CreateChildTask(ctx, parent, ChildTaskSpec{})
if err == nil || !strings.Contains(err.Error(), "title") {
t.Fatalf("expected title error, got: %v", err)
}
}
func TestCreateTask_SubtaskOfSubtask_Kanban_Rejected(t *testing.T) {
svc, _, repo := createTestService(t)
ctx := context.Background()
_ = repo.CreateWorkspace(ctx, &models.Workspace{ID: "ws-1", Name: "Workspace"})
_ = repo.CreateWorkflow(ctx, &models.Workflow{ID: "wf-1", WorkspaceID: "ws-1", Name: "Board"})
root, err := svc.CreateTask(ctx, &CreateTaskRequest{
WorkspaceID: "ws-1",
WorkflowID: "wf-1",
Title: "Root",
})
if err != nil {
t.Fatalf("create root: %v", err)
}
child, err := svc.CreateTask(ctx, &CreateTaskRequest{
WorkspaceID: "ws-1",
WorkflowID: "wf-1",
ParentID: root.ID,
Title: "Child",
})
if err != nil {
t.Fatalf("create child: %v", err)
}
_, err = svc.CreateTask(ctx, &CreateTaskRequest{
WorkspaceID: "ws-1",
WorkflowID: "wf-1",
ParentID: child.ID,
Title: "Grandchild",
})
if err == nil || !errors.Is(err, ErrSubtaskDepthExceeded) {
t.Fatalf("expected ErrSubtaskDepthExceeded, got: %v", err)
}
}
func TestCreateTask_SubtaskOfSubtask_Office_Allowed(t *testing.T) {
svc, repo := setupOfficeTest(t)
ctx := context.Background()
root, err := svc.CreateTask(ctx, &CreateTaskRequest{
WorkspaceID: "ws-1",
Title: "Root office",
ProjectID: "proj-1",
})
if err != nil {
t.Fatalf("create root: %v", err)
}
child, err := svc.CreateTask(ctx, &CreateTaskRequest{
WorkspaceID: "ws-1",
ParentID: root.ID,
Title: "Child office",
ProjectID: "proj-1",
})
if err != nil {
t.Fatalf("create child: %v", err)
}
grandchild, err := svc.CreateTask(ctx, &CreateTaskRequest{
WorkspaceID: "ws-1",
ParentID: child.ID,
Title: "Grandchild office",
ProjectID: "proj-1",
})
if err != nil {
t.Fatalf("create grandchild: %v", err)
}
got, err := repo.GetTask(ctx, grandchild.ID)
if err != nil {
t.Fatalf("GetTask: %v", err)
}
if got.ParentID != child.ID {
t.Errorf("parent_id = %q, want %q", got.ParentID, child.ID)
}
}