Verify resolve/normalize/hash stability, policy gating, stale token rejection, and HTTP error mapping for the new duty policy path. Co-authored-by: Cursor <cursoragent@cursor.com>
191 lines
7.4 KiB
Go
191 lines
7.4 KiB
Go
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)
|
|
}
|
|
}
|
|
|