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

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