Files
agentic-coding-workflow/internal/orchestrator/gate_test.go
T
2026-06-22 08:55:57 +08:00

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