You've already forked agentic-coding-workflow
131 lines
4.1 KiB
Go
131 lines
4.1 KiB
Go
package orchestrator
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
|
|
"github.com/yan1h/agent-coding-workflow/internal/project"
|
|
)
|
|
|
|
// fakeApplier: 内存版 DecompositionApplier,记录建出的 issue 与边。
|
|
type fakeApplier struct {
|
|
nextNum int
|
|
created []string // titles in order
|
|
edges [][2]int // (blockedNumber, blockerNumber)
|
|
byNum map[int]*project.Issue // number → issue
|
|
}
|
|
|
|
func newFakeApplier() *fakeApplier {
|
|
return &fakeApplier{byNum: map[int]*project.Issue{}}
|
|
}
|
|
|
|
func (f *fakeApplier) mk(in project.CreateIssueInput, parent *int) *project.Issue {
|
|
f.nextNum++
|
|
prio := 0
|
|
if in.Priority != nil {
|
|
prio = *in.Priority
|
|
}
|
|
var pid *uuid.UUID
|
|
if parent != nil {
|
|
id := uuid.New()
|
|
pid = &id
|
|
}
|
|
iss := &project.Issue{ID: uuid.New(), Number: f.nextNum, Title: in.Title, Priority: prio, ParentID: pid}
|
|
f.byNum[iss.Number] = iss
|
|
f.created = append(f.created, in.Title)
|
|
return iss
|
|
}
|
|
|
|
func (f *fakeApplier) Create(_ context.Context, _ project.Caller, _ string, in project.CreateIssueInput) (*project.Issue, error) {
|
|
return f.mk(in, nil), nil
|
|
}
|
|
func (f *fakeApplier) CreateSubtask(_ context.Context, _ project.Caller, _ string, parentNumber int, in project.CreateIssueInput) (*project.Issue, error) {
|
|
return f.mk(in, &parentNumber), nil
|
|
}
|
|
func (f *fakeApplier) AddDependency(_ context.Context, _ project.Caller, _ string, in project.AddDependencyInput) (*project.Dependency, error) {
|
|
f.edges = append(f.edges, [2]int{in.BlockedNumber, in.BlockerNumber})
|
|
return &project.Dependency{ID: uuid.New()}, nil
|
|
}
|
|
|
|
func TestApplyDecomposition_RejectsCycleBeforeAnyWrite(t *testing.T) {
|
|
f := newFakeApplier()
|
|
d := Decomposition{
|
|
Subtasks: []SubtaskSpec{{Ref: "a", Title: "A"}, {Ref: "b", Title: "B"}},
|
|
Edges: []EdgeSpec{
|
|
{BlockedRef: "a", BlockerRef: "b"},
|
|
{BlockedRef: "b", BlockerRef: "a"}, // 环
|
|
},
|
|
}
|
|
_, err := ApplyDecomposition(context.Background(), f, project.Caller{}, "demo", d)
|
|
ae, ok := errs.As(err)
|
|
require.True(t, ok)
|
|
require.Equal(t, errs.CodeInvalidInput, ae.Code)
|
|
require.Empty(t, f.created, "校验失败前不应有任何写入")
|
|
require.Empty(t, f.edges)
|
|
}
|
|
|
|
func TestApplyDecomposition_ValidDAGCreatesIssuesThenEdges(t *testing.T) {
|
|
f := newFakeApplier()
|
|
parent := 100
|
|
f.byNum[parent] = &project.Issue{Number: parent}
|
|
d := Decomposition{
|
|
ParentNumber: &parent,
|
|
Subtasks: []SubtaskSpec{
|
|
{Ref: "a", Title: "A", Priority: 2},
|
|
{Ref: "b", Title: "B"},
|
|
{Ref: "c", Title: "C"},
|
|
},
|
|
Edges: []EdgeSpec{
|
|
{BlockedRef: "a", BlockerRef: "b"},
|
|
{BlockedRef: "a", BlockerRef: "c"},
|
|
},
|
|
}
|
|
res, err := ApplyDecomposition(context.Background(), f, project.Caller{}, "demo", d)
|
|
require.NoError(t, err)
|
|
require.Len(t, f.created, 3)
|
|
require.Len(t, f.edges, 2)
|
|
// refs 映射到真实 number
|
|
require.Contains(t, res.RefToNumber, "a")
|
|
require.Contains(t, res.RefToNumber, "b")
|
|
require.Contains(t, res.RefToNumber, "c")
|
|
// 边以真实 number 落库
|
|
na, nb := res.RefToNumber["a"], res.RefToNumber["b"]
|
|
require.Contains(t, f.edges, [2]int{na, nb})
|
|
}
|
|
|
|
func TestApplyDecomposition_RejectsEmpty(t *testing.T) {
|
|
f := newFakeApplier()
|
|
_, err := ApplyDecomposition(context.Background(), f, project.Caller{}, "demo", Decomposition{})
|
|
ae, ok := errs.As(err)
|
|
require.True(t, ok)
|
|
require.Equal(t, errs.CodeInvalidInput, ae.Code)
|
|
}
|
|
|
|
func TestApplyDecomposition_RejectsUnknownEdgeRef(t *testing.T) {
|
|
f := newFakeApplier()
|
|
d := Decomposition{
|
|
Subtasks: []SubtaskSpec{{Ref: "a", Title: "A"}},
|
|
Edges: []EdgeSpec{{BlockedRef: "a", BlockerRef: "zzz"}},
|
|
}
|
|
_, err := ApplyDecomposition(context.Background(), f, project.Caller{}, "demo", d)
|
|
ae, ok := errs.As(err)
|
|
require.True(t, ok)
|
|
require.Equal(t, errs.CodeInvalidInput, ae.Code)
|
|
require.Empty(t, f.created)
|
|
}
|
|
|
|
func TestApplyDecomposition_RejectsDuplicateRef(t *testing.T) {
|
|
f := newFakeApplier()
|
|
d := Decomposition{
|
|
Subtasks: []SubtaskSpec{{Ref: "a", Title: "A"}, {Ref: "a", Title: "A2"}},
|
|
}
|
|
_, err := ApplyDecomposition(context.Background(), f, project.Caller{}, "demo", d)
|
|
ae, ok := errs.As(err)
|
|
require.True(t, ok)
|
|
require.Equal(t, errs.CodeInvalidInput, ae.Code)
|
|
}
|