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

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)
}