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>
111 lines
3.3 KiB
Go
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)
|
|
}
|
|
}
|