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