You've already forked agentic-coding-workflow
112 lines
3.3 KiB
Go
112 lines
3.3 KiB
Go
package orchestrator
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/yan1h/agent-coding-workflow/internal/project"
|
|
)
|
|
|
|
type fakeGateArtifacts struct {
|
|
byPhase map[project.Phase]int // phase → 产物数
|
|
}
|
|
|
|
func (f fakeGateArtifacts) ListArtifacts(_ context.Context, _, _ uuid.UUID, phase project.Phase) ([]*project.Artifact, error) {
|
|
n := f.byPhase[phase]
|
|
out := make([]*project.Artifact, 0, n)
|
|
for i := 0; i < n; i++ {
|
|
out = append(out, &project.Artifact{Phase: phase, Version: i + 1})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
type fakeGateIssues struct{ open int }
|
|
|
|
func (f fakeGateIssues) CountOpenIssues(_ context.Context, _ uuid.UUID) (int, error) {
|
|
return f.open, nil
|
|
}
|
|
|
|
func gateRun() *Run {
|
|
return &Run{ID: uuid.New(), ProjectID: uuid.New(), RequirementID: uuid.New()}
|
|
}
|
|
|
|
func TestGate_PassOnEndTurnWithArtifact(t *testing.T) {
|
|
g := NewV1Gate(fakeGateArtifacts{byPhase: map[project.Phase]int{project.PhasePlanning: 1}}, nil)
|
|
step := &Step{Phase: project.PhasePlanning}
|
|
res, err := g.Evaluate(context.Background(), gateRun(), step, "end_turn")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !res.Pass {
|
|
t.Errorf("expected pass; got %+v", res)
|
|
}
|
|
}
|
|
|
|
func TestGate_RetryOnEndTurnMissingArtifact(t *testing.T) {
|
|
g := NewV1Gate(fakeGateArtifacts{byPhase: map[project.Phase]int{}}, nil)
|
|
step := &Step{Phase: project.PhasePlanning}
|
|
res, err := g.Evaluate(context.Background(), gateRun(), step, "end_turn")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res.Pass || !res.Retry {
|
|
t.Errorf("expected no-pass retryable; got %+v", res)
|
|
}
|
|
}
|
|
|
|
func TestGate_FailNoRetryOnRefusalAndMaxTokens(t *testing.T) {
|
|
g := NewV1Gate(fakeGateArtifacts{byPhase: map[project.Phase]int{project.PhasePlanning: 1}}, nil)
|
|
step := &Step{Phase: project.PhasePlanning}
|
|
for _, sr := range []string{"refusal", "max_tokens"} {
|
|
res, err := g.Evaluate(context.Background(), gateRun(), step, sr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res.Pass || res.Retry {
|
|
t.Errorf("stopReason %q: expected fail no-retry; got %+v", sr, res)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGate_RetryOnEmptyOrCancelled(t *testing.T) {
|
|
g := NewV1Gate(fakeGateArtifacts{byPhase: map[project.Phase]int{project.PhasePlanning: 1}}, nil)
|
|
step := &Step{Phase: project.PhasePlanning}
|
|
for _, sr := range []string{"", "cancelled"} {
|
|
res, err := g.Evaluate(context.Background(), gateRun(), step, sr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res.Pass || !res.Retry {
|
|
t.Errorf("stopReason %q: expected retryable; got %+v", sr, res)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGate_ReviewingRequiresNoOpenIssues(t *testing.T) {
|
|
arts := fakeGateArtifacts{byPhase: map[project.Phase]int{project.PhaseReviewing: 1}}
|
|
step := &Step{Phase: project.PhaseReviewing}
|
|
|
|
gOpen := NewV1Gate(arts, fakeGateIssues{open: 2})
|
|
res, _ := gOpen.Evaluate(context.Background(), gateRun(), step, "end_turn")
|
|
if res.Pass || !res.Retry {
|
|
t.Errorf("open issues should block reviewing (retryable); got %+v", res)
|
|
}
|
|
|
|
gClean := NewV1Gate(arts, fakeGateIssues{open: 0})
|
|
res, _ = gClean.Evaluate(context.Background(), gateRun(), step, "end_turn")
|
|
if !res.Pass {
|
|
t.Errorf("no open issues should pass reviewing; got %+v", res)
|
|
}
|
|
}
|
|
|
|
func TestGate_DonePassesOnEndTurn(t *testing.T) {
|
|
g := NewV1Gate(nil, nil)
|
|
step := &Step{Phase: project.PhaseDone}
|
|
res, _ := g.Evaluate(context.Background(), gateRun(), step, "end_turn")
|
|
if !res.Pass {
|
|
t.Errorf("done should pass on end_turn; got %+v", res)
|
|
}
|
|
}
|