package bizdev_test import ( "context" "errors" "testing" "time" "chat-agent-system/internal/bizdev" "chat-agent-system/internal/config" "chat-agent-system/internal/docgov" ) func TestBizDev_DocGov_ClaimingAndLifecycleE2E(t *testing.T) { cfg := &config.Config{ DataDir: t.TempDir(), } svc := bizdev.MustNewService(t, cfg) ctx := context.Background() taskKey := "feasibility_reg" projectID := int64(888) authorID := int64(101) authorName := "张工(编制人)" reviewerID := int64(102) reviewerName := "李工(审核人)" approverID := int64(103) approverName := "王总监(批准人)" docID := "doc_proj888_feasibility_reg" // ------------------------------------------------------------------------- // 阶段 1:首次触发任务执行 (Stub 模式) // 预期:未认领状态,智能体小队直接生成初稿正文,并脱离模板状态 // ------------------------------------------------------------------------- req1 := bizdev.TaskRunRequest{ TaskKey: taskKey, ProjectID: projectID, ProjectName: "超声软组织手术刀系统研发项目", CustomerName: "北京某三甲创新转化中心", DeviceType: "超声软组织切割止血手术设备", IntendedUse: "软组织切割止血与血管闭合", TargetMarket: "中国NMPA二类/三类", Backend: "stub", Timeout: 10 * time.Second, DocID: docID, AuthorID: authorID, AuthorName: authorName, ReviewerID: reviewerID, ReviewerName: reviewerName, ApproverID: approverID, ApproverName: approverName, } res1, err := svc.ExecuteTask(ctx, req1) if err != nil { t.Fatalf("step 1 ExecuteTask failed: %v", err) } if !res1.Success { t.Fatalf("step 1 task should succeed, error: %s", res1.ErrorReason) } if res1.DocRecord == nil { t.Fatalf("step 1 DocRecord should not be nil") } if res1.DocRecord.AuthorClaimed { t.Fatalf("step 1 author should NOT be claimed yet") } if res1.DocRecord.IsTemplateOnly { t.Fatalf("step 1 document should no longer be template-only after squad output") } // ------------------------------------------------------------------------- // 阶段 2:未认领状态下再次触发修改 // 预期:由于编制人尚未查看正式成果物,系统允许智能体小队直接覆写更新 // ------------------------------------------------------------------------- req2 := req1 req2.IntendedUse = "软组织切割止血及大血管(≤5mm)安全闭合" // 输入变更 res2, err := svc.ExecuteTask(ctx, req2) if err != nil { t.Fatalf("step 2 ExecuteTask failed: %v", err) } if !res2.Success { t.Fatalf("step 2 should allow direct edit without proposal when unclaimed, got: %s", res2.ErrorReason) } // ------------------------------------------------------------------------- // 阶段 3:编制负责人查看已生成的正式交付物 // 预期:触发认领机制,author_claimed 变为 true! // ------------------------------------------------------------------------- docAfterView, err := bizdev.Snapshot(svc).Gov.RecordDocumentViewed(ctx, docID, authorID, docgov.RoleAuthor, authorName) if err != nil { t.Fatalf("RecordDocumentViewed failed: %v", err) } if !docAfterView.AuthorClaimed { t.Fatalf("document MUST be claimed after author views non-template artifact") } // ------------------------------------------------------------------------- // 阶段 4:已认领状态下系统/阶段发起修改 // 预期:系统严禁静默覆写!必须拦截,并自动生成变更提案等待编制人拍板 // ------------------------------------------------------------------------- req3 := req1 req3.TargetMarket = "中国NMPA + 欧盟CE MDR" // 上游输入发生重大变更 res3, err := svc.ExecuteTask(ctx, req3) if err != nil { t.Fatalf("step 4 ExecuteTask unexpected hard error: %v", err) } if res3.Success { t.Fatalf("step 4 should have been blocked because document is claimed!") } if !res3.RequiresAuthorApproval { t.Fatalf("step 4 expected RequiresAuthorApproval=true") } if res3.PendingProposal == nil { t.Fatalf("step 4 expected PendingProposal to be generated") } t.Logf("✓ 已认领文档成功拦截直接修改,生成变更提案: ID=%s, 变更提要: %s", res3.PendingProposal.ProposalID, res3.PendingProposal.ProposedChanges) // ------------------------------------------------------------------------- // 阶段 5:编制人决策同意变更提案,并授权执行修改 // 预期:提案变为 approved_by_author,并放行修改 // ------------------------------------------------------------------------- propID := res3.PendingProposal.ProposalID _, err = bizdev.Snapshot(svc).Gov.DecideModificationProposal(ctx, propID, authorID, true, "经论证追加CE市场符合出海战略,同意更新注册可行性报告") if err != nil { t.Fatalf("DecideModificationProposal failed: %v", err) } req4 := req3 req4.ForceDirectEdit = true // 获得编制人授权后强制更新 res4, err := svc.ExecuteTask(ctx, req4) if err != nil { t.Fatalf("step 5 ExecuteTask failed: %v", err) } if !res4.Success { t.Fatalf("step 5 task should succeed after author authorization, got: %s", res4.ErrorReason) } // ------------------------------------------------------------------------- // 阶段 6:送审、审核批注、审核驳回(必填原因)退回编制草稿、重新送审通过 // ------------------------------------------------------------------------- // 6.1 编制人送审 docDraft, err := bizdev.Snapshot(svc).Gov.SubmitForReview(ctx, docID, authorID, docgov.RoleAuthor, authorName) if err != nil { t.Fatalf("SubmitForReview failed: %v", err) } if docDraft.CurrentState != docgov.StateReviewing || !docDraft.IsLocked { t.Fatalf("doc must be in reviewing state and locked") } // 6.2 审核人在锁定状态下严禁直接修改正文 _, err = bizdev.Snapshot(svc).Gov.RecordDocumentEdited(ctx, docID, reviewerID, docgov.RoleReviewer, reviewerName) if !errors.Is(err, docgov.ErrDocumentLocked) { t.Fatalf("reviewer modifying locked document should return ErrDocumentLocked, got: %v", err) } // 6.3 审核人添加修改建议 _, err = bizdev.Snapshot(svc).Gov.AddComment(ctx, docID, reviewerID, reviewerName, docgov.RoleReviewer, "## 临床评价路径", docgov.CommentSuggestion, "请增加与同类同品种比对器械的产品注册证号") if err != nil { t.Fatalf("AddComment failed: %v", err) } // 6.4 审核人驳回 (验证原因必填) _, err = bizdev.Snapshot(svc).Gov.ReviewReject(ctx, docID, reviewerID, docgov.RoleReviewer, reviewerName, "") if err != docgov.ErrRejectReasonRequired { t.Fatalf("ReviewReject without reason should fail with ErrRejectReasonRequired") } // 审核人提供原因驳回 -> 退回编制草稿并解锁 docRejected, err := bizdev.Snapshot(svc).Gov.ReviewReject(ctx, docID, reviewerID, docgov.RoleReviewer, reviewerName, "同品种比对缺少实质等同临床文献支持,请补充") if err != nil { t.Fatalf("ReviewReject with reason failed: %v", err) } if docRejected.CurrentState != docgov.StateDrafting || docRejected.IsLocked { t.Fatalf("rejected document must rollback to drafting and be unlocked") } // 6.5 编制人修订后重新送审 _, _ = bizdev.Snapshot(svc).Gov.RecordDocumentEdited(ctx, docID, authorID, docgov.RoleAuthor, authorName) docResubmitted, err := bizdev.Snapshot(svc).Gov.SubmitForReview(ctx, docID, authorID, docgov.RoleAuthor, authorName) if err != nil { t.Fatalf("resubmit review failed: %v", err) } if docResubmitted.CurrentState != docgov.StateReviewing || !docResubmitted.IsLocked { t.Fatalf("expected resubmitted doc in reviewing state and locked") } // 6.6 审核人审核通过 -> 进入 approving docReviewApproved, err := bizdev.Snapshot(svc).Gov.ReviewApprove(ctx, docID, reviewerID, docgov.RoleReviewer, reviewerName, "临床比对资料已补充完备,同意转交批准") if err != nil { t.Fatalf("ReviewApprove failed: %v", err) } if docReviewApproved.CurrentState != docgov.StateApproving || !docReviewApproved.IsLocked { t.Fatalf("expected doc in approving state and locked") } // ------------------------------------------------------------------------- // 阶段 7:批准人最终批准 -> approved 终态归档锁定 // ------------------------------------------------------------------------- docFinalApproved, err := bizdev.Snapshot(svc).Gov.ApproverApprove(ctx, docID, approverID, docgov.RoleApprover, approverName, "可行性评估与合规路径严谨可靠,准予受控生效") if err != nil { t.Fatalf("ApproverApprove failed: %v", err) } if docFinalApproved.CurrentState != docgov.StateApproved || !docFinalApproved.IsLocked || docFinalApproved.ApprovedAt == nil { t.Fatalf("document must be in approved state and permanently locked") } t.Logf("✓ 受控文档全生命周期(编制、认领、提案拦截、驳回退回、审核批准)闭环测试完美通过!最终状态: %s, 批准时间: %v", docFinalApproved.CurrentState, docFinalApproved.ApprovedAt) }