You've already forked agentic-coding-workflow
bugfix
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user