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