You've already forked agentic-coding-workflow
143 lines
4.8 KiB
Go
143 lines
4.8 KiB
Go
package project
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
|
|
)
|
|
|
|
// fakeWorkspaceState is a controllable WorkspaceStateLookup for Done-gate tests.
|
|
type fakeWorkspaceState struct {
|
|
merged bool
|
|
err error
|
|
}
|
|
|
|
func (f fakeWorkspaceState) PullRequestMerged(_ context.Context, _, _ uuid.UUID) (bool, error) {
|
|
return f.merged, f.err
|
|
}
|
|
|
|
// seedRequirement creates an open requirement under the seeded "demo" project.
|
|
func seedRequirement(t *testing.T, repo *fakeRepo, owner uuid.UUID, ws *uuid.UUID) *Requirement {
|
|
t.Helper()
|
|
reqSvc, _, _ := newReqSvc(repo)
|
|
r, err := reqSvc.Create(context.Background(), Caller{UserID: owner}, "demo", CreateRequirementInput{
|
|
Title: "R", WorkspaceID: ws,
|
|
})
|
|
require.NoError(t, err)
|
|
return r
|
|
}
|
|
|
|
func requireGateFailed(t *testing.T, err error) {
|
|
t.Helper()
|
|
ae, ok := errs.As(err)
|
|
require.True(t, ok)
|
|
require.Equal(t, errs.CodePhaseGateFailed, ae.Code)
|
|
}
|
|
|
|
func TestPhaseGate_Implementing_BlockedWithoutApprovedAuditing(t *testing.T) {
|
|
repo := newFakeRepo()
|
|
owner := uuid.New()
|
|
seedProject(t, repo, owner)
|
|
r := seedRequirement(t, repo, owner, nil)
|
|
|
|
gate := NewGateRunner(GateDeps{Repo: repo}, nil)
|
|
err := gate.Check(context.Background(), r, PhasePlanning, PhaseImplementing)
|
|
requireGateFailed(t, err)
|
|
}
|
|
|
|
func TestPhaseGate_Implementing_AllowedAfterApproval(t *testing.T) {
|
|
repo := newFakeRepo()
|
|
owner := uuid.New()
|
|
seedProject(t, repo, owner)
|
|
artSvc, _, _, _ := newArtifactSvc(repo)
|
|
r := seedRequirement(t, repo, owner, nil)
|
|
caller := Caller{UserID: owner}
|
|
|
|
_, err := artSvc.Create(context.Background(), caller, "demo", r.Number, CreateArtifactInput{Phase: PhaseAuditing, Content: "report"})
|
|
require.NoError(t, err)
|
|
_, err = artSvc.Approve(context.Background(), caller, "demo", r.Number, PhaseAuditing, 1, VerdictPass)
|
|
require.NoError(t, err)
|
|
|
|
gate := NewGateRunner(GateDeps{Repo: repo}, nil)
|
|
require.NoError(t, gate.Check(context.Background(), r, PhasePlanning, PhaseImplementing))
|
|
}
|
|
|
|
func TestPhaseGate_Implementing_BlockedWhenVerdictFail(t *testing.T) {
|
|
repo := newFakeRepo()
|
|
owner := uuid.New()
|
|
seedProject(t, repo, owner)
|
|
artSvc, _, _, _ := newArtifactSvc(repo)
|
|
r := seedRequirement(t, repo, owner, nil)
|
|
caller := Caller{UserID: owner}
|
|
|
|
_, err := artSvc.Create(context.Background(), caller, "demo", r.Number, CreateArtifactInput{Phase: PhaseAuditing, Content: "report"})
|
|
require.NoError(t, err)
|
|
// verdict=fail 不写指针,gate 仍应阻止。
|
|
_, err = artSvc.Approve(context.Background(), caller, "demo", r.Number, PhaseAuditing, 1, VerdictFail)
|
|
require.NoError(t, err)
|
|
|
|
gate := NewGateRunner(GateDeps{Repo: repo}, nil)
|
|
requireGateFailed(t, gate.Check(context.Background(), r, PhasePlanning, PhaseImplementing))
|
|
}
|
|
|
|
func TestPhaseGate_Done_BlockedWhenNoWorkspace(t *testing.T) {
|
|
repo := newFakeRepo()
|
|
owner := uuid.New()
|
|
seedProject(t, repo, owner)
|
|
r := seedRequirement(t, repo, owner, nil) // no workspace
|
|
|
|
gate := NewGateRunner(GateDeps{Repo: repo, Workspace: fakeWorkspaceState{merged: true}}, nil)
|
|
requireGateFailed(t, gate.Check(context.Background(), r, PhaseReviewing, PhaseDone))
|
|
}
|
|
|
|
func TestPhaseGate_Done_BlockedWhenNotMerged(t *testing.T) {
|
|
repo := newFakeRepo()
|
|
owner := uuid.New()
|
|
seedProject(t, repo, owner)
|
|
ws := uuid.New()
|
|
r := seedRequirement(t, repo, owner, &ws)
|
|
|
|
gate := NewGateRunner(GateDeps{Repo: repo, Workspace: fakeWorkspaceState{merged: false}}, nil)
|
|
requireGateFailed(t, gate.Check(context.Background(), r, PhaseReviewing, PhaseDone))
|
|
}
|
|
|
|
func TestPhaseGate_Done_AllowedWhenMerged(t *testing.T) {
|
|
repo := newFakeRepo()
|
|
owner := uuid.New()
|
|
seedProject(t, repo, owner)
|
|
ws := uuid.New()
|
|
r := seedRequirement(t, repo, owner, &ws)
|
|
|
|
gate := NewGateRunner(GateDeps{Repo: repo, Workspace: fakeWorkspaceState{merged: true}}, nil)
|
|
require.NoError(t, gate.Check(context.Background(), r, PhaseReviewing, PhaseDone))
|
|
}
|
|
|
|
func TestPhaseGate_Done_BlockedWhenWorkspaceLookupNil(t *testing.T) {
|
|
repo := newFakeRepo()
|
|
owner := uuid.New()
|
|
seedProject(t, repo, owner)
|
|
ws := uuid.New()
|
|
r := seedRequirement(t, repo, owner, &ws)
|
|
|
|
// Workspace 缺失 → 优雅降级为"未合并",阻止流转。
|
|
gate := NewGateRunner(GateDeps{Repo: repo, Workspace: nil}, nil)
|
|
requireGateFailed(t, gate.Check(context.Background(), r, PhaseReviewing, PhaseDone))
|
|
}
|
|
|
|
func TestPhaseGate_OpenPhases_AlwaysAllowed(t *testing.T) {
|
|
repo := newFakeRepo()
|
|
owner := uuid.New()
|
|
seedProject(t, repo, owner)
|
|
r := seedRequirement(t, repo, owner, nil)
|
|
|
|
gate := NewGateRunner(GateDeps{Repo: repo}, nil)
|
|
// planning/prototyping/auditing/reviewing 无 entry 网关 → 任意流转放行(回归 any-to-any)。
|
|
for _, to := range []Phase{PhasePlanning, PhasePrototyping, PhaseAuditing, PhaseReviewing} {
|
|
require.NoError(t, gate.Check(context.Background(), r, PhasePlanning, to), "to=%s", to)
|
|
}
|
|
}
|