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 { key := workflowdef.OfficialKeyMain ver := &workflowdef.Version{ ID: 100, WorkflowID: 10, Version: 1, Status: workflowdef.StatusPublished, WorkflowKey: key, WorkflowName: workflowdef.OfficialWorkflowName(key), Definition: workflowdef.VersionDefinition{ SchemaVersion: 2, WorkflowKey: key, Stages: []workflowdef.StageDef{{ Key: "V0.1_triage", Name: "V0.1 需求摸底", Optional: false, Position: 10, DocTasks: []workflowdef.DocTaskDef{{ Key: "intake", Title: "用户初始需求收集表", CreatorConfigurable: true, BlockingWhenEnabled: true, DefaultEnabled: true, OutputDocType: "intake", OutputDocTitle: "用户初始需求收集表", }}, }}, }, } return &stubVersionResolver{ defs: map[string]*workflowdef.Definition{ key: {ID: 10, Key: key, Name: workflowdef.OfficialWorkflowName(key), LatestPublishedVersion: 1}, }, versions: map[string]*workflowdef.Version{key: ver}, } } func validPlan() WorkflowPlan { return WorkflowPlan{ WorkflowDefinitionKey: workflowdef.OfficialKeyMain, WorkflowVersion: 1, Stages: []StagePlan{{ StageKey: "V0.1_triage", 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, 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 != workflowdef.OfficialKeyMain { 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 != workflowdef.OfficialKeyMain { 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, 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[workflowdef.OfficialKeyMain]) if res.Valid { t.Fatal("expected separation of duty violation") } } func TestService_SavePlanReturnsValidationIssues(t *testing.T) { ctx := context.Background() store := newMemStore() _ = SeedProjectMeta(store, 8, LifecyclePlanning, "", 1, 0) svc := NewService(store, testResolver(), nil, nil) bad := validPlan() bad.Stages[0].DocTasks[0].Duties = DocDuty{} _, err := svc.SaveWorkflowPlan(ctx, 8, 1, true, SavePlanInput{ Plan: bad, ExpectedPlanRevision: 0, ExpectedSelectionVersion: 1, }) var validationErr *ValidationError if !errors.As(err, &validationErr) { t.Fatalf("expected ValidationError, got %v", err) } if len(validationErr.Issues) != 3 { t.Fatalf("expected three missing-duty issues, got %+v", validationErr.Issues) } }