Files
Agent-System/internal/bizdev/docgov_bizdev_test.go
T
weichenandCursor 3f42149457 feat(bizdev): snapshot collaborators onto Runtime and Server ports
Composition roots copy Parts once so HTTP/CLI handlers stop calling locator getters on every request.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 13:00:33 +00:00

212 lines
8.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}