Files
weichenandCursor 1497473186 feat(projectworkflow): controlled post-activation document duty reassign
Add PATCH /doc-tasks/{taskKey}/duties that updates live snapshot,
assignments, and document_records while blocking when a squad run holds
the document lease. Planning PUT remains PLAN_FROZEN.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-20 09:48:27 +00:00

111 lines
3.3 KiB
Go

package projectinstance
import (
"context"
"errors"
"testing"
)
type stubBusyChecker struct {
busy bool
reason string
}
func (s *stubBusyChecker) DocTaskSquadBusy(context.Context, int64, string) (bool, string, error) {
return s.busy, s.reason, nil
}
type stubProjector struct {
calls int
last DocDuty
}
func (s *stubProjector) ProjectDocTaskDuties(_ context.Context, _ int64, _ string, duties DocDuty, _ string, _ DutyDisplayNames) error {
s.calls++
s.last = duties
return nil
}
func TestReassignDocTaskDuties_Success(t *testing.T) {
ctx := context.Background()
store := newMemStore()
_ = SeedProjectMeta(store, 80, LifecyclePlanning, "", 1, 0)
svc := NewService(store, testResolver(), nil, nil)
svc.SetActiveMemberLookup(&mapMemberLookup{ids: map[int64]struct{}{1: {}, 2: {}, 3: {}, 4: {}}})
mustActivateTestPlan(t, svc, 80, 1)
proj := &stubProjector{}
svc.SetDocumentDutyProjector(proj)
svc.SetDocTaskBusyChecker(&stubBusyChecker{busy: false})
settings, err := svc.ReassignDocTaskDuties(ctx, 80, 1, true, ReassignDocTaskDutiesInput{
TaskKey: "intake",
Duties: DocDuty{AuthorUserID: 4, ReviewerUserID: 2, ApproverUserID: 3},
})
if err != nil {
t.Fatalf("reassign: %v", err)
}
if settings == nil || settings.LiveInstance == nil {
t.Fatal("expected live settings")
}
dt := findDocTaskPlan(settings.LiveInstance.ConfigSnapshot, "intake")
if dt == nil || dt.Duties.AuthorUserID != 4 {
t.Fatalf("duties not updated: %+v", dt)
}
if proj.calls != 1 || proj.last.AuthorUserID != 4 {
t.Fatalf("projector not called: %+v", proj)
}
}
func TestReassignDocTaskDuties_Busy(t *testing.T) {
ctx := context.Background()
store := newMemStore()
_ = SeedProjectMeta(store, 81, LifecyclePlanning, "", 1, 0)
svc := NewService(store, testResolver(), nil, nil)
svc.SetActiveMemberLookup(&mapMemberLookup{ids: map[int64]struct{}{1: {}, 2: {}, 3: {}}})
mustActivateTestPlan(t, svc, 81, 1)
svc.SetDocTaskBusyChecker(&stubBusyChecker{busy: true, reason: "squad running"})
_, err := svc.ReassignDocTaskDuties(ctx, 81, 1, true, ReassignDocTaskDutiesInput{
TaskKey: "intake",
Duties: DocDuty{AuthorUserID: 1, ReviewerUserID: 2, ApproverUserID: 3},
})
var busy *DocTaskBusyError
if !errors.As(err, &busy) {
t.Fatalf("expected DocTaskBusyError, got %v", err)
}
}
func TestReassignDocTaskDuties_Forbidden(t *testing.T) {
ctx := context.Background()
store := newMemStore()
_ = SeedProjectMeta(store, 82, LifecyclePlanning, "", 1, 0)
svc := NewService(store, testResolver(), nil, nil)
mustActivateTestPlan(t, svc, 82, 1)
_, err := svc.ReassignDocTaskDuties(ctx, 82, 1, false, ReassignDocTaskDutiesInput{
TaskKey: "intake",
Duties: DocDuty{AuthorUserID: 1, ReviewerUserID: 2, ApproverUserID: 3},
})
if !errors.Is(err, ErrForbiddenDutyReassign) {
t.Fatalf("expected forbidden, got %v", err)
}
}
func TestReassignDocTaskDuties_PlanStillFrozen(t *testing.T) {
ctx := context.Background()
store := newMemStore()
_ = SeedProjectMeta(store, 83, LifecyclePlanning, "", 1, 0)
svc := NewService(store, testResolver(), nil, nil)
mustActivateTestPlan(t, svc, 83, 1)
_, err := svc.SaveWorkflowPlan(ctx, 83, 1, true, SavePlanInput{
Plan: validPlan(),
ExpectedPlanRevision: 1,
ExpectedSelectionVersion: 2,
})
if !errors.Is(err, ErrPlanFrozen) {
t.Fatalf("expected plan frozen after activate, got %v", err)
}
}