package projectworkflow import ( "context" "errors" "testing" "unicode/utf8" "chat-agent-system/internal/auth" ) func TestGenerateProjectCode_ValidUTF8ForChineseName(t *testing.T) { longChinese := "数字化高频超声手术吸引系统治理链测试项目名称" code := generateProjectCode(longChinese) if !utf8.ValidString(code) { t.Fatalf("project code must be valid UTF-8, got %q bytes=% x", code, []byte(code)) } if code == "" { t.Fatal("expected non-empty code") } } func TestProjectWorkflowService_CreateAndCreatorACL(t *testing.T) { ctx := context.Background() store := NewStore(nil) catalog := NewDefaultCatalog() svc := NewService(store, catalog) creator := &auth.UserAccount{ID: 101, Username: "zhangsan", Name: "张工"} otherUser := &auth.UserAccount{ID: 102, Username: "lisi", Name: "李主管"} // 1. 未认证创建应被拦截 _, err := svc.CreateProject(ctx, nil, CreateProjectInput{Name: "测试项目A"}) if err != ErrUnauthenticated { t.Fatalf("expected ErrUnauthenticated, got: %v", err) } // 2. 正常创建 proj, err := svc.CreateProject(ctx, creator, CreateProjectInput{ Name: "神经脉冲射频仪 (NeuroPulse)", DeviceType: "高频神经射频治疗设备", IntendedUse: "用于中枢神经阻滞与射频毁损治疗", TargetMarket: "NMPA Class III", }) if err != nil { t.Fatalf("CreateProject failed: %v", err) } if proj.CreatorUserID != creator.ID { t.Errorf("expected CreatorUserID %d, got %d", creator.ID, proj.CreatorUserID) } if proj.LifecycleStatus != "planning" { t.Errorf("expected lifecycle 'planning', got '%s'", proj.LifecycleStatus) } if proj.ActiveWorkflowKey != "" { t.Errorf("expected empty active workflow in planning mode, got '%s'", proj.ActiveWorkflowKey) } if proj.WorkflowSelectionVersion != 1 { t.Errorf("expected initial version 1, got %d", proj.WorkflowSelectionVersion) } // 3. 验证自动挂载的工作流清单 wfs, err := svc.ListProjectWorkflows(ctx, proj.ID) if err != nil { t.Fatalf("ListProjectWorkflows failed: %v", err) } if len(wfs) != 3 { t.Fatalf("expected 3 default workflows, got %d", len(wfs)) } // 4. 非创建人尝试切换工作流 -> 必须被拒绝 _, err = svc.ActivateWorkflow(ctx, proj.ID, otherUser.ID, ActivateWorkflowInput{ WorkflowKey: "product_manager", ExpectedVersion: 1, Reason: "尝试越权切换", }) if err != ErrForbiddenNotCreator { t.Fatalf("expected ErrForbiddenNotCreator, got: %v", err) } // 5. 创建人正常切换工作流 -> 成功升级版本 res, err := svc.ActivateWorkflow(ctx, proj.ID, creator.ID, ActivateWorkflowInput{ WorkflowKey: "product_manager", ExpectedVersion: 1, Reason: "进入产品需求阶段", }) if err != nil { t.Fatalf("ActivateWorkflow failed: %v", err) } if res.ActiveWorkflowKey != "product_manager" { t.Errorf("expected active workflow 'product_manager', got '%s'", res.ActiveWorkflowKey) } if res.Version != 2 { t.Errorf("expected version 2 after switch, got %d", res.Version) } // 6. 并发冲突:使用旧版本 1 再次切换 -> 必须返回 ErrConcurrentConflict _, err = svc.ActivateWorkflow(ctx, proj.ID, creator.ID, ActivateWorkflowInput{ WorkflowKey: "test_manager", ExpectedVersion: 1, // 旧版本 Reason: "并发落后切换", }) if err != ErrConcurrentConflict { t.Fatalf("expected ErrConcurrentConflict, got: %v", err) } // 7. 幂等测试:重复激活当前工作流 'product_manager' resIdempotent, err := svc.ActivateWorkflow(ctx, proj.ID, creator.ID, ActivateWorkflowInput{ WorkflowKey: "product_manager", ExpectedVersion: 2, Reason: "重复激活", }) if err != nil { t.Fatalf("idempotent switch failed: %v", err) } if resIdempotent.ActiveWorkflowKey != "product_manager" { t.Errorf("expected active 'product_manager', got '%s'", resIdempotent.ActiveWorkflowKey) } // 8. 切换到不存在的工作流 -> 返回 ErrWorkflowNotFound _, err = svc.ActivateWorkflow(ctx, proj.ID, creator.ID, ActivateWorkflowInput{ WorkflowKey: "non_existent_wf", }) if err == nil || !errors.Is(err, ErrWorkflowNotFound) { t.Fatalf("expected ErrWorkflowNotFound, got: %v", err) } } func TestProjectWorkflowService_ListAllProjects(t *testing.T) { ctx := context.Background() svc := NewService(NewStore(nil), NewDefaultCatalog()) creator := &auth.UserAccount{ID: 102, Username: "lisi", Name: "李主管"} created, err := svc.CreateProject(ctx, creator, CreateProjectInput{Name: "管理员跨项目查看测试"}) if err != nil { t.Fatalf("CreateProject failed: %v", err) } visible, err := svc.ListProjects(ctx, 999) if err != nil { t.Fatalf("ListProjects failed: %v", err) } if len(visible) != 0 { t.Fatalf("unrelated user should not see projects, got %d", len(visible)) } all, err := svc.ListAllProjects(ctx) if err != nil { t.Fatalf("ListAllProjects failed: %v", err) } found := false for _, project := range all { if project.ID == created.ID { found = true break } } if !found { t.Fatalf("ListAllProjects should include project %d", created.ID) } } func TestProjectWorkflowService_LineageAndStale(t *testing.T) { ctx := context.Background() store := NewStore(nil) catalog := NewDefaultCatalog() svc := NewService(store, catalog) creator := &auth.UserAccount{ID: 101, Username: "zhangsan", Name: "张工"} proj, _ := svc.CreateProject(ctx, creator, CreateProjectInput{Name: "超声手术刀"}) // 记录测试经理方案依赖产品经理需求规格书 v1 err := svc.RecordLineage(ctx, &DocumentLineage{ ProjectID: proj.ID, DownstreamDocID: "doc_test_plan_001", DownstreamVersion: 1, UpstreamDocID: "doc_system_reqs_001", UpstreamVersion: 1, UpstreamSHA256: "hash_v1_aaa", }) if err != nil { t.Fatalf("RecordLineage failed: %v", err) } lineages, err := svc.GetLineageForDownstream(ctx, "doc_test_plan_001", 1) if err != nil || len(lineages) == 0 { t.Fatalf("expected lineage record, got err: %v, len: %d", err, len(lineages)) } if lineages[0].IsStale { t.Errorf("initial lineage should not be stale") } // 上游生成相同 SHA256 -> 不触发 stale staleUpdates, err := svc.CheckAndPropagateStale(ctx, "doc_system_reqs_001", "hash_v1_aaa", "") if err != nil { t.Fatalf("CheckAndPropagateStale failed: %v", err) } if len(staleUpdates) > 0 { t.Errorf("expected 0 stale updates for identical hash, got %d", len(staleUpdates)) } // 上游生成不同 SHA256 (内容更新) -> 触发下游 stale 传播 staleUpdates, err = svc.CheckAndPropagateStale(ctx, "doc_system_reqs_001", "hash_v2_bbb", "系统需求新增 RF-009 射频功率限制") if err != nil { t.Fatalf("CheckAndPropagateStale failed: %v", err) } if len(staleUpdates) != 1 { t.Fatalf("expected 1 stale update, got %d", len(staleUpdates)) } if !staleUpdates[0].IsStale { t.Errorf("lineage should be marked as stale") } if staleUpdates[0].DownstreamDocID != "doc_test_plan_001" { t.Errorf("expected downstream 'doc_test_plan_001', got '%s'", staleUpdates[0].DownstreamDocID) } }