Files
agentic-coding-workflow/internal/project/requirement_service_test.go
T

178 lines
5.7 KiB
Go

package project
import (
"context"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/yan1h/agent-coding-workflow/internal/audit"
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
)
func newReqSvc(repo *fakeRepo) (RequirementService, ProjectService, *spyAudit) {
spy := &spyAudit{}
return NewRequirementService(repo, spy), NewProjectService(repo, spy), spy
}
func TestRequirementService_Create_HappyAndAutoIncrement(t *testing.T) {
repo := newFakeRepo()
owner := uuid.New()
seedProject(t, repo, owner)
svc, _, spy := newReqSvc(repo)
caller := Caller{UserID: owner}
r1, err := svc.Create(context.Background(), caller, "demo", CreateRequirementInput{Title: "First"})
require.NoError(t, err)
require.Equal(t, 1, r1.Number)
r2, err := svc.Create(context.Background(), caller, "demo", CreateRequirementInput{Title: "Second"})
require.NoError(t, err)
require.Equal(t, 2, r2.Number)
require.Contains(t, spy.actions(), "requirement.create")
}
func TestRequirementService_PhaseChange_FollowsEnum(t *testing.T) {
repo := newFakeRepo()
owner := uuid.New()
seedProject(t, repo, owner)
svc, _, _ := newReqSvc(repo)
caller := Caller{UserID: owner}
r, err := svc.Create(context.Background(), caller, "demo", CreateRequirementInput{Title: "T"})
require.NoError(t, err)
// 任意跳跃合法:planning → reviewing
out, err := svc.ChangePhase(context.Background(), caller, "demo", r.Number, PhaseReviewing)
require.NoError(t, err)
require.Equal(t, PhaseReviewing, out.Phase)
// 非枚举字符串 → invalid
_, err = svc.ChangePhase(context.Background(), caller, "demo", r.Number, "bogus")
ae, ok := errs.As(err)
require.True(t, ok)
require.Equal(t, errs.CodeInvalidInput, ae.Code)
}
func TestRequirementService_PhaseChange_BlockedWhenClosed(t *testing.T) {
repo := newFakeRepo()
owner := uuid.New()
seedProject(t, repo, owner)
svc, _, _ := newReqSvc(repo)
caller := Caller{UserID: owner}
r, err := svc.Create(context.Background(), caller, "demo", CreateRequirementInput{Title: "T"})
require.NoError(t, err)
require.NoError(t, svc.Close(context.Background(), caller, "demo", r.Number))
_, err = svc.ChangePhase(context.Background(), caller, "demo", r.Number, PhaseAuditing)
ae, ok := errs.As(err)
require.True(t, ok)
require.Equal(t, errs.CodeRequirementClosed, ae.Code)
}
func TestRequirementService_Write_BlockedWhenProjectArchived(t *testing.T) {
repo := newFakeRepo()
owner := uuid.New()
seedProject(t, repo, owner)
reqSvc, projSvc, _ := newReqSvc(repo)
caller := Caller{UserID: owner}
require.NoError(t, projSvc.Archive(context.Background(), caller, "demo"))
_, err := reqSvc.Create(context.Background(), caller, "demo", CreateRequirementInput{Title: "X"})
ae, ok := errs.As(err)
require.True(t, ok)
require.Equal(t, errs.CodeProjectArchived, ae.Code)
}
func TestRequirementService_Reopen_AllowsPhaseAgain(t *testing.T) {
repo := newFakeRepo()
owner := uuid.New()
seedProject(t, repo, owner)
svc, _, _ := newReqSvc(repo)
caller := Caller{UserID: owner}
r, err := svc.Create(context.Background(), caller, "demo", CreateRequirementInput{Title: "T"})
require.NoError(t, err)
require.NoError(t, svc.Close(context.Background(), caller, "demo", r.Number))
require.NoError(t, svc.Reopen(context.Background(), caller, "demo", r.Number))
out, err := svc.ChangePhase(context.Background(), caller, "demo", r.Number, PhaseDone)
require.NoError(t, err)
require.Equal(t, PhaseDone, out.Phase)
}
func TestRequirementService_Create_WithWorkspaceID(t *testing.T) {
repo := newFakeRepo()
owner := uuid.New()
seedProject(t, repo, owner)
svc, _, spy := newReqSvc(repo)
caller := Caller{UserID: owner}
wsID := uuid.New()
out, err := svc.Create(context.Background(), caller, "demo", CreateRequirementInput{
Title: "R", WorkspaceID: &wsID,
})
require.NoError(t, err)
require.NotNil(t, out.WorkspaceID)
require.Equal(t, wsID, *out.WorkspaceID)
// 再 Get 一次确认持久化
got, err := svc.Get(context.Background(), caller, "demo", out.Number)
require.NoError(t, err)
require.NotNil(t, got.WorkspaceID)
require.Equal(t, wsID, *got.WorkspaceID)
// audit metadata 包含 workspace_id
require.NotEmpty(t, spy.entries)
var createEntry *audit.Entry
for i := range spy.entries {
if spy.entries[i].Action == "requirement.create" {
createEntry = &spy.entries[i]
}
}
require.NotNil(t, createEntry)
require.Equal(t, wsID.String(), createEntry.Metadata["workspace_id"])
}
func TestRequirementService_Update_WorkspaceID_TriState(t *testing.T) {
repo := newFakeRepo()
owner := uuid.New()
seedProject(t, repo, owner)
svc, _, _ := newReqSvc(repo)
caller := Caller{UserID: owner}
wsA := uuid.New()
r, err := svc.Create(context.Background(), caller, "demo", CreateRequirementInput{
Title: "R", WorkspaceID: &wsA,
})
require.NoError(t, err)
require.NotNil(t, r.WorkspaceID)
require.Equal(t, wsA, *r.WorkspaceID)
// 1) nil → 不变(结合一个其它 patch 字段触发 update)
newTitle := "R2"
out, err := svc.Update(context.Background(), caller, "demo", r.Number, UpdateRequirementInput{
Title: &newTitle,
})
require.NoError(t, err)
require.NotNil(t, out.WorkspaceID)
require.Equal(t, wsA, *out.WorkspaceID)
// 2) &id(非 Nil)→ 关联到新 workspace
wsB := uuid.New()
out, err = svc.Update(context.Background(), caller, "demo", r.Number, UpdateRequirementInput{
WorkspaceID: &wsB,
})
require.NoError(t, err)
require.NotNil(t, out.WorkspaceID)
require.Equal(t, wsB, *out.WorkspaceID)
// 3) &uuid.Nil → 显式解绑
nilUUID := uuid.Nil
out, err = svc.Update(context.Background(), caller, "demo", r.Number, UpdateRequirementInput{
WorkspaceID: &nilUUID,
})
require.NoError(t, err)
require.Nil(t, out.WorkspaceID)
}