New projectinstance package validates three-duty separation, issues confirmation tokens, and activates immutable workflow instances; new projects start in planning mode. Co-authored-by: Cursor <cursoragent@cursor.com>
141 lines
4.1 KiB
Go
141 lines
4.1 KiB
Go
package projectinstance
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"chat-agent-system/internal/workflowdef"
|
|
)
|
|
|
|
type stubVersionResolver struct {
|
|
versions map[string]*workflowdef.Version
|
|
defs map[string]*workflowdef.Definition
|
|
}
|
|
|
|
func (s *stubVersionResolver) GetDefinitionByKey(_ context.Context, key string) (*workflowdef.Definition, error) {
|
|
d, ok := s.defs[key]
|
|
if !ok {
|
|
return nil, workflowdef.ErrNotFound
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
func (s *stubVersionResolver) GetVersion(_ context.Context, workflowID int64, version int) (*workflowdef.Version, error) {
|
|
for _, v := range s.versions {
|
|
if v.WorkflowID == workflowID && v.Version == version {
|
|
copyV := *v
|
|
return ©V, nil
|
|
}
|
|
}
|
|
return nil, workflowdef.ErrNotFound
|
|
}
|
|
|
|
func (s *stubVersionResolver) ListDefinitions(_ context.Context) ([]workflowdef.Definition, error) {
|
|
out := make([]workflowdef.Definition, 0, len(s.defs))
|
|
for _, d := range s.defs {
|
|
out = append(out, *d)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func testResolver() *stubVersionResolver {
|
|
ver := &workflowdef.Version{
|
|
ID: 100, WorkflowID: 10, Version: 1, Status: workflowdef.StatusPublished,
|
|
WorkflowKey: "medical_device_development", WorkflowName: "医疗器械开发",
|
|
Definition: workflowdef.VersionDefinition{
|
|
Stages: []workflowdef.StageDef{{
|
|
Key: "biz_intake", Name: "商务 intake", Optional: false,
|
|
DocTasks: []workflowdef.DocTaskDef{{
|
|
Key: "intake", Required: true, Blocking: true, DefaultEnabled: true,
|
|
}},
|
|
}},
|
|
},
|
|
}
|
|
return &stubVersionResolver{
|
|
defs: map[string]*workflowdef.Definition{
|
|
"medical_device_development": {ID: 10, Key: "medical_device_development", Name: "医疗器械开发", LatestPublishedVersion: 1},
|
|
},
|
|
versions: map[string]*workflowdef.Version{"medical_device_development": ver},
|
|
}
|
|
}
|
|
|
|
func validPlan() WorkflowPlan {
|
|
return WorkflowPlan{
|
|
WorkflowDefinitionKey: "medical_device_development",
|
|
WorkflowVersion: 1,
|
|
Stages: []StagePlan{{
|
|
StageKey: "biz_intake", Enabled: true,
|
|
DocTasks: []DocTaskPlan{{
|
|
TaskKey: "intake", Enabled: true,
|
|
Duties: DocDuty{AuthorUserID: 1, ReviewerUserID: 2, ApproverUserID: 3},
|
|
}},
|
|
}},
|
|
}
|
|
}
|
|
|
|
func TestService_PlanPreviewActivate(t *testing.T) {
|
|
ctx := context.Background()
|
|
store := newMemStore()
|
|
_ = SeedProjectMeta(store, 42, LifecyclePlanning, "", 1, 0)
|
|
svc := NewService(store, testResolver(), nil)
|
|
|
|
settings, err := svc.SaveWorkflowPlan(ctx, 42, 1, true, SavePlanInput{
|
|
Plan: validPlan(), ExpectedPlanRevision: 0, ExpectedSelectionVersion: 1,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("SaveWorkflowPlan: %v", err)
|
|
}
|
|
if settings.Plan == nil || settings.Plan.WorkflowDefinitionKey != "medical_device_development" {
|
|
t.Fatalf("expected saved plan, got %+v", settings.Plan)
|
|
}
|
|
|
|
preview, err := svc.ActivationPreview(ctx, 42, 1, true)
|
|
if err != nil {
|
|
t.Fatalf("ActivationPreview: %v", err)
|
|
}
|
|
if preview.ConfirmationToken == "" || preview.PlanHash == "" {
|
|
t.Fatal("expected confirmation token and plan hash")
|
|
}
|
|
|
|
result, err := svc.Activate(ctx, 42, 1, true, ActivateInput{
|
|
ConfirmationToken: preview.ConfirmationToken,
|
|
ExpectedProjectVersion: preview.ExpectedProjectVersion,
|
|
ExpectedPlanRevision: preview.ExpectedPlanRevision,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Activate: %v", err)
|
|
}
|
|
if result.LifecycleStatus != LifecycleActive || result.ActiveWorkflowKey != "medical_device_development" {
|
|
t.Fatalf("unexpected activate result: %+v", result)
|
|
}
|
|
|
|
live, _ := store.HasLiveInstance(ctx, 42)
|
|
if !live {
|
|
t.Fatal("expected live instance after activation")
|
|
}
|
|
}
|
|
|
|
func TestService_BlockPlanEditWhenActive(t *testing.T) {
|
|
ctx := context.Background()
|
|
store := newMemStore()
|
|
_ = SeedProjectMeta(store, 7, LifecycleActive, "main", 2, 1)
|
|
svc := NewService(store, testResolver(), nil)
|
|
|
|
_, err := svc.SaveWorkflowPlan(ctx, 7, 1, true, SavePlanInput{
|
|
Plan: validPlan(), ExpectedPlanRevision: 1,
|
|
})
|
|
if !errors.Is(err, ErrPlanFrozen) {
|
|
t.Fatalf("expected ErrPlanFrozen, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestService_SeparationOfDutyValidation(t *testing.T) {
|
|
bad := validPlan()
|
|
bad.Stages[0].DocTasks[0].Duties.ReviewerUserID = 1
|
|
res := validatePlan(bad, testResolver().versions["medical_device_development"])
|
|
if res.Valid {
|
|
t.Fatal("expected separation of duty violation")
|
|
}
|
|
}
|