Files
2026-06-22 08:55:57 +08:00

221 lines
7.2 KiB
Go

package project
import (
"context"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
)
// mkIssue 在 demo 项目创建一个游离 issue,返回其编号。
func mkIssue(t *testing.T, svc IssueService, caller Caller, title string) int {
t.Helper()
i, err := svc.Create(context.Background(), caller, "demo", CreateIssueInput{Title: title})
require.NoError(t, err)
return i.Number
}
func TestCreateSubtask_SetsParentAndPriority(t *testing.T) {
repo := newFakeRepo()
owner := uuid.New()
seedProject(t, repo, owner)
svc, _, _ := newIssueSvc(repo)
caller := Caller{UserID: owner}
parentNum := mkIssue(t, svc, caller, "Parent")
prio := 2
sub, err := svc.CreateSubtask(context.Background(), caller, "demo", parentNum,
CreateIssueInput{Title: "Child", Priority: &prio})
require.NoError(t, err)
require.NotNil(t, sub.ParentID)
require.Equal(t, 2, sub.Priority)
// 父不存在 → NotFound
_, err = svc.CreateSubtask(context.Background(), caller, "demo", 9999, CreateIssueInput{Title: "X"})
ae, ok := errs.As(err)
require.True(t, ok)
require.Equal(t, errs.CodeNotFound, ae.Code)
}
func TestCreateIssue_RejectsBadPriority(t *testing.T) {
repo := newFakeRepo()
owner := uuid.New()
seedProject(t, repo, owner)
svc, _, _ := newIssueSvc(repo)
caller := Caller{UserID: owner}
bad := 5
_, err := svc.Create(context.Background(), caller, "demo", CreateIssueInput{Title: "X", Priority: &bad})
ae, ok := errs.As(err)
require.True(t, ok)
require.Equal(t, errs.CodeInvalidInput, ae.Code)
}
func TestAddDependency_RejectsSelf(t *testing.T) {
repo := newFakeRepo()
owner := uuid.New()
seedProject(t, repo, owner)
svc, _, _ := newIssueSvc(repo)
caller := Caller{UserID: owner}
a := mkIssue(t, svc, caller, "A")
_, err := svc.AddDependency(context.Background(), caller, "demo",
AddDependencyInput{BlockedNumber: a, BlockerNumber: a})
ae, ok := errs.As(err)
require.True(t, ok)
require.Equal(t, errs.CodeInvalidInput, ae.Code)
}
func TestAddDependency_RejectsDuplicate(t *testing.T) {
repo := newFakeRepo()
owner := uuid.New()
seedProject(t, repo, owner)
svc, _, _ := newIssueSvc(repo)
caller := Caller{UserID: owner}
a := mkIssue(t, svc, caller, "A")
b := mkIssue(t, svc, caller, "B")
_, err := svc.AddDependency(context.Background(), caller, "demo",
AddDependencyInput{BlockedNumber: a, BlockerNumber: b})
require.NoError(t, err)
_, err = svc.AddDependency(context.Background(), caller, "demo",
AddDependencyInput{BlockedNumber: a, BlockerNumber: b})
ae, ok := errs.As(err)
require.True(t, ok)
require.Equal(t, errs.CodeInvalidInput, ae.Code)
}
func TestAddDependency_RejectsDirectCycle(t *testing.T) {
repo := newFakeRepo()
owner := uuid.New()
seedProject(t, repo, owner)
svc, _, _ := newIssueSvc(repo)
caller := Caller{UserID: owner}
a := mkIssue(t, svc, caller, "A")
b := mkIssue(t, svc, caller, "B")
// A blocked-by B
_, err := svc.AddDependency(context.Background(), caller, "demo",
AddDependencyInput{BlockedNumber: a, BlockerNumber: b})
require.NoError(t, err)
// B blocked-by A → 直接环,应拒绝
_, err = svc.AddDependency(context.Background(), caller, "demo",
AddDependencyInput{BlockedNumber: b, BlockerNumber: a})
ae, ok := errs.As(err)
require.True(t, ok)
require.Equal(t, errs.CodeInvalidInput, ae.Code)
}
func TestAddDependency_RejectsTransitiveCycle(t *testing.T) {
repo := newFakeRepo()
owner := uuid.New()
seedProject(t, repo, owner)
svc, _, _ := newIssueSvc(repo)
caller := Caller{UserID: owner}
a := mkIssue(t, svc, caller, "A")
b := mkIssue(t, svc, caller, "B")
c := mkIssue(t, svc, caller, "C")
// A blocked-by B, B blocked-by C
_, err := svc.AddDependency(context.Background(), caller, "demo",
AddDependencyInput{BlockedNumber: a, BlockerNumber: b})
require.NoError(t, err)
_, err = svc.AddDependency(context.Background(), caller, "demo",
AddDependencyInput{BlockedNumber: b, BlockerNumber: c})
require.NoError(t, err)
// C blocked-by A → A->B->C->A 传递环,应拒绝
_, err = svc.AddDependency(context.Background(), caller, "demo",
AddDependencyInput{BlockedNumber: c, BlockerNumber: a})
ae, ok := errs.As(err)
require.True(t, ok)
require.Equal(t, errs.CodeInvalidInput, ae.Code)
}
func TestAddDependency_AllowsValidDAG(t *testing.T) {
repo := newFakeRepo()
owner := uuid.New()
seedProject(t, repo, owner)
svc, _, _ := newIssueSvc(repo)
caller := Caller{UserID: owner}
a := mkIssue(t, svc, caller, "A")
b := mkIssue(t, svc, caller, "B")
c := mkIssue(t, svc, caller, "C")
// A blocked-by B, A blocked-by C —— 合法 DAG(无环)
_, err := svc.AddDependency(context.Background(), caller, "demo",
AddDependencyInput{BlockedNumber: a, BlockerNumber: b})
require.NoError(t, err)
_, err = svc.AddDependency(context.Background(), caller, "demo",
AddDependencyInput{BlockedNumber: a, BlockerNumber: c})
require.NoError(t, err)
blockedBy, blocks, err := svc.ListDependencies(context.Background(), caller, "demo", a)
require.NoError(t, err)
require.Len(t, blockedBy, 2)
require.Len(t, blocks, 0)
}
func TestRemoveDependency(t *testing.T) {
repo := newFakeRepo()
owner := uuid.New()
seedProject(t, repo, owner)
svc, _, _ := newIssueSvc(repo)
caller := Caller{UserID: owner}
a := mkIssue(t, svc, caller, "A")
b := mkIssue(t, svc, caller, "B")
_, err := svc.AddDependency(context.Background(), caller, "demo",
AddDependencyInput{BlockedNumber: a, BlockerNumber: b})
require.NoError(t, err)
require.NoError(t, svc.RemoveDependency(context.Background(), caller, "demo", a, b))
// 再删 → NotFound
err = svc.RemoveDependency(context.Background(), caller, "demo", a, b)
ae, ok := errs.As(err)
require.True(t, ok)
require.Equal(t, errs.CodeNotFound, ae.Code)
blockedBy, _, err := svc.ListDependencies(context.Background(), caller, "demo", a)
require.NoError(t, err)
require.Len(t, blockedBy, 0)
}
func TestListReadyLeafSubtasks_OnlyUnblockedOpen(t *testing.T) {
repo := newFakeRepo()
owner := uuid.New()
seedProject(t, repo, owner)
svc, _, _ := newIssueSvc(repo)
caller := Caller{UserID: owner}
parent := mkIssue(t, svc, caller, "Parent")
prio1 := 1
prio3 := 3
s1, err := svc.CreateSubtask(context.Background(), caller, "demo", parent, CreateIssueInput{Title: "S1", Priority: &prio1})
require.NoError(t, err)
s2, err := svc.CreateSubtask(context.Background(), caller, "demo", parent, CreateIssueInput{Title: "S2", Priority: &prio3})
require.NoError(t, err)
blocker := mkIssue(t, svc, caller, "Blocker")
s3, err := svc.CreateSubtask(context.Background(), caller, "demo", parent, CreateIssueInput{Title: "S3"})
require.NoError(t, err)
// S3 blocked-by Blocker(未关闭)→ 不就绪
_, err = svc.AddDependency(context.Background(), caller, "demo",
AddDependencyInput{BlockedNumber: s3.Number, BlockerNumber: blocker})
require.NoError(t, err)
proj, err := repo.GetProjectBySlug(context.Background(), "demo")
require.NoError(t, err)
ready, err := repo.ListReadyLeafSubtasks(context.Background(), proj.ID, 10)
require.NoError(t, err)
got := map[uuid.UUID]bool{}
for _, i := range ready {
got[i.ID] = true
}
require.True(t, got[s1.ID])
require.True(t, got[s2.ID])
require.False(t, got[s3.ID], "S3 被未关闭 blocker 阻塞,不应就绪")
}