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
+128 -1
View File
@@ -13,7 +13,7 @@ import (
func newReqSvc(repo *fakeRepo) (RequirementService, ProjectService, *spyAudit) {
spy := &spyAudit{}
return NewRequirementService(repo, spy), NewProjectService(repo, spy), spy
return NewRequirementService(repo, spy, nil, nil), NewProjectService(repo, spy), spy
}
func TestRequirementService_Create_HappyAndAutoIncrement(t *testing.T) {
@@ -175,3 +175,130 @@ func TestRequirementService_Update_WorkspaceID_TriState(t *testing.T) {
require.NoError(t, err)
require.Nil(t, out.WorkspaceID)
}
// fakeEmitter 收集 EmitPhaseChange 调用,便于断言事件发出且仅发一次。
type fakeEmitter struct {
events []PhaseChangeEvent
}
func (e *fakeEmitter) EmitPhaseChange(_ context.Context, ev PhaseChangeEvent) {
e.events = append(e.events, ev)
}
func newReqSvcWith(repo *fakeRepo, emitter PhaseEventEmitter, gate GateRunner) (RequirementService, *spyAudit) {
spy := &spyAudit{}
return NewRequirementService(repo, spy, emitter, gate), spy
}
func TestRequirementService_ChangePhase_EmitsEvent(t *testing.T) {
repo := newFakeRepo()
owner := uuid.New()
seedProject(t, repo, owner)
emitter := &fakeEmitter{}
svc, spy := newReqSvcWith(repo, emitter, nil)
caller := Caller{UserID: owner}
r, err := svc.Create(context.Background(), caller, "demo", CreateRequirementInput{Title: "T"})
require.NoError(t, err)
out, err := svc.ChangePhase(context.Background(), caller, "demo", r.Number, PhaseAuditing)
require.NoError(t, err)
require.Equal(t, PhaseAuditing, out.Phase)
require.Len(t, emitter.events, 1)
ev := emitter.events[0]
require.Equal(t, PhasePlanning, ev.From)
require.Equal(t, PhaseAuditing, ev.To)
require.Equal(t, "demo", ev.ProjectSlug)
require.Equal(t, r.Number, ev.RequirementNumber)
require.Equal(t, owner, ev.ActorID)
// 事件与审计互补:审计仍记一条。
require.Contains(t, spy.actions(), "requirement.phase_change")
}
func TestRequirementService_ChangePhase_NoopEmitsNothing(t *testing.T) {
repo := newFakeRepo()
owner := uuid.New()
seedProject(t, repo, owner)
emitter := &fakeEmitter{}
svc, _ := newReqSvcWith(repo, emitter, nil)
caller := Caller{UserID: owner}
r, err := svc.Create(context.Background(), caller, "demo", CreateRequirementInput{Title: "T"})
require.NoError(t, err)
// 当前已是 planning,切到 planning 是 no-op。
_, err = svc.ChangePhase(context.Background(), caller, "demo", r.Number, PhasePlanning)
require.NoError(t, err)
require.Empty(t, emitter.events)
}
func TestRequirementService_ChangePhase_GateBlocksAndSkipsEmit(t *testing.T) {
repo := newFakeRepo()
owner := uuid.New()
seedProject(t, repo, owner)
emitter := &fakeEmitter{}
gate := NewGateRunner(GateDeps{Repo: repo}, nil)
svc, spy := newReqSvcWith(repo, emitter, gate)
caller := Caller{UserID: owner}
r, err := svc.Create(context.Background(), caller, "demo", CreateRequirementInput{Title: "T"})
require.NoError(t, err)
// implementing 需要已批准的 auditing 产物,缺失 → phase_gate_failed。
_, err = svc.ChangePhase(context.Background(), caller, "demo", r.Number, PhaseImplementing)
ae, ok := errs.As(err)
require.True(t, ok)
require.Equal(t, errs.CodePhaseGateFailed, ae.Code)
// gate 失败不写库、不发事件、不记 phase_change 审计。
require.Empty(t, emitter.events)
require.NotContains(t, spy.actions(), "requirement.phase_change")
got, err := svc.Get(context.Background(), caller, "demo", r.Number)
require.NoError(t, err)
require.Equal(t, PhasePlanning, got.Phase)
}
func TestRequirementService_ChangePhase_ClosedCheckedBeforeGate(t *testing.T) {
repo := newFakeRepo()
owner := uuid.New()
seedProject(t, repo, owner)
emitter := &fakeEmitter{}
gate := NewGateRunner(GateDeps{Repo: repo}, nil)
svc, _ := newReqSvcWith(repo, emitter, gate)
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))
// 关闭检查先于 gate:即便目标是被网关的 implementing,也返回 requirement_closed。
_, err = svc.ChangePhase(context.Background(), caller, "demo", r.Number, PhaseImplementing)
ae, ok := errs.As(err)
require.True(t, ok)
require.Equal(t, errs.CodeRequirementClosed, ae.Code)
require.Empty(t, emitter.events)
}
func TestRequirementService_ChangePhase_GateAllowsAfterApproval(t *testing.T) {
repo := newFakeRepo()
owner := uuid.New()
seedProject(t, repo, owner)
emitter := &fakeEmitter{}
gate := NewGateRunner(GateDeps{Repo: repo}, nil)
svc, _ := newReqSvcWith(repo, emitter, gate)
artSvc := NewArtifactService(repo, nil)
caller := Caller{UserID: owner}
r, err := svc.Create(context.Background(), caller, "demo", CreateRequirementInput{Title: "T"})
require.NoError(t, err)
_, err = artSvc.Create(context.Background(), caller, "demo", r.Number, CreateArtifactInput{Phase: PhaseAuditing, Content: "report"})
require.NoError(t, err)
_, err = artSvc.Approve(context.Background(), caller, "demo", r.Number, PhaseAuditing, 1, VerdictPass)
require.NoError(t, err)
out, err := svc.ChangePhase(context.Background(), caller, "demo", r.Number, PhaseImplementing)
require.NoError(t, err)
require.Equal(t, PhaseImplementing, out.Phase)
require.Len(t, emitter.events, 1)
}