You've already forked agentic-coding-workflow
主流程
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
package project
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
|
||||
)
|
||||
|
||||
func newArtifactSvc(repo *fakeRepo) (ArtifactService, RequirementService, ProjectService, *spyAudit) {
|
||||
spy := &spyAudit{}
|
||||
return NewArtifactService(repo, spy), NewRequirementService(repo, spy), NewProjectService(repo, spy), spy
|
||||
}
|
||||
|
||||
func TestArtifactService_Create_VersionAutoIncrement(t *testing.T) {
|
||||
repo := newFakeRepo()
|
||||
owner := uuid.New()
|
||||
seedProject(t, repo, owner)
|
||||
artSvc, reqSvc, _, spy := newArtifactSvc(repo)
|
||||
caller := Caller{UserID: owner}
|
||||
|
||||
r, err := reqSvc.Create(context.Background(), caller, "demo", CreateRequirementInput{Title: "R"})
|
||||
require.NoError(t, err)
|
||||
|
||||
a1, err := artSvc.Create(context.Background(), caller, "demo", r.Number, CreateArtifactInput{Phase: PhasePlanning, Content: "v1"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, a1.Version)
|
||||
|
||||
a2, err := artSvc.Create(context.Background(), caller, "demo", r.Number, CreateArtifactInput{Phase: PhasePlanning, Content: "v2", Note: "second"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, a2.Version)
|
||||
require.Equal(t, "second", a2.Note)
|
||||
|
||||
require.Contains(t, spy.actions(), "requirement_artifact.create")
|
||||
}
|
||||
|
||||
func TestArtifactService_Create_VersionsIndependentPerPhase(t *testing.T) {
|
||||
repo := newFakeRepo()
|
||||
owner := uuid.New()
|
||||
seedProject(t, repo, owner)
|
||||
artSvc, reqSvc, _, _ := newArtifactSvc(repo)
|
||||
caller := Caller{UserID: owner}
|
||||
|
||||
r, err := reqSvc.Create(context.Background(), caller, "demo", CreateRequirementInput{Title: "R"})
|
||||
require.NoError(t, err)
|
||||
|
||||
// planning 走到 v2,prototyping 的版本序列从 1 重新开始。
|
||||
_, err = artSvc.Create(context.Background(), caller, "demo", r.Number, CreateArtifactInput{Phase: PhasePlanning, Content: "p1"})
|
||||
require.NoError(t, err)
|
||||
_, err = artSvc.Create(context.Background(), caller, "demo", r.Number, CreateArtifactInput{Phase: PhasePlanning, Content: "p2"})
|
||||
require.NoError(t, err)
|
||||
|
||||
proto, err := artSvc.Create(context.Background(), caller, "demo", r.Number, CreateArtifactInput{Phase: PhasePrototyping, Content: "x1"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, proto.Version)
|
||||
}
|
||||
|
||||
func TestArtifactService_Create_RejectsNonArtifactPhase(t *testing.T) {
|
||||
repo := newFakeRepo()
|
||||
owner := uuid.New()
|
||||
seedProject(t, repo, owner)
|
||||
artSvc, reqSvc, _, _ := newArtifactSvc(repo)
|
||||
caller := Caller{UserID: owner}
|
||||
|
||||
r, err := reqSvc.Create(context.Background(), caller, "demo", CreateRequirementInput{Title: "R"})
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, phase := range []Phase{PhaseImplementing, PhaseReviewing, PhaseDone, Phase("bogus"), Phase("")} {
|
||||
_, err = artSvc.Create(context.Background(), caller, "demo", r.Number, CreateArtifactInput{Phase: phase, Content: "x"})
|
||||
ae, ok := errs.As(err)
|
||||
require.True(t, ok, "phase=%s", phase)
|
||||
require.Equal(t, errs.CodeInvalidInput, ae.Code, "phase=%s", phase)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArtifactService_Create_RetriesOnVersionConflict(t *testing.T) {
|
||||
repo := newFakeRepo()
|
||||
owner := uuid.New()
|
||||
seedProject(t, repo, owner)
|
||||
artSvc, reqSvc, _, _ := newArtifactSvc(repo)
|
||||
caller := Caller{UserID: owner}
|
||||
|
||||
r, err := reqSvc.Create(context.Background(), caller, "demo", CreateRequirementInput{Title: "R"})
|
||||
require.NoError(t, err)
|
||||
|
||||
// 首轮 CreateArtifact 撞 UNIQUE,service 应捕获并重试一次后成功。
|
||||
repo.createArtifactFailOnce = true
|
||||
a, err := artSvc.Create(context.Background(), caller, "demo", r.Number, CreateArtifactInput{Phase: PhasePlanning, Content: "v1"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, a.Version)
|
||||
require.False(t, repo.createArtifactFailOnce, "失败开关应已被消费")
|
||||
}
|
||||
|
||||
func TestArtifactService_Create_RequiresContent(t *testing.T) {
|
||||
repo := newFakeRepo()
|
||||
owner := uuid.New()
|
||||
seedProject(t, repo, owner)
|
||||
artSvc, reqSvc, _, _ := newArtifactSvc(repo)
|
||||
caller := Caller{UserID: owner}
|
||||
|
||||
r, err := reqSvc.Create(context.Background(), caller, "demo", CreateRequirementInput{Title: "R"})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = artSvc.Create(context.Background(), caller, "demo", r.Number, CreateArtifactInput{Phase: PhasePlanning, Content: ""})
|
||||
ae, ok := errs.As(err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, errs.CodeInvalidInput, ae.Code)
|
||||
}
|
||||
|
||||
func TestArtifactService_Create_BlockedWhenRequirementClosed(t *testing.T) {
|
||||
repo := newFakeRepo()
|
||||
owner := uuid.New()
|
||||
seedProject(t, repo, owner)
|
||||
artSvc, reqSvc, _, _ := newArtifactSvc(repo)
|
||||
caller := Caller{UserID: owner}
|
||||
|
||||
r, err := reqSvc.Create(context.Background(), caller, "demo", CreateRequirementInput{Title: "R"})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, reqSvc.Close(context.Background(), caller, "demo", r.Number))
|
||||
|
||||
_, err = artSvc.Create(context.Background(), caller, "demo", r.Number, CreateArtifactInput{Phase: PhasePlanning, Content: "v1"})
|
||||
ae, ok := errs.As(err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, errs.CodeRequirementClosed, ae.Code)
|
||||
}
|
||||
|
||||
func TestArtifactService_Create_RecordsSourceMessageID(t *testing.T) {
|
||||
repo := newFakeRepo()
|
||||
owner := uuid.New()
|
||||
seedProject(t, repo, owner)
|
||||
artSvc, reqSvc, _, _ := newArtifactSvc(repo)
|
||||
caller := Caller{UserID: owner}
|
||||
|
||||
r, err := reqSvc.Create(context.Background(), caller, "demo", CreateRequirementInput{Title: "R"})
|
||||
require.NoError(t, err)
|
||||
|
||||
srcID := int64(42)
|
||||
a, err := artSvc.Create(context.Background(), caller, "demo", r.Number, CreateArtifactInput{
|
||||
Phase: PhaseAuditing, Content: "from chat", SourceMessageID: &srcID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, a.SourceMessageID)
|
||||
require.Equal(t, srcID, *a.SourceMessageID)
|
||||
}
|
||||
|
||||
func TestArtifactService_List_FilterByPhaseAndAll(t *testing.T) {
|
||||
repo := newFakeRepo()
|
||||
owner := uuid.New()
|
||||
seedProject(t, repo, owner)
|
||||
artSvc, reqSvc, _, _ := newArtifactSvc(repo)
|
||||
caller := Caller{UserID: owner}
|
||||
|
||||
r, err := reqSvc.Create(context.Background(), caller, "demo", CreateRequirementInput{Title: "R"})
|
||||
require.NoError(t, err)
|
||||
for i := 0; i < 2; i++ {
|
||||
_, err := artSvc.Create(context.Background(), caller, "demo", r.Number, CreateArtifactInput{Phase: PhasePlanning, Content: "x"})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
_, err = artSvc.Create(context.Background(), caller, "demo", r.Number, CreateArtifactInput{Phase: PhaseAuditing, Content: "y"})
|
||||
require.NoError(t, err)
|
||||
|
||||
planning, err := artSvc.List(context.Background(), caller, "demo", r.Number, PhasePlanning)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, planning, 2)
|
||||
require.Equal(t, 1, planning[0].Version)
|
||||
require.Equal(t, 2, planning[1].Version)
|
||||
|
||||
all, err := artSvc.List(context.Background(), caller, "demo", r.Number, "")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, all, 3)
|
||||
|
||||
_, err = artSvc.List(context.Background(), caller, "demo", r.Number, Phase("bogus"))
|
||||
ae, ok := errs.As(err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, errs.CodeInvalidInput, ae.Code)
|
||||
}
|
||||
|
||||
func TestArtifactService_GetByVersion_NotFound(t *testing.T) {
|
||||
repo := newFakeRepo()
|
||||
owner := uuid.New()
|
||||
seedProject(t, repo, owner)
|
||||
artSvc, reqSvc, _, _ := newArtifactSvc(repo)
|
||||
caller := Caller{UserID: owner}
|
||||
|
||||
r, err := reqSvc.Create(context.Background(), caller, "demo", CreateRequirementInput{Title: "R"})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = artSvc.GetByVersion(context.Background(), caller, "demo", r.Number, PhasePlanning, 99)
|
||||
ae, ok := errs.As(err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, errs.CodeNotFound, ae.Code)
|
||||
}
|
||||
|
||||
func TestArtifactService_GetByVersion_HappyPath(t *testing.T) {
|
||||
repo := newFakeRepo()
|
||||
owner := uuid.New()
|
||||
seedProject(t, repo, owner)
|
||||
artSvc, reqSvc, _, _ := newArtifactSvc(repo)
|
||||
caller := Caller{UserID: owner}
|
||||
|
||||
r, err := reqSvc.Create(context.Background(), caller, "demo", CreateRequirementInput{Title: "R"})
|
||||
require.NoError(t, err)
|
||||
_, err = artSvc.Create(context.Background(), caller, "demo", r.Number, CreateArtifactInput{Phase: PhasePrototyping, Content: "hello", Note: "n"})
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := artSvc.GetByVersion(context.Background(), caller, "demo", r.Number, PhasePrototyping, 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello", got.Content)
|
||||
require.Equal(t, "n", got.Note)
|
||||
require.Equal(t, owner, got.CreatedBy)
|
||||
}
|
||||
|
||||
func TestArtifactService_Create_ForbiddenForOutsider(t *testing.T) {
|
||||
repo := newFakeRepo()
|
||||
owner := uuid.New()
|
||||
seedProject(t, repo, owner)
|
||||
artSvc, reqSvc, _, _ := newArtifactSvc(repo)
|
||||
owns := Caller{UserID: owner}
|
||||
|
||||
r, err := reqSvc.Create(context.Background(), owns, "demo", CreateRequirementInput{Title: "R"})
|
||||
require.NoError(t, err)
|
||||
|
||||
// 私有项目下,非 owner / 非 admin 调用者写入被拒(统一返回 not_found 防探测)。
|
||||
outsider := Caller{UserID: uuid.New()}
|
||||
_, err = artSvc.Create(context.Background(), outsider, "demo", r.Number, CreateArtifactInput{Phase: PhasePlanning, Content: "v1"})
|
||||
ae, ok := errs.As(err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, errs.CodeNotFound, ae.Code)
|
||||
}
|
||||
|
||||
func TestArtifactService_List_ForbiddenForOutsider(t *testing.T) {
|
||||
repo := newFakeRepo()
|
||||
owner := uuid.New()
|
||||
seedProject(t, repo, owner)
|
||||
artSvc, reqSvc, _, _ := newArtifactSvc(repo)
|
||||
owns := Caller{UserID: owner}
|
||||
|
||||
r, err := reqSvc.Create(context.Background(), owns, "demo", CreateRequirementInput{Title: "R"})
|
||||
require.NoError(t, err)
|
||||
|
||||
outsider := Caller{UserID: uuid.New()}
|
||||
_, err = artSvc.List(context.Background(), outsider, "demo", r.Number, "")
|
||||
ae, ok := errs.As(err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, errs.CodeNotFound, ae.Code)
|
||||
}
|
||||
Reference in New Issue
Block a user