198 lines
6.6 KiB
Go
198 lines
6.6 KiB
Go
package projectinstance
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"chat-agent-system/internal/workflowdef"
|
|
)
|
|
|
|
func TestNarrativeGating_PendingStageDocsRunnable(t *testing.T) {
|
|
ctx := context.Background()
|
|
store := newMemStore()
|
|
_ = SeedProjectMeta(store, 880, LifecyclePlanning, "", 1, 0)
|
|
svc := NewService(store, twoStageResolver(workflowdef.StageGatingNarrative), nil, nil)
|
|
activateTwoStage(t, svc, 880)
|
|
forcePartialDuty(store, 880, "partial_doc")
|
|
|
|
access, err := svc.ResolveStageAccess(ctx, 880, 99, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if access.ActiveStageKey != "V0.1_triage" {
|
|
t.Fatalf("active=%s", access.ActiveStageKey)
|
|
}
|
|
if !access.TaskKeyAllowed("research") {
|
|
t.Fatalf("narrative must allow pending-stage research, %+v", access.AllowedTaskKeys)
|
|
}
|
|
if !access.StageKeyAllowed("V0.5_feasibility") {
|
|
t.Fatal("narrative must open later stage keys")
|
|
}
|
|
if access.TaskKeyAllowed("disabled_doc") {
|
|
t.Fatal("disabled docs stay out of AllowedTaskKeys")
|
|
}
|
|
if access.TaskKeyAllowed("partial_doc") {
|
|
t.Fatal("partial duty stays out of AllowedTaskKeys")
|
|
}
|
|
|
|
run := svc.CheckDocTaskAccess(ctx, 880, 99, "research", DocAccessActionRun, false)
|
|
if !run.Allowed {
|
|
t.Fatalf("narrative run: %+v", run)
|
|
}
|
|
submit := svc.CheckDocTaskAccess(ctx, 880, 99, "research", DocAccessActionSubmit, false)
|
|
if submit.Allowed || submit.Code != AccessCodeForbiddenDuty {
|
|
t.Fatalf("narrative must keep duty on submit: %+v", submit)
|
|
}
|
|
authorSubmit := svc.CheckDocTaskAccess(ctx, 880, 1, "research", DocAccessActionSubmit, false)
|
|
if !authorSubmit.Allowed {
|
|
t.Fatalf("author submit: %+v", authorSubmit)
|
|
}
|
|
disabled := svc.CheckDocTaskAccess(ctx, 880, 1, "disabled_doc", DocAccessActionRun, true)
|
|
if disabled.Allowed || disabled.Code != AccessCodeDocTaskDisabled {
|
|
t.Fatalf("disabled: %+v", disabled)
|
|
}
|
|
partial := svc.CheckDocTaskAccess(ctx, 880, 1, "partial_doc", DocAccessActionEdit, true)
|
|
if partial.Allowed || partial.Code != AccessCodeForbiddenDuty {
|
|
t.Fatalf("partial: %+v", partial)
|
|
}
|
|
}
|
|
|
|
func TestStrictGating_PendingStageDocsDenied(t *testing.T) {
|
|
ctx := context.Background()
|
|
store := newMemStore()
|
|
_ = SeedProjectMeta(store, 881, LifecyclePlanning, "", 1, 0)
|
|
svc := NewService(store, twoStageResolver(""), nil, nil)
|
|
activateTwoStage(t, svc, 881)
|
|
|
|
access, err := svc.ResolveStageAccess(ctx, 881, 99, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if access.TaskKeyAllowed("research") {
|
|
t.Fatal("strict must not run pending-stage research")
|
|
}
|
|
if !access.TaskKeyVisible("research") {
|
|
t.Fatal("strict view exemption stays")
|
|
}
|
|
decision := svc.CheckDocTaskAccess(ctx, 881, 1, "research", DocAccessActionRun, true)
|
|
if decision.Allowed || decision.Code != AccessCodeNotCurrentStage {
|
|
t.Fatalf("strict run: %+v", decision)
|
|
}
|
|
view := svc.CheckDocTaskAccess(ctx, 881, 1, "research", DocAccessActionView, true)
|
|
if !view.Allowed {
|
|
t.Fatalf("strict view: %+v", view)
|
|
}
|
|
}
|
|
|
|
func TestStampPlanStageGating_OmittedForStrictHash(t *testing.T) {
|
|
version := &workflowdef.Version{Definition: workflowdef.VersionDefinition{}}
|
|
plan := WorkflowPlan{WorkflowDefinitionKey: "main", WorkflowVersion: 1}
|
|
stampPlanStageGating(&plan, version)
|
|
if plan.StageGating != "" {
|
|
t.Fatalf("strict stamp=%q", plan.StageGating)
|
|
}
|
|
h1, err := computePlanHash(plan)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
plan.StageGating = workflowdef.StageGatingStrict
|
|
stampPlanStageGating(&plan, version)
|
|
h2, err := computePlanHash(plan)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if h1 != h2 {
|
|
t.Fatalf("strict hash drifted %s vs %s", h1, h2)
|
|
}
|
|
version.Definition.StageGating = workflowdef.StageGatingNarrative
|
|
stampPlanStageGating(&plan, version)
|
|
if plan.StageGating != workflowdef.StageGatingNarrative {
|
|
t.Fatalf("narrative stamp=%q", plan.StageGating)
|
|
}
|
|
}
|
|
|
|
func twoStageResolver(gating string) *stubVersionResolver {
|
|
r := testResolver()
|
|
ver := r.versions[workflowdef.OfficialKeyMain]
|
|
ver.Definition.StageGating = gating
|
|
ver.Definition.Stages = []workflowdef.StageDef{
|
|
{
|
|
Key: "V0.1_triage", Name: "V0.1 需求摸底", Position: 10,
|
|
DocTasks: []workflowdef.DocTaskDef{{
|
|
Key: "intake", Title: "用户初始需求收集表", CreatorConfigurable: true, BlockingWhenEnabled: true, DefaultEnabled: true,
|
|
OutputDocType: "intake", OutputDocTitle: "用户初始需求收集表",
|
|
}},
|
|
},
|
|
{
|
|
Key: "V0.5_feasibility", Name: "V0.5 方案与可行性", Position: 20,
|
|
DocTasks: []workflowdef.DocTaskDef{
|
|
{
|
|
Key: "research", Title: "调研报告", CreatorConfigurable: true, BlockingWhenEnabled: true, DefaultEnabled: true,
|
|
OutputDocType: "research", OutputDocTitle: "调研报告",
|
|
},
|
|
{
|
|
Key: "disabled_doc", Title: "关闭文档", CreatorConfigurable: true, DefaultEnabled: false,
|
|
OutputDocType: "disabled_doc", OutputDocTitle: "关闭文档",
|
|
},
|
|
{
|
|
Key: "partial_doc", Title: "半配置", CreatorConfigurable: true, BlockingWhenEnabled: true, DefaultEnabled: true,
|
|
OutputDocType: "partial_doc", OutputDocTitle: "半配置",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
return r
|
|
}
|
|
|
|
func activateTwoStage(t *testing.T, svc *Service, projectID int64) {
|
|
t.Helper()
|
|
full := DocDuty{AuthorUserID: 1, ReviewerUserID: 2, ApproverUserID: 3}
|
|
plan := WorkflowPlan{
|
|
WorkflowDefinitionKey: workflowdef.OfficialKeyMain,
|
|
WorkflowVersion: 1,
|
|
Stages: []StagePlan{
|
|
{
|
|
StageKey: "V0.1_triage", Enabled: true,
|
|
DocTasks: []DocTaskPlan{{TaskKey: "intake", Enabled: true, Duties: full}},
|
|
},
|
|
{
|
|
StageKey: "V0.5_feasibility", Enabled: true,
|
|
DocTasks: []DocTaskPlan{
|
|
{TaskKey: "research", Enabled: true, Duties: full},
|
|
{TaskKey: "disabled_doc", Enabled: false, Duties: full},
|
|
{TaskKey: "partial_doc", Enabled: true, Duties: full},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
if _, err := svc.SaveWorkflowPlan(context.Background(), projectID, 1, true, SavePlanInput{
|
|
Plan: plan, ExpectedPlanRevision: 0, ExpectedSelectionVersion: 1,
|
|
}); err != nil {
|
|
t.Fatalf("save: %v", err)
|
|
}
|
|
preview, err := svc.ActivationPreview(context.Background(), projectID, 1, true)
|
|
if err != nil {
|
|
t.Fatalf("preview: %v", err)
|
|
}
|
|
if _, err := svc.Activate(context.Background(), projectID, 1, true, ActivateInput{
|
|
ConfirmationToken: preview.ConfirmationToken, ExpectedProjectVersion: preview.ExpectedProjectVersion,
|
|
ExpectedPlanRevision: preview.ExpectedPlanRevision,
|
|
}); err != nil {
|
|
t.Fatalf("activate: %v", err)
|
|
}
|
|
}
|
|
|
|
func forcePartialDuty(store *memStore, projectID int64, taskKey string) {
|
|
store.mu.Lock()
|
|
defer store.mu.Unlock()
|
|
inst := store.live[projectID]
|
|
for i := range inst.ConfigSnapshot.Stages {
|
|
for j := range inst.ConfigSnapshot.Stages[i].DocTasks {
|
|
dt := &inst.ConfigSnapshot.Stages[i].DocTasks[j]
|
|
if dt.TaskKey == taskKey {
|
|
dt.Duties = DocDuty{AuthorUserID: 1}
|
|
}
|
|
}
|
|
}
|
|
}
|