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 阻塞,不应就绪") }