Files
Agent-System/internal/projectinstance/narrative_gating_test.go
T

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}
}
}
}
}