This commit is contained in:
2026-06-22 08:55:57 +08:00
parent dbb87823e8
commit 6ade6e8fa9
325 changed files with 41131 additions and 855 deletions
+283 -9
View File
@@ -19,11 +19,14 @@ import (
// fakeRepo 是内存版 Repository,仅用于 service 单测;按 ID/slug 查找用 map,
// number 自增手动维护。并发安全(一把大锁就够)。
type fakeRepo struct {
mu sync.Mutex
projects map[uuid.UUID]*Project
requirements map[uuid.UUID]*Requirement
issues map[uuid.UUID]*Issue
artifacts map[uuid.UUID]*Artifact
mu sync.Mutex
projects map[uuid.UUID]*Project
requirements map[uuid.UUID]*Requirement
issues map[uuid.UUID]*Issue
artifacts map[uuid.UUID]*Artifact
phasePointers map[string]*PhasePointer // key: requirementID + "|" + phase
dependencies map[uuid.UUID]*Dependency
members map[string]*Member // key: projectID + "|" + userID
// 置 true 时,下一次 CreateArtifact 返回一次真实的 PG 唯一约束冲突后自动复位,
// 用于覆盖 artifactService.createWithRetry 的 version 竞争重试分支。
@@ -32,13 +35,24 @@ type fakeRepo struct {
func newFakeRepo() *fakeRepo {
return &fakeRepo{
projects: map[uuid.UUID]*Project{},
requirements: map[uuid.UUID]*Requirement{},
issues: map[uuid.UUID]*Issue{},
artifacts: map[uuid.UUID]*Artifact{},
projects: map[uuid.UUID]*Project{},
requirements: map[uuid.UUID]*Requirement{},
issues: map[uuid.UUID]*Issue{},
artifacts: map[uuid.UUID]*Artifact{},
phasePointers: map[string]*PhasePointer{},
dependencies: map[uuid.UUID]*Dependency{},
members: map[string]*Member{},
}
}
func memberKey(projectID, userID uuid.UUID) string {
return projectID.String() + "|" + userID.String()
}
func phasePointerKey(reqID uuid.UUID, phase Phase) string {
return reqID.String() + "|" + string(phase)
}
func (r *fakeRepo) cloneProject(p *Project) *Project { v := *p; return &v }
func (r *fakeRepo) cloneReq(p *Requirement) *Requirement { v := *p; return &v }
func (r *fakeRepo) cloneIssue(p *Issue) *Issue { v := *p; return &v }
@@ -127,6 +141,51 @@ func (r *fakeRepo) UnarchiveProject(_ context.Context, id uuid.UUID) error {
return nil
}
// ===== ProjectMember =====
func (r *fakeRepo) UpsertMember(_ context.Context, projectID, userID uuid.UUID, role Role, addedBy *uuid.UUID) (*Member, error) {
r.mu.Lock()
defer r.mu.Unlock()
m := &Member{ProjectID: projectID, UserID: userID, Role: role, AddedBy: addedBy, CreatedAt: time.Now()}
r.members[memberKey(projectID, userID)] = m
cp := *m
return &cp, nil
}
func (r *fakeRepo) DeleteMember(_ context.Context, projectID, userID uuid.UUID) (int64, error) {
r.mu.Lock()
defer r.mu.Unlock()
k := memberKey(projectID, userID)
if _, ok := r.members[k]; !ok {
return 0, nil
}
delete(r.members, k)
return 1, nil
}
func (r *fakeRepo) GetMemberRole(_ context.Context, projectID, userID uuid.UUID) (Role, error) {
r.mu.Lock()
defer r.mu.Unlock()
m, ok := r.members[memberKey(projectID, userID)]
if !ok {
return RoleNone, nil
}
return m.Role, nil
}
func (r *fakeRepo) ListMembers(_ context.Context, projectID uuid.UUID) ([]*Member, error) {
r.mu.Lock()
defer r.mu.Unlock()
out := make([]*Member, 0)
for _, m := range r.members {
if m.ProjectID == projectID {
cp := *m
out = append(out, &cp)
}
}
return out, nil
}
// ===== Requirement =====
func (r *fakeRepo) nextRequirementNumber(projectID uuid.UUID) int {
@@ -279,6 +338,17 @@ func (r *fakeRepo) GetIssueByNumber(_ context.Context, projectID uuid.UUID, numb
return nil, errs.New(errs.CodeNotFound, "issue 不存在")
}
func (r *fakeRepo) GetIssueByID(_ context.Context, id uuid.UUID) (*Issue, error) {
r.mu.Lock()
defer r.mu.Unlock()
for _, x := range r.issues {
if x.ID == id {
return r.cloneIssue(x), nil
}
}
return nil, errs.New(errs.CodeNotFound, "issue 不存在")
}
func (r *fakeRepo) ListIssues(_ context.Context, projectID uuid.UUID, filter IssueFilter, requirementID *uuid.UUID) ([]*Issue, error) {
r.mu.Lock()
defer r.mu.Unlock()
@@ -369,6 +439,145 @@ func (r *fakeRepo) ReopenIssue(_ context.Context, id uuid.UUID) error {
return nil
}
func (r *fakeRepo) UpdateIssuePriority(_ context.Context, id uuid.UUID, priority int) (*Issue, error) {
r.mu.Lock()
defer r.mu.Unlock()
cur, ok := r.issues[id]
if !ok {
return nil, errs.New(errs.CodeNotFound, "issue 不存在")
}
cur.Priority = priority
cur.UpdatedAt = time.Now()
return r.cloneIssue(cur), nil
}
func (r *fakeRepo) CreateDependency(_ context.Context, d *Dependency) (*Dependency, error) {
r.mu.Lock()
defer r.mu.Unlock()
for _, x := range r.dependencies {
if x.BlockedID == d.BlockedID && x.BlockerID == d.BlockerID {
return nil, errs.New(errs.CodeInvalidInput, "依赖已存在")
}
}
d.CreatedAt = time.Now()
v := *d
r.dependencies[d.ID] = &v
out := *d
return &out, nil
}
func (r *fakeRepo) DeleteDependency(_ context.Context, projectID, blockedID, blockerID uuid.UUID) (int64, error) {
r.mu.Lock()
defer r.mu.Unlock()
for id, x := range r.dependencies {
if x.ProjectID == projectID && x.BlockedID == blockedID && x.BlockerID == blockerID {
delete(r.dependencies, id)
return 1, nil
}
}
return 0, nil
}
func (r *fakeRepo) ListBlockers(_ context.Context, blockedID uuid.UUID) ([]*Issue, error) {
r.mu.Lock()
defer r.mu.Unlock()
out := make([]*Issue, 0)
for _, d := range r.dependencies {
if d.BlockedID == blockedID {
if iss, ok := r.issues[d.BlockerID]; ok {
out = append(out, r.cloneIssue(iss))
}
}
}
return out, nil
}
func (r *fakeRepo) ListBlocked(_ context.Context, blockerID uuid.UUID) ([]*Issue, error) {
r.mu.Lock()
defer r.mu.Unlock()
out := make([]*Issue, 0)
for _, d := range r.dependencies {
if d.BlockerID == blockerID {
if iss, ok := r.issues[d.BlockedID]; ok {
out = append(out, r.cloneIssue(iss))
}
}
}
return out, nil
}
func (r *fakeRepo) ListBlockerIDs(_ context.Context, blockedID uuid.UUID) ([]uuid.UUID, error) {
r.mu.Lock()
defer r.mu.Unlock()
out := make([]uuid.UUID, 0)
for _, d := range r.dependencies {
if d.BlockedID == blockedID {
out = append(out, d.BlockerID)
}
}
return out, nil
}
func (r *fakeRepo) ListSubtasks(_ context.Context, parentID uuid.UUID) ([]*Issue, error) {
r.mu.Lock()
defer r.mu.Unlock()
out := make([]*Issue, 0)
for _, x := range r.issues {
if x.ParentID != nil && *x.ParentID == parentID {
out = append(out, r.cloneIssue(x))
}
}
return out, nil
}
func (r *fakeRepo) ListReadyLeafSubtasks(_ context.Context, projectID uuid.UUID, limit int) ([]*Issue, error) {
r.mu.Lock()
defer r.mu.Unlock()
out := make([]*Issue, 0)
for _, x := range r.issues {
if x.ProjectID != projectID || x.Status != StatusOpen || x.ParentID == nil {
continue
}
blocked := false
for _, d := range r.dependencies {
if d.BlockedID == x.ID {
if b, ok := r.issues[d.BlockerID]; ok && b.Status != StatusClosed {
blocked = true
break
}
}
}
if blocked {
continue
}
out = append(out, r.cloneIssue(x))
}
if limit > 0 && len(out) > limit {
out = out[:limit]
}
return out, nil
}
func (r *fakeRepo) ListSchedulableProjects(_ context.Context) ([]uuid.UUID, error) {
r.mu.Lock()
defer r.mu.Unlock()
seen := map[uuid.UUID]struct{}{}
out := make([]uuid.UUID, 0)
for _, x := range r.issues {
if x.Status == StatusOpen && x.ParentID != nil {
if _, ok := seen[x.ProjectID]; !ok {
seen[x.ProjectID] = struct{}{}
out = append(out, x.ProjectID)
}
}
}
return out, nil
}
func (r *fakeRepo) CountActiveSessionsForProject(_ context.Context, _ uuid.UUID) (int, error) {
return 0, nil
}
func (r *fakeRepo) GetMaxArtifactVersion(_ context.Context, requirementID uuid.UUID, phase Phase) (int, error) {
r.mu.Lock()
defer r.mu.Unlock()
@@ -395,6 +604,9 @@ func (r *fakeRepo) CreateArtifact(_ context.Context, in *Artifact) (*Artifact, e
}
}
in.CreatedAt = time.Now()
if in.Verdict == "" {
in.Verdict = VerdictNone // 模拟 DB 列默认值
}
r.artifacts[in.ID] = r.cloneArtifact(in)
return r.cloneArtifact(in), nil
}
@@ -428,6 +640,68 @@ func (r *fakeRepo) GetArtifactByVersion(_ context.Context, requirementID uuid.UU
return nil, errs.New(errs.CodeNotFound, "artifact 不存在")
}
func (r *fakeRepo) GetArtifactByID(_ context.Context, id uuid.UUID) (*Artifact, error) {
r.mu.Lock()
defer r.mu.Unlock()
if x, ok := r.artifacts[id]; ok {
return r.cloneArtifact(x), nil
}
return nil, errs.New(errs.CodeNotFound, "artifact 不存在")
}
func (r *fakeRepo) ApproveArtifact(_ context.Context, artifactID uuid.UUID, verdict Verdict) (*Artifact, error) {
r.mu.Lock()
defer r.mu.Unlock()
x, ok := r.artifacts[artifactID]
if !ok {
return nil, errs.New(errs.CodeNotFound, "artifact 不存在")
}
x.Verdict = verdict
return r.cloneArtifact(x), nil
}
func (r *fakeRepo) UpsertPhasePointer(_ context.Context, requirementID uuid.UUID, phase Phase, artifactID, approvedBy uuid.UUID) (*PhasePointer, error) {
r.mu.Lock()
defer r.mu.Unlock()
now := time.Now()
aid, by := artifactID, approvedBy
ptr := &PhasePointer{
RequirementID: requirementID,
Phase: phase,
ApprovedArtifactID: &aid,
ApprovedBy: &by,
ApprovedAt: &now,
}
r.phasePointers[phasePointerKey(requirementID, phase)] = ptr
v := *ptr
return &v, nil
}
func (r *fakeRepo) GetPhasePointer(_ context.Context, requirementID uuid.UUID, phase Phase) (*PhasePointer, error) {
r.mu.Lock()
defer r.mu.Unlock()
ptr, ok := r.phasePointers[phasePointerKey(requirementID, phase)]
if !ok {
return nil, errs.New(errs.CodeNotFound, "phase pointer 不存在")
}
v := *ptr
return &v, nil
}
func (r *fakeRepo) ListPhasePointers(_ context.Context, requirementID uuid.UUID) ([]*PhasePointer, error) {
r.mu.Lock()
defer r.mu.Unlock()
out := make([]*PhasePointer, 0)
for _, ptr := range r.phasePointers {
if ptr.RequirementID == requirementID {
v := *ptr
out = append(out, &v)
}
}
sort.Slice(out, func(i, j int) bool { return out[i].Phase < out[j].Phase })
return out, nil
}
// spyAudit 收集所有写入的 audit entry,方便断言。
type spyAudit struct {
mu sync.Mutex