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