Composition roots copy Parts once so HTTP/CLI handlers stop calling locator getters on every request. Co-authored-by: Cursor <cursoragent@cursor.com>
212 lines
8.9 KiB
Go
212 lines
8.9 KiB
Go
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)
|
||
}
|