257 lines
8.5 KiB
Go
257 lines
8.5 KiB
Go
package projectinstance
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"chat-agent-system/internal/workflowdef"
|
|
)
|
|
|
|
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{AuthorUserID: 1}
|
|
|
|
_, 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) != 1 || validationErr.Issues[0].Code != issueDutyPartialAssignment {
|
|
t.Fatalf("expected DUTY_PARTIAL_ASSIGNMENT, got %+v", validationErr.Issues)
|
|
}
|
|
}
|
|
|
|
type mapMemberLookup struct {
|
|
ids map[int64]struct{}
|
|
}
|
|
|
|
func (m *mapMemberLookup) ActiveMemberIDs(_ context.Context, _ int64) (map[int64]struct{}, error) {
|
|
out := make(map[int64]struct{}, len(m.ids))
|
|
for id := range m.ids {
|
|
out[id] = struct{}{}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (m *mapMemberLookup) remove(id int64) {
|
|
delete(m.ids, id)
|
|
}
|
|
|
|
func TestService_OptionalNoneSaveAndZeroFullActivate(t *testing.T) {
|
|
ctx := context.Background()
|
|
store := newMemStore()
|
|
_ = SeedProjectMeta(store, 81, LifecyclePlanning, "", 1, 0)
|
|
svc := NewService(store, optionalDutyResolver(), nil, nil)
|
|
|
|
plan := optionalDutyPlan(DocDuty{}, DocDuty{})
|
|
settings, err := svc.SaveWorkflowPlan(ctx, 81, 1, true, SavePlanInput{
|
|
Plan: plan, ExpectedPlanRevision: 0, ExpectedSelectionVersion: 1,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("zero full should save: %v", err)
|
|
}
|
|
if settings.Plan == nil || countFullDutyDocs(*settings.Plan) != 0 {
|
|
t.Fatalf("saved plan should stay ungoverned, got %+v", settings.Plan)
|
|
}
|
|
_, err = svc.ActivationPreview(ctx, 81, 1, true)
|
|
var previewErr *ValidationError
|
|
if !errors.As(err, &previewErr) || !hasIssue(previewErr.Issues, issueNoGovernedDocument) {
|
|
t.Fatalf("preview must reject zero governance, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestService_RequiredEmptyDutiesRejected(t *testing.T) {
|
|
ctx := context.Background()
|
|
store := newMemStore()
|
|
_ = SeedProjectMeta(store, 82, LifecyclePlanning, "", 1, 0)
|
|
svc := NewService(store, requiredDutyResolver(), nil, nil)
|
|
plan := validPlan()
|
|
plan.Stages[0].DocTasks[0].Duties = DocDuty{}
|
|
_, err := svc.SaveWorkflowPlan(ctx, 82, 1, true, SavePlanInput{
|
|
Plan: plan, ExpectedPlanRevision: 0, ExpectedSelectionVersion: 1,
|
|
})
|
|
var validationErr *ValidationError
|
|
if !errors.As(err, &validationErr) || !hasIssue(validationErr.Issues, issueDutyAssignmentRequired) {
|
|
t.Fatalf("required empty duties: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestService_InactiveMemberRejected(t *testing.T) {
|
|
ctx := context.Background()
|
|
store := newMemStore()
|
|
_ = SeedProjectMeta(store, 83, LifecyclePlanning, "", 1, 0)
|
|
svc := NewService(store, testResolver(), nil, nil)
|
|
svc.SetActiveMemberLookup(&mapMemberLookup{ids: map[int64]struct{}{1: {}, 2: {}}})
|
|
_, err := svc.SaveWorkflowPlan(ctx, 83, 1, true, SavePlanInput{
|
|
Plan: validPlan(), ExpectedPlanRevision: 0, ExpectedSelectionVersion: 1,
|
|
})
|
|
var validationErr *ValidationError
|
|
if !errors.As(err, &validationErr) || !hasIssue(validationErr.Issues, issueDutyAssigneeNotActive) {
|
|
t.Fatalf("inactive member: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestService_ActivateRechecksMembersAfterPreview(t *testing.T) {
|
|
ctx := context.Background()
|
|
store := newMemStore()
|
|
_ = SeedProjectMeta(store, 84, LifecyclePlanning, "", 1, 0)
|
|
svc := NewService(store, testResolver(), nil, nil)
|
|
lookup := &mapMemberLookup{ids: map[int64]struct{}{1: {}, 2: {}, 3: {}}}
|
|
svc.SetActiveMemberLookup(lookup)
|
|
|
|
if _, err := svc.SaveWorkflowPlan(ctx, 84, 1, true, SavePlanInput{
|
|
Plan: validPlan(), ExpectedPlanRevision: 0, ExpectedSelectionVersion: 1,
|
|
}); err != nil {
|
|
t.Fatalf("SaveWorkflowPlan: %v", err)
|
|
}
|
|
preview, err := svc.ActivationPreview(ctx, 84, 1, true)
|
|
if err != nil {
|
|
t.Fatalf("ActivationPreview: %v", err)
|
|
}
|
|
lookup.remove(3)
|
|
_, err = svc.Activate(ctx, 84, 1, true, ActivateInput{
|
|
ConfirmationToken: preview.ConfirmationToken,
|
|
ExpectedProjectVersion: preview.ExpectedProjectVersion,
|
|
ExpectedPlanRevision: preview.ExpectedPlanRevision,
|
|
})
|
|
var validationErr *ValidationError
|
|
if !errors.As(err, &validationErr) || !hasIssue(validationErr.Issues, issueDutyAssigneeNotActive) {
|
|
t.Fatalf("activate after member left: %v", err)
|
|
}
|
|
_, reuseErr := svc.Activate(ctx, 84, 1, true, ActivateInput{
|
|
ConfirmationToken: preview.ConfirmationToken,
|
|
ExpectedProjectVersion: preview.ExpectedProjectVersion,
|
|
ExpectedPlanRevision: preview.ExpectedPlanRevision,
|
|
})
|
|
if reuseErr == nil {
|
|
t.Fatal("stale token must not skip the failed membership recheck")
|
|
}
|
|
}
|
|
|
|
func TestService_FullPlusNoneActivateAndNormalize(t *testing.T) {
|
|
ctx := context.Background()
|
|
store := newMemStore()
|
|
_ = SeedProjectMeta(store, 85, LifecyclePlanning, "", 1, 0)
|
|
svc := NewService(store, optionalDutyResolver(), nil, nil)
|
|
svc.SetActiveMemberLookup(&mapMemberLookup{ids: map[int64]struct{}{1: {}, 2: {}, 3: {}}})
|
|
|
|
disabled := optionalDutyPlan(
|
|
DocDuty{AuthorUserID: 1, ReviewerUserID: 2, ApproverUserID: 3},
|
|
DocDuty{AuthorUserID: 9, ReviewerUserID: 8, ApproverUserID: 7},
|
|
)
|
|
disabled.Stages[0].DocTasks[1].Enabled = false
|
|
|
|
saved, err := svc.SaveWorkflowPlan(ctx, 85, 1, true, SavePlanInput{
|
|
Plan: disabled, ExpectedPlanRevision: 0, ExpectedSelectionVersion: 1,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("save disabled leftover: %v", err)
|
|
}
|
|
if saved.Plan.Stages[0].DocTasks[1].Duties != (DocDuty{}) {
|
|
t.Fatalf("disabled snapshot must drop old IDs, got %+v", saved.Plan.Stages[0].DocTasks[1].Duties)
|
|
}
|
|
|
|
fullNone := optionalDutyPlan(
|
|
DocDuty{AuthorUserID: 1, ReviewerUserID: 2, ApproverUserID: 3},
|
|
DocDuty{},
|
|
)
|
|
saved, err = svc.SaveWorkflowPlan(ctx, 85, 1, true, SavePlanInput{
|
|
Plan: fullNone, ExpectedPlanRevision: saved.WorkflowPlanRevision, ExpectedSelectionVersion: 1,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("save full+none: %v", err)
|
|
}
|
|
preview, err := svc.ActivationPreview(ctx, 85, 1, true)
|
|
if err != nil {
|
|
t.Fatalf("ActivationPreview: %v", err)
|
|
}
|
|
if _, err := svc.Activate(ctx, 85, 1, true, ActivateInput{
|
|
ConfirmationToken: preview.ConfirmationToken,
|
|
ExpectedProjectVersion: preview.ExpectedProjectVersion,
|
|
ExpectedPlanRevision: preview.ExpectedPlanRevision,
|
|
}); err != nil {
|
|
t.Fatalf("activate full+none: %v", err)
|
|
}
|
|
}
|
|
|
|
func hasIssue(issues []workflowdef.ValidationIssue, code string) bool {
|
|
for _, issue := range issues {
|
|
if issue.Code == code {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|