You've already forked agentic-coding-workflow
221 lines
7.2 KiB
Go
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 阻塞,不应就绪")
|
|
}
|