package projectinstance import ( "testing" "chat-agent-system/internal/workflowdef" ) func TestClassifyDocDuty(t *testing.T) { t.Parallel() cases := []struct { name string duty DocDuty want DutyMode }{ {name: "all_zero", duty: DocDuty{}, want: DutyModeNone}, {name: "negative", duty: DocDuty{AuthorUserID: -1}, want: DutyModePartial}, {name: "one_role", duty: DocDuty{AuthorUserID: 9}, want: DutyModePartial}, {name: "two_roles", duty: DocDuty{AuthorUserID: 9, ReviewerUserID: 2}, want: DutyModePartial}, {name: "full_distinct", duty: DocDuty{AuthorUserID: 9, ReviewerUserID: 2, ApproverUserID: 3}, want: DutyModeFull}, {name: "full_duplicate", duty: DocDuty{AuthorUserID: 9, ReviewerUserID: 9, ApproverUserID: 3}, want: DutyModeFull}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() if got := classifyDocDuty(tc.duty); got != tc.want { t.Fatalf("classifyDocDuty(%+v)=%s want %s", tc.duty, got, tc.want) } }) } } func TestDocTaskRequiresDuty(t *testing.T) { t.Parallel() if docTaskRequiresDuty(workflowdef.DocTaskDef{CreatorConfigurable: true}) { t.Fatal("configurable optional doc must not require duties") } if !docTaskRequiresDuty(workflowdef.DocTaskDef{Required: true, CreatorConfigurable: true}) { t.Fatal("required doc must keep duties") } if !docTaskRequiresDuty(workflowdef.DocTaskDef{Blocking: true, CreatorConfigurable: true}) { t.Fatal("blocking doc must keep duties") } if !docTaskRequiresDuty(workflowdef.DocTaskDef{Required: true}) { t.Fatal("non-configurable required doc must keep duties") } } func TestNormalizeDocTaskDuties(t *testing.T) { t.Parallel() cleared := normalizeDocTaskDuties(DocTaskPlan{ Enabled: false, Duties: DocDuty{AuthorUserID: 9, ReviewerUserID: 2, ApproverUserID: 3}, }) if cleared.Duties != (DocDuty{}) { t.Fatalf("disabled task must drop leftover duties, got %+v", cleared.Duties) } none := normalizeDocTaskDuties(DocTaskPlan{Enabled: true, Duties: DocDuty{AuthorUserID: 0, ReviewerUserID: -0}}) if none.Duties != (DocDuty{}) { t.Fatalf("none must canonicalize to zero duties, got %+v", none.Duties) } partial := DocTaskPlan{Enabled: true, Duties: DocDuty{AuthorUserID: 9}} if got := normalizeDocTaskDuties(partial); got.Duties.AuthorUserID != 9 { t.Fatalf("partial must not auto-clear, got %+v", got.Duties) } } func TestValidateDocTaskDuties(t *testing.T) { t.Parallel() optional := workflowdef.DocTaskDef{Key: "research", Title: "调研", CreatorConfigurable: true} required := workflowdef.DocTaskDef{Key: "intake", Title: "需求", Required: true} if issues := validateDocTaskDuties(DocTaskPlan{TaskKey: "research", Enabled: true}, optional); len(issues) != 0 { t.Fatalf("optional none should pass, got %+v", issues) } reqIssues := validateDocTaskDuties(DocTaskPlan{TaskKey: "intake", TaskTitle: "需求", Enabled: true}, required) if len(reqIssues) != 1 || reqIssues[0].Code != issueDutyAssignmentRequired || reqIssues[0].Field != "intake.duties" { t.Fatalf("required none: %+v", reqIssues) } partial := validateDocTaskDuties(DocTaskPlan{ TaskKey: "research", Enabled: true, Duties: DocDuty{AuthorUserID: 1}, }, optional) if len(partial) != 1 || partial[0].Code != issueDutyPartialAssignment || partial[0].Field != "research.duties" { t.Fatalf("partial field must be exact, got %+v", partial) } sep := validateDocTaskDuties(DocTaskPlan{ TaskKey: "intake", Enabled: true, Duties: DocDuty{AuthorUserID: 1, ReviewerUserID: 1, ApproverUserID: 2}, }, required) if len(sep) != 1 || sep[0].Code != issueSeparationOfDutyViolation { t.Fatalf("duplicate full must keep SEPARATION_OF_DUTY_VIOLATION, got %+v", sep) } } func TestCountFullDutyDocs(t *testing.T) { t.Parallel() plan := WorkflowPlan{Stages: []StagePlan{{ Enabled: true, DocTasks: []DocTaskPlan{ {Enabled: true, Duties: DocDuty{AuthorUserID: 1, ReviewerUserID: 2, ApproverUserID: 3}}, {Enabled: true}, {Enabled: false, Duties: DocDuty{AuthorUserID: 1, ReviewerUserID: 2, ApproverUserID: 3}}, {Enabled: true, DutyPolicy: DutyPolicyAuthorOnly, Duties: DocDuty{AuthorUserID: 9}}, }, }}} if n := countFullDutyDocs(plan); n != 2 { t.Fatalf("countFullDutyDocs=%d want 2 (full + author_only)", n) } } func TestResolveDocTaskDuty(t *testing.T) { t.Parallel() mode, eff, only := ResolveDocTaskDuty(DocTaskPlan{Enabled: false, DutyPolicy: DutyPolicyAuthorOnly, Duties: DocDuty{AuthorUserID: 1}}) if mode != DutyModeNone || only || eff != (DocDuty{}) { t.Fatalf("disabled: mode=%s only=%v eff=%+v", mode, only, eff) } mode, eff, only = ResolveDocTaskDuty(DocTaskPlan{ Enabled: true, DutyPolicy: DutyPolicyAuthorOnly, Duties: DocDuty{AuthorUserID: 7}, }) if mode != DutyModeFull || !only || eff != (DocDuty{AuthorUserID: 7, ReviewerUserID: 7, ApproverUserID: 7}) { t.Fatalf("author_only: mode=%s only=%v eff=%+v", mode, only, eff) } mode, _, only = ResolveDocTaskDuty(DocTaskPlan{Enabled: true, DutyPolicy: DutyPolicyAuthorOnly}) if mode != DutyModePartial || !only { t.Fatalf("author_only missing author: mode=%s only=%v", mode, only) } mode, _, only = ResolveDocTaskDuty(DocTaskPlan{Enabled: true, DutyPolicy: "weird"}) if mode != DutyModePartial || only { t.Fatalf("unknown policy: mode=%s only=%v", mode, only) } } func TestNormalizeAuthorOnlyDuties(t *testing.T) { t.Parallel() same := normalizeDocTaskDuties(DocTaskPlan{ Enabled: true, DutyPolicy: DutyPolicyAuthorOnly, Duties: DocDuty{AuthorUserID: 5, ReviewerUserID: 5, ApproverUserID: 5}, }) if same.Duties.ReviewerUserID != 0 || same.Duties.ApproverUserID != 0 || same.Duties.AuthorUserID != 5 { t.Fatalf("same-as-author must clear derived slots: %+v", same.Duties) } conflict := normalizeDocTaskDuties(DocTaskPlan{ Enabled: true, DutyPolicy: DutyPolicyAuthorOnly, Duties: DocDuty{AuthorUserID: 5, ReviewerUserID: 8}, }) if conflict.Duties.ReviewerUserID != 8 { t.Fatalf("conflicting reviewer must remain for validation: %+v", conflict.Duties) } } func TestValidateAuthorOnlyDocTaskDuties(t *testing.T) { t.Parallel() optional := workflowdef.DocTaskDef{Key: "research", Title: "调研", CreatorConfigurable: true} ok := validateDocTaskDuties(DocTaskPlan{ TaskKey: "research", Enabled: true, DutyPolicy: DutyPolicyAuthorOnly, Duties: DocDuty{AuthorUserID: 3}, }, optional) if len(ok) != 0 { t.Fatalf("valid author_only: %+v", ok) } missing := validateDocTaskDuties(DocTaskPlan{ TaskKey: "research", Enabled: true, DutyPolicy: DutyPolicyAuthorOnly, }, optional) if len(missing) != 1 || missing[0].Code != issueAuthorOnlyAuthorRequired { t.Fatalf("missing author: %+v", missing) } conflict := validateDocTaskDuties(DocTaskPlan{ TaskKey: "research", Enabled: true, DutyPolicy: DutyPolicyAuthorOnly, Duties: DocDuty{AuthorUserID: 3, ReviewerUserID: 9}, }, optional) if len(conflict) != 1 || conflict[0].Code != issueAuthorOnlyDutyConflict { t.Fatalf("conflict: %+v", conflict) } unknown := validateDocTaskDuties(DocTaskPlan{ TaskKey: "research", Enabled: true, DutyPolicy: "nope", Duties: DocDuty{AuthorUserID: 1, ReviewerUserID: 2, ApproverUserID: 3}, }, optional) if len(unknown) != 1 || unknown[0].Code != issueUnknownDutyPolicy { t.Fatalf("unknown: %+v", unknown) } // separated A/A/A still rejected sep := validateDocTaskDuties(DocTaskPlan{ TaskKey: "research", Enabled: true, Duties: DocDuty{AuthorUserID: 1, ReviewerUserID: 1, ApproverUserID: 1}, }, optional) if len(sep) != 1 || sep[0].Code != issueSeparationOfDutyViolation { t.Fatalf("separated A/A/A: %+v", sep) } }