package app import ( "context" "sync" "testing" "chat-agent-system/internal/projectinstance" "github.com/jackc/pgx/v5" ) type stubAuthorOnlyPolicy struct { allowed bool revision int64 } func (s *stubAuthorOnlyPolicy) GetAuthorOnlyApprovalPolicy(_ context.Context) (projectinstance.AuthorOnlyPolicySnapshot, error) { return projectinstance.AuthorOnlyPolicySnapshot{Allowed: s.allowed, Revision: s.revision}, nil } func (s *stubAuthorOnlyPolicy) LockAuthorOnlyApprovalPolicy(ctx context.Context, _ pgx.Tx) (projectinstance.AuthorOnlyPolicySnapshot, error) { return s.GetAuthorOnlyApprovalPolicy(ctx) } type stubMemberLookup struct { mu sync.Mutex ids map[int64]struct{} } func (s *stubMemberLookup) ActiveMemberIDs(_ context.Context, _ int64) (map[int64]struct{}, error) { s.mu.Lock() defer s.mu.Unlock() out := make(map[int64]struct{}, len(s.ids)) for id := range s.ids { out[id] = struct{}{} } return out, nil } func TestProjectInstanceDutyAdapter_AuthorOnlyLive(t *testing.T) { ctx := context.Background() store := projectinstance.NewMemoryStore() _ = projectinstance.SeedProjectMeta(store, 501, projectinstance.LifecyclePlanning, "", 1, 0) svc := projectinstance.NewService(store, projectinstance.TestResolver(), nil, nil) svc.SetAuthorOnlyPolicySource(&stubAuthorOnlyPolicy{allowed: true, revision: 1}) svc.SetActiveMemberLookup(&stubMemberLookup{ids: map[int64]struct{}{7: {}}}) plan := projectinstance.ValidTestPlan() plan.Stages[0].DocTasks[0].DutyPolicy = projectinstance.DutyPolicyAuthorOnly plan.Stages[0].DocTasks[0].Duties = projectinstance.DocDuty{AuthorUserID: 7} if _, err := svc.SaveWorkflowPlan(ctx, 501, 1, true, projectinstance.SavePlanInput{ Plan: plan, ExpectedPlanRevision: 0, ExpectedSelectionVersion: 1, }); err != nil { t.Fatalf("save: %v", err) } preview, err := svc.ActivationPreview(ctx, 501, 1, true) if err != nil { t.Fatalf("preview: %v", err) } if _, err := svc.Activate(ctx, 501, 1, true, projectinstance.ActivateInput{ ConfirmationToken: preview.ConfirmationToken, ExpectedProjectVersion: preview.ExpectedProjectVersion, ExpectedPlanRevision: preview.ExpectedPlanRevision, }); err != nil { t.Fatalf("activate: %v", err) } adapter := &projectInstanceDutyAdapter{inst: svc} author, reviewer, approver, policy, governed, err := adapter.ResolveGovernedDuty(ctx, 501, "intake") if err != nil { t.Fatalf("resolve: %v", err) } if !governed || policy != projectinstance.DutyPolicyAuthorOnly { t.Fatalf("governed=%v policy=%q", governed, policy) } if author != 7 || reviewer != 7 || approver != 7 { t.Fatalf("want 7/7/7 got %d/%d/%d", author, reviewer, approver) } } func TestProjectInstanceDutyAdapter_NoLiveInstance(t *testing.T) { ctx := context.Background() store := projectinstance.NewMemoryStore() _ = projectinstance.SeedProjectMeta(store, 502, projectinstance.LifecyclePlanning, "", 1, 0) svc := projectinstance.NewService(store, projectinstance.TestResolver(), nil, nil) adapter := &projectInstanceDutyAdapter{inst: svc} _, _, _, policy, governed, err := adapter.ResolveGovernedDuty(ctx, 502, "intake") if err != nil { t.Fatalf("resolve: %v", err) } if governed || policy != "" { t.Fatalf("want ungoverned, got governed=%v policy=%q", governed, policy) } }