167 lines
5.6 KiB
Go
167 lines
5.6 KiB
Go
package projectworkflow
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"chat-agent-system/internal/auth"
|
|
)
|
|
|
|
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.ActiveWorkflowKey != "main" {
|
|
t.Errorf("expected default active workflow 'main', 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_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)
|
|
}
|
|
}
|